Debugging6 Agustus 20268 mnt baca

Webhook GitHub Tidak Terkirim: Anda Mendapat Satu Upaya dan Tiga Hari

GitHub tidak mencoba ulang pengiriman webhook yang gagal. Tidak sekali pun. Catatannya bertahan tiga hari, lalu event dari deploy buruk hilang selamanya.

SEMUA PROVIDER LAINpengirimangagalcoba ulangcoba ulangcoba ulangjaringan punya beberapa kesempatan untuk tersendatGITHUBpengirimangagaltidak adatanpa upaya kedua3 hari untuk menyadari dan mengirim ulang manual,lalu catatan pengirimannya juga hilang
Setiap pengirim besar lain menganggap kegagalan sebagai hal sementara. GitHub menganggapnya final, dan memberi Anda waktu singkat untuk menyadarinya.

CI Anda tidak berjalan. Notifikasi Slack tidak pernah terkirim. Anda memeriksa tab Recent Deliveries (pengiriman terbaru) di GitHub, dan event-nya ada di sana dengan tanda merah di sebelahnya, berstempel waktu di tengah sembilan puluh detik saat aplikasi Anda sedang restart.

Sekarang bagian yang sering mengecoh orang. GitHub tidak akan mengirimnya lagi. Tidak dalam lima menit, tidak dalam satu jam, tidak akan pernah. Dokumentasinya menyatakan ini dalam satu kalimat tanpa basa-basi: “GitHub does not automatically redeliver failed deliveries.”

Jika Anda datang ke webhook GitHub dari Stripe atau Shopify, ini kebalikan dari apa yang sudah terbiasa Anda harapkan.

TL;DR

  • GitHub memberi endpoint Anda 10 detik untuk mengembalikan 2XX
  • Tidak ada percobaan ulang (retry) otomatis. Pengiriman yang gagal berarti event yang hilang, kecuali ada manusia yang turun tangan
  • Catatan pengiriman bertahan 3 hari. Setelah itu Anda bahkan tidak bisa melihat apa yang terlewat
  • X-GitHub-Delivery tetap sama saat pengiriman ulang manual, jadi itulah idempotency key yang tepat
  • Karena tidak ada jaring pengaman, solusinya harus “jangan pernah gagal sejak awal”, artinya simpan dulu sebelum memproses

Yang dilakukan pengirim lain, dan yang dilakukan GitHub

Stripe mencoba ulang dengan backoff eksponensial selama tiga hari. Shopify mencoba ulang delapan kali dalam empat jam. Slack mencoba ulang tiga kali. Paddle melakukan hingga enam puluh upaya dalam sekitar dua hari. Setiap pengirim itu mengasumsikan endpoint Anda sesekali tidak tersedia dan merancang sistemnya untuk itu, dan angka-angkanya terkumpul di referensi provider webhook kami.

GitHub mengirim permintaan. Jika tidak mendapat 2XX dalam 10 detik, GitHub mencatat kegagalannya dan lanjut.

SEMUA PROVIDER LAINpengirimangagalcoba ulangcoba ulangcoba ulangjaringan punya beberapa kesempatan untuk tersendatGITHUBpengirimangagaltidak adatanpa upaya kedua3 hari untuk menyadari dan mengirim ulang manual,lalu catatan pengirimannya juga hilang

Itu keputusan desain yang sah, bukan kelalaian. GitHub mengirim event dalam volume yang sangat besar, sebagian besar hanya bersifat informasi, dan antrean percobaan ulang pada skala itu mahal. Tetapi keputusan ini memindahkan seluruh beban keandalan kepada Anda, dan itu terjadi diam-diam, karena tidak ada satu pun di alur penyiapan yang memberi tahu bahwa provider ini berperilaku berbeda dari yang lain.

Jendela tiga hari adalah batasan yang sebenarnya

Pengiriman yang gagal terlihat di Settings → Webhooks → Recent Deliveries, dan Anda bisa mengeklik Redeliver (kirim ulang) pada pengiriman mana pun. Ada REST API untuk hal yang sama, untuk webhook repository, organisasi, dan GitHub App.

Batasnya: “You can redeliver webhook deliveries that occurred in the past 3 days.”

Tiga hari terdengar longgar sampai Anda mencocokkannya dengan cara tim benar-benar menyadari masalah ini. Sebuah webhook gagal saat deploy Jumat malam. Tidak ada yang membuka tab pengiriman, karena tidak ada peringatan. Senin pagi seseorang bertanya mengapa sebuah PR tidak pernah diberi label. Pada hari Selasa catatan pengirimannya sudah kedaluwarsa, dan Anda bahkan tidak bisa mendaftar apa saja yang hilang, apalagi memutarnya ulang (replay).

