Pengembangan Web

Idempotency Key untuk API PHP Kecil - Ulangi Operasinya, Bukan Side Effect-nya

Idempotency Key untuk API PHP Kecil - Ulangi Operasinya, Bukan Side Effect-nya

Sebuah client mengirim POST /orders. Server mungkin sudah melakukan commit untuk order tersebut, tetapi response menghilang saat terjadi timeout. Haruskah client mencoba lagi? Jika iya, ia mungkin membuat order kedua. Jika tidak, pengguna bisa saja tidak mendapatkan order sama sekali.

Ini bukan sekadar pertanyaan tentang sintaks retry. Ini adalah pertanyaan tentang identitas: bagaimana client dapat mengatakan, “ini adalah percobaan lain untuk operasi yang sama,” dan bagaimana server dapat mengingat keputusan itu di tengah request bersamaan dan restart proses?

Idempotency key adalah salah satu jawabannya. Key ini dapat membuat retry yang tidak pasti menjadi lebih mudah dikelola, tetapi header saja tidak menjamin apa pun. Jaminan yang berguna berasal dari kontrak yang terdokumentasi, penyimpanan otoritatif, dan batas atomic yang mencakup key serta perubahan bisnis.

Idempotency HTTP dan idempotency operasi berada di layer yang berbeda

RFC 9110 mendefinisikan HTTP method sebagai idempotent ketika beberapa request identik memiliki intended effect yang sama dengan satu request. PUT dan DELETE memiliki semantik idempotent; POST tidak mendapatkan jaminan umum tersebut. Ini bukan berarti method idempotent tidak memiliki efek tambahan. Server dapat mencatat setiap request, dan response berulang tidak harus identik byte demi byte.

Aplikasi tetap dapat memberi kontrak retry kepada operasi POST tertentu. Client membuat identifier operasi yang unik dan mengirimkannya pada setiap percobaan. Server memakai identifier itu untuk membedakan retry dari niat baru:

POST /orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: "8e03978e-40d5-43e8-bc93-6894a57f9324"

{"product_id":42,"quantity":1}

Sintaks field dengan tanda kutip di atas mengikuti proposal saat ini dalam draft Idempotency-Key milik IETF HTTPAPI Working Group. Per 3 November 2026, dokumen tersebut adalah draft 08 yang diterbitkan pada 22 Oktober 2026. Dokumen itu masih berupa Internet-Draft aktif, bukan RFC final. Sebuah API dapat memakai pola ini sekarang, tetapi tidak seharusnya menyajikan setiap detail draft sebagai aturan protokol yang sudah tetap.

API memerlukan kontrak, bukan sekadar nama header

Client tidak dapat menyimpulkan dengan aman apa yang dilakukan server yang belum dikenal terhadap Idempotency-Key. Dokumentasi API setidaknya perlu menjawab pertanyaan berikut:

  • Operasi mana yang menerima atau mewajibkan key?
  • Format dan panjang maksimum apa yang valid?
  • Dalam scope apa key harus unik: pengguna, API credential, tenant, endpoint, atau yang lain?
  • Berapa lama hasil disimpan?
  • Apa yang terjadi jika key yang sama datang dengan payload berbeda?
  • Apa yang terjadi ketika percobaan pertama masih diproses?
  • Apakah retry yang selesai menerima response tersimpan atau response ekuivalen yang dibuat ulang?
  • Kegagalan mana yang boleh dicoba ulang oleh client, dan dengan jeda seperti apa?

Pilihan ini tidak universal. Stripe mendokumentasikan perbandingan parameter, penyimpanan response, dan perilaku pruning untuk API miliknya. Adyen mendokumentasikan scope akun, interval retensi, batasan regional, dan perilaku transient error. Kebijakan mereka menunjukkan desain yang telah digunakan; kebijakan itu bukan default yang harus disalin oleh setiap API PHP kecil.

State machine kecil membuat perilakunya terlihat

Satu scoped key dapat dimodelkan dengan tiga state yang relevan:

  1. Absent: belum ada operasi yang cocok dan telah diklaim. Satu request boleh mengklaimnya dan memulai pekerjaan.
  2. Processing: sebuah percobaan memiliki key tersebut, tetapi hasil final belum tersedia. Duplikat yang datang bersamaan tidak boleh memulai operasi bisnis lagi.
  3. Completed: operasi sudah memiliki outcome yang tercatat. Retry berikutnya menerima outcome tersimpan atau ekuivalen secara semantik tanpa mengulang pekerjaan.

