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
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:
| Varian | HTTP | Kapan dipakai |
|---|---|---|
ApiError::Unauthorized | 401 | JWT hilang, kadaluarsa, atau tidak valid |
ApiError::Forbidden | 403 | Role pengguna tidak diizinkan (RBAC) |
ApiError::NotFound | 404 | Resource tidak ada (termasuk sqlx::Error::RowNotFound) |
ApiError::BadRequest | 400 | Body request tidak valid secara struktur |
ApiError::Validation | 422 | Gagal validasi semantik (format email, length, dll) |
ApiError::Database | 500 | Query PostgreSQL gagal (pesan asli disembunyikan di log) |
ApiError::MongoDB | 500 | Operasi MongoDB gagal |
ApiError::Kafka | 500 | Producer gagal mengirim ke Redpanda/Kafka |
ApiError::Internal | 500 | Kesalahan 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
Layer diterapkan di router utama (main.rs:250-253):
Code
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 dariApiError). - Background task di
main.rs:227-233memanggilretain_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
| Gejala | Kemungkinan penyebab | Cara 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 salah | Pastikan 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 database | Cross-check dengan GET pada list endpoint terkait |
| 422 "Validation error: ..." | Body request gagal validasi serde/semantik | Baca pesan — umumnya menyebut field yang bermasalah |
429 (dari tower_governor) | Melebihi 60 req/menit dari satu IP | Tambahkan backoff eksponensial; untuk batch gunakan endpoint bulk |
| 500 "Internal database error" | Query Postgres gagal; detail ada di log api-gateway | docker 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.