Bandingkan dengan Stripe: event tetap bisa diambil dari Events API selama 30 hari, terlepas dari apakah pengirimannya pernah berhasil. Di GitHub, catatan pengiriman adalah salinannya. Saat catatan itu kedaluwarsa, satu-satunya sumber kebenaran yang tersisa adalah state repository itu sendiri, dan Anda merekonstruksi “push mana saja yang terjadi antara pukul 18.40 dan 18.42” dari log commit.

Mengapa pengiriman gagal

Telusuri daftar ini secara berurutan.

Endpoint butuh lebih dari 10 detik

“Your server should respond with a 2XX response within 10 seconds of receiving a webhook delivery.”

Event push di monorepo yang sibuk membawa payload besar, dan handler yang mem-parse-nya, menelusuri daftar commit, lalu memanggil balik GitHub API untuk mengambil isi file akan melewati sepuluh detik tanpa melakukan kesalahan yang kentara. Bot yang meminta model meninjau diff melewatinya di setiap push. Saran GitHub sendiri adalah memakai antrean: “you may want to set up a queue to process webhook payloads asynchronously.” Pemeriksa timeout mengukur handler Anda terhadap sepuluh detik itu, dan Handler Webhook Anda Memanggil LLM menunjukkan solusi “jawab dulu” di setiap platform.

Antrean memang mengatasinya. Begitu juga menjawab sebelum handler Anda berjalan sama sekali: ide yang sama, dipindah satu langkah lebih awal. Menerima Webhook di Cloudflare Workers menjelaskan cara relay di artikel ini melakukannya: 2XX keluar dari edge dalam sekitar satu detik, sehingga sepuluh detik itu terpakai untuk jaringan, bukan untuk penelusuran commit Anda.

Secret tidak cocok

Jika webhook punya secret yang dikonfigurasi, GitHub menandatangani dengan X-Hub-Signature-256, dan verifikasi tanda tangan (signature) Anda harus berjalan atas raw body. Menyerialisasi ulang JSON sebelum hashing menghasilkan byte yang berbeda dan pemeriksaan gagal, yang mengembalikan 4xx, yang dihitung sebagai pengiriman gagal tanpa percobaan ulang. Detail per provider ada di Verifikasi Tanda Tangan Webhook Stripe, GitHub, dan Shopify.

Sesuatu di depan aplikasi menolaknya

Aturan WAF, filter bot, atau middleware auth yang mengharapkan cookie sesi. Trafik webhook adalah trafik POST anonim dari rentang IP pusat data, persis pola yang memang disetel untuk diblokir oleh sebagian besar aturan proteksi. Respons yang dicatat GitHub adalah 401, 403, atau 429, bukan sesuatu dari kode Anda.

Jenis event tidak di-subscribe

Pengirimannya tidak muncul sama sekali, bukan muncul lalu gagal. Periksa daftar event webhook tersebut.

Ini GitHub App dan instalasinya kehilangan akses

Webhook GitHub App berhenti untuk repository yang sudah dikeluarkan dari instalasi. Tab pengiriman tidak menampilkan apa pun, dan itu terlihat sama persis dengan “pemicunya tidak pernah aktif”.

Idempotency key

X-GitHub-Delivery adalah GUID yang mengidentifikasi event. Dua sifat menjadikannya key yang tepat:

Nilainya tetap sama saat pengiriman ulang manual. Panduan GitHub menyatakannya secara eksplisit: “if you request a redelivery, the X-GitHub-Delivery header will be the same as in the original delivery”, jadi saat Anda mengeklik Redeliver setelah deploy yang buruk, handler Anda melihat ID yang sama dengan yang akan dilihatnya pertama kali dan bisa mengenali event itu sebagai event yang sudah diproses.

Nilainya per event, bukan per payload, dan memang itu yang Anda inginkan. Dua push yang benar-benar terpisah menghasilkan dua ID meskipun payload-nya mirip.

export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifyGitHubSignature(raw, req.headers.get("x-hub-signature-256"))) {
    return new Response("unauthorized", { status: 401 });
  }

  const deliveryId = req.headers.get("x-github-delivery");
  const { rowCount } = await db.query(
    `INSERT INTO processed_events (event_id, provider)
     VALUES ($1, 'github') ON CONFLICT (event_id) DO NOTHING`,
    [deliveryId],
  );
  if (rowCount === 0) return new Response(null, { status: 200 });

  await queue.publish({ deliveryId, event: req.headers.get("x-github-event"), body: raw });
  return new Response(null, { status: 200 });
}