Ada pula transisi yang tidak valid: key sudah ada, tetapi request baru menjelaskan pekerjaan yang berbeda. Itu bukan retry. Itu adalah niat yang bertentangan dan perlu ditolak dengan jelas.

Tabel konseptual dapat berisi identifier tenant atau principal, nama endpoint atau operasi, key, request fingerprint, state, metadata response, timestamp, dan waktu kedaluwarsa. Aturan database yang penting adalah unique constraint atas scope dan key yang dipilih. Urutan “periksa, lalu insert” di PHP tidak cukup: dua worker dapat sama-sama melihat bahwa data belum ada sebelum salah satunya melakukan insert. Database harus menentukan request mana yang menang.

Key dan business write memerlukan satu batas atomic

Celah paling berbahaya muncul ketika aplikasi hanya mencatat separuh kebenaran:

  • Jika aplikasi mencatat “completed” sebelum membuat order, crash dapat menekan operasi yang sebenarnya belum pernah terjadi.
  • Jika aplikasi membuat order sebelum mencatat key, crash dapat membuat retry menghasilkan order lain.

Ketika record idempotency dan record bisnis berada di database relasional yang sama, satu transaction dapat mengklaim key, menulis order, dan mencatat outcome sebagai satu commit. Unique constraint menyelesaikan klaim yang bersamaan. Ini adalah kasus terbatas ketika transaction lokal memberi batas yang bersih.

Batas tersebut tidak secara ajaib mencakup layanan email, payment provider, atau database lain. Rollback MariaDB tidak dapat membatalkan charge eksternal yang sudah berhasil. Workflow seperti ini membutuhkan desain tambahan: fitur idempotency milik provider di hilir, transactional outbox, state machine yang dapat dilanjutkan, atau reconciliation terhadap ID operasi eksternal yang stabil. Menyebut endpoint luar sebagai “idempotent” tidak boleh menyembunyikan celah kegagalan yang masih ada.

Catatan AWS tentang desain API idempotent menyampaikan hal yang berkaitan: pencatatan request token dan pekerjaan yang mengubah data harus memenuhi batas all-or-nothing. Sumber tersebut juga menjelaskan mengapa identifier dari caller sering lebih jelas daripada menebak dari parameter yang sama. Dua request dengan field identik bisa merupakan dua order yang disengaja, bukan satu order yang terduplikasi.

Gunakan fingerprint untuk mendeteksi perubahan niat

Client key menyatakan identitas. Fingerprint yang dibuat server memeriksa konsistensi. Sebagai contoh, server dapat melakukan canonicalization atas method, route, authenticated principal, dan field tervalidasi yang relevan, lalu menyimpan digest. Request berikutnya dengan scoped key yang sama harus cocok dengan fingerprint tersebut.

Pembuatan fingerprint bergantung pada aplikasinya. Melakukan hash atas byte JSON mentah itu sederhana, tetapi whitespace yang tidak penting atau urutan member object dapat membuat dokumen ekuivalen terlihat berbeda. Melakukan hash terhadap nilai tervalidasi yang dipilih menghindari masalah itu, tetapi field yang dipilih harus mencakup semua hal yang mengubah intended operation. Perbandingan juga harus dilakukan setelah API menetapkan authenticated scope; jika tidak, key seorang pengguna dapat bertabrakan dengan atau membocorkan hasil pengguna lain.

Draft IETF saat ini menyarankan penolakan terhadap penggunaan ulang dengan payload berbeda dan membahas 422 Unprocessable Content. Draft itu menyarankan 409 Conflict ketika request awal masih berlangsung. Ini adalah rekomendasi draft yang berguna, bukan izin untuk menghilangkan dokumentasi khusus API.

Tentukan apa yang dikembalikan oleh retry yang sudah selesai

Satu implementasi dapat menyimpan status pertama, header tertentu, dan response body. Implementasi lain dapat menyimpan ID resource yang dibuat lalu menyusun ulang response yang ekuivalen secara semantik. Kedua pendekatan memiliki tradeoff.

