一位客戶寄信來:「我的 Stripe Webhook 壞了。」你查 Stripe 儀表板,事件都有發出。你查伺服器的 log,什麼都沒有。Sentry 也沒有。事件確實送到了某個地方,只是不在你預期的地方。
這篇是在正式環境除錯 Webhook 傳遞問題的逐步框架,根據的是最常出現的失敗模式。
重點摘要
- 先看服務商的儀表板:事件有沒有發出?收到的回應狀態是什麼?
4xx代表是你拒收了它(簽章、中介軟體、網址錯誤)5xx代表處理程式當掉了。到錯誤監控裡查那個確切的時間戳記- 逾時代表處理程式太慢。把繁重的工作改成非同步,快速回 200
- 要解決「花了 2 小時除錯」,就是有一個地方能同時看到收到的請求、轉送出去的請求、重試和重播,通常也就是一個 Webhook 中繼服務(webhook relay)
決策樹
從這裡開始。它能省下 30 分鐘的瞎猜。
Q1:服務商真的有送出事件嗎?
查看服務商的儀表板(Stripe:Developers → Events;GitHub:Settings → Webhooks → Recent Deliveries;Shopify:Dev Dashboard → Apps → 你的應用程式 → Logs)。
如果事件不在那裡,代表觸發條件沒有發生,那就去查本該產生它的上游狀態。如果事件在,就看 Q2。
Q2:服務商有收到 2xx 嗎?
在同一個儀表板裡,看你的端點回傳的回應狀態。
2xx:服務商認為傳遞成功。如果你的應用程式從沒看到它,代表事件到了,卻在回應之後被丟掉。檢查你的非同步佇列、資料庫寫入和處理邏輯。4xx:你拒收了它。最常見的是簽章驗證失敗、缺少標頭,或中介軟體(WAF、速率限制器、驗證層)擋下了請求。5xx:你的端點當掉了。查看錯誤監控,而且不是只看「最近一小時」,而是這次傳遞的確切時間戳記。- 逾時:你的端點太慢。見 Q4。
- 連線被拒(connection refused)或 DNS 錯誤:你的端點連不到,這是另一種問題,出在網路、DNS 或防火牆的某處。
Q3:如果是 4xx,是哪一個?
400,請求格式錯誤。通常是標頭不符,或簽章檢查在回傳明確錯誤之前就拋出例外。檢查請求主體格式錯誤或缺少簽章時,處理程式回傳的是什麼。401/403,你把請求當成未授權而拒收。是簽章驗證失敗嗎?還是中介軟體在一個不需要使用者 session 的端點上檢查 session?404,網址錯了。你是不是部署了一次路由變更,把 Webhook 端點搬走了?413,payload 太大。調高 body parser 的上限。429,速率限制器被觸發。不管是什麼在限制 Webhook 路由的速率,都不該這樣做。大多數 Webhook 服務商都從一組有限的 IP 發送,你的限制器可能把它們全當成同一個攻擊者。
Q4:如果是逾時,是什麼慢?
GitHub 給你 10 秒。Shopify 給 5 秒。Slack 和 Discord 給 3 秒。Stripe 從沒公布數字;它的文件只說要在任何耗時邏輯之前回傳 2xx。完整的表格與每一列的出處,都在 Webhook 服務商參考資料。
大多數逾時,都是因為處理程式裡做了太多事。常見的元凶有:對已經滿載的連線池做同步資料庫寫入;在處理程式裡呼叫其他服務(Stripe、Salesforce、Notion、你自己的 API);同步寄送 email 或 Slack 訊息;以及在一個正在施加背壓(backpressure)的 log 收集器上,把整個請求主體寫到 stdout。
修正的模式是:驗證簽章、把事件排入佇列、回 200。所有商業邏輯都在下游以非同步方式進行。
通常真正壞掉的是什麼
看了幾年下來,這是 80/20 的清單:
- 簽章驗證把請求主體重新序列化了。見簽章驗證指南
- 代理伺服器或 WAF 在 Webhook 到達處理程式之前就把它吃掉了。檢查 Cloudflare、Vercel,或你的平台用來記錄請求的地方
- 速率限制被觸發了。Cloudflare 的 Bot Management 可能會把正當的 Webhook 誤判成惡意流量
- 端點網址在某次部署中改了,卻沒有人去服務商那邊更新
- 正式環境少了某個環境變數(簽章密鑰、資料庫網址),處理程式就默默地回 500
- 背景工作在 Webhook 回 200 之後當掉。服務商以為一切正常,但其實什麼都沒被處理
第 6 項在無伺服器環境裡隨處可見,因為排在回應之後的工作完全沒有持久性保證:「在 Vercel 上接收 Webhook,不讓事件遺失」。如果症狀是重複而不是漏收,那通常是照設計在運作:「同一個 Webhook 你收到了兩次」。
真正有用的工具
服務商的儀表板適合一次看一個事件,不適合看出模式。如果你想看「過去 24 小時內所有失敗的事件」,就只能手動捲動。
ngrok 和 webhook.site 是給本機開發用的。到了正式環境,它們幫不上忙。
你自己的 log 有沒有用,要看你記了什麼。如果你在簽章檢查之前就記下「webhook received for event evt_xxx」,至少能知道它到了。大多數人是在檢查之後才記,於是簽章失敗的事件就完全看不見。
像 AnyHook 這樣的 Webhook 中繼服務,位在處理程式前面,會記錄標頭、請求主體、時間、回應狀態和嘗試次數。除錯流程就變成:看事件串流、按一下失敗的那一個、檢查原始 payload、重播。
AnyHook 的除錯流程
使用者寫信給 AnyHook 客服說「我的 Webhook 不能用」時,儀表板通常不到一分鐘就能讓原因一目瞭然:
- 開啟事件串流,篩選
status=failed - 按一下失敗的事件
- 看收到的標頭(簽章在不在?)、收到的請求主體(結構符不符合預期?)、轉送後的回應(你的伺服器說了什麼?),以及時間分段(邊緣節點 → 佇列 → 傳遞)
- 如果只是偶發狀況,例如一次出包的部署或不穩的網路,就按一下 Replay(重播),看著它成功
- 如果每個事件都失敗,你現在也確切知道要修什麼;在你修的同時,事件會一直儲存著,在方案的可重播期間內都能重播(Free 方案 3 天,Pro 方案 30 天,Scale 方案 90 天)
能把 2 小時的除錯縮短成 2 分鐘的,是把服務商實際送出的請求、你的伺服器實際回傳的回應,以及兩者之間的時間戳記,全部放在同一個地方。少了這些,你就得從三個儀表板和自己的 log 裡拼湊出來。
想在註冊之前先看看,一個指令就能拿到可用的接收網址(inbound URL),不需要帳號:
curl -X POST https://anyhook.net/api/v1/quickstart
回應裡有一個 inbound_url,讓你設定給服務商;一個 api_key,用來透過 API 讀取事件紀錄;還有一個 claim_url。這個端點是暫時的:除非你開啟認領網址並登入,否則端點和它的事件會在 7 天後刪除。認領之後,網址維持不變。
結論
如果你的週日下午常常這樣度過,AnyHook 能把大部分的工作變成在儀表板上按一下。