Respons kesalahan API Anda adalah bagian dari kontraknya. Klien mengurainya, logika percobaan ulang bercabang padanya, dan insinyur dukungan mencarinya pada pukul 2 dini hari. Namun sebagian besar tim merancang alur sukses secara rinci dan membiarkan kesalahan muncul dari apa pun yang dilakukan kerangka kerja secara default. Begitulah cara Anda berakhir dengan tiga bentuk kesalahan berbeda dalam satu API, respons 200 yang membungkus "success": false, dan jejak tumpukan yang membocorkan skema basis data Anda ke internet publik.
Panduan ini mencakup praktik terbaik penanganan kesalahan API untuk layanan REST secara menyeluruh: memilih kode status yang tepat, membakukan satu badan kesalahan dengan RFC 9457 Problem Details, memisahkan kode yang dapat dibaca mesin dari pesan manusia, menandai kesalahan yang dapat dicoba ulang, dan menjaga kerahasiaan dari respons. Ini dibangun di atas penjelasan kami tentang kode status HTTP mana yang harus digunakan API REST dan menambahkan keputusan tingkat kontrak yang tidak dibahas secara rinci dalam panduan tersebut. Anda juga akan melihat cara menguji setiap jalur kegagalan di Apidog, karena kontrak kesalahan yang tidak pernah Anda uji adalah kontrak yang tidak Anda miliki.
Mulai dengan kode status, bukan badan respons
HTTP sudah memberi Anda lapisan pertama semantik kesalahan secara gratis. RFC 9110 mendefinisikan keluarga kode status: 4xx berarti klien melakukan kesalahan dan mengulang permintaan yang sama akan gagal lagi; 5xx berarti server gagal dan permintaan klien mungkin baik-baik saja. Pahami pembagian ini dengan benar sebelum Anda menulis satu baris pun badan kesalahan, karena klien umum, proksi, cache, dan pustaka percobaan ulang semuanya membuat keputusan berdasarkan hal tersebut tanpa pernah membaca JSON Anda.
Kesalahan paling umum berkelompok di sekitar beberapa pasangan yang mirip. Buka referensi kode status HTTP MDN saat Anda mendesain, dan gunakan tabel keputusan ini untuk kode-kode yang membingungkan tim.
| Situasi | Gunakan | Bukan | Mengapa |
|---|---|---|---|
| Permintaan salah format: JSON rusak, tipe konten salah, bidang wajib hilang | 400 Bad Request | 422 | Server tidak dapat mengurai atau memahami permintaan sama sekali |
| Permintaan yang terbentuk dengan baik namun melanggar aturan semantik: jumlah negatif, mata uang tidak didukung | 422 Unprocessable Content | 400 | Sintaks baik-baik saja; nilainya tidak |
| Tidak ada kredensial, atau token kedaluwarsa/tidak valid | 401 Unauthorized | 403 | Klien belum membuktikan identitasnya. Kirim WWW-Authenticate |
| Kredensial valid, izin tidak mencukupi | 403 Forbidden | 401 | Identitas diketahui; akses ditolak. Otentikasi ulang tidak akan membantu |
| Sumber daya tidak pernah ada, atau Anda tidak akan mengonfirmasi keberadaannya | 404 Not Found | 410 | Default yang aman; juga menyembunyikan sumber daya dari pemeriksaan tidak sah |
| Sumber daya ada dan sengaja, dihapus secara permanen | 410 Gone | 404 | Memberi tahu klien dan perayap untuk menghapus referensi mereka |
| Konflik status: kunci duplikat, versi lama, tabrakan pengeditan | 409 Conflict | 400 | Permintaan valid tetapi bertentangan dengan status sumber daya saat ini |
| Klien melampaui batas kecepatan | 429 Too Many Requests | 503 | Selalu sertakan Retry-After agar klien mundur dengan benar |
| Pengecualian yang tidak tertangani dalam kode Anda | 500 Internal Server Error | 502 | Server Anda rusak |
| Layanan hulu mengembalikan data yang tidak relevan ke gateway Anda | 502 Bad Gateway | 500 | Kegagalan berada di hilir dari tepi, bukan di dalamnya |
| Server kelebihan beban atau dalam pemeliharaan | 503 Service Unavailable | 500 | Sementara secara definisi; tambahkan Retry-After jika Anda bisa |
| Layanan hulu waktu habis | 504 Gateway Timeout | 500 | Membedakan "ketergantungan lambat" dari "kode rusak" |
Dua di antaranya layak mendapatkan penekanan ekstra. Pertama, 401 vs 403 adalah batas keamanan, bukan pilihan gaya: mengembalikan 403 ke pemanggil yang tidak terautentikasi membocorkan fakta bahwa sumber daya tersebut ada. Kedua, 429 tanpa Retry-After melatih klien untuk menyerang Anda dalam perulangan yang ketat. Jika Anda membatasi kecepatan, dan Anda harus melakukannya, pasangkan status tersebut dengan sinyal penundaan yang konkret; panduan kami tentang pembatasan kecepatan API mencakup matematika header dan algoritma di baliknya.
Satu bentuk badan kesalahan: RFC 9457 Problem Details
Setelah kode status benar, setiap kesalahan yang dikembalikan API Anda harus berbagi satu tipe media dan satu skema. Jawaban standarnya adalah RFC 9457 Problem Details, disajikan sebagai application/problem+json. Ini mendefinisikan lima anggota inti: type (URI yang mengidentifikasi kategori kesalahan), title (ringkasan singkat yang mudah dibaca manusia), status (kode HTTP, diulang untuk kenyamanan), detail (apa yang salah dalam kejadian ini), dan instance (URI untuk kegagalan spesifik ini). Hal lainnya masuk ke anggota ekstensi yang Anda definisikan sendiri.
Kami tidak akan menguraikan spesifikasi di sini; penjelasan RFC 9457 kami membahas setiap anggota, aturan registri, dan bagaimana hal itu menggantikan RFC 7807. Yang penting untuk kontrak Anda adalah polanya: amplop standar, ekstensi kustom. Berikut adalah kegagalan validasi pada titik akhir pembayaran.
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Validasi permintaan gagal",
"status": 422,
"detail": "Satu atau lebih bidang gagal validasi.",
"instance": "/v1/payments/requests/req_9f3c1a7b",
"code": "PAYMENT_VALIDATION_FAILED",
"errors": [
{
"field": "amount",
"code": "AMOUNT_NOT_POSITIVE",
"message": "jumlah harus berupa bilangan bulat positif dalam satuan kecil"
}
],
"request_id": "req_9f3c1a7b"
}
Array errors[] adalah anggota ekstensi, dan itu adalah yang paling disukai klien: ini memungkinkan frontend memetakan setiap kegagalan ke bidang formulir yang tepat alih-alih menampilkan satu spanduk yang samar. Jaga agar jalur bidang dalam format yang stabil (JSON Pointer atau jalur bertitik, pilih salah satu) sehingga kode klien dapat mengikatnya secara terprogram.
Satu aturan menyelamatkan Anda dari sebagian besar masalah: kembalikan bentuk ini untuk setiap kesalahan, termasuk yang dihasilkan oleh kerangka kerja atau gateway Anda. Klien yang mendapatkan Problem Details dari penangan Anda tetapi HTML dari halaman 502 penyeimbang beban Anda masih harus menulis dua parser.
Kode yang dapat dibaca mesin vs pesan manusia
Perhatikan contohnya membawa bidang code dan message. Itu disengaja. Keduanya melayani audiens yang berbeda dan tidak boleh digabungkan menjadi satu string.
Kode yang dapat dibaca mesin (AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED, IDEMPOTENCY_KEY_REUSED) adalah kontrak. Klien membuat cabang berdasarkan itu, jadi mereka harus stabil, didokumentasikan, dan dapat dihitung. Jangan pernah membuat klien mengurai prosa; saat seseorang menulis if (message.includes("positive")), revisi salinan Anda menjadi perubahan yang merusak.
Pesan manusia adalah kebalikannya: bebas untuk ditingkatkan kapan saja, ditulis untuk pengembang yang membaca log, dan tidak pernah menjadi penentu beban. Sebutkan apa yang gagal dan bagaimana cara memperbaikinya: "jumlah harus berupa bilangan bulat positif dalam satuan kecil" lebih baik daripada "jumlah tidak valid". Jika Anda melokalkan, lokalkan pesan dan biarkan kodenya.
Pembagian ini bahkan lebih penting sekarang karena konsumen API termasuk agen otonom. Klien berbasis LLM pulih jauh lebih baik dari kesalahan terstruktur yang mendeskripsikan diri; kami membahas sudut pandang itu dalam desain kesalahan API untuk agen AI.
Apa yang tidak pernah masuk dalam respons kesalahan
Respons kesalahan adalah saluran pengintaian favorit bagi penyerang, karena kegagalan yang tidak tertangani cenderung bertele-tele. Middleware kesalahan Anda harus menjamin bahwa tidak ada hal berikut yang pernah mencapai klien:
- Jejak tumpukan, nama kelas, atau jalur file
- SQL mentah, fragmen kueri, atau kesalahan ORM
- Nama host internal, IP, port, atau nama layanan
- Versi pustaka dan string banner kerangka kerja
- Rahasia, token, atau string koneksi yang tertanam dalam teks pengecualian
- Apakah akun pengguna ada (pada alur masuk dan reset kata sandi, jaga agar kegagalan tetap simetris)
Polanya sederhana: tangkap semuanya di batas, catat pengecualian lengkap di sisi server dengan ID permintaan, dan kembalikan badan Problem Details generik dengan ID yang sama. Klien mendapatkan "detail": "Terjadi kesalahan internal", "request_id": "req_51ad0", log Anda mendapatkan kebenarannya, dan dukungan dapat menghubungkan keduanya.
Tandai kesalahan yang dapat dicoba ulang atau terminal
Setiap kesalahan yang Anda kembalikan menjawab pertanyaan yang akan diajukan klien: haruskah saya mencoba ini lagi? Sertakan jawabannya ke dalam kontrak alih-alih membiarkan setiap tim klien menebak.
Kode status membawa semantik default. 429, 502, 503, dan 504 dapat dicoba ulang dengan penundaan eksponensial dan jitter. 500 ambigu tetapi biasanya layak untuk satu percobaan ulang yang hati-hati. Hampir semua kode 4xx lainnya adalah terminal: mencoba ulang 401, 403, 404, atau 422 dengan permintaan yang sama membuang kuota dan mencemari log. Batas waktu memerlukan perhatian tersendiri, karena permintaan mungkin berhasil setelah klien menyerah; itu adalah masalah batas waktu permintaan 408 klasik, dan itulah mengapa titik akhir yang mengubah data harus menerima kunci idempoten sehingga pembayaran yang dicoba ulang tidak dapat menagih dua kali.
Anda juga dapat membuat kemampuan percobaan ulang eksplisit dengan anggota ekstensi:
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Terlalu banyak permintaan",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
Flag retryable eksplisit memungkinkan Anda mengganti pengaturan default saat Anda perlu, seperti menandai subkode 500 tertentu sebagai terminal karena mencobanya kembali merusak status. Dokumentasikan flag tersebut sekali dan setiap SDK klien yang Anda kirim akan mendapatkan perilaku penundaan yang seragam.
ID Korelasi dan pembuatan versi kontrak kesalahan
Dua keputusan yang lebih kecil melengkapi kontrak, dan keduanya murah sekarang, mahal nanti.
Berikan ID untuk setiap permintaan. Terima header X-Request-Id masuk (atau buat satu), capkan di setiap baris log, dan gema di setiap badan kesalahan sebagai request_id. Ketika pelanggan menempelkan kesalahan ke tiket dukungan, satu bidang itu mengubah satu jam penggalian log menjadi satu kueri. Dalam pengaturan terdistribusi, sebarkan traceparent W3C bersamanya sehingga ID mengikuti permintaan di seluruh layanan.
Buat versi kontrak kesalahan Anda seperti API itu sendiri. Menambahkan anggota ekstensi baru atau kode kesalahan baru itu aman. Mengganti nama errors[].field, mengubah arti suatu kode, atau beralih dari bentuk ad-hoc ke Problem Details adalah perubahan yang merusak, dan itu merusak jalur kode yang paling sedikit diuji oleh tim. URI type memberi Anda mekanisme yang bersih: jaga agar URI tipe lama stabil selamanya, perkenalkan yang baru untuk semantik baru, dan nyatakan dalam dokumen Anda bahwa anggota ekstensi yang tidak dikenal dan kode yang tidak dikenal harus diabaikan, tidak diperlakukan sebagai kegagalan. Klausul kompatibilitas ke depan itulah yang memungkinkan Anda berkembang tanpa versi 2.
Uji setiap jalur kesalahan di Apidog
Ini adalah kebenaran yang tidak nyaman: kontrak kesalahan membusuk karena tidak ada yang mengujinya. Alur sukses berjalan di setiap demo; cabang 422 berjalan ketika pelanggan menemukannya. Solusinya adalah menjadikan kasus kegagalan sebagai warga kelas satu dalam rangkaian pengujian Anda, dan di sinilah Apidog mendapatkan tempatnya dalam alur kerja.
Dua fitur memetakan secara langsung ke masalah ini.
Skenario pengujian untuk sisi server. Untuk setiap titik akhir, bangun skenario per kasus kegagalan: autentikasi yang hilang mengharapkan 401, peran yang tidak memadai mengharapkan 403, jumlah negatif mengharapkan 422 dengan errors[0].code sama dengan AMOUNT_NOT_POSITIVE, lalu lintas padat mengharapkan 429 dengan header Retry-After. Asersi visual Apidog memeriksa status, header, dan bidang badan tanpa skrip, dan Anda dapat memvalidasi seluruh payload terhadap Skema JSON Problem Details Anda sehingga setiap penyimpangan dalam bentuk kesalahan akan menyebabkan kegagalan CI, bukan produksi. Panduan asersi API kami menunjukkan pola asersi secara rinci.
Server tiruan untuk sisi klien. Tim frontend dan SDK Anda perlu membuat terhadap respons 4xx dan 5xx sebelum backend dapat menghasilkannya sesuai permintaan. Server tiruan Apidog mengembalikan badan Problem Details persis dari spesifikasi API Anda, sehingga Anda dapat mensimulasikan 503 dengan Retry-After: 120, 409 pada pengiriman ganda, atau payload validasi errors[] lengkap, lalu mengamati bagaimana klien merender dan mencoba ulang. Tidak ada stub Express yang dibuat secara manual, tidak ada mengomentari kode backend untuk memaksa kegagalan.
Rancang kontrak kesalahan, kodekan sebagai skenario dan mock, dan hubungkan keduanya ke CI. Unduh Apidog dan coba gratis; mengimpor spesifikasi OpenAPI yang ada akan memberi Anda respons kesalahan yang dapat dimock dalam beberapa menit.
FAQ
Haruskah saya menggunakan 400 atau 422 untuk kesalahan validasi?
Gunakan 400 ketika permintaan salah format dan server tidak dapat memahaminya: JSON tidak valid, tipe konten salah, bidang wajib hilang. Gunakan 422 ketika permintaan diurai dengan benar tetapi nilainya melanggar aturan domain Anda, seperti jumlah pembayaran negatif atau mata uang yang tidak didukung. Manfaat praktisnya adalah diagnostik: 422 memberi tahu klien "perbaiki data Anda," sementara 400 mengatakan "perbaiki format permintaan Anda." Apapun pembagian yang Anda pilih, terapkan secara konsisten di setiap titik akhir.
Apa itu application/problem+json?
Ini adalah tipe media yang ditentukan oleh RFC 9457 untuk Problem Details, format kesalahan JSON standar untuk API HTTP. Respons dengan tipe konten ini membawa anggota type, title, status, detail, dan instance, ditambah ekstensi apa pun yang Anda definisikan, seperti array errors[] untuk kegagalan validasi tingkat bidang. Menggunakan tipe media terdaftar memungkinkan klien umum dan middleware mengenali kesalahan Anda tanpa konfigurasi kustom. Penjelasan RFC 9457 kami mencakup spesifikasi lengkapnya.
Kesalahan HTTP mana yang harus dicoba ulang secara otomatis oleh klien?
Coba ulang 429, 502, 503, dan 504 dengan penundaan eksponensial ditambah jitter, dengan menghormati Retry-After jika ada. Anggap 500 sebagai layak untuk satu percobaan ulang yang hati-hati. Jangan mencoba ulang respons 4xx lainnya; permintaan akan gagal dengan cara yang sama setiap saat. Untuk titik akhir yang mengubah data, pasangkan percobaan ulang dengan kunci idempoten sehingga permintaan yang diputar ulang tidak dapat menagih ganda atau membuat ganda.
Bagaimana cara menguji respons kesalahan API tanpa merusak backend saya?
Simulasikan. Arahkan klien Anda ke server tiruan Apidog yang mengembalikan badan 4xx dan 5xx persis dari spesifikasi Anda, lalu verifikasi perilaku rendering dan percobaan ulang terhadap masing-masing. Di sisi server, tulis skenario pengujian yang mengirimkan payload tidak valid, autentikasi yang hilang, dan lalu lintas padat, lalu lakukan asersi pada kode status, header, dan skema badan kesalahan. Kedua bagian berjalan di CI, sehingga kontrak kesalahan tetap akurat tanpa ada yang secara manual memaksa kegagalan.
