Skip to main content

Pengenalan

Jika endpoint webhook Anda gagal merespons atau mengembalikan status code non-2xx, CSKU AI akan secara otomatis mencoba mengirim ulang webhook.
Mekanisme retry memastikan webhook Anda terkirim meskipun terjadi masalah sementara.

Konfigurasi Retry

ParameterNilai
Maksimum Retry3 kali
Jeda Retry30 detik antar percobaan
Metode RetryExponential backoff

Proses Retry

Request Awal → Gagal

   Tunggu 30 detik

   Retry 1 → Gagal

   Tunggu 30 detik

   Retry 2 → Gagal

   Tunggu 30 detik

   Retry 3 → Gagal

   Tandai sebagai kegagalan permanen
Setelah 3 percobaan yang gagal, webhook ditandai sebagai failed_permanent dan tidak akan di-retry lagi.

Memicu Retry

Webhook di-retry ketika:
Server Anda tidak merespons dalam periode timeout.
Server Anda mengembalikan status code di luar rentang 200-299.
Koneksi gagal karena masalah jaringan.
Server Anda mengembalikan status 500 atau error serupa.

Respons Sukses

Webhook dianggap berhasil terkirim ketika server Anda mengembalikan:
HTTP 200 OK
Konten body respons tidak penting. Hanya status code yang dipertimbangkan.

Kegagalan Permanen

Setelah 3 percobaan yang gagal, webhook ditandai sebagai kegagalan permanen dan tidak akan di-retry. Anda dapat:
  • Memeriksa webhook yang gagal di dashboard CSKU AI Anda
  • Query tabel tbl_resend_webhook untuk percobaan yang gagal
  • Mengirim ulang webhook secara manual jika diperlukan

Idempotensi

Kritis: Endpoint webhook Anda harus idempoten. Event yang sama mungkin diterima beberapa kali.

Mengapa Idempotensi Penting

Karena retry, webhook Anda mungkin menerima event yang sama beberapa kali. Tanpa idempotensi, Anda bisa:
  • Memproses pesan yang sama dua kali
  • Mengirim notifikasi duplikat
  • Membuat record database duplikat
  • Menagih pelanggan dua kali

Mengimplementasikan Idempotensi

Menggunakan Message ID

const processedEvents = new Set();

app.post('/webhook', async (req, res) => {
  const { event, message } = req.body;
  
  // Buat identifier event unik
  const eventId = `${event}_${message.id}`;
  
  // Periksa apakah sudah diproses
  if (processedEvents.has(eventId)) {
    console.log('Event duplikat, melewati');
    return res.status(200).send('OK');
  }
  
  try {
    // Proses event
    await handleEvent(event, message);
    
    // Tandai sebagai sudah diproses
    processedEvents.add(eventId);
    
    // Opsional: Persist ke database
    await db.processedEvents.create({ event_id: eventId });
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('Error memproses webhook:', error);
    res.status(500).send('Error');
  }
});

Menggunakan Database untuk Persistensi

app.post('/webhook', async (req, res) => {
  const { event, message } = req.body;
  const eventId = `${event}_${message.id}`;
  
  try {
    // Periksa database untuk event yang ada
    const existing = await db.processedEvents.findOne({ event_id: eventId });
    
    if (existing) {
      console.log('Event sudah diproses, melewati');
      return res.status(200).send('OK');
    }
    
    // Proses event
    await handleEvent(event, message);
    
    // Simpan sebagai sudah diproses
    await db.processedEvents.create({ event_id: eventId, processed_at: new Date() });
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('Error memproses webhook:', error);
    // Jangan kembalikan 200 pada error untuk memicu retry
    res.status(500).send('Error');
  }
});

Membersihkan Event yang Diproses

// Pembersihan in-memory (batasi ke 10,000 event)
if (processedEvents.size > 10000) {
  const eventsArray = Array.from(processedEvents);
  processedEvents.clear();
  eventsArray.slice(5000).forEach(id => processedEvents.add(id));
}

// Pembersihan database (hapus event lebih dari 7 hari)
await db.processedEvents.deleteMany({
  processed_at: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
});

Praktik Terbaik untuk Retry

