Untuk memastikan kontrak webhook Laravel tetap konsisten, tim harus mendefinisikan schema request/response secara eksplisit sekaligus menetapkan aturan idempoten agar retry tidak merusak data. Pendekatan ini memastikan konsumen bisa mempercayai struktur payload dan respons, sekaligus memberi tim keamanan operasional saat webhook gagal.
Artikel ini membahas langkah praktis mulai dari definisi schema valid, validasi middleware atau FormRequest, implementasi idempoten di controller/job, hingga strategi error handling dan fallback agar setiap retry dapat diidentifikasi dan ditangani tanpa duplikasi.
Menentukan Schema Kontrak Webhook
Langkah pertama adalah mendokumentasikan schema request dan response yang diharapkan. Gunakan format JSON Schema atau deskripsi OpenAPI agar pengirim bisa memvalidasi payload sebelum dikirim. Sertakan field yang wajib, tipe data, serta batasan tersendiri (misalnya maksimal panjang string atau enumerasi status). Dokumentasi harus juga menyertakan contoh header penting seperti X-Signature dan X-Request-Id.
Gunakan alat seperti json-schema validator atau skrip sederhana untuk memverifikasi payload di sisi pengirim dan penerima. Pastikan kontrak mencakup:
- Field wajib dan opsional beserta tipe data mereka.
- Respons HTTP yang harus dikembalikan (misalnya 2xx untuk sukses, 4xx/5xx untuk kesalahan) dan pesan body standar.
- Header yang wajib disertakan, seperti
Content-Type,X-Signature, dan idempotensi key.
Tanpa dokumentasi schema seperti ini, perubahan kecil bisa merusak integrasi. Gunakan versi kontrak apabila perlu, supaya klien bisa tahu kapan mereka harus migrasi.
Validasi Request dengan Middleware atau FormRequest
Setelah schema jelas, implementasikan validasi di Laravel. FormRequest cocok untuk validasi payload yang komplek karena memberi tempat terpusat untuk rules dan authorize logic.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class IncomingWebhookRequest extends FormRequest
{
public function rules(): array
{
return [
'event' => 'required|string',
'payload.id' => 'required|uuid',
'payload.status' => 'required|in:created,updated',
'metadata.timestamp' => 'required|date_iso8601',
];
}
public function wantsJson(): bool
{
return true;
}
}
Jika perlu memeriksa header khusus seperti signature atau idempotensi key, gunakan middleware untuk memisahkan logika tersebut:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class VerifyWebhookSignature
{
public function handle(Request $request, Closure $next)
{
$signature = $request->header('X-Signature');
if (! $signature || ! hash_equals($this->expectedSignature($request), $signature)) {
return response()->json(['message' => 'Signature invalid'], 403);
}
if (! $request->hasHeader('X-Request-Id')) {
return response()->json(['message' => 'Missing request id'], 400);
}
return $next($request);
}
private function expectedSignature(Request $request): string
{
return hash_hmac('sha256', $request->getContent(), config('webhook.secret'));
}
}
Letakkan middleware ini di grup route webhook agar semua entry point memeriksa signature dan header kritikal terlebih dahulu. Kombinasikan dengan FormRequest agar body juga tervalidasi.
Menerapkan Idempotensi di Controller atau Job
Webhooks biasanya dikirim dengan retry otomatis. Untuk menghindari efek samping ganda, tandai setiap request dengan idempotensi key (misalnya header X-Request-Id). Simpan key ini bersama status eksekusi di tabel atau cache before processing.
Contoh pola di job:
public function handle(IncomingWebhookRequest $request)
{
$key = $request->header('X-Request-Id');
$record = WebhookExecution::firstOrCreate(
['request_id' => $key],
['status' => 'processing']
);
if ($record->status === 'completed') {
return; // sudah diproses
}
DB::transaction(function () use ($record, $request) {
// logika bisnis, misalnya update order
$this->processPayload($request->input('payload'));
$record->status = 'completed';
$record->save();
});
}
Gunakan transaksi untuk memastikan perubahan data dan status idempotensi tercatat bersama. Jika operasi panjang, pertimbangkan queue job sehingga respons webhook tetap cepat.
Gunakan storage yang cocok: Redis untuk latensi rendah, database untuk audit trail. Pastikan masa berlaku record idempoten disesuaikan agar tidak menimbulkan kebocoran data.
Penanganan Error, Retry, dan Fallback
Ketika webhook gagal, Anda perlu mengelola respons non-200, header hilang, atau payload malformed secara eksplisit.
- Non-200 dari internal: Kembalikan respons 429/500 dengan pesan spesifik dan log error. Tambahkan header custom seperti
X-Retry-Afterjika Anda ingin mengontrol retry. - Payload malformed: Validasi FormRequest sudah memblokir ini. Pastikan respons mencantumkan field yang bermasalah dan jangan memproses data lebih lanjut.
- Retry otomatis: Gunakan idempotensi key untuk mendeteksi retry. Jika request sudah diproses tapi ingin memperbarui status, pertimbangkan endpoint verifikasi untuk memeriksa status terakhir untuk menghindari duplikasi.
Fallback strategy: jika job gagal di tengah transaksi, gunakan mekanisme deduplication untuk menandai job sebagai failed dan rilis ulang dengan backoff. Catat error ke sistem observability sehingga tim bisa menelusuri apakah ada masalah schema atau data eksternal.
Selain itu, implementasikan endpoint health check yang bisa digunakan sistem pengirim untuk mengetahui apakah service menerima webhook. Tambahkan monitor untuk header penting agar alert terpicu bila payload tanpa idempotensi key meningkat.
Dokumentasi Kontrak dan Monitoring
Sebagai bagian dari kontrak, sertakan dokumentasi yang menjelaskan schema, header, dan strategi idempoten. Gunakan file Markdown atau portal API untuk mengekspor schema valid secara berkala.
Monitor throughput webhook, rasio error 4xx/5xx, dan latensi respons. Jika terjadi perubahan schema, buat versi baru dan informasikan pengirim agar mereka tetap dalam kontrak. Dokumentasi yang baik membantu tim external menghindari payload tidak valid dan memastikan webhooks tetap reliable.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!