OmniStream Docs
  • Panduan Pengguna
  • Developer
  • API Reference
Developer Hub
Pendahuluan
Autentikasi
Model Data
Webhook
WebSocket
Self-Hosting
Error & Rate Limit
Developer

Error & Rate Limit

Error & Rate Limit

Seluruh endpoint api-gateway mengembalikan format error yang seragam dan dibatasi oleh rate limiter berbasis IP. Halaman ini menjelaskan bentuk body error, kode HTTP yang dipakai, serta kebijakan rate limit yang di-enforce di layer Tower.

Format body error

Implementasi bersumber dari enum ApiError di crates/api-gateway/src/errors.rs:7 dan implementasi IntoResponse pada crates/api-gateway/src/errors.rs:36.

Semua error — validasi, otentikasi, otorisasi, maupun kesalahan internal — dibungkus dalam satu bentuk JSON yang konsisten:

Code
{ "error": { "message": "Resource not found", "code": 404 } }

Field:

  • error.message — string deskriptif untuk konsumen API (dalam bahasa Inggris)
  • error.code — status HTTP numerik (redundan dengan header HTTP, tapi berguna untuk klien yang hanya membaca body)

Mapping enum → status HTTP

Mapping lengkap dari varian ApiError ke status HTTP ada di crates/api-gateway/src/errors.rs:38-72:

VarianHTTPKapan dipakai
ApiError::Unauthorized401JWT hilang, kadaluarsa, atau tidak valid
ApiError::Forbidden403Role pengguna tidak diizinkan (RBAC)
ApiError::NotFound404Resource tidak ada (termasuk sqlx::Error::RowNotFound)
ApiError::BadRequest400Body request tidak valid secara struktur
ApiError::Validation422Gagal validasi semantik (format email, length, dll)
ApiError::Database500Query PostgreSQL gagal (pesan asli disembunyikan di log)
ApiError::MongoDB500Operasi MongoDB gagal
ApiError::Kafka500Producer gagal mengirim ke Redpanda/Kafka
ApiError::Internal500Kesalahan tak terduga; pesan asli ditulis ke log, bukan ke klien

Untuk varian Database, MongoDB, Kafka, dan Internal, pesan yang dikirim ke klien disanitasi menjadi pesan generik ("Internal database error", "Internal messaging error", dst.) agar tidak membocorkan detail infrastruktur. Pesan aslinya tetap bisa dicari di log server lewat tracing::error! (lihat crates/api-gateway/src/errors.rs:44-71).

Otomatis From implementasi

Beberapa tipe error lain otomatis dikonversi menjadi ApiError via impl From di crates/api-gateway/src/errors.rs:85-121:

  • sqlx::Error::RowNotFound → ApiError::NotFound("Resource not found")
  • sqlx::Error::* lainnya → ApiError::Database(...)
  • mongodb::error::Error → ApiError::MongoDB(...)
  • serde_json::Error → ApiError::Validation(...)
  • jsonwebtoken::errors::Error → ApiError::Unauthorized("Invalid token: ...")
  • argon2::password_hash::Error::Password → ApiError::Unauthorized("Invalid email or password")

Artinya handler cukup pakai ? pada Result<_, sqlx::Error> dan konversi ke respons HTTP terjadi otomatis.

Rate limit 60 req/menit per IP

Rate limit diaktifkan lewat tower_governor di crates/api-gateway/src/main.rs:217-233:

Code
let governor_conf = GovernorConfigBuilder::default() .per_second(1) // 1 token per detik diisi ulang .burst_size(60) // kapasitas bucket = 60 request .finish() .expect("Failed to build governor config");

Layer diterapkan di router utama (main.rs:250-253):

Code
let app = routes::build_router(state) .layer(GovernorLayer::new(governor_conf)) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive());

Model token bucket bekerja seperti ini:

  • Setiap IP punya bucket kapasitas 60 token.
  • 1 token diisi ulang per detik.
  • Setiap request yang lolos middleware memakai 1 token.
  • Saat bucket kosong, governor mengembalikan HTTP 429 Too Many Requests dengan body yang dihasilkan oleh tower_governor (bukan dari ApiError).
  • Background task di main.rs:227-233 memanggil retain_recent() setiap 60 detik untuk membersihkan entri IP yang sudah tidak aktif sehingga memori tidak bocor.

Efeknya: klien yang mematuhi batas bisa burst hingga 60 request langsung kemudian tetap jalan di rata-rata 60 req/menit; klien yang agresif terkena 429 sampai bucket terisi kembali.

Kunci rate limit per IP asli

Extractor IP yang dipakai adalah PeerIpKeyExtractor — karenanya router dipasang dengan into_make_service_with_connect_info::<SocketAddr>() di main.rs:261-265. Jika anda menaruh api-gateway di belakang reverse proxy (Nginx, Cloudflare, dll), atur X-Forwarded-For dan konfigurasi proxy agar api-gateway mendapat IP klien asli — kalau tidak, seluruh traffic akan terlihat datang dari IP proxy dan saling "mencuri" token.

Troubleshooting umum

GejalaKemungkinan penyebabCara cek
401 "Invalid token: ExpiredSignature"JWT sudah expired (default 24 jam, lihat JWT_EXPIRATION_HOURS)Panggil ulang POST /api/auth/login
401 "Unauthorized: ..."Header Authorization tidak ada atau formatnya salahPastikan Authorization: Bearer <token> terpasang
403 "Forbidden: ..."Role user tidak punya izin (RBAC)Cek role di payload JWT; supervisor/admin punya akses berbeda
404 "Resource not found"ID tidak ada di databaseCross-check dengan GET pada list endpoint terkait
422 "Validation error: ..."Body request gagal validasi serde/semantikBaca pesan — umumnya menyebut field yang bermasalah
429 (dari tower_governor)Melebihi 60 req/menit dari satu IPTambahkan backoff eksponensial; untuk batch gunakan endpoint bulk
500 "Internal database error"Query Postgres gagal; detail ada di log api-gatewaydocker compose logs api-gateway | grep ERROR
500 "Internal messaging error"Producer Kafka/Redpanda gagal (broker down)docker compose ps redpanda; cek KAFKA_BROKERS

Untuk error signature mismatch pada webhook inbound, lihat developer/webhook/inbound-whatsapp, inbound-instagram, atau inbound-email — mereka dihasilkan oleh webhook-ingestor, bukan api-gateway, dan tidak melewati ApiError.

Last modified on June 8, 2026
CORS di Produksi
On this page
  • Format body error
  • Mapping enum → status HTTP
  • Otomatis From implementasi
  • Rate limit 60 req/menit per IP
    • Kunci rate limit per IP asli
  • Troubleshooting umum
JSON
Rust
Rust