Skip to main content

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');
});
Jangan pernah hardcode secret key. Gunakan environment variables atau secrets management.
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

  • Validasi header Authorization pada setiap request
  • Gunakan environment variables untuk secret key
  • Aktifkan HTTPS untuk produksi
  • Implementasikan rate limiting
  • Jangan pernah log informasi sensitif

Keandalan

  • Respons dalam 5 detik
  • Selalu kembalikan HTTP 200 untuk sukses
  • Tangani error dengan baik
  • Implementasikan logging komprehensif
  • Uji dengan curl atau tool pengujian webhook

Idempotensi

  • Gunakan message.id untuk deduplikasi
  • Simpan ID event yang diproses
  • Tangani retry dengan benar
  • Implementasikan operasi idempoten

Skalabilitas

  • Gunakan pemrosesan async
  • Queue tugas berat
  • Optimalkan query database
  • Lakukan load testing

Monitoring

  • Lacak status pengiriman
  • Siapkan alert kegagalan
  • Monitor waktu respons
  • Pertahankan log error

Integritas Data

  • Validasi struktur payload
  • Tangani data yang hilang dengan baik
  • Kompatibel dengan versi
  • Backup data webhook penting

Langkah Selanjutnya