Pengenalan
Mengikuti praktik terbaik ini memastikan integrasi webhook Anda andal, aman, dan dapat diskalakan.
Menerapkan praktik-praktik ini akan membantu Anda menghindari masalah webhook yang umum.
1. Keamanan
Selalu Validasi Autentikasi
const SECRET_KEY = process . env . WEBHOOK_SECRET_KEY ;
app . post ( '/webhook' , ( req , res ) => {
const authHeader = req . headers . authorization ;
if ( authHeader !== SECRET_KEY ) {
return res . status ( 401 ). json ({ error: 'Unauthorized' });
}
// Proses webhook...
res . status ( 200 ). send ( 'OK' );
});
Gunakan Environment Variables
Jangan pernah hardcode secret key. Gunakan environment variables atau secrets management.
Rotasi Key Secara Berkala
Ubah secret key Anda secara berkala untuk meningkatkan keamanan.
Selalu gunakan HTTPS untuk endpoint webhook produksi.
Implementasikan rate limiting untuk mencegah penyalahgunaan.
2. Keandalan
Respons dengan Cepat
Proses webhook secara efisien dan respons dalam 5 detik untuk menghindari timeout.
app . post ( '/webhook' , async ( req , res ) => {
// Acknowledge segera
res . status ( 200 ). send ( 'OK' );
// Proses di background
setImmediate ( async () => {
await processWebhook ( req . body );
});
});
Kembalikan 200 OK
Selalu kembalikan HTTP 200 untuk mengonfirmasi penerimaan:
app . post ( '/webhook' , ( req , res ) => {
try {
// Proses webhook...
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
// Tetap kembalikan 200 untuk error pemrosesan untuk menghindari retry
console . error ( 'Error pemrosesan:' , error );
res . status ( 200 ). send ( 'OK' );
}
});
Tangani Error dengan Baik
app . post ( '/webhook' , async ( req , res ) => {
try {
// Validasi payload
const { event , message } = req . body ;
if ( ! event || ! message ) {
console . error ( 'Struktur payload tidak valid' );
return res . status ( 400 ). send ( 'Invalid payload' );
}
// Proses event
await handleEvent ( event , message );
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
console . error ( 'Error webhook:' , error );
// Tentukan apakah retry diperlukan
if ( isTemporaryError ( error )) {
res . status ( 500 ). send ( 'Error sementara' );
} else {
res . status ( 200 ). send ( 'OK' );
}
}
});
Log Semua
Pertahankan log detail pengiriman webhook untuk debugging dan analitik.
app . post ( '/webhook' , async ( req , res ) => {
const startTime = Date . now ();
logger . info ( 'Webhook diterima' , {
event: req . body . event ,
timestamp: req . body . timestamp ,
ip: req . ip ,
userAgent: req . headers [ 'user-agent' ]
});
try {
await processWebhook ( req . body );
const duration = Date . now () - startTime ;
logger . info ( 'Webhook diproses' , {
duration: duration + 'ms' ,
status: 'success'
});
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
const duration = Date . now () - startTime ;
logger . error ( 'Webhook gagal' , {
duration: duration + 'ms' ,
error: error . message ,
stack: error . stack
});
res . status ( 500 ). send ( 'Error' );
}
});
3. Idempotensi
Kritis: Gunakan message ID untuk deduplikasi event.
Implementasikan Deduplikasi
const processedEvents = new Set ();
app . post ( '/webhook' , async ( req , res ) => {
const { event , message } = req . body ;
const eventId = ` ${ event } _ ${ message . id } ` ;
// Periksa apakah sudah diproses
if ( processedEvents . has ( eventId )) {
logger . info ( 'Event duplikat dilewati' , { eventId });
return res . status ( 200 ). send ( 'OK' );
}
// Proses event
await processEvent ( event , message );
// Tandai sebagai sudah diproses
processedEvents . add ( eventId );
res . status ( 200 ). send ( 'OK' );
});
Gunakan Database untuk Persistensi
app . post ( '/webhook' , async ( req , res ) => {
const { event , message } = req . body ;
const eventId = ` ${ event } _ ${ message . id } ` ;
try {
// Periksa database
const existing = await db . ProcessedEvent . findOne ({ event_id: eventId });
if ( existing ) {
return res . status ( 200 ). send ( 'OK' );
}
// Proses event
await processEvent ( event , message );
// Simpan event yang diproses
await db . ProcessedEvent . create ({
event_id: eventId ,
processed_at: new Date (),
event_type: event
});
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
console . error ( 'Error webhook:' , error );
res . status ( 500 ). send ( 'Error' );
}
});
4. Skalabilitas
Pemrosesan Async
Jangan blokir respons webhook pada operasi yang memakan waktu.
// Contoh sistem queue
const queue = new Queue ( 'webhook-processor' , {
redis: process . env . REDIS_URL
});
app . post ( '/webhook' , async ( req , res ) => {
// Tambahkan ke queue
await queue . add ( req . body );
// Respons segera
res . status ( 200 ). send ( 'OK' );
});
// Proses queue secara terpisah
queue . process ( async ( job ) => {
const { event , message } = job . data ;
await processEvent ( event , message );
});
Queue Tugas Berat
// Contoh dengan Bull (Node.js)
const Queue = require ( 'bull' );
const webhookQueue = new Queue ( 'webhooks' , {
redis: { port: 6379 , host: 'localhost' }
});
app . post ( '/webhook' , async ( req , res ) => {
await webhookQueue . add ( 'process' , req . body );
res . status ( 200 ). send ( 'OK' );
});
webhookQueue . process ( 'process' , async ( job ) => {
await processWebhook ( job . data );
});
Optimasi Database
// Gunakan index untuk lookup lebih cepat
db . ProcessedEvent . collection . createIndex (
{ event_id: 1 },
{ unique: true }
);
db . ProcessedEvent . collection . createIndex (
{ processed_at: 1 },
{ expireAfterSeconds: 604800 } // TTL 7 hari
);
Load Testing
Uji endpoint webhook di bawah volume tinggi untuk memastikan dapat menangani traffic puncak.
# Gunakan Artillery untuk load testing
# config.yaml
config:
target: "https://your-webhook-url.com/webhook"
phases:
- duration: 60
arrivalRate: 100 # 100 request per detik
scenarios:
- name: "Webhook Test"
flow:
- post:
url: "/"
json:
event: "channel.message_in"
timestamp: 1738056000
5. Monitoring
Lacak Status Pengiriman
const metrics = {
total: 0 ,
success: 0 ,
failure: 0 ,
duplicate: 0
};
app . post ( '/webhook' , async ( req , res ) => {
metrics . total ++ ;
try {
// Pemeriksaan deduplikasi
if ( isDuplicate ( req . body )) {
metrics . duplicate ++ ;
return res . status ( 200 ). send ( 'OK' );
}
// Proses webhook
await processWebhook ( req . body );
metrics . success ++ ;
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
metrics . failure ++ ;
res . status ( 500 ). send ( 'Error' );
}
// Laporkan metrik setiap 1000 request
if ( metrics . total % 1000 === 0 ) {
const successRate = (( metrics . success / metrics . total ) * 100 ). toFixed ( 2 );
console . log ( `Metrik webhook: ${ JSON . stringify ( metrics ) } ` );
console . log ( `Tingkat keberhasilan: ${ successRate } %` );
}
});
Alert pada Kegagalan
// Konfigurasi alert
const ALERT_THRESHOLDS = {
failureRate: 0.05 , // 5%
consecutiveFailures: 10 ,
responseTime: 5000 // 5 detik
};
let consecutiveFailures = 0 ;
app . post ( '/webhook' , async ( req , res ) => {
const startTime = Date . now ();
try {
await processWebhook ( req . body );
consecutiveFailures = 0 ;
const duration = Date . now () - startTime ;
// Periksa waktu respons
if ( duration > ALERT_THRESHOLDS . responseTime ) {
await sendAlert ({
level: 'warning' ,
message: `Respons webhook lambat: ${ duration } ms`
});
}
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
consecutiveFailures ++ ;
// Periksa tingkat kegagalan
const failureRate = metrics . failure / metrics . total ;
if ( failureRate > ALERT_THRESHOLDS . failureRate ) {
await sendAlert ({
level: 'critical' ,
message: `Tingkat kegagalan webhook tinggi: ${ ( failureRate * 100 ). toFixed ( 2 ) } %`
});
}
// Periksa kegagalan berturut-turut
if ( consecutiveFailures >= ALERT_THRESHOLDS . consecutiveFailures ) {
await sendAlert ({
level: 'critical' ,
message: ` ${ consecutiveFailures } kegagalan webhook berturut-turut`
});
}
res . status ( 500 ). send ( 'Error' );
}
});
Monitor Waktu Respons
const responseTimes = [];
app . post ( '/webhook' , async ( req , res ) => {
const startTime = Date . now ();
try {
await processWebhook ( req . body );
const duration = Date . now () - startTime ;
responseTimes . push ( duration );
// Simpan 1000 pengukuran terakhir
if ( responseTimes . length > 1000 ) {
responseTimes . shift ();
}
// Hitung metrik
const avg = responseTimes . reduce (( a , b ) => a + b , 0 ) / responseTimes . length ;
const p95 = responseTimes . sort (( a , b ) => a - b )[ Math . floor ( responseTimes . length * 0.95 )];
if ( avg > 3000 ) { // > 3 detik rata-rata
console . warn ( `Waktu respons rata-rata tinggi: ${ avg } ms` );
}
if ( p95 > 5000 ) { // > 5 detik P95
console . warn ( `Waktu respons P95 tinggi: ${ p95 } ms` );
}
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
res . status ( 500 ). send ( 'Error' );
}
});
6. Integritas Data
Validasi Payload
app . post ( '/webhook' , ( req , res ) => {
const { event , message } = req . body ;
// Validasi field wajib
if ( ! event ) {
return res . status ( 400 ). send ( 'Field event tidak ada' );
}
if ( ! message ) {
return res . status ( 400 ). send ( 'Field message tidak ada' );
}
// Validasi jenis event
const validEvents = [
'channel.message_in' ,
'agent.message_out' ,
'ai.message_generated' ,
'device.connected' ,
'device.disconnected'
];
if ( ! validEvents . includes ( event )) {
console . warn ( `Jenis event tidak dikenal: ${ event } ` );
}
// Validasi message ID
if ( ! message . id ) {
return res . status ( 400 ). send ( 'Message ID tidak ada' );
}
// Proses webhook...
res . status ( 200 ). send ( 'OK' );
});
Tangani Data yang Hilang
function safeGet ( obj , path , defaultValue = null ) {
return path . split ( '.' ). reduce (( acc , part ) => acc && acc [ part ], obj ) || defaultValue ;
}
app . post ( '/webhook' , ( req , res ) => {
const { message } = req . body ;
// Akses field nested dengan aman
const senderName = safeGet ( message , 'sender_name' , 'Unknown' );
const channelName = safeGet ( message , 'channel.name' , 'Unknown' );
const contentType = safeGet ( message , 'content.type' , 'unknown' );
// Proses dengan default yang aman
console . log ( `Pesan dari ${ senderName } via ${ channelName } ` );
res . status ( 200 ). send ( 'OK' );
});
Kompatibilitas Versi
Bersiaplah untuk penambahan dan perubahan field di masa depan.
// Contoh: Tangani field yang tidak dikenal dengan baik
app . post ( '/webhook' , async ( req , res ) => {
const { event , message } = req . body ;
// Proses field yang dikenal
await processMessage ( message );
// Simpan payload lengkap untuk referensi masa depan
await db . RawWebhook . create ({
payload: req . body ,
received_at: new Date (),
processed: true
});
res . status ( 200 ). send ( 'OK' );
});
Checklist
Gunakan checklist ini untuk memastikan implementasi webhook Anda mengikuti praktik terbaik.
Keamanan
Keandalan
Idempotensi
Skalabilitas
Monitoring
Integritas Data
Langkah Selanjutnya