Memastikan Idempotensi Webhook Go Fiber

Kontrak API webhook harus tetap konsisten walau klien mengirim ulang event karena timeout atau pemrosesan yang lambat. Untuk itu, pendekatan praktis adalah menjadikan setiap handler Go Fiber idempoten: tidak mengubah state dua kali untuk payload yang sama. Di bawah ini strategi lengkap mencakup definisi schema, autentikasi HMAC, penyimpanan idempotency key, perlakuan duplicate, serta pengujian retry dan observability.

Langkah ini menjawab langsung kebutuhan sistem API yang menerima webhook: mengenali event unik, memverifikasi asalnya, dan mengabaikan pengiriman ulang tanpa mengganggu validitas kontrak.

Mendesain Schema Payload dan Status Transaksional

Schema payload harus memuat identifier unik (misalnya event_id) serta metadata status yang digunakan klien. Buat kontrak JSON seperti:

{
  "event_id": "uuid-v4",
  "type": "payment.confirmed",
  "payload": {...},
  "timestamp": "ISO-8601",
  "idempotency_key": "client-generated"
}

Field idempotency_key bisa sama atau berbeda dari event_id, tetapi pastikan server bisa menggunakannya untuk mengecek apakah event sudah diproses. State transaksi harus disimpan atomik, misalnya menggunakan transaksi database atau queue yang menjamin exactly-once semantics dengan menandai status seperti pending, completed, atau failed.

Middleware Autentikasi Signature/HMAC

Untuk menjaga kontrak, verifikasi bahwa request berasal dari sumber terpercaya. Contoh middleware Go Fiber:

func validateWebhook(c *fiber.Ctx) error {
  secret := os.Getenv("WEBHOOK_SECRET")
  signature := c.Get("X-Signature")
  body := c.Body()

  mac := hmac.New(sha256.New, []byte(secret))
  mac.Write(body)
  expected := hex.EncodeToString(mac.Sum(nil))

  if !hmac.Equal([]byte(signature), []byte(expected)) {
    return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid signature"})
  }
  return c.Next()
}

Pasang middleware ini di router sebelum handler webhook. Dengan validasi HMAC, Anda memfilter payload tidak sah sehingga tidak terjadi perubahan state atau risiko kontrak yang cacat.

Handler Go Fiber dengan Idempotency Key

Handler harus membaca idempotency key lalu memutuskan apakah eksekusi perlu dijalankan. Contoh pattern:

func handleWebhook(c *fiber.Ctx) error {
  var body WebhookPayload
  if err := c.BodyParser(&body); err != nil {
    return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid payload"})
  }

  if existed, err := reserveIdempotencyKey(body.IdempotencyKey); err != nil {
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "storage failure"})
  } else if existed {
    return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "duplicate ignored"})
  }

  if err := processEvent(body); err != nil {
    markIdempotencyStatus(body.IdempotencyKey, "failed")
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "processing failed"})
  }

  markIdempotencyStatus(body.IdempotencyKey, "completed")
  return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "processed"})
}

Function reserveIdempotencyKey harus atomik agar tidak terjadi race. Simpan status saat sukses atau gagal untuk memudahkan troubleshooting.

Pola Penyimpanan Idempotency

Gunakan database relasional, key-value store, atau cache persistensi yang mendukung TTL. Struktur sederhana:

  • idempotency_key: primary key
  • status: pending/completed/failed
  • response: optional response body agar bisa mengembalikan payload yang sama
  • updated_at: timestamp terakhir

Contoh implementasi dengan PostgreSQL:

INSERT INTO webhook_idempotency(key, status)
VALUES($1, 'pending')
ON CONFLICT (key) DO NOTHING
RETURNING status;

-- cek apakah ada row sebelum memproses

Jika row sudah ada dengan status completed, kembalikan ulang respons sebelumnya tanpa memproses ulang. Jika status pending atau failed, sesuaikan logika retry sesuai kebutuhan.

Perlakuan Webhook Duplikat

Setelah idempotency key direkam, handler harus memberikan respons yang konsisten. Kebanyakan klien hanya memerlukan status HTTP 200/202. Kembalikan pesan sederhana untuk mengindikasikan duplicate:

return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "duplicate", "detail": "event already processed"})

Jika sistem memerlukan notifikasi eksplisit, buat mekanisme logging/metrics di layer duplicate agar bisa dimonitor.

Menguji Retry dan Observability

Simulasi retry dengan skrip yang mengirim request identik secara berulang untuk memastikan hanya satu pemrosesan. Bisa memakai curl:

for i in {1..5}; do
  curl -X POST http://localhost:3000/webhook \
    -H "Content-Type: application/json" \
    -H "X-Signature: " \
    -d '@payload.json'
  sleep 0.5
done

Perhatikan log Fiber dan catat ketika idempotency key ditolak. Gunakan monitoring seperti Prometheus + Grafana untuk mencatat meter webhook_processed_total dan webhook_duplicate_total. Logging structured menjelaskan status pemrosesan serta idempotency key sehingga tim operasi bisa menelusuri kasus fail-fast.

Tips Monitoring dan Debugging

  • Log lengkap: Sertakan event_id, idempotency_key, dan stage processing pada setiap log.
  • Expose metrics: Counter untuk sukses, duplikat, dan error membantu mendeteksi pola retry abnormal.
  • Gunakan tracing: Bawa trace ID dari header webhook agar dari klien sampai handler bisa ditelusuri.
  • Terapkan alert: Jika duplicate spike mencapai ambang tertentu, periksa apakah ada delay di downstream yang menyebabkan timeout.

Penutup

Dengan schema payload kuat, autentikasi HMAC, penyimpanan idempotency yang atomik, dan observability lengkap, webhook Go Fiber bisa tetap menjaga kontrak API saat retry terjadi. Pastikan test retry rutin dan monitoring aktif untuk menjamin sistem selalu dalam kondisi konsisten.