其他每一篇 Webhook 文章,包括我們自己其他的文章,都從同一個假設出發:服務商準時送出了事件,是之後才出了問題。WooCommerce 在請求離開伺服器之前,就打破了這個假設。
下單時,事件並不會送出。它會被放進佇列,佇列由 WordPress cron 消化,而 WordPress cron 並不是真正的 cron。它要等有人載入頁面時才會執行。
如果你的商店很忙,延遲最多大約一分鐘,你永遠不會察覺。如果你的商店一小時只有幾個訪客,那個「即時」的訂單通知,要等下一位訪客碰巧出現才會送到。
重點摘要
- 傳遞會以
woocommerce_deliver_webhook_async動作的形式,排進 Action Scheduler。 - Action Scheduler 由 WP-Cron 驅動,而 WP-Cron 是在頁面載入時觸發,不是照排程觸發。
- 所以低流量商店的 Webhook 傳遞又慢又難以預測,而且這裡面沒有任何東西壞掉。
- 「WooCommerce 會在 Webhook 連續傳遞失敗超過五次後,自動將它停用。」
- 任何不是
2xx、301或302的回應,都算失敗。 - 自架 WordPress 真正的解法,是停用 WP-Cron,改由系統的 cron 驅動。
先確認到底有沒有送出
在對端點除錯之前,先確定你碰到的是兩個截然不同問題中的哪一個:傳遞失敗了,還是傳遞根本還沒發生。
WooCommerce → 設定 → 進階 → Webhooks(WooCommerce → Settings → Advanced → Webhooks)會顯示每個 Webhook 的狀態,WooCommerce → 狀態 → 日誌紀錄(WooCommerce → Status → Logs,來源 webhooks-delivery)則會列出最近的傳遞和回應狀態碼。如果有列出傳遞而且都失敗,直接看下面其餘的檢查項目。如果紀錄是空的,訂單卻明明存在,那傳遞就還躺在佇列裡。
要看佇列本身,就到 Action Scheduler 的管理畫面,也就是工具 → 已排程動作(Tools → Scheduled Actions),篩選 woocommerce_deliver_webhook_async。時間戳記已經過去、卻還在等待中的動作就是鐵證:它們早該執行,卻沒有任何東西去跑它們。
WP-Cron 為什麼會這樣
WordPress 沒有背景行程。它所謂的 cron,是一份在請求進來時才檢查的到期工作清單,所以只有流量進來,時鐘才會往前走。對 2005 年的共享主機來說,這是合理的設計;對任何講求時效的東西來說,則是糟糕的設計。
對 Webhook 而言,具體的後果是:
傳遞延遲跟著你的流量走,而不是跟著事件走。流量穩定的商店,大約一分鐘內就會送出。訂單在半夜進來、早上之前都沒人瀏覽的 B2B 商店,排進佇列的傳遞可能一等就是好幾個小時。
積壓會越滾越大。失敗的動作會留在資料表裡,在運作一段時間的商店上,Action Scheduler 的資料表會大到拖慢那些本來應該消化它們的請求。
有快取時,靠頁面載入觸發的工作並不可靠。WordPress 前面若有整頁快取或 CDN,很多造訪根本不會到達 PHP,也就永遠不會推進 cron。
解法是標準的 WordPress 維運操作,只要 Webhook 對商店很重要,就值得做:
// wp-config.php
define('DISABLE_WP_CRON', true);
# then drive it from a real scheduler, every minute
* * * * * cd /var/www/store && wp cron event run --due-now >/dev/null 2>&1
如果有 WP-CLI 套件可用,wp action-scheduler run 是直接消化 Action Scheduler 佇列的對應指令。cron 一旦跑在真正的計時器上,傳遞延遲就不再取決於有沒有人在瀏覽。
失敗超過五次,Webhook 就沒了
暫時的問題,就是在這裡變成永久的問題。
「WooCommerce 會在 Webhook 連續傳遞失敗超過五次後,自動將它停用。」失敗的定義很寬:「任何不是 2xx、301 或 302 的 HTTP 狀態碼回應。例如 4xx 或 5xx 回應都算失敗。」
這個數字很小。在訂單量穩定的商店上,一次讓端點停擺兩分鐘的部署,就足以用掉這麼多次傳遞。這時 Webhook 的狀態會變成停用,而且一直維持停用。沒有通知信,沒有後台提示,沒有任何會出現在人眼前的東西。訂單照樣成立,什麼都沒送出,看起來就跟生意清淡的一天一模一樣。
這些都不是 WooCommerce 獨有的。n8n Webhook 沒反應依照實際出現的頻率,排列了自動化工具這一端的七個原因;而像這樣消化得不均勻的佇列,傳遞時順序錯亂是理所當然的事,這正是 HubSpot 說它的 eventId 可能不唯一這篇的主題。當重試真的送達兩次,重複預約描述的就是訂單主人眼中看到的樣子。
這和 Shopify 刪除訂閱是同一種失敗型態,只是有兩點差異讓它更糟:門檻是連續失敗超過五次,而不是 Shopify 的八次;而且完全沒有警告信。
如果你在正式環境跑 WooCommerce,替 Webhook 的狀態加上檢查。一個透過 REST API 讀取 Webhook、在 status !== 'active' 時發出警示的排程工作,幾乎不花成本,也是唯一會告訴你的東西。
簽章驗證
WooCommerce 會送出 X-WC-Webhook-Signature,內容是用你在 Webhook 上設定的密鑰,對原始請求主體(raw body)計算的 HMAC-SHA256,再以 base64 編碼。
是 Base64 而不是 hex,這和 Shopify 的慣例相同,和 Stripe、GitHub 相反。各服務商選用的編碼,都整理在 Webhook 服務商參考資料裡。如果你有一個跨服務商共用的驗證輔助函式,這個編碼差異就是「payload 看起來完全正確,簽章卻永遠對不上」的常見來源。
import crypto from "node:crypto";
function verifyWooCommerce(raw: string, header: string | null, secret: string) {
if (!header) return false;
const expected = crypto.createHmac("sha256", secret).update(raw, "utf8").digest("base64");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
和其他地方一樣,對你收到的位元組計算雜湊,而不是對重新序列化的物件。各服務商的比較見驗證 Stripe、GitHub 和 Shopify 的 Webhook 簽章。
有一個 WooCommerce 特有的陷阱:如果建立 Webhook 時沒有設定密鑰,簽章會用網站自己的金鑰材料產生,而不是你選的值,你的驗證就會因為和程式碼毫無關係的原因失敗。明確設定一個密鑰。
檢查清單的其餘項目
傳遞存在而且一直失敗時,逐項檢查以下幾點。
狀態是 active,不是 paused 或 disabled。先檢查這一項,因為停用狀態一次就能解釋其他所有現象。
主旨(topic)對應的是你以為的那個事件。order.created 在訂單建立時觸發,而對很多金流閘道來說,這時訂單還是 pending。如果你要等的是付款完成,通常該用的是 order.updated 加上狀態檢查。很多「Webhook 沒有觸發」的回報,其實是「在和預期不同的事件上觸發了」。
遞送網址(Delivery URL)從伺服器連得到,而不只是從你的筆電連得到。位於企業內網後面,或主機的對外連線規則很嚴格的商店,可能在這裡無聲無息地失敗。
某個外掛讓這個動作短路了。WooCommerce 的 hook 都可以被 filter 修改,而快取、安全性或效能類外掛,都曾經干擾過 Action Scheduler。停用外掛來測試一點也不光鮮,卻常常是決定性的一步。
Webhook 的 API 版本。WooCommerce 的 Webhook payload 有版本之分,好幾年前建立的 v1 Webhook,送出的結構和 v3 不一樣。如果你的處理程式是在 WooCommerce 更新後開始失敗,檢查這個 Webhook 固定在哪個版本。
AnyHook 的角色
上面的問題裡,有兩個是結構性的,再好的處理程式也解決不了。
第一個是連續失敗就停用。把 Webhook 指向 in.anyhook.net/you/woo,AnyHook 會從邊緣節點大約一秒就回 200,所以連續失敗的計數,不再取決於你自己的端點狀況。對你端點的重試依照 AnyHook 的排程進行,而不是 WooCommerce 的,Webhook 也會維持啟用。
第二個是 WooCommerce 自己的傳遞紀錄放在商店裡,跟著商店的保存期限,待在某個管理畫面中。AnyHook 會保存每個事件的完整標頭和請求主體,儲存時已加密,並可依時間範圍重播,所以部署期間漏掉的事件,不必動到 WordPress 就能復原。
- 連續失敗 1、5、20 次時發出失敗警示,這正是 WooCommerce 不會發的通知。
- 以指數退避重試傳遞,涵蓋的時間遠比單一次嘗試長。
- 如果你接收多個平台的 Webhook,用單一的
AnyHook-Signature標頭取代 base64 的變體。
中繼服務解決不了的,是它前面的那個佇列。WP-Cron 如果沒在跑,事件根本到不了任何地方。先把 cron 修好;之後的一切交給中繼服務。
結論
在主要平台裡,WooCommerce 是唯一一個「Webhook 遲到」通常既不是 bug、也不是你端點問題的平台。先檢查 Action Scheduler 佇列,再檢查其他任何東西;如果商店流量很低,就把 cron 移到真正的排程器上。
接著設定密鑰,用 base64 對原始位元組驗證,並替 Webhook 本身的狀態加上監控。區區幾次連續失敗,能容錯的空間不大,而停用會在毫無預警下到來。