Pendahuluan

Webhook banyak digunakan untuk komunikasi event-driven, tetapi rentan terhadap retry klien yang terjadi otomatis. Kontrak webhook terautentikasi di API Route Next.js harus memastikan hanya request sah yang diproses satu kali, sambil memberi sinyal yang jelas agar klien tahu kapan boleh mengulang. Artikel ini menjelaskan validasi signature/token, idempotency key, rotasi secret, dan respons status yang tepat.

Validasi Signature atau Token

Permintaan webhook harus dibuktikan keasliannya sebelum diproses. Di Next.js API Route, pendekatan umum adalah memeriksa header yang membawa HMAC signature atau token bearer.

Struktur validasi

1) Ambil header yang relevan (misalnya X-Signature atau Authorization). 2) Hitung HMAC dari body menggunakan secret yang disimpan aman (environment variable atau secret manager). 3) Bandingkan hasil dengan nilai header secara konstan waktu.

Memakai crypto.timingSafeEqual menghindari serangan timing. Jika menggunakan token, cek apakah token masih valid dan belum kedaluwarsa.

Rotasi secret atau token

Untuk rotasi secret tanpa downtime, simpan beberapa secret aktif (misalnya current dan previous). Saat menerima webhook, coba validasi terhadap semua secret aktif hingga salah satunya cocok. Implementasi token bearer bisa menyertakan key ID agar backend tahu secret mana yang dipakai untuk verifikasi.

Rotasi otomatis menuntut proses penggantian secret di klien webhook sebelum secret lama dicabut sepenuhnya. Log setiap kegagalan validasi supaya Anda bisa memantau klien yang belum diperbarui.

Idempotency Key dan Penyimpanan

Webhooks dengan retry harus idempotent. Klien yang baik akan mengirimkan header seperti Idempotency-Key. Backend harus menyimpan key ini di layer data untuk menolak duplikat.

Desain penyimpanan:

  • Database relasional: tabel webhook_events dengan kolom idempotency_key unik. Simpan status (pending, succeeded, failed).
  • Redis: gunakan SETNX dengan TTL pendek agar key yang sama tidak diproses dua kali.

Alur lengkap:

  1. Ambil key dari header. Jika kosong, log dan tolak dengan 400 karena kontrak mengharuskan key.
  2. Periksa apakah key sudah ada dengan status succeeded atau pending. Jika sudah diproses, short circuit dengan 200 atau 409 sesuai kontrak.
  3. Jika key baru, simpan dengan status pending sebelum memproses payload.
  4. Setelah selesai, update status menjadi succeeded atau failed.

Pastikan operasi penyimpanan dan pengecekan key atomik. Pada Next.js route handler, operasi ini bisa dilakukan dalam satu transaksi database atau dengan set Redis + TTL.

Response Status untuk Memberi Sinyal Retry

Kontrak webhook harus eksplisit menentukan kapan klien boleh retry. Status HTTP yang umum:

  • 2xx: proses berhasil. Klien tidak boleh retry.
  • 4xx: kesalahan klien (misalnya signature invalid). Jangan izinkan retry karena tidak akan berhasil tanpa perbaikan.
  • 5xx atau 429: kesalahan atau overload server. Sinyalkan agar klien boleh retry setelah interval.

Untuk kesalahan sementara (misalnya database down), kembalikan 503 dengan header Retry-After untuk memberi tahu klien kapan mencoba lagi.

Implementasi Route Handler Next.js

Berikut contoh route handler di direktori app/api/webhook/route.ts yang menunjukkan parsing header, validasi signature, logging, dan short-circuit ketika request duplikat terjadi.

import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';import crypto from 'node:crypto';import { getIdempotencyRecord, markAsSucceeded, markAsFailed, lockIdempotentKey } from '@/lib/webhookStore';const SHARED_SECRETS = [process.env.WEBHOOK_SECRET_CURRENT, process.env.WEBHOOK_SECRET_PREVIOUS].filter(Boolean);const SIGNATURE_HEADER = 'x-signature';const IDEMPOTENCY_HEADER = 'x-idempotency-key';const MAX_BODY_BYTES = 1_048_576;export async function POST(req: NextRequest) {  const idempotencyKey = req.headers.get(IDEMPOTENCY_HEADER);  if (!idempotencyKey) {    return NextResponse.json({ message: 'Idempotency key required' }, { status: 400 });  }  const locked = await lockIdempotentKey(idempotencyKey);  if (!locked) {    return NextResponse.json({ message: 'Duplicate request' }, { status: 200 });  }  try {    const rawBody = await req.text();    await verifySignature(req.headers.get(SIGNATURE_HEADER), rawBody);    const record = await getIdempotencyRecord(idempotencyKey);    if (record?.status === 'succeeded') {      return NextResponse.json({ message: 'Already processed' }, { status: 200 });    }    // Proses payload di sini (misalnya enqueue job atau simpan ke DB)    await markAsSucceeded(idempotencyKey);    return NextResponse.json({ message: 'Accepted' }, { status: 200 });  } catch (error) {    await markAsFailed(idempotencyKey);    if (error instanceof SignatureError) {      return NextResponse.json({ message: 'Invalid signature' }, { status: 401 });    }    return NextResponse.json({ message: 'Temporary failure' }, { status: 503, headers: { 'Retry-After': '10' } });  } finally {    // Lepas lock otomatis atau lewat finally di helper    await releaseLock(idempotencyKey);  }}

Fungsi verifySignature mencoba setiap secret dalam SHARED_SECRETS dan menggunakan crypto.timingSafeEqual untuk membandingkan nilai HMAC. Pastikan helper seperti lockIdempotentKey dan markAsSucceeded menangani penyimpanan di data layer (Redis/DB) secara atomic.

Strategi Logging dan Observabilitas

Log setiap langkah penting:

  • Validasi signature gagal (termasuk secret yang digunakan). Catat trace_id atau event_id.
  • Duplikasi idempotency key ditolak.
  • Timeout atau kegagalan data layer (simpan error stack).

Gabungkan dengan metrics seperti:

  • Rate webhook masuk (per endpoint).
  • Persentase failure signature/token.
  • Waktu pemrosesan idempotency.

Checklist Observabilitas dan Edge Case

  1. Logging: Pastikan request ID, idempotency key, dan status signature tercatat.
  2. Monitoring: Pantau 4xx/5xx dan jumlah retry yang diterima.
  3. Timeout: Set timeout untuk validasi token agar client tidak menunggu selamanya.
  4. Rotasi Secret: Verifikasi klien memperbarui secret sesuai jadwal rotasi.
  5. Idempotency Data: Bersihkan record lama (TTL) agar tidak menumpuk.
  6. Edge Case: Tangani body parsing gagal, header hilang, atau IDempotency key reuse setelah TTL.

Dengan kontrak yang eksplisit, Next.js API Route dapat menerima webhook terautentikasi yang tahan retry klien sambil menjaga keamanan dan observabilitas.