Webhook retry dapat memperkenalkan kompleksitas karena sender bisa mencoba kembali permintaan yang sama beberapa kali. Artikel ini langsung menjelaskan bagaimana membangun API Route Next.js (App Router) yang menjaga kontrak login, memvalidasi signature/token, memanfaatkan idempotency key, dan mengamankan retry dengan strategi backoff dan observabilitas yang jelas.

1. Menentukan Kontrak API Webhook

Tujuan utama dari sender dan konsumen webhook adalah menyepakati kontrak request/response yang eksplisit. Untuk API Route Next.js, kontrak ini terdiri dari:

  • Schema payload: Gunakan tipe data yang terdefinisi (misalnya JSON dengan schema yang bisa divalidasi melalui Zod atau Yup). Sertakan field penting seperti eventType, timestamp, dan data.
  • Header otentikasi: Tentukan header tetap seperti X-Sender-Signature atau Authorization: Bearer <token>. Kontrak harus menyebutkan algoritme hashing, secret, dan urutan validasi.
  • Idempotensi: Mintalah sender menyertakan header X-Idempotency-Key. Nilai ini harus unik per event, dan App Router menyimpannya selama waktu tertentu untuk mencegah pemrosesan ulang.
  • Response status: Tetapkan bahwa 2xx berarti sukses, 4xx berarti error permanen (tidak di-retry), 5xx berarti akan di-retry (atau sesuai ketentuan sender) agar sistem dapat mendeteksi dan mendokumentasikan penyebab penolakan.

Memiliki dokumentasi kontrak ini memudahkan integrasi rangkaian webhook, membuat pengujian lebih mudah, dan juga membantu dalam debugging karena ada ekspektasi yang jelas.

2. Memvalidasi Signature dan Token di App Router

Pengamanan webhook memerlukan verifikasi signature atau token di middleware sebelum request mencapai handler. Di Next.js App Router, middleware dapat memeriksa header dan menyimpulkan apakah request dapat diproses.

Contoh middleware sederhana untuk memvalidasi signature dan menyimpan idempotency key:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifySignature, cacheIdempotencyKey } from '@/lib/webhook-utils';

export async function middleware(req: NextRequest) {
  const signature = req.headers.get('x-sender-signature');
  const idempotencyKey = req.headers.get('x-idempotency-key');

  if (!signature || !idempotencyKey) {
    return NextResponse.json({ error: 'Missing headers' }, { status: 400 });
  }

  const body = await req.text();
  const valid = verifySignature(body, signature);
  if (!valid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const lockAcquired = await cacheIdempotencyKey(idempotencyKey);
  if (!lockAcquired) {
    return NextResponse.json({ error: 'Duplicate or locked request' }, { status: 409 });
  }

  return NextResponse.next();
}

Fungsi verifySignature melakukan hashing body sesuai kontrak, sedangkan cacheIdempotencyKey mengunci kunci unik di Redis atau penyimpanan lain. Middleware ini memastikan request yang tidak valid atau duplikat tidak sampai ke logika bisnis.

3. Handler App Router: Validasi dan Logging

Setelah middleware, handler bisa fokus pada logika event. Masukan schema validasi agar payload mengikuti kontrak. Contoh handler di app/api/webhook/route.ts:

import { NextResponse } from 'next/server';
import { z } from 'zod';
import { recordRetryEvent, releaseIdempotencyKey } from '@/lib/webhook-utils';

const webhookSchema = z.object({
  eventType: z.string(),
  timestamp: z.string(),
  data: z.record(z.any()),
});

export async function POST(request: Request) {
  const payload = await request.json();
  const parsed = webhookSchema.safeParse(payload);
  if (!parsed.success) {
    return NextResponse.json({ error: 'Invalid payload' }, { status: 422 });
  }

  try {
    await handleEvent(parsed.data);
    return NextResponse.json({ status: 'ok' });
  } catch (error) {
    await recordRetryEvent({
      idempotencyKey: request.headers.get('x-idempotency-key') ?? 'unknown',
      error: (error as Error).message,
    });
    await releaseIdempotencyKey(request.headers.get('x-idempotency-key') ?? '');
    return NextResponse.json({ error: 'processing failed' }, { status: 500 });
  }
}

Catatan penting: jangan lupa melepas kunci idempotensi jika pemrosesan gagal sehingga retry bisa mencoba lagi. Simpan log event retry agar tim operasi bisa mengaudit dan memetakan pola kegagalan.

4. Strategi Backoff, Lock, dan Observabilitas

Sender webhook biasanya menerapkan retry dengan backoff eksponensial. Di sisi penerima, tanggapi dengan status yang sesuai (500 untuk retry, 400 untuk error permanen). Untuk menghindari duplikasi akibat retry cepat, kunci idempotensi harus memegang lock minimal selama waktu pemrosesan plus margin timeout.

Strategi observabilitas:

  • Log level: Catat setiap request dengan status, signature, idempotency key, dan hasil validasi.
  • Tracing: Sertakan trace ID dalam header response agar mudah dikorelasi ke sistem observasi (misalnya OpenTelemetry).
  • Metrics retry: Ukur jumlah retry sukses vs gagal, durasi pemrosesan per jenis event, dan average backoff duration.
  • Alerting: Buat alert jika jumlah retry gagal dalam window tertentu melebihi ambang batas.

Dengan observabilitas, Anda dapat memisahkan apakah kegagalan karena payload yang tidak sesuai konrak, signature tidak valid, atau sistem back-end yang tertutup.

5. Debugging dan Hal yang Sering Terjadi

Kegagalan Validasi Signature

Periksa canonicalization body. Pastikan body di middleware sama persis dengan yang ditandatangani. Gunakan logging untuk menampilkan hash yang dihitung dan bandingkan dengan header.

Duplikasi Karena Idempotency Key Belum Terhapus

Jika sistem tidak melepaskan kunci saat terjadi error, retry akan cepat ditolak. Terapkan mekanisme otomatis untuk menghapus kunci setelah timeout tertentu di cache atau gunakan TTL yang lebih pendek.

Retry Tanpa Backoff

Sender harus menerapkan backoff. Jika Anda mengontrol sender, dokumentasikan interval, misalnya 1s, 2s, 5s. Jika tidak, pastikan sistem Anda tidak overload dengan menolak dengan 429 saat batas throughput tercapai.

Kesimpulan

Pengelolaan webhook retry di Next.js App Router mengharuskan kontrak request/response yang konsisten, validasi signature/token, penguncian idempotensi, dan observabilitas. Dengan middleware yang memasukkan semua lapisan tersebut, handler dapat fokus pada logika bisnis tanpa dibebani duplikasi atau data tidak valid. Pemantauan retry dan strategi backoff yang terkoordinasi menjamin system Anda tangguh menghadapi sender yang agresif sekalipun.