Penyimpanan response secara persis memberi replay yang mudah diprediksi, termasuk kegagalan sebelumnya jika itu memang kebijakan yang didokumentasikan. Cara ini juga memakan ruang dan dapat mempertahankan response yang basi atau sensitif. Penyusunan ulang menyimpan lebih sedikit data, tetapi representasinya dapat berubah setelah resource diubah atau dihapus. Karena itu, janji yang tepat tidak otomatis berupa “byte yang sama.” Janjinya bisa berupa “operasi yang sama tidak dijalankan lagi, dan retry menerima outcome yang konsisten dengan niat awal.”

Retensi menentukan berapa lama janji tersebut bertahan. Setelah kedaluwarsa, server mungkin tidak lagi mengenali key itu. Rentangnya perlu mencakup retry dan request tertunda yang masuk akal sambil tetap memperhatikan penyimpanan, privasi, dan batasan domain. Rentang itu perlu dinyatakan secara eksplisit, bukan diwarisi dari contoh vendor.

Idempotency bukan kebijakan retry lengkap atau security boundary

Client tetap memerlukan aturan retry yang konservatif. Client hanya boleh memakai ulang key yang sama untuk intended operation yang sama, menerapkan time limit, mengikuti panduan server seperti Retry-After ketika relevan, dan menggunakan backoff alih-alih menciptakan retry storm. Key baru berarti operasi baru.

Server perlu memvalidasi sintaks dan panjang key sebelum menyimpannya, melakukan rate limiting terhadap client yang abusif, dan membatasi lookup ke authenticated principal. Key sebaiknya berupa nilai opaque dengan entropy tinggi, bukan alamat email atau data pribadi lainnya. Idempotency key bukan authentication, authorization, perlindungan CSRF, input validation, atau pengganti database transaction.

Observability juga penting. Log dapat menyertakan hash key yang ditangani dengan aman, state operasi, resource ID, jumlah percobaan, dan alasan konflik. Metric untuk payload mismatch, duplikat bersamaan, penggunaan ulang key yang kedaluwarsa, dan state processing yang terlalu lama dapat mengungkap bug client serta operasi macet tanpa menganggap setiap retry sebagai error.

Ketika desain lain lebih sederhana

Tidak setiap POST membutuhkan idempotency store. Identifier bisnis yang secara alami unik mungkin sudah mencegah duplikat. URI resource yang dipilih client dengan PUT dapat menyatakan semantik replacement dengan lebih langsung. Operasi read-only tidak memerlukan mekanisme ini. Untuk tindakan berdampak rendah yang duplikatnya tidak berbahaya, biaya state, retensi, dan cleanup mungkin lebih besar daripada manfaatnya.

Pola ini sepadan dengan kompleksitasnya ketika outcome dapat menjadi ambigu dan pengulangan side effect mahal: membuat order, menjadwalkan job, melakukan provisioning resource, atau memulai tindakan eksternal. Meski demikian, implementasi hanya boleh menjanjikan apa yang dapat ditegakkannya dalam scope dan rentang retensi yang disebutkan.

Checklist review yang praktis

  • Definisikan satu intended operation dan authenticated scope dari key-nya.
  • Validasi format key yang terbatas dan opaque, lalu tolak payload yang berubah.
  • Tegakkan keunikan key di database otoritatif, bukan hanya di PHP.
  • Tempatkan klaim key dan perubahan bisnis lokal dalam satu transaction jika memungkinkan.
  • Rancang perilaku untuk duplikat bersamaan, crash, dan external side effect secara eksplisit.
  • Dokumentasikan perilaku completed response, error yang dapat dicoba ulang, retensi, dan kedaluwarsa.
  • Uji response yang hilang, dua request bersamaan, payload berubah, process crash, dan retry setelah kedaluwarsa.
  • Pantau state processing yang macet dan lakukan reconciliation untuk outcome yang melintasi batas sistem.

Kesimpulan

Idempotency key tidak membuat delivery terjadi exactly once. Key ini memberi client cara untuk menamai satu niat dan memberi server cara untuk mengenali percobaan lain. Keamanan sebenarnya berasal dari bagaimana nama itu diberi scope, diklaim, dibandingkan, disimpan, dan dihubungkan dengan business write.

Untuk API PHP kecil, titik awal yang paling berguna cukup sederhana: kontrak yang jelas, key unik yang ditegakkan database, request fingerprint, state machine yang eksplisit, dan satu transaction untuk efek lokal. Begitu operasi masuk ke sistem lain, ketidakpastian yang tersisa sebaiknya ditampilkan dan dikelola, bukan disembunyikan di balik header yang terdengar meyakinkan.

References