你的資料庫在凌晨 2 點當機。等你醒來修好,三個小時份的 Webhook 已經送出並被拒絕。現在你盯著 Slack,想著有多少筆 Stripe 付款沒有履約,又有多少次 GitHub push 沒有觸發該跑的 CI 工作。
這篇是復原漏掉的 Webhook 的實際步驟,逐一說明各服務商的做法,最後是一種讓這件事按一下就解決、而不是耗掉一整個週日下午的做法。
重點摘要
- Stripe:在 Dashboard 逐一手動重新傳送每個事件,或透過 Events API 寫腳本處理。沒有批次操作的介面。
- GitHub:每次傳遞各有一個重新傳送按鈕。同樣一次只能一個,除非你用 Redelivery API 寫腳本。
- Shopify:在 4 小時內重試 8 次,之後事件就沒了,你得重新查詢 Orders API 來對帳。
- 要能一鍵重播,就必須在傳遞之前、在上游先記錄事件,而這正是 Webhook 中繼服務(webhook relay)的整個設計。
Stripe
Stripe 讓你從 Dashboard 手動重新傳送 Webhook 事件:
- 在 Stripe Dashboard 前往 Developers → Events。
- 按一下要重新傳送的事件。
- 捲動到 Webhook attempts(Webhook 傳遞嘗試),按一下 Resend(重新傳送)。
這一次只能處理一個事件,而且只限事件建立後的 15 天內。如果你漏掉 300 個事件,你就要按 300 次按鈕。Stripe CLI 有一個指令(stripe events resend <event_id>),但你仍然得知道每個事件的 ID。
批次重播 Stripe 事件
Stripe 沒有官方的批次重播介面。替代做法:
- 寫一個腳本,用 Stripe API 列出服務中斷期間的事件,依狀態篩選,再自己把每個事件 POST 回你的端點。你是從事件物件重新組出 Webhook,形狀和原本的不完全一樣。
- 或者,如果你在發生中斷的那一層之前就記錄了 Webhook,就從你自己的事件紀錄重播。
在上游記錄的做法,是正式環境的團隊最後都會自己做的。這也正是 Webhook 中繼服務替你做的事。
GitHub
GitHub 在 repo 的 Settings → Webhooks →(選擇 Webhook)→ Recent Deliveries 裡,每次傳遞都有一個重新傳送按鈕。在每一筆旁邊按一下 Redeliver(重新傳送)。
問題一樣:一次只能一個。GitHub 只讓傳遞在 3 天內可以重新傳送,大量重播仍然得寫腳本呼叫 Redelivery API。而且那份紀錄就是你的全部,因為失敗的傳遞,GitHub 從來不會自己重試。
Shopify
Shopify 在四小時內重試 8 次,然後放棄。之後,唯一的復原方式,是重新向 Shopify API 查詢你漏掉的資料。Webhook 本身已經沒了。
這個特別痛,因為 Shopify 讓你復原事件的期間,是三大服務商裡最短的,而且差距很大。如果你的伺服器週五晚上當機,你直到週一才發現,就得從 Orders API 做一次完整的對帳。
通用的做法
不管是哪個服務商,沒有中繼服務時的復原流程大致如下:
- 找出服務中斷的時段。你的伺服器是從什麼時候開始不再回 2xx?
- 拉出受影響的事件,通常每個服務商都要一次分頁的 API 呼叫。
- 把每個事件 POST 到你自己的端點重新觸發,或重新查詢資料的權威來源,手動同步狀態。
- 去重,因為你的處理程式必須冪等,而且事件可能已經部分送達。
這要花上好幾個小時。而且你只有一次機會:超過重試期間還沒補到的事件,就沒了。
一鍵完成的版本
AnyHook 存在的理由,就是把這件事變成按一下就好。當你把 Webhook 指向 in.anyhook.net/you/app:
- 每個事件都在傳遞前儲存,儲存時已加密。
- 如果你的伺服器回 5xx,AnyHook 會以指數退避重試(Free=3、Pro=5、Scale=10 次嘗試)。
- 如果你的伺服器當機久到重試用盡,事件會被標成失敗,但 payload 仍然在你的事件紀錄裡。
- 打開這個 App(一個接收網址,對應一個 Webhook 來源)的頁面,在失敗事件的橫幅選擇時間範圍(最近一小時、最近 24 小時,或整個可重播期間),按一下 Replay(重播)。它一次送出 100 個,從最舊的開始。就這樣。
重播失敗的事件,不計入每月配額,但會用到方案的運算額度。每個方案都有每日重播上限(Free 方案 100 次、Pro 方案 500 次、Scale 方案 1,000 次),而因為超過配額而被保留(已儲存、不傳遞,升級後可重播)的事件,重播時會當成新事件計算。重播會帶著新的簽章重新送到你的端點,原本收到的請求主體和標頭都保持不變,所以你的處理程式看到的事件,和服務商送出時一模一樣。
對應的 API 只是一個 POST:
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
-H "Authorization: Bearer $ANYHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{"since": "2026-04-18T02:00:00Z", "limit": 100}'
一次呼叫,最多重播 100 個事件,不用在儀表板上點來點去。
設計上的意涵
讓重播變便宜的,是在傳遞之前、在邊緣節點就保存事件。如果你的 Webhook 紀錄從你的原始伺服器開始,你只能重播原始伺服器收到的東西。如果紀錄從網路的邊緣節點開始,也就是在原始伺服器有機會失敗之前,你就能重播任何曾經抵達的東西。
這些嘗試之間的排程,本身就是一個設計問題。AnyHook 的排程是不加隨機抖動的固定階梯,Webhook 重試的指數退避與隨機抖動說明了為什麼這樣的階梯會在服務中斷後讓每個用戶端擠到同一秒,以及為什麼無論如何,低成本的重播都勝過激進的重試。如果你發現自己經常需要重播,更前面的問題是:這個邊界到底該不該用 Webhook,這是 Webhook vs 輪詢討論的主題。
把記錄移到上游,讓重播冪等,你最糟的 Webhook 服務中斷,就會變成一分鐘的復原,而不是賠上一個週末。
有兩篇延伸閱讀,值得在你需要之前先看。「讓重播冪等」是承重的那一半,一旦重試並行抵達,它就比一張去重表難得多:同一個 Webhook 收到兩次。另外,如果 Stripe 已經放棄並停用了你的端點,它就不會再重試,你得在 30 天期限內從 Events API 回填:Stripe 停用了你的 Webhook 端點。
試試看:anyhook.net。