Pengembangan Web

Penerima Webhook yang Andal untuk Aplikasi Kecil — Verifikasi, Catat, Akui, Lalu Proses

Penerima Webhook yang Andal untuk Aplikasi Kecil — Verifikasi, Catat, Akui, Lalu Proses

Endpoint webhook bisa terlihat nyaris sepele: terima HTTP POST, decode JSON, perbarui sebuah record, lalu kembalikan 200. Pertanyaan sulitnya muncul ketika salah satu langkah terputus. Bagaimana jika request itu palsu, provider mengirimnya ulang, process berhenti setelah membalas, atau dua event yang berkaitan tiba dalam urutan yang tidak terduga?

Itu bukan sekadar edge case yang bisa disembunyikan di balik server yang lebih besar. Semua itu merupakan batas antara sistem-sistem independen, dan tidak satu pun pihak dapat mengamati pihak lain secara sempurna. Karena itu, receiver yang andal memerlukan lebih dari sekadar route dan JSON parser. Ia membutuhkan protokol kecil untuk menentukan apa yang dapat dipercaya, apa yang perlu disimpan, kapan harus memberi acknowledgement, dan bagaimana mengulang pekerjaan dengan aman.

Artikel ini menyusun protokol sisi receiver tersebut untuk aplikasi kecil. Pembahasannya tidak mengasumsikan framework atau provider tertentu karena format signature, header, jadwal retry, dan batas timeout berbeda-beda. Tujuannya adalah membuat desain yang dapat diterapkan secara umum, lalu menunjukkan bagian-bagian tempat dokumentasi provider harus mengalahkan saran umum.

Delivery tidak sama dengan pekerjaan yang selesai

Webhook lazim dijelaskan sebagai HTTP callback: satu layanan mengirim event ke endpoint milik layanan lain. Spesifikasi Standard Webhooks menyebutnya semacam “reverse API”. Producer memulai request, sedangkan consumer menerimanya.

Pertukaran HTTP tersebut dan pekerjaan bisnis aplikasi saling berkaitan, tetapi keduanya bukan transaction yang sama. Response 2xx menyatakan bahwa HTTP request berhasil menurut semantik dan kontrak provider yang berlaku. Response itu tidak dapat membuktikan bahwa email kemudian terkirim, akun diperbarui, atau panggilan ke API eksternal akhirnya berhasil. RFC 9110 mendefinisikan semantik HTTP; dokumen itu tidak mengubah sebuah response menjadi jaminan atas background workflow yang tidak teramati.

Perbedaan ini mengarah pada dua tahap terpisah:

  1. Admission: autentikasi request, putuskan apakah request dapat diterima, lalu simpan secara durable.
  2. Processing: terapkan efek bisnis yang dimaksud, ulangi kegagalan yang dapat dipulihkan, dan catat hasilnya.

Menggabungkan kedua tahap dalam satu request panjang mungkin berjalan pada skala sangat kecil, tetapi menciptakan batas kegagalan yang canggung. Jika processing terlalu lama, provider dapat mengalami timeout dan mengirim ulang meski percobaan pertama masih berjalan. GitHub, misalnya, mendokumentasikan batas response sepuluh detik dan merekomendasikan processing asynchronous; angka itu adalah aturan GitHub, bukan timeout universal untuk webhook. Stripe juga meminta receiver untuk mengembalikan response sukses sebelum menjalankan logic yang kompleks.

Urutan receiver: simpan, verifikasi, catat, akui, lalu kerjakan

Receiver yang berguna dapat dijelaskan dengan urutan singkat:

  1. Baca raw request body dan header yang diperlukan tanpa mengubah body.
  2. Verifikasi signature provider, signed timestamp, dan metadata lain yang diwajibkan.
  3. Tolak event type yang tidak didukung atau payload yang jelas tidak valid.
  4. Masukkan inbox record ke durable storage dengan scoped unique key.
  5. Kembalikan success response yang diharapkan provider.
  6. Biarkan worker mengambil inbox record dan menerapkan business operation.
  7. Catat keberhasilan atau kegagalan terkendali yang dapat diulang atau diperiksa.