Kembalikan 200 untuk duplikat, bukan error. Pola yang lebih lengkap, termasuk apa yang harus dilakukan saat penulisan penanda dan efek sampingnya tidak bisa berada dalam satu transaksi, ada di Anda Menerima Webhook yang Sama Dua Kali.

Merancang untuk pengirim yang tidak mencoba ulang

Dengan provider lain, Anda boleh sedikit lalai soal ketersediaan, karena jadwal percobaan ulang menutupinya. Di sini batasan desainnya berbeda: satu-satunya pengiriman yang dijamin adalah yang pertama, jadi yang pertama harus berhasil.

Artinya handler mengerjakan seminimal mungkin sebelum mengembalikan 200. Verifikasi tanda tangan, tulis raw body ke tempat yang persisten, kembalikan respons. Semua hal lain, termasuk bagian yang paling mungkin lambat atau melempar error, terjadi setelah respons dan bisa dicoba ulang dengan jadwalnya sendiri tanpa diawasi GitHub.

Ini juga berarti Anda perlu tahu tentang kegagalan di dalam jendela tiga hari itu, bukan kapan pun seseorang kebetulan menyadarinya. GitHub tidak akan memberi tahu Anda. Tidak ada email kegagalan, tidak ada notifikasi endpoint dinonaktifkan, tidak ada yang setara dengan peringatan dari Shopify atau pemberitahuan penonaktifan dari Stripe. Jika satu-satunya hal yang bisa memunculkan celah itu adalah manusia yang membuka tab pengiriman, yang Anda punya bukan monitoring, melainkan keberuntungan.

Pemeriksaan berguna yang tidak memakan biaya: pasang peringatan saat repository yang biasanya rutin mengirim event menjadi sepi lebih lama dari jeda normalnya. Itu menangkap “endpoint kami gagal” sekaligus “webhook terhapus saat merapikan pengaturan”, yang ternyata cukup sering menjadi akhir cerita ini.

Peran AnyHook

Masalah strukturalnya: satu-satunya salinan event di GitHub adalah catatan pengiriman yang hangus dalam tiga hari, dan satu-satunya salinan Anda adalah apa pun yang sempat ditulis handler Anda sebelum tumbang.

Arahkan webhook ke in.anyhook.net/you/github dan salinannya pindah. AnyHook mengembalikan 200 ke GitHub dalam sekitar satu detik dari edge worker yang tugasnya hanya memverifikasi dan menyimpan, sehingga batas waktu 10 detik tidak pernah berperan, dan handler yang lambat atau gagal tidak bisa lagi membuat pengiriman masuk ke status gagal yang tidak bisa dipulihkan GitHub.

Dari situ:

  • Pengiriman ke endpoint Anda sendiri dicoba ulang dengan backoff eksponensial, jaring pengaman yang tidak disediakan GitHub
  • Event disimpan lengkap dengan header dan body, dengan enkripsi saat disimpan (at rest), dan tetap dapat diputar ulang selama masa putar ulang paket Anda (3 hari di Free, 30 di Pro, 90 di Scale), bukan 3 hari seperti di GitHub
  • Putar ulang bisa mencakup satu rentang waktu, jadi jendela deploy hari Jumat cukup dipilih lalu diklik, bukan direkonstruksi dari riwayat commit
  • Peringatan kegagalan dikirim pada 1, 5, dan 20 kegagalan berturut-turut, sinyal yang tidak dikirim GitHub

Handler Anda tetap harus idempoten, karena pengiriman ulang manual dan putar ulang sama-sama ada. Tidak ada yang menghapus kebutuhan itu.

Kesimpulan

Setiap kebiasaan debugging yang Anda bangun dengan Stripe tidak cocok di sini. Tidak ada percobaan ulang yang bisa ditunggu, tidak ada dead-letter queue untuk diperiksa, dan tidak ada API tiga puluh hari untuk backfill. Yang ada hanya satu upaya, catatan tiga hari tentang berhasil atau tidaknya, lalu sunyi.

Kembalikan 200 dengan cepat untuk sesuatu yang sudah Anda simpan, lakukan deduplikasi berdasarkan X-GitHub-Delivery, dan pasang peringatan untuk sepi yang tidak terduga. Jika pengiriman yang gagal saat ini tidak bisa dipulihkan di setup Anda, itu bukan keanehan GitHub yang sedang Anda akali, melainkan desain yang sudah Anda terima.

Semua artikel6 Agustus 2026 · 8 mnt

Berhenti kehilangan webhook.

Ganti satu URL. Dapatkan percobaan ulang, log event, dan putar ulang sekali klik. Atau lihat dulu: daftar dan jalankan latihan gangguan 30 detik, lalu saksikan event tetap selamat saat server Anda mati. Tidak perlu server.