Pengenalan Cepat
Untuk menjaga integritas webhook, API route Next.js harus menegakkan kontrak payload, memverifikasi tanda tangan, dan mendukung idempoten serta retry. Artikel ini langsung menunjukkan bagaimana mendesain kontrak JSON, mengamankan auth, menyimpan idempotency key, dan menambahkan observability agar duplikasi bisa terdeteksi.
1. Kontrak JSON untuk Webhook yang Konsisten
Untuk memastikan API bisa menangani retry tanpa ambiguitas, definisikan struktur payload yang tetap, misalnya:
{
"event": "payment.succeeded",
"resource_id": "txn_123",
"timestamp": "2024-10-10T08:32:00Z",
"data": {"amount": 12000, "currency": "IDR"},
"idempotency_key": "evt-abc-123"
}
Gunakan event dan resource_id sebagai bagian kontrak yang tidak boleh berubah, serta sertakan idempotency_key yang unik dari pengirim webhook. Validasi tipe data (string, ISO timestamp, numeric) di awal handler agar failure ada sebelum side effect.
Jika payload tidak sesuai, kirimkan 400 Bad Request dengan detail field yang gagal:
{"error": "invalid_payload", "details": "data.amount harus angka"}Jangan lakukan perubahan state sebelum validasi selesai.
2. Auth dan Verifikasi Tanda Tangan
Untuk mencegah spoofing, webhook harus menyertakan header seperti X-Signature. Di Next.js App Router, periksa signature sebelum memproses payload:
import { NextResponse } from 'next/server';
import { createHmac } from 'crypto';
export async function POST(req) {
const raw = await req.text();
const signature = req.headers.get('x-signature');
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(raw)
.digest('hex');
if (!signature || expected !== signature) {
return NextResponse.json({error: 'unauthorized'}, {status: 401});
}
const payload = JSON.parse(raw);
// lanjutkan ke validasi payload
}
Gunakan raw body agar signature dihitung atas payload persis. Simpan WEBHOOK_SECRET di environment variable dan jangan log secret. Jika signature gagal, kirim 401 Unauthorized untuk menghentikan retry berlebihan.
3. Menyimpan dan Menggunakan Idempotency Key
Setelah signature dan struktur valid, simpan idempotency_key dengan status yang bisa dicek ulang sebelum mengeksekusi side effect. Contoh strategi:
- Simpan record di tabel database
webhook_requestsdengan kolomidempotency_key,status,payload_hash, danprocessed_at. - Gunakan transaksi untuk menulis record awal dengan status pending. Jika key sudah ada, periksa
status—jika completed bisa langsung return200 OKtanpa memproses ulang. - Jika proses gagal, catat
statuserror agar retry berikutnya dapat mencoba ulang.
Contoh logika cek:
const existing = await db.webhookRequests.findUnique({
where: {idempotency_key: payload.idempotency_key}
});
if (existing) {
if (existing.status === 'completed') {
return NextResponse.json({ok: true, duplicate: true});
}
// opsi: tunggu atau kirim kembali untuk retried in progress
}
await db.webhookRequests.create({
data: {idempotency_key: payload.idempotency_key, status: 'processing'}
});
Jangan menjalankan side effect (kirim email, update saldo) sebelum entry dibuat agar retry tidak melewatkan pengecekan.
4. State versus Side Effect
Pisahkan mutasi state dengan aksi berdampak luar (eg. kirim notifikasi). Setelah idempotency key berhasil dicatat, jalankan operasi state (update basis data) lalu lakukan side effect terakhir. Dengan urutan ini, jika side effect gagal, Anda masih bisa mengetahui state dan memutuskan apakah retry perlu dilanjutkan.
Jika side effect tidak dapat di-rollback, rekomendasikan konsumer webhook menunggu dan retry karena API sudah jamin idempoten, atau buat mekanisme compensation (rollback manual) sebelum mengakui pengiriman.
5. Observability: Log dan Metrics untuk Duplikasi
Observability membantu mendeteksi request duplikat dan kegagalan retry. Implementasi minimal:
- Log entry ketika payload diterima, signature diverifikasi, dan saat idempotency key sudah ada. Sertakan
event,resource_id, danidempotency_key. - Gunakan metric counter (misalnya via Prometheus) untuk
webhook_duplicatedanwebhook_processing_error. - Catat latency proses agar bisa memonitor backlog retry.
Contoh log message:
logger.info('webhook.received', {event: payload.event, key: payload.idempotency_key});
logger.warn('webhook.duplicate', {key: payload.idempotency_key});
Jika log menunjukkan banyak duplikat, pertimbangkan untuk menyesuaikan TTL atau retry interval di sisi pengirim.
6. Ringkasan Respons dan Tips Debug
Pastikan response API konsisten:
- 200 OK dengan body
{"ok": true, "duplicate": false}saat status baru selesai. - 200 OK dengan
duplicate: trueuntuk request yang sudah diproses. - 400 Bad Request dengan penjelasan validasi field.
- 401 Unauthorized jika tanda tangan tidak cocok.
Untuk debugging, rekam request_id dan idempotency key. Jika retry stuck, periksa database entry status dan gunakan trace ID di log untuk mengikuti perjalanan request.
Dengan pendekatan ini, handler Next.js mampu bertahan terhadap retry agresif, menjaga keamanan, dan memberikan observability yang dibutuhkan ketika konsumen webhook mencoba kembali mengirim data.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!