Urutannya penting. Verifikasi setelah update sudah terlambat. Membalas sebelum durable storage menciptakan celah ketika process dapat mengakui event lalu kehilangan satu-satunya salinan. Menjalankan pekerjaan berat sebelum membalas membuat delivery attempt dari producer bergantung pada latency setiap downstream dependency.

“Durable” tidak selalu berarti message broker yang besar. Untuk aplikasi kecil, inbox table di database yang sama mungkin sudah cukup. Hal pentingnya adalah acknowledgement sukses diberikan setelah record berhasil di-commit, bukan setelah entry hanya tersimpan di process memory. Queue khusus juga dapat menjalankan peran ini jika acknowledgement-nya benar-benar berarti message telah disimpan sesuai jaminan yang dibutuhkan aplikasi.

Verifikasi byte yang benar-benar dikirim

Route webhook publik adalah input publik. HTTPS melindungi koneksi, tetapi tidak dengan sendirinya membuktikan bahwa request berasal dari provider yang diklaim. GitHub dan Stripe sama-sama merekomendasikan webhook secret dan signature verification. Standard Webhooks juga menetapkan message metadata dan payload yang ditandatangani.

Salah satu kesalahan yang mudah terjadi adalah parse JSON lebih dulu, lalu memverifikasi versi yang baru diserialisasi. Whitespace, escaping, dan format key dapat berubah meskipun data hasil parsing memiliki arti yang sama. Baik dokumentasi Stripe maupun spesifikasi Standard Webhooks memperingatkan bahwa verifikasi dapat bergantung pada byte body asli. Aturan amannya adalah mempertahankan raw body sampai verifikasi selesai dan mengikuti konstruksi signed content yang didokumentasikan provider secara persis.

Tidak ada code snippet universal yang bertanggung jawab untuk langkah ini. Setiap provider dapat berbeda dalam nama header, encoding, field yang ditandatangani, secret rotation, dan aturan timestamp. SDK terpelihara dari provider sering kali lebih baik daripada cryptography buatan sendiri. Jika verifikasi manual tidak dapat dihindari, algoritma terbaru dari provider harus diikuti secara persis, termasuk constant-time signature comparison ketika diwajibkan.

Signed timestamp dan identifier yang stabil menyelesaikan masalah berbeda. Freshness window dapat menolak request lama yang ditangkap ulang sehingga mengurangi risiko replay. Event atau delivery ID dapat mengenali request yang sebelumnya sudah diterima aplikasi. Receiver biasanya membutuhkan kedua pemeriksaan itu, dengan identifier yang dibatasi pada provider, account atau endpoint, serta event namespace yang dijelaskan provider tersebut. Menganggap ID dari setiap vendor unik secara global tidak diperlukan dan tidak aman.

Gunakan durable inbox sebagai batas duplicate

Duplicate delivery cukup lazim sehingga GitHub dan Stripe sama-sama mendokumentasikan identifier atau teknik untuk mengenali event berulang. HTTP POST sendiri tidak bersifat idempotent secara inheren menurut RFC 9110. Jika menerima payload yang sama dua kali tidak boleh menagih, menerbitkan, atau melakukan provisioning dua kali, sifat tersebut harus berasal dari aplikasi.

Inbox record minimal dapat memuat:

  • ID record lokal;
  • scope provider dan endpoint atau account;
  • event atau delivery ID dari provider;
  • event type dan timestamp provider;
  • waktu penerimaan dan processing status;
  • attempt count dan ringkasan error yang dibatasi;
  • payload, atau referensi terlindungi menuju payload, hanya jika retensinya beralasan.

Database uniqueness constraint pada scoped provider ID lebih kuat daripada urutan “periksa, lalu insert” dalam application code. Dua delivery concurrent dapat sama-sama lolos dari lookup sebelumnya; hanya storage constraint yang membuat race tersebut selesai pada satu batas. Jalur duplicate biasanya perlu memastikan bahwa event yang sudah ada telah diterima dan mengembalikan success response yang diharapkan provider, bukan menerapkan ulang efek bisnis.

Retensi payload perlu dibatasi. Full payload dapat membantu recovery dan audit, tetapi juga dapat berisi data pribadi atau sensitif. Thin event mungkin membutuhkan API request berikutnya, sedangkan snapshot dapat mempertahankan state seperti saat dikirim. Standard Webhooks membahas tradeoff ini tanpa menyatakan satu bentuk selalu benar. Receiver sebaiknya hanya menyimpan hal yang dibenarkan oleh kebutuhan recovery, audit, dan hukumnya, melindunginya seperti data aplikasi lain, serta menghindari pencatatan secret atau payload lengkap di log rutin.

Idempotent receipt bukan universal exactly-once processing

Inbox dapat memastikan bahwa satu event provider dipetakan ke satu local admission record. Inbox tidak dapat menghapus semua batas kegagalan.

Bayangkan worker membaca event yang sudah diterima, memanggil layanan email atau billing eksternal, lalu berhenti sebelum menandai inbox row sebagai selesai. Saat retry, worker tidak dapat mengetahui hanya dari local row apakah layanan eksternal telah menerapkan panggilan pertama. Karena itu, lapisan berikutnya memerlukan strateginya sendiri: idempotency key yang diterima downstream API, local state transition yang dapat diperiksa secara transactional, atau reconciliation terhadap authoritative system.

Itulah sebabnya “exactly once” terlalu kuat untuk dijanjikan begitu saja. Target yang lebih jujur adalah:

  • terima setiap event teridentifikasi ke dalam satu durable local record;
  • buat setiap business transition aman untuk diulang;
  • deteksi hasil yang masih ambigu;
  • sediakan cara terkendali untuk melakukan reconciliation atau replay.

Beberapa operation secara alami mudah diulang, misalnya mengatur row ke state tertentu. Operation lain, seperti menambah counter atau memanggil sistem tanpa dukungan idempotency, membutuhkan desain tambahan. Event ID dapat menjadi bagian dari desain itu, tetapi sekadar menyimpannya tidak membuat setiap downstream effect menjadi idempotent.

Jangan diam-diam mengasumsikan urutan event

Retry, worker paralel, dan network delay dapat mengubah urutan kedatangan dan penyelesaian. Stripe secara eksplisit mendokumentasikan bahwa event-nya tidak dijamin tiba sesuai urutan pembuatannya. Provider lain mungkin memiliki kontrak berbeda, sehingga dokumentasi mereka tetap menjadi rujukan utama.

Ketika urutan penting, event handler dapat menggunakan resource version, event sequence yang disediakan provider, atau fresh read dari API provider. Tidak ada satu opsi yang otomatis paling baik. Mengambil current state dapat merangkum beberapa event lama menjadi satu langkah reconciliation, tetapi menambah network dependency dan mungkin menghilangkan detail historis. Strict sequence processing mempertahankan riwayat, tetapi memerlukan respons yang sudah ditentukan saat ada event yang hilang. Pilihan yang tepat bergantung pada apakah aplikasi membutuhkan state terbaru, setiap transition, atau keduanya.

Bagian pentingnya adalah membuat asumsi tersebut eksplisit. Handler yang diam-diam menganggap urutan kedatangan sebagai kebenaran mungkin terlihat tepat sampai retry pertama mendahului event yang lebih baru.

Acknowledgement harus cepat, tetapi tidak prematur

“Kembalikan response dengan cepat” kadang ditafsirkan sebagai “balas sebelum menyimpan apa pun.” Itu hanya menukar risiko timeout dengan risiko kehilangan data. Batas yang lebih berguna adalah: jalankan hanya admission work yang terbatas di dalam request, commit hasilnya, lalu kirim response.

Admission sebaiknya menghindari email yang lambat, image processing, API call yang luas, atau business workflow bertahap. Tahap ini masih dapat mencakup signature verification, pemeriksaan schema dan event type dasar, serta satu durable write singkat. Jika write tersebut gagal, mengembalikan sukses akan meminta producer berhenti melakukan retry meskipun receiver tidak menyimpan apa pun. Failure response dapat memicu delivery berikutnya, dan batas deduplication memang dirancang untuk menanganinya.

Penanganan status bergantung pada provider. Standard Webhooks memperlakukan 2xx sebagai delivery success dan menjelaskan response lain sebagai failure, sedangkan setiap provider mendokumentasikan perilaku timeout, redirect, dan retry-nya sendiri. Receiver perlu menerapkan kontrak tersebut secara sengaja. Success response berarti “receiver ini telah menerima tanggung jawab,” belum tentu “semua konsekuensinya sudah selesai.”

Jaga agar failure terlihat dan replay terkendali

Worker asynchronous membuat endpoint responsif, tetapi juga dapat memindahkan kegagalan keluar dari pandangan. Inbox yang berguna membutuhkan observable state seperti pending, processing, completed, dan failed, beserta timestamp dan attempt count yang dibatasi. Record yang tersangkut di processing memerlukan aturan recovery setelah worker crash.

Lakukan retry hanya pada failure yang mungkin membaik seiring waktu. Network error sementara berbeda dari payload dengan event type yang tidak dipahami aplikasi. Retry langsung tanpa batas dapat mengubah satu event bermasalah menjadi load permanen. Backoff, attempt limit, serta dead-letter atau review state menjaga kegagalan tetap terbatas. Spesifikasi Standard Webhooks juga merekomendasikan visibilitas atas failed delivery dan mekanisme manual replay.

Manual replay tidak boleh melewati verification history, uniqueness rule, atau audit record. Mekanisme ini perlu menjawab siapa yang meminta replay, stored event mana yang dipakai, dan apa hasil processing attempt baru. Observability bukan hanya dashboard: aplikasi kecil dapat memulai dengan structured log, query untuk pending row yang terlalu lama, dan alert ketika failure melewati threshold yang bermakna.

Checklist ringkas untuk receiver

  • Sediakan endpoint melalui HTTPS yang valid dan simpan signing secret di luar source code.
  • Pertahankan raw request body dan verifikasi sesuai dokumentasi terbaru provider.
  • Periksa signed timestamp, event type yang diharapkan, ukuran request, dan schema sebelum business processing.
  • Batasi durable uniqueness constraint sesuai semantik identifier provider.
  • Commit event yang diterima sebelum mengembalikan success response milik provider.
  • Pindahkan pekerjaan lambat ke worker dan buat setiap effect aman untuk diulang jika memungkinkan.
  • Jangan mengasumsikan ordering kecuali provider menjaminnya dan desain menegakkannya.
  • Lacak hasil pending, completed, failed, dan ambigu tanpa mencatat secret atau payload data yang tidak diperlukan.
  • Uji event valid, signature tidak valid, timestamp kedaluwarsa, concurrent duplicate, worker crash, dan downstream timeout.
  • Dokumentasikan prosedur retention, replay, secret rotation, dan reconciliation.

Kesimpulan

Receiver webhook yang andal bukan terutama soal menerima JSON, melainkan memilih batas yang jujur. Sebelum batas itu, aplikasi masih menentukan apakah request autentik dan dapat diterima. Setelah durable inbox commit, aplikasi telah menerima tanggung jawab dan dapat memberi acknowledgement kepada producer. Business processing kemudian menjadi workflow yang terlihat dan dapat diulang, bukan pekerjaan tersembunyi di dalam HTTP request.

Desain ini tidak membuat kegagalan sistem terdistribusi menghilang, dan tidak seharusnya dipasarkan sebagai automatic exactly-once processing. Ia melakukan sesuatu yang lebih praktis: menyediakan tempat eksplisit dalam sistem untuk duplicate, crash, delay, dan ambiguous side effect. Untuk aplikasi kecil, satu inbox table dan worker yang dibatasi dengan cermat mungkin sudah cukup. Pertanyaan berikutnya bukan berapa banyak infrastructure yang perlu ditambahkan, melainkan failure mana yang masih tidak dapat dideteksi atau diulang dengan aman oleh aplikasi.

References