Kontrak idempoten untuk webhook dan API retry memastikan bahwa setiap event hanya diproses sekali secara semantik, meskipun dikirim ulang karena timeout atau kegagalan jaringan. Artikel ini langsung menjawab bagaimana menyusun kontrak tersebut: kombinasikan penanda unik, status acknowledgement, signature, dan observabilitas operasional agar retry tetap konsisten.
Masalah Duplikasi dan Retry Tidak Terduga
Server penerima webhook sering menerima payload yang sama lebih dari sekali. Retry mekanisme HTTP (misalnya 5xx response) memicu pengiriman ulang, dan jika tidak ada logika idempoten, data bisa tercatat dua kali atau menyebabkan aksi berulang. Disparitas waktu juga bisa menghasilkan out-of-order processing di sistem downstream, membuat state konsisten sulit dipertahankan.
Menetapkan Kontrak Idempoten untuk Webhook
Kunci kontrak idempoten adalah kesepakatan teknis antara pemberi event dan penerima webhook. Kontrak ini perlu mencakup format payload, penanda unik, acknowledgement status, dan informasi retry.
Penanda Unik dan Status Acknowledgement
Setiap event harus membawa penanda unik yang benar-benar sistemik, bukan sekadar timestamp atau incremental counter. Misalnya, gabungan {event_type, resource_id, timestamp_event} bisa dibungkus dalam event_id yang di-hash. Penerima webhook mencatat event_id ke storage mode persistensi ringan (Redis, Cassandra, atau persistence log) saat pertama kali berhasil diproses.
Status acknowledgement perlu di-ekspos agar pengirim tahu kapan boleh menghentikan retry. Kontrak sederhana dapat mensyaratkan response JSON seperti:
{
"event_id": "e8b1f3a2",
"status": "processed",
"ack_timestamp": "2024-10-05T13:45:00Z"
}
Jika penerima belum memproses payload karena validasi gagal, status bisa rejected atau pending dengan pesan error, sehingga pengirim memilih apakah perlu retry atau skip.
Retry API yang Konsisten
Dalam API yang menerima retry, lapisan idempoten dapat berupa middleware yang menggunakan event_id sebagai kunci deduplikasi. Saat event diterima:
- Periksa apakah event_id sudah terproses di tabel deduplication.
- Jika belum, eksekusi bisnis logikanya, lalu simpan event_id bersama hasil (success/failure) sebelum merespons.
- Jika sudah, langsung kembalikan status terakhir tanpa menjalankan logika lagi.
Struktur ini menjamin retry tidak memodifikasi state kedua kali, karena logika bisnis tidak dipanggil ulang. Penyimpanan status juga membantu troubleshooting, karena fase acknowledgement bisa dikembalikan ke pengirim.
Keamanan dan Observabilitas
Kontrak idempoten harus disertai signature untuk memastikan integritas payload. Kontrak bisa menentukan header seperti X-Signature (HMAC-SHA256) dan X-Event-ID. Penerima menghitung ulang signature dari body yang diterima, lalu membandingkannya sebelum memproses.
Observabilitas diperlukan untuk memantau retry yang gagal. Simpan log dengan metadata:
- event_id dan status (processed, pending, failed)
- Timestamps untuk setiap retry
- Header signature yang diterima
Gunakan metric (counter success/failure per event_type, histogram latency ack) untuk memperlihatkan apakah sistem retry bekerja dengan baik, dan alert ketika deduplikasi terlalu sering gagal.
Implementasi Praktis
Contoh berikut menunjukkan middleware Node.js/Express untuk idempoten dan signature. Middleware ini menggunakan store sederhana untuk deduplikasi.
const express = require('express');
const crypto = require('crypto');
const dedupeStore = new Map();
function verifySignature(req, res, next) {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const computed = crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
if (signature !== computed) {
return res.status(401).json({ status: 'rejected', reason: 'signature mismatch' });
}
next();
}
function idempotentHandler(req, res, next) {
const eventId = req.headers['x-event-id'];
if (!eventId) {
return res.status(400).json({ status: 'rejected', reason: 'event_id missing' });
}
const existing = dedupeStore.get(eventId);
if (existing) {
return res.json({ event_id: eventId, status: existing.status });
}
dedupeStore.set(eventId, { status: 'processing' });
req.eventContext = { event_id: eventId };
next();
}
app.post('/webhook', express.raw({ type: '*/*' }), verifySignature, idempotentHandler, (req, res) => {
// business logic
dedupeStore.set(req.eventContext.event_id, { status: 'processed' });
res.json({ event_id: req.eventContext.event_id, status: 'processed' });
});
Catatan: Untuk skala produksi, ganti Map dengan store seperti Redis atau PostgreSQL, dan pastikan deduplikasi memiliki TTL agar tidak menumpuk.
Trade-off dan Debugging
Trade-off: menggunakan deduplikasi menambah latensi karena pengecekan storage. Pastikan store memiliki latency rendah dan gunakan caching bila perlu. Kontrak yang terlalu ketat (misalnya signature + header tambahan) dapat mempersulit integrasi pihak ketiga.
Debugging tips:
- Log retry request lengkap dengan event_id dan timestamp. Periksa apakah event_id berubah setiap retry.
- Gunakan trace ID untuk mengikuti perjalanan event dari penerima ke sistem downstream.
- Periksa status acknowledgement terakhir agar pengirim tahu apakah perlu menghentikan retry.
Dengan kontrak yang jelas, sistem webhook dan retry API akan lebih tahan terhadap duplikasi, out-of-order processing, dan penyalahgunaan. Fokus pada penanda unik, acknowledgement, signature, dan observabilitas operasional adalah inti dari kontrak idempoten yang konsisten.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!