重送、順序、冪等性的設計
適用於實作 Webhook 接收端的開發者。說明在「同一事件收到多次」「舊事件在新事件之後才到」等情況下也能正確運作的接收端做法。
Webhook 是方便的機制,但網路是不穩定的,伺服器是會宕機的。ReceiptRoller 具備「沒送達」時自動重送的機制,但其結果是同一事件送達 2 次以上與順序前後顛倒都可能發生。接收端請以此為前提設計。
重送政策
接收端沒有回傳 2xx 的情況下,ReceiptRoller 會以指數退避重試重送。
| 次 | 發送時機 |
|---|---|
| 第 1 次 | 即時 |
| 第 2 次 | 1 分後 |
| 第 3 次 | 5 分後 |
| 第 4 次 | 30 分後 |
| 第 5 次 | 2 小時後 |
| 第 6 次 | 6 小時後 |
| 第 7 次 | 24 小時後 |
合計約 32 小時期間內最多重送 7 次。即使如此仍未成功的情況下,會作為無效信件記錄,可在開發者入口的「Webhook 投放紀錄」確認。
作為重送對象的回應
5xx(伺服器錯誤)429(速率限制)- 逾時(10 秒無回應)
- 連線錯誤(DNS / TLS / TCP)
不作為重送對象的回應
2xx:視為成功而不重送3xx:不自動跟隨重新導向(視為失敗但也不重送)4xx(含 401/410):視為接收端的永續性拒絕而不重送
5xx。回傳 4xx 則不會被重送。
順序不被保證
ReceiptRoller Webhook 不保證投放順序。例如可能發生下列這樣的情況。
receipt.refunded比receipt.issued更早到達product.updated比product.created更早到達- 第 1 次的投放被重送,舊事件在新事件之後到達
在需要處理順序的業務中,請看酬載中含的 occurred_at(事件發生時刻),設計為在接收端重新排序,或以 API 重新取得最新狀態。將 Webhook 不當作「最新狀態的通知」而當作「有變更的觸發」處理較安全。
冪等性的實作
讓同一事件收到多次結果也不改變,稱為冪等性。ReceiptRoller 為各事件附帶唯一的 event_id(以及 X-RR-Event-Id 標頭),請以此為鍵避免重複處理。
模式 1:記錄已處理 ID
async function handleWebhook(event) {
// 若已處理則忽略
const exists = await db.processedEvents.findOne({ event_id: event.event_id });
if (exists) return;
// 業務處理
await processEvent(event);
// 記錄(與業務處理在同一交易內為理想)
await db.processedEvents.insert({
event_id: event.event_id,
received_at: new Date(),
});
}
已處理表設定 TTL,自動刪除舊記錄,可防止肥大化(考慮重送期間 32 小時,最低也保持 7 天)。
模式 2:以 UPSERT 寫入
若業務處理是「將某筆記錄設為最新狀態」,設為 UPSERT(INSERT or UPDATE)則自然變冪等。
// 接收庫存事件,以商品 ID 為鍵 UPSERT
await db.inventory.upsert({
where: { product_id: event.data.product_id },
update: { quantity: event.data.quantity, updated_at: event.occurred_at },
create: { product_id: event.data.product_id, quantity: event.data.quantity },
});
在這個模式中,為了在舊事件後到時不覆蓋最新值,需要加入 occurred_at 比較的巧思。
模式 3:以自然鍵判定
若知道業務上的自然鍵(例:receipt_id),以此偵測重複也很簡單。與使用 event_id 的模式併用會更堅固。
順序顛倒的因應
對同一實體的多個事件順序顛倒時的典型對策。
- 以 occurred_at 進行覆蓋判定:若比接收端持有的最終更新時刻更舊的事件則不套用
- 版本號:酬載有
version的情況,僅採用更大的值 - 狀態轉移檢查:像「refunded → issued」這樣業務上不可能的轉移則忽略
- API 重新取得:收到通知後以 API 重新取得最新狀態(不用考慮順序就完成)
無效信件的處理
重送 7 次仍未成功的事件會作為「無效信件」保存,可在開發者入口 → 該端點 → 「投放紀錄」分頁確認。各記錄含有下列資訊。
- 事件 ID、種類、時間戳
- 各試行的回應碼與主體(開頭 1KB)
- 「重新投放」按鈕
修正接收端後,可用「重新投放」按鈕手動重送。期間為自投放起 30 天。
常見反模式
| 容易做錯的實作 | 問題 |
|---|---|
因為處理重,投入非同步佇列後不回傳 2xx,等到處理完成 |
10 秒逾時而進入重送迴圈 |
業務處理失敗時回傳 200 |
不會被重送而事件漏失 |
| 無冪等性地對庫存加減算 | 重複投放使庫存數混亂 |
| 不考慮順序顛倒就覆蓋最新旗標 | 舊事件踐踏新狀態 |
| 沒監視無效信件 | 沒察覺故障而經過數天 |
建議的接收模式
- 簽章、時間戳驗證
- 以
event_id進行冪等性檢查(已處理則即200) - 將酬載投入內部佇列(SQS / Cloud Tasks 等)
- 即刻回傳
200(到此為止 1 秒以內為目標) - 以佇列 worker 執行業務處理(重試在內部進行)
這個模式的話,接收端點總能高速回應,業務處理的失敗不會連結到重送迴圈。
相關指南
-
SNS Webhook 旁路(LINE 等外部 SNS 的 Webhook 轉發)解說 ReceiptRoller 將從 LINE 等 SNS 平台接收到的 Webhook,在店鋪端同意下轉發至開發者應用程式的「SNS Webhook 旁路」功能的機制、設定方法、簽章的處理、注意事項。
-
監視與失敗時的對應解說 ReceiptRoller Webhook 的投放紀錄的檢視方式、應監視的指標與警報設計、無效信件的重新投放、常見故障模式與復原步驟。
-
Webhook 的概要解說 ReceiptRoller 的 Webhook 所投放的主要事件種類、應該使用 Webhook 而非輪詢的理由、投放形式(HTTPS POST + JSON)、投放保證的思考方式。
-
開發者向幫助目次ReceiptRoller 開發者向幫助目次。彙整了開發者申請、應用程式登錄、OAuth 驗證與權限範圍、實作指南(錢包應用程式、店鋪向 Webhook、Survey API)、依資料領域別指南、運用與安全性、社群、疑難排解。
-
簽章驗證與安全性解說 ReceiptRoller Webhook 的 HMAC-SHA256 簽章驗證的機制、驗證程式碼的範例(Node.js / Python / C#)、重放攻擊對策、密鑰的安全管理方法。