Proses webhook dengan cepat dan respons dalam 5 detik untuk menghindari timeout.
Selalu kembalikan HTTP 200 setelah berhasil memproses webhook.
Kembalikan status 5xx untuk memicu retry jika Anda mengalami error pemrosesan sementara.
Log pengiriman webhook yang berhasil maupun gagal untuk debugging.
Gunakan exponential backoff jika Anda melakukan retry operasi dalam handler webhook Anda.

Contoh: Handler Retry Komprehensif

const processedEvents = new Map(); // Simpan eventId dengan timestamp

app.post('/webhook', async (req, res) => {
  const { event, message } = req.body;
  const eventId = `${event}_${message.id}`;
  const now = Date.now();
  
  // Periksa duplikat (dalam 1 jam)
  const existing = processedEvents.get(eventId);
  if (existing && (now - existing.timestamp) < 3600000) {
    console.log('Event duplikat:', eventId);
    return res.status(200).send('OK');
  }
  
  try {
    // Proses event
    await handleEvent(event, message);
    
    // Tandai sebagai sudah diproses
    processedEvents.set(eventId, { timestamp: now });
    
    // Bersihkan event lama (lebih dari 24 jam)
    const hourAgo = now - 86400000;
    for (const [id, data] of processedEvents.entries()) {
      if (data.timestamp < hourAgo) {
        processedEvents.delete(id);
      }
    }
    
    res.status(200).send('OK');
  } catch (error) {
    console.error('Error pemrosesan webhook:', error);
    
    // Tentukan apakah retry harus terjadi
    if (isTemporaryError(error)) {
      // Kembalikan 500 untuk memicu retry
      res.status(500).send('Error sementara');
    } else {
      // Kembalikan 200 untuk menghindari retry (kegagalan idempoten)
      res.status(200).send('OK');
    }
  }
});

function isTemporaryError(error) {
  // Retry pada masalah koneksi database, error jaringan, dll.
  return (
    error.code === 'ECONNREFUSED' ||
    error.code === 'ETIMEDOUT' ||
    error.name === 'DatabaseConnectionError'
  );
}

Monitoring Retry

Lacak Metrik Retry

const metrics = {
  attempts: 0,
  successes: 0,
  failures: 0,
  duplicates: 0
};

app.post('/webhook', async (req, res) => {
  metrics.attempts++;
  
  const { event, message } = req.body;
  const eventId = `${event}_${message.id}`;
  
  // Periksa duplikat
  if (isDuplicate(eventId)) {
    metrics.duplicates++;
    return res.status(200).send('OK');
  }
  
  try {
    await handleEvent(event, message);
    metrics.successes++;
    
    // Log metrik setiap 100 request
    if (metrics.attempts % 100 === 0) {
      console.log('Metrik webhook:', metrics);
    }
    
    res.status(200).send('OK');
  } catch (error) {
    metrics.failures++;
    console.error('Error webhook:', error);
    res.status(500).send('Error');
  }
});

Contoh Output Metrik

Metrik webhook: {
  attempts: 100,
  successes: 95,
  failures: 3,
  duplicates: 2
}
Jumlah duplikat yang tinggi mungkin menandakan webhook Anda memproses lambat, menyebabkan retry.

Troubleshooting Retry

Duplikat Berlebihan

Gejala: Jumlah event duplikat yang tinggi Solusi:
  • Periksa waktu pemrosesan webhook (harus < 5 detik)
  • Optimalkan query database
  • Gunakan pemrosesan async untuk operasi berat
  • Periksa resource server (CPU, memory)

Kegagalan Permanen

Gejala: Semua webhook ditandai sebagai kegagalan permanen Solusi:
  • Verifikasi URL webhook benar dan dapat diakses
  • Periksa autentikasi (secret key)
  • Uji endpoint dengan curl
  • Tinjau log server untuk error
  • Pastikan firewall mengizinkan koneksi masuk

Kegagalan Intermiten

Gejala: Beberapa webhook berhasil, yang lain gagal Solusi:
  • Implementasikan penanganan error yang tepat
  • Gunakan exponential backoff untuk retry
  • Periksa stabilitas koneksi database
  • Monitor penggunaan resource server
  • Implementasikan health check

Langkah Selanjutnya