Audit kontrak API diperlukan agar integrasi gagal dengan cara yang aman, terdeteksi, dan mudah ditelusuri. Banyak insiden integrasi bukan berasal dari logika bisnis utama, tetapi dari asumsi kecil yang salah: parser menerima format ambigu, path dianggap sama padahal berbeda setelah canonicalization, header diproses tidak konsisten, atau error dikembalikan dengan bentuk yang berubah-ubah.

Konteksnya mirip dengan pelajaran dari berbagai kasus pada stack web seperti IIS: perilaku yang aneh tapi masih valid sering cukup untuk memicu bug serius ketika satu komponen menafsirkan request secara berbeda dari komponen lain. Dalam API, masalah ini muncul saat gateway, proxy, framework, SDK klien, dan service backend memiliki aturan parsing yang tidak sepenuhnya sama. Karena itu, kontrak API tidak cukup hanya ditulis di dokumen; ia perlu diaudit sebagai batas perilaku yang diuji.

Mengapa audit kontrak API penting

Dokumentasi endpoint seperti POST /orders dan daftar field JSON belum cukup untuk memastikan integrasi aman. Yang perlu dijaga adalah kesepakatan perilaku:

  • Input mana yang diterima, ditolak, atau dinormalisasi.
  • Bagaimana server memperlakukan path, query, dan header yang bentuknya berbeda tetapi maknanya mirip.
  • Status code apa yang benar-benar dipakai untuk tiap kelas kegagalan.
  • Bagaimana klien harus melakukan retry tanpa membuat duplikasi efek samping.
  • Apakah error cukup kaya untuk didiagnosis, tetapi tidak membocorkan detail sensitif.

Jika kontrak longgar, dua jenis kegagalan sering terjadi. Pertama, silent failure: request salah tetap diterima, tetapi hasilnya tidak sesuai harapan. Kedua, split interpretation: gateway menganggap request A, service menganggap request B. Dua masalah ini jauh lebih mahal dibanding penolakan eksplisit dengan error yang konsisten.

Prinsip audit: fail closed, canonical, observable

1. Fail closed

Ketika input ambigu, sistem sebaiknya menolak dengan jelas, bukan menebak maksud klien. Parser yang terlalu permisif sering tampak ramah di awal, tetapi berbahaya saat integrasi berkembang.

2. Canonical

Semua komponen yang memproses request harus berbagi representasi yang sama untuk path, header, host, body, dan identitas pengguna. Jika ada normalisasi, lakukan di satu tempat yang jelas dan dokumentasikan hasil akhirnya.

3. Observable

Kegagalan integrasi harus terlihat. Artinya ada correlation ID, error code stabil, metrik status code, log penolakan validasi, dan jejak retry yang dapat ditelusuri.

Checklist audit kontrak API sebelum dibuka ke pihak ketiga

Validasi input dan output

Validasi input bukan hanya memeriksa field wajib. Audit juga:

  • Tipe data: string, integer, boolean, array, object.
  • Batas nilai: panjang minimum/maksimum, rentang numerik, enum.
  • Field tambahan yang tidak dikenal: ditolak atau diabaikan?
  • Nullability: apakah null, string kosong, dan field yang hilang punya arti berbeda?
  • Format ambigu: tanggal, angka desimal, zona waktu, encoding.

Output juga perlu divalidasi. Banyak tim ketat pada request, tetapi longgar pada response. Akibatnya, klien bergantung pada field yang kadang hilang atau berubah tipe.

Praktik aman: gunakan skema request dan response yang dapat diuji otomatis. Jika sebuah field kadang string dan kadang number, anggap itu bug kontrak, bukan fleksibilitas.

Canonicalization path dan header

Ini area yang sering diremehkan. Audit beberapa hal berikut:

  • Apakah /orders dan /orders/ dianggap sama?
  • Bagaimana perlakuan terhadap multiple slash, URL-encoded slash, dot-segment seperti ../, dan karakter yang bisa dinormalisasi berbeda oleh proxy dan app server?
  • Apakah nama header diperlakukan case-insensitive secara konsisten?
  • Bagaimana jika header yang sama dikirim dua kali?
  • Siapa sumber kebenaran untuk Host, X-Forwarded-For, X-Forwarded-Proto, dan header lain dari proxy?

Kasus nyata yang sering muncul: gateway memverifikasi signature berdasarkan path mentah, tetapi aplikasi memproses path yang sudah dinormalisasi. Hasilnya, request yang tampak identik bisa lolos di satu lapisan dan ditafsirkan berbeda di lapisan lain.

Batas ukuran request dan response

Tentukan batas eksplisit untuk:

  • Ukuran body maksimum.
  • Jumlah field atau kedalaman JSON.
  • Panjang header.
  • Ukuran file upload.

Tanpa batas ini, API rawan terhadap request yang mahal diparse, menghabiskan memori, atau menunda worker. Jika API menerima payload besar, dokumentasikan juga perilaku saat melewati batas: status code, error schema, dan apakah koneksi diputus lebih awal.

Content-Type dan content negotiation

Jangan menerima body JSON hanya karena isi payload mirip JSON. Audit:

  • Apakah endpoint hanya menerima Content-Type: application/json untuk body JSON?
  • Apakah charset non-standar diterima?
  • Bagaimana perilaku jika body kosong tetapi content-type JSON?
  • Apakah response selalu menyertakan Content-Type yang benar?

Anti-pattern umum: endpoint diam-diam menerima text/plain lalu mencoba parse JSON. Ini mempersulit debugging dan bisa membuat aturan keamanan berbeda antar komponen.

Auth boundary dan identitas dari proxy

Audit batas otentikasi secara eksplisit:

  • Siapa yang memverifikasi token: gateway, service, atau keduanya?
  • Header identitas mana yang hanya boleh diisi oleh proxy tepercaya?
  • Apakah service bisa diakses langsung tanpa melewati gateway?
  • Apakah scope atau role dipetakan ke aksi API dengan jelas?

Kesalahan klasik adalah mempercayai header seperti X-User-Id atau X-Forwarded-For dari internet publik. Header seperti ini hanya aman jika semua jalur akses langsung ditutup dan hanya proxy tepercaya yang boleh menyuntikkannya.

Idempotency key dan retry semantics

Untuk endpoint yang menciptakan efek samping, terutama POST pembayaran, order, atau provisioning, audit dua hal: apa yang terjadi jika request sama terkirim dua kali, dan bagaimana klien tahu aman untuk retry.

Gunakan idempotency key bila duplikasi dapat terjadi karena timeout, retry otomatis, atau jaringan putus setelah server memproses request tetapi sebelum klien menerima response.

POST /payments
Idempotency-Key: 7e1d9b90-5b1e-4f8b-9b20-1fae4c0c1234
Content-Type: application/json

{
  "order_id": "ORD-10027",
  "amount": 150000,
  "currency": "IDR"
}

Audit implementasinya:

  • Apakah key unik per operasi logis atau per request mentah?
  • Berapa lama key disimpan?
  • Apa yang terjadi jika key sama dipakai dengan payload berbeda?
  • Apakah response sukses sebelumnya dikembalikan ulang secara konsisten?

Tanpa jawaban jelas, klien akan menebak-nebak kapan aman retry, dan duplikasi transaksi sangat mungkin terjadi.

Webhook signature dan replay protection

Jika API mengirim webhook, kontraknya harus lebih ketat daripada endpoint biasa. Audit:

  • Bagaimana signature dihitung: dari body mentah atau body yang sudah diparse?
  • Header timestamp apakah wajib?
  • Berapa toleransi waktu untuk mencegah replay?
  • Apakah penerima harus memverifikasi event ID agar event sama tidak diproses dua kali?

Kesalahan yang sering muncul adalah memverifikasi signature dari JSON yang sudah di-serialize ulang. Ini rapuh karena urutan key, spasi, atau encoding bisa berubah. Signature harus dihitung dari representasi mentah yang disepakati.

Error schema yang stabil

Klien butuh error yang bisa diotomasi, bukan hanya string bebas. Kontrak error sebaiknya punya bentuk stabil seperti:

{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "field amount must be a positive integer",
    "details": [
      {"field": "amount", "reason": "must be positive"}
    ],
    "request_id": "req_01HXYZ..."
  }
}

Audit beberapa hal penting:

  • Apakah ada code yang stabil untuk mesin?
  • Apakah message aman ditampilkan ke manusia?
  • Apakah validation error menyebut field yang salah?
  • Apakah semua error membawa request ID?

Hindari mencampur error bisnis, validasi, dan kegagalan infrastruktur dalam satu format yang tidak konsisten.

Status code yang konsisten

Status code adalah bagian dari kontrak, bukan detail internal. Audit pemetaannya:

  • 400 untuk request tidak valid secara sintaks atau kontrak.
  • 401 untuk tidak terautentikasi.
  • 403 untuk terautentikasi tetapi tidak berwenang.
  • 404 bila resource memang tidak ditemukan atau sengaja disamarkan.
  • 409 untuk konflik state, termasuk bentrok idempotency tertentu.
  • 415 untuk media type yang tidak didukung.
  • 422 jika tim Anda membedakan validasi semantik dari kesalahan sintaks.
  • 429 untuk rate limit.
  • 5xx hanya untuk kesalahan server yang memang bukan salah klien.

Anti-pattern yang sangat umum: semua error bisnis dibungkus menjadi 200 OK dengan field success: false. Ini merusak retry policy, caching, dashboard operasional, dan perilaku SDK.

Timeout, cancellation, dan retry

Audit kontrak waktu secara eksplisit:

  • Berapa target timeout server untuk endpoint sinkron?
  • Apakah ada operasi yang sebaiknya asinkron?
  • Apa yang terjadi jika klien membatalkan koneksi di tengah proses?
  • Response mana yang aman di-retry otomatis?

Untuk operasi lama, lebih aman mengembalikan token pekerjaan dan menyediakan endpoint status daripada menahan koneksi terlalu lama. Retry tanpa batas pada endpoint non-idempotent adalah resep untuk efek samping ganda.

Observability: log, metrik, tracing

Kontrak yang baik harus menghasilkan sinyal operasional yang baik. Minimal, audit:

  • request_id atau correlation ID pada request dan response.
  • Log penolakan validasi berikut alasan ringkasnya.
  • Metrik per endpoint: latensi, error rate, ukuran payload, rate limit hit.
  • Pencatatan retry, timeout, dan duplicate idempotency key.
  • Trace antar gateway, worker, dan downstream service.

Tujuannya sederhana: jika partner berkata, “request kami gagal,” tim Anda bisa menjawab dengan bukti, bukan menebak.

Contoh kasus nyata yang sering lolos review

Kasus 1: parser longgar menerima angka sebagai string

Endpoint pembayaran menerima {"amount":"150000"} dan diam-diam mengonversinya ke integer. Pada awalnya terlihat membantu, tetapi masalah muncul ketika klien lain mengirim "150000.00", "0150000", atau format lokal tertentu. Sebagian request diterima, sebagian dibulatkan, sebagian gagal di downstream.

Perbaikan: tetapkan tipe tunggal, tolak bentuk lain, dan kembalikan error validasi yang jelas.

Kasus 2: path yang berbeda dianggap resource yang sama oleh satu lapisan, berbeda oleh lapisan lain

Gateway melakukan auth berdasarkan /v1/orders/123, tetapi aplikasi melakukan routing setelah normalisasi slash atau decode tertentu. Penyerang atau klien buggy bisa memicu perilaku yang tidak diduga. Ini bukan hanya isu keamanan; integrasi normal pun bisa salah target resource.

Perbaikan: dokumentasikan canonical path, normalisasi di satu titik, lalu verifikasi auth dan signature berdasarkan bentuk canonical yang sama.

Kasus 3: webhook gagal diverifikasi karena body diubah middleware

Penerima webhook memakai parser JSON yang mengubah representasi body sebelum verifikasi HMAC. Signature cocok di lingkungan uji tertentu tetapi gagal acak di produksi karena perbedaan whitespace, escaping, atau pipeline middleware.

Perbaikan: simpan body mentah untuk verifikasi, baru parse setelah signature lolos.

Kasus 4: semua error dikembalikan sebagai 200

SDK klien menganggap request sukses karena status code 200, lalu meneruskan alur bisnis seolah transaksi berhasil. Error sebenarnya tersembunyi dalam field status di body.

Perbaikan: gunakan status code HTTP yang sesuai dan skema error yang stabil.

Anti-pattern yang perlu dicari saat audit

  • Menerima field tambahan tanpa dokumentasi dan tanpa logging.
  • Mengandalkan default framework untuk parsing tanpa menguji kasus ambigu.
  • Mencampur otorisasi di gateway dan service tanpa sumber kebenaran yang jelas.
  • Menandatangani payload setelah normalisasi yang tidak konsisten antar layanan.
  • Retry otomatis untuk semua 5xx tanpa melihat apakah operasi idempotent.
  • Tidak membedakan timeout upstream, timeout aplikasi, dan pembatalan dari klien.
  • Response sukses yang strukturnya berbeda-beda untuk kasus mirip.
  • Tidak punya negative test untuk input yang valid secara teknis tetapi aneh bentuknya.

Langkah audit yang bisa dipakai tim backend

1. Inventaris kontrak aktual, bukan hanya dokumen

Kumpulkan spesifikasi endpoint, contoh request/response, aturan gateway, middleware proxy, aturan auth, dan implementasi validasi. Sering kali kontrak nyata tersebar di beberapa tempat dan tidak identik.

2. Definisikan matriks perilaku

Untuk tiap endpoint, tulis tabel sederhana:

  • Input valid minimal.
  • Input valid maksimal.
  • Input ambigu yang harus ditolak.
  • Status code per kelas error.
  • Apakah endpoint idempotent.
  • Apakah aman di-retry.
  • Batas ukuran dan timeout.

3. Uji canonicalization end-to-end

Jangan hanya menguji handler aplikasi. Uji jalur lengkap lewat CDN, load balancer, gateway, dan service. Bandingkan bagaimana tiap lapisan melihat:

  • Path mentah vs path ter-decode.
  • Header duplikat.
  • Host dan forwarded headers.
  • Body mentah untuk signature.

4. Tambahkan contract test dan negative test

Contract test memastikan response tetap sesuai skema. Negative test memastikan request yang aneh ditolak secara konsisten. Ini penting untuk mencegah regresi saat ada perubahan middleware atau upgrade infrastruktur.

# Contoh ide pengujian dengan curl
curl -i https://api.contoh.test/orders \
  -H 'Content-Type: text/plain' \
  --data '{"customer_id":"c_123"}'

# Harus gagal jelas, bukan diam-diam diparse sebagai JSON

5. Audit retry dan duplikasi secara sengaja

Simulasikan timeout setelah server memulai proses, lalu kirim ulang request dengan idempotency key yang sama. Verifikasi bahwa hasilnya satu efek samping, bukan dua.

6. Verifikasi observability sebelum go-live

Sebelum membuka integrasi ke pihak ketiga, pastikan support engineer bisa menjawab tiga pertanyaan ini dalam hitungan menit:

  1. Request mana yang gagal?
  2. Mengapa gagal menurut kontrak?
  3. Apakah klien aman untuk retry?

Contoh checklist singkat untuk review internal

  1. Apakah semua endpoint punya skema request dan response yang diuji?
  2. Apakah field tambahan ditolak atau diperlakukan konsisten?
  3. Apakah canonicalization path, query, dan header sudah didefinisikan?
  4. Apakah content-type yang diterima terbatas dan tervalidasi?
  5. Apakah batas ukuran body, header, dan payload terdokumentasi?
  6. Apakah status code konsisten dan tidak menyamarkan error menjadi 200?
  7. Apakah endpoint mutasi mendukung idempotency key bila perlu?
  8. Apakah retry policy aman dan terdokumentasi?
  9. Apakah webhook memakai signature atas body mentah dan replay protection?
  10. Apakah timeout, cancellation, dan perilaku async jelas?
  11. Apakah correlation ID, log validasi, metrik, dan tracing tersedia?
  12. Apakah jalur akses langsung ke service yang melewati auth boundary sudah ditutup?

Penutup

Audit kontrak API pada dasarnya adalah upaya menyempitkan ruang ambiguitas. Semakin sedikit request “aneh tapi valid” yang dibiarkan lolos, semakin kecil peluang integrasi gagal secara diam-diam atau berbeda tafsir antar lapisan. Targetnya bukan membuat API kaku tanpa alasan, tetapi memastikan setiap kegagalan terjadi dengan cara yang aman, konsisten, dan terlihat.

Sebelum membuka integrasi ke pihak ketiga, uji bukan hanya skenario ideal, tetapi juga input yang ambigu, header yang tidak biasa, retry, timeout, dan payload yang melanggar asumsi. Jika sistem Anda mampu menolak dengan jelas, memberi error yang stabil, dan meninggalkan jejak observability yang baik, maka masalah integrasi akan jauh lebih mudah dikendalikan.