Menjawab Tantangan Kontrak Webhook Pihak Ketiga
Tim backend yang menerima webhook dari layanan eksternal perlu memastikan kontrak API tidak rusak saat berevolusi, payload tetap aman, dan penerimaan tidak menggandakan efek samping. Solusi Spring Boot yang matang memadukan schema versioning, validasi payload, otentikasi signature, idempotensi, dan strategi retry/observabilitas sehingga integrasi tetap dapat dipantau, diperbaiki, dan diandalkan.
Artikel ini menyajikan langkah praktis untuk menghadapi masing-masing aspek itu tanpa asumsi pemasok webhook dapat berubah sesuai keinginan: dari pengecekan schema hingga logging dan metrics.
1. Menjaga Kontrak API: Schema Versioning dan Validasi
Setiap payload webhook harus dibandingkan dengan kontrak yang dinyatakan oleh pihak kita. Gunakan JSON Schema atau OpenAPI untuk versi schema, lalu validasi payload sebelum diproses. Kontrak versi baru dapat dirilis paralel dengan batasan version header atau path (misalnya /webhook/v2/) agar tidak mengganggu consumer lama.
- Versi schema: simpan schema di repository, sertakan checksum, dan pastikan webhook penerima mengecek header
X-Webhook-Schemauntuk menentukan schema mana yang digunakan. - Validasi payload: gunakan library seperti
javax.validationdi DTO yang mengikat payload atau validator custom yang menolak struktur berbeda. - Perubahan backward compatible: tambahkan field opsional ketimbang menghapus. Gunakan flag feature toggle untuk memegang versi baru sampai pihak ketiga siap.
Contoh validator sederhana:
public void validatePayload(WebhookRequest request) {
if (!schemaRegistry.matchesVersion(request.getSchemaVersion(), request.getPayload())) {
throw new InvalidWebhookException("Versi schema tidak cocok");
}
}Letakkan pengecekan ini di layer awal controller sehingga proses downstream tidak berjalan bila schema berbeda.
2. Otentikasi Payload: Signature atau Shared Secret
Webhook rentan dipalsukan, jadi verifikasi signature atau shared secret wajib. Strategi umum adalah menandatangani payload dengan HMAC + timestamp lalu memverifikasi dengan secret yang tersimpan di backend.
Contoh konfigurasi Spring Security sederhana untuk filter signature:
@Component
public class WebhookSignatureFilter extends OncePerRequestFilter {
private final SecretProvider secretProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String signature = request.getHeader("X-Webhook-Signature");
String payload = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
if (!signatureValidator.isValid(payload, signature, secretProvider.getSecret())) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
chain.doFilter(new CachedBodyHttpServletRequest(request, payload), response);
}
}
Gunakan CachedBodyHttpServletRequest agar payload bisa dibaca ulang setelah verifikasi. Simpan secret secara aman di vault atau environment variable.
3. Idempotensi: Dedup Store dan Idempotency Key
Penerimaan ulang webhook sering terjadi saat retry. Gunakan idempotency key yang dikirim oleh vendor (misalnya header X-Webhook-Id). Jika tidak tersedia, hitung hash dari payload + event timestamp. Simpan key ini di store seperti Redis dengan TTL sesuai jeda retry maksimum agar duplikat bisa diabaikan.
@Service
public class IdempotencyService {
private final RedisTemplate redis;
public boolean tryClaim(String idempotencyKey) {
Boolean success = redis.opsForValue()
.setIfAbsent(idempotencyKey, "processing", Duration.ofMinutes(5));
return Boolean.TRUE.equals(success);
}
}
Gunakan metode tryClaim sebelum menjalankan logic utama. Bila tidak berhasil, kembalikan 200 OK dengan catatan log bahwa event diabaikan karena sudah diproses.
4. Strategi Retry Aman: Backoff, Circuit Breaker, dan Dead-letter
Retry otomatis penting, tapi harus dikombinasikan dengan exponential backoff untuk mencegah spam. WebClient Spring Boot dengan Retry dari Reactor atau Resilience4j dapat membantu.
@Bean
public WebClient webhookClient(WebClient.Builder builder) {
return builder.baseUrl("https://vendor.example.com")
.filter(Resilience4jRetryExchangeFilter.of("webhook-retry"))
.filter(Resilience4jCircuitBreakerExchangeFilter.of("webhook-cb"))
.build();
}
Gunakan configuration Resilience4j untuk exponential backoff dan circuit breaker yang membuka jalur saat vendor gagal berulang.
Untuk background processing, kirim event ke queue (misalnya RabbitMQ/Redis Stream) dengan konfigurasi dead-letter exchange untuk event yang terus gagal. Ini menjaga sistem tidak terjebak dalam retry loop tanpa visibility.
5. Pengujian Integrasi dan Observabilitas
Pastikan ada pengujian integrasi yang memverifikasi bypass filter signature, validasi schema, idempotensi, dan retry. Gunakan Spring Boot Test dengan MockWebServer atau WireMock untuk meniru vendor.
Contoh test snippet:
@SpringBootTest
@AutoConfigureMockMvc
class WebhookControllerTest {
@Autowired MockMvc mockMvc;
@Test
void whenDuplicateWebhook_thenIgnored() throws Exception {
String payload = "{...}";
mockMvc.perform(post("/webhook")
.header("X-Webhook-Id", "abc")
.content(payload)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
mockMvc.perform(post("/webhook")
.header("X-Webhook-Id", "abc")
.content(payload)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// assert bahwa business logic hanya dijalankan sekali
}
}
Untuk observabilitas, catat setiap event penting:
- Schema mismatch, signature failure, idempotency skip, dan retry outcomes di level
INFO/WARN. - Gunakan Micrometer untuk mengekspor metric seperti
webhook.received,webhook.processed,webhook.retry, danwebhook.failed.signature. - Integrasikan tracing (misalnya OpenTelemetry) agar proses async/queue masih terlihat.
Dengan pendekatan ini, tim backend Spring Boot dapat memastikan kontrak webhook tetap terjaga, payload aman, proses idempoten, dan retry diberlakukan dengan kendali. Observabilitas lengkap membantu mendeteksi dan memperbaiki regresi sebelum berdampak pada layanan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!