Debug6 tháng 8, 20268 phút đọc

Webhook WooCommerce không được gửi ngay lúc đặt hàng

WooCommerce xếp webhook vào Action Scheduler trên WP-Cron, mà WP-Cron chỉ chạy khi có người vào site. Lỗi hơn năm lần liên tiếp, webhook bị vô hiệu hóa.

ĐƠN ĐÃ ĐẶT, NHƯNG WEBHOOK CHƯA ĐƯỢC GỬIđơn được đặtAction Schedulerwoocommerce_deliver_webhook_asyncchờmột lượt tải trangđã gửiWP-Cron không phải đồng hồ. nó chỉ chạy khi có người vào trang.với cửa hàng vắng khách, đó là vài phút. cửa hàng rất vắng thì là vài giờ.hơn năm lần thất bại liên tiếp là WooCommerce vô hiệu hóa webhook luôn
Đơn hàng được đặt ngay. Lượt gửi phải chờ lượt xem trang tiếp theo, vì WP-Cron không phải đồng hồ.

Mọi bài viết khác về webhook, kể cả các bài còn lại của chúng tôi, đều xuất phát từ cùng một giả định: nhà cung cấp đã gửi sự kiện kịp thời và có gì đó trục trặc sau đó. WooCommerce phá vỡ giả định này trước cả khi request rời khỏi server.

Sự kiện không được gửi lúc đơn hàng được đặt. Nó được đưa vào một hàng đợi, hàng đợi đó được WordPress cron xử lý dần, và WordPress cron thì không phải cron. Nó chạy khi có ai đó tải một trang.

Nếu cửa hàng của bạn đông khách, độ trễ lên tới khoảng một phút và bạn sẽ không bao giờ để ý. Nếu cửa hàng chỉ có vài lượt truy cập mỗi giờ, thông báo đơn hàng “thời gian thực” sẽ đến vào bất cứ lúc nào người truy cập kế tiếp tình cờ ghé vào.

TL;DR

  • Các lượt gửi được xếp hàng dưới dạng action woocommerce_deliver_webhook_async trong Action Scheduler
  • Action Scheduler được WP-Cron điều khiển, mà WP-Cron chạy theo lượt tải trang chứ không theo lịch
  • Vì vậy một cửa hàng ít traffic sẽ gửi webhook chậm và khó đoán, mà chẳng có gì bị hỏng cả
  • “WooCommerce tự động vô hiệu hóa webhook sau hơn năm lần gửi thất bại liên tiếp”
  • Thất bại là mọi response không phải 2xx, 301 hoặc 302
  • Cách sửa thật sự trên WordPress tự host là tắt WP-Cron và chạy nó bằng cron của hệ thống

Trước hết, kiểm tra xem webhook đã được gửi hay chưa

Trước khi gỡ lỗi endpoint, bạn cần xác định mình đang gặp vấn đề nào trong hai vấn đề rất khác nhau: lượt gửi đã thất bại, hay lượt gửi chưa hề diễn ra.

WooCommerce → Settings → Advanced → Webhooks hiển thị trạng thái của từng webhook, còn WooCommerce → Status → Logs (nguồn webhooks-delivery) hiển thị các lượt gửi gần đây kèm mã phản hồi. Nếu có lượt gửi được liệt kê và đang thất bại, chuyển xuống phần còn lại của danh sách kiểm tra bên dưới. Nếu log trống trong khi đơn hàng rõ ràng đã tồn tại, lượt gửi vẫn đang nằm trong hàng đợi.

ĐƠN ĐÃ ĐẶT, NHƯNG WEBHOOK CHƯA ĐƯỢC GỬIđơn được đặtAction Schedulerwoocommerce_deliver_webhook_asyncchờmột lượt tải trangđã gửiWP-Cron không phải đồng hồ. nó chỉ chạy khi có người vào trang.với cửa hàng vắng khách, đó là vài phút. cửa hàng rất vắng thì là vài giờ.hơn năm lần thất bại liên tiếp là WooCommerce vô hiệu hóa webhook luôn

Để xem chính hàng đợi, mở màn hình quản trị của Action Scheduler trong Tools → Scheduled Actions và lọc theo woocommerce_deliver_webhook_async. Các action đang chờ có dấu thời gian (timestamp) nằm trong quá khứ chính là bằng chứng rõ ràng: chúng đã đến hạn, nhưng không có gì chạy chúng.

Vì sao WP-Cron hoạt động như vậy

WordPress không có tiến trình chạy nền. Thứ nó gọi là cron là một danh sách tác vụ đến hạn, được kiểm tra mỗi khi có request đến, nên đồng hồ chỉ nhích lên khi có traffic. Đó là một thiết kế hợp lý cho shared hosting năm 2005, và là thiết kế tồi cho bất cứ thứ gì nhạy cảm về thời gian.

Hệ quả riêng với webhook:

Độ trễ gửi đi theo traffic, không theo sự kiện. Một cửa hàng có traffic đều đặn sẽ gửi trong vòng khoảng một phút. Một cửa hàng B2B có đơn hàng đến trong đêm và không ai truy cập cho tới sáng có thể để một lượt gửi nằm trong hàng đợi hàng giờ liền.

Tồn đọng chồng chất. Các action thất bại vẫn nằm lại trong bảng, và ở những cửa hàng đã chạy một thời gian, các bảng của Action Scheduler phình to đến mức làm chậm chính những request lẽ ra phải xử lý chúng.

Tác vụ kích hoạt theo lượt tải trang không đáng tin khi có cache. Một full-page cache hoặc một CDN đứng trước WordPress khiến nhiều lượt truy cập không bao giờ tới được PHP, nghĩa là chúng không bao giờ đẩy cron chạy tiếp.

Cách sửa là một thao tác vận hành WordPress tiêu chuẩn, và đáng làm trên mọi cửa hàng mà webhook có vai trò quan trọng:

// 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 action-scheduler run là lệnh tương đương để xử lý trực tiếp hàng đợi của Action Scheduler, nếu bạn có sẵn package WP-CLI. Khi cron chạy theo một bộ hẹn giờ thật, độ trễ gửi không còn phụ thuộc vào việc có ai đang truy cập hay không.

Hơn năm lần thất bại là webhook biến mất

Đây là phần biến một vấn đề tạm thời thành vĩnh viễn.

“WooCommerce tự động vô hiệu hóa webhook sau hơn năm lần gửi thất bại liên tiếp”. Thất bại được định nghĩa rất rộng: “mọi response có mã trạng thái HTTP không phải 2xx, 301 hoặc 302. Ví dụ, response 4xx hoặc 5xx đều được tính là thất bại”.

Đó là một con số nhỏ. Một lần deploy làm endpoint ngừng hoạt động hai phút trên một cửa hàng có đơn đều đặn sẽ dễ dàng đốt hết ngần ấy lượt gửi. Khi chuyện đó xảy ra, trạng thái của webhook chuyển sang disabled và cứ thế giữ nguyên. Không có email, không có thông báo trong trang quản trị, không có gì hiện ra trước mắt ai. Đơn hàng vẫn tiếp tục được tạo và không có gì được gửi đi, trông y hệt một ngày bán ế.

Không điều nào ở đây là đặc thù của WooCommerce. Bài Webhook n8n không chạy xếp bảy nguyên nhân ở phía công cụ tự động hóa theo tần suất mỗi nguyên nhân hóa ra là thủ phạm, và một hàng đợi được xử lý thất thường như hàng đợi này thì đương nhiên sẽ gửi sai thứ tự, chủ đề của bài HubSpot nói eventId có thể không duy nhất. Khi các lần thử lại (retry) thực sự đến hai lần, bài Đặt chỗ hai lần cho thấy chuyện đó trông ra sao với chính người đặt đơn.

Đây cùng một kiểu lỗi với chuyện Shopify xóa subscription, với hai khác biệt khiến nó tệ hơn: ngưỡng là hơn năm lần thất bại liên tiếp thay vì tám lần như Shopify, và hoàn toàn không có email cảnh báo nào.

Nếu bạn chạy WooCommerce trên production, hãy đặt một phép kiểm tra trạng thái webhook. Một tác vụ định kỳ đọc webhook qua REST API và cảnh báo nếu status !== 'active' gần như không tốn gì, và là thứ duy nhất sẽ báo cho bạn biết.

Xác minh chữ ký

WooCommerce gửi X-WC-Webhook-Signature, một HMAC-SHA256 mã hóa base64 của raw body trong request, dùng secret (khóa bí mật dùng để ký) mà bạn đã cấu hình cho webhook.

Base64 chứ không phải hex, cùng quy ước với Shopify và ngược với Stripe lẫn GitHub. Cách encode mà từng nhà cung cấp chọn có trong tài liệu tham chiếu các nhà cung cấp webhook. Nếu bạn dùng chung một hàm xác minh cho nhiều nhà cung cấp, khác biệt về encode đó là nguồn gốc phổ biến của lỗi “chữ ký không bao giờ khớp” với một payload trông hoàn hảo.

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);
}

Như mọi nơi khác, băm đúng các byte bạn nhận được chứ không phải một object đã serialize lại. Phần so sánh giữa các nhà cung cấp có trong bài Xác minh chữ ký webhook của Stripe, GitHub và Shopify.

Một cái bẫy riêng của WooCommerce: nếu webhook được tạo mà không có secret, chữ ký được sinh từ dữ liệu khóa của chính site chứ không phải thứ bạn chọn, và bước xác minh của bạn sẽ không qua vì những lý do chẳng liên quan gì đến code. Hãy tự đặt secret.

Phần còn lại của danh sách kiểm tra

Kiểm tra lần lượt các mục sau khi đã có lượt gửi và chúng đang thất bại.

Trạng thái là active, không phải paused hay disabled. Kiểm tra mục này trước tiên, vì trạng thái disabled giải thích mọi thứ còn lại cùng một lúc.

Topic khớp với sự kiện bạn nghĩ. order.created chạy lúc đơn hàng được tạo, mà với nhiều cổng thanh toán, lúc đó đơn hàng vẫn đang ở trạng thái pending. Nếu bạn đang chờ thanh toán, order.updated kèm kiểm tra trạng thái thường mới là thứ bạn cần. Rất nhiều báo lỗi “webhook không chạy” thực ra là “webhook chạy trên một sự kiện khác với dự kiến”.

URL đích (delivery URL) truy cập được từ server, không chỉ từ laptop của bạn. Các cửa hàng nằm sau mạng nội bộ công ty hoặc trên host có rule egress khắt khe có thể thất bại ở bước này trong im lặng.

Một plugin đang chặn ngang action. Các hook của WooCommerce có thể bị filter, và các plugin cache, bảo mật hoặc hiệu năng từng được biết là can thiệp vào Action Scheduler. Test với các plugin bị tắt chẳng có gì hấp dẫn, nhưng thường mang tính quyết định.

Phiên bản API của webhook. Webhook của WooCommerce có payload theo phiên bản, và một webhook v1 tạo từ nhiều năm trước gửi dữ liệu có cấu trúc khác với v3. Nếu handler (đoạn code xử lý webhook trên server của bạn) bắt đầu lỗi sau một lần cập nhật WooCommerce, kiểm tra xem webhook đang được ghim vào phiên bản nào.

AnyHook giúp được gì

Hai vấn đề ở trên mang tính cấu trúc, và không vấn đề nào được giải quyết bằng một handler tốt hơn.

Thứ nhất là việc webhook bị vô hiệu hóa sau chuỗi thất bại liên tiếp. Trỏ webhook về in.anyhook.net/you/woo, AnyHook sẽ trả 200 sau khoảng một giây từ edge (lớp máy chủ biên của Cloudflare), nên bộ đếm thất bại liên tiếp không còn phụ thuộc vào tình trạng endpoint của chính bạn. Các lần thử lại tới endpoint của bạn chạy theo lịch của AnyHook, không phải của WooCommerce, và webhook vẫn ở trạng thái active.

Thứ hai là nhật ký gửi của WooCommerce nằm bên trong cửa hàng, theo thời gian lưu trữ của cửa hàng, trong một màn hình quản trị. AnyHook giữ mọi sự kiện kèm đầy đủ header và body, được mã hóa khi lưu trữ, phát lại (replay) được theo khoảng thời gian, nên một khung giờ deploy có thể khôi phục được mà không cần đụng tới WordPress.

  • Cảnh báo lỗi ở mốc 1, 5 và 20 lần thất bại liên tiếp, đúng loại thông báo mà WooCommerce không gửi
  • Lượt gửi được thử lại theo exponential backoff (khoảng chờ tăng theo cấp số nhân), kéo dài hơn nhiều so với một lần gửi duy nhất
  • Một header AnyHook-Signature duy nhất thay cho biến thể base64, nếu bạn nhận webhook từ nhiều nền tảng

Thứ mà một dịch vụ trung chuyển webhook (webhook relay) không sửa được là hàng đợi nằm trước nó. Nếu WP-Cron không chạy, sự kiện không bao giờ tới được đâu cả. Sửa cron trước; dịch vụ trung chuyển lo mọi thứ sau đó.

Điều cần nhớ

WooCommerce là nền tảng lớn duy nhất mà ở đó “webhook đến trễ” thường không phải bug và cũng không phải do endpoint của bạn. Kiểm tra hàng đợi Action Scheduler trước mọi thứ khác, và nếu cửa hàng ít traffic, chuyển cron sang một bộ lập lịch thật.

Sau đó đặt secret, xác minh trên các byte gốc với base64, và giám sát trạng thái của chính webhook. Vài lần thất bại liên tiếp không phải là nhiều khoảng trống, và việc vô hiệu hóa đến mà không một lời báo trước.

Tất cả bài viết6 tháng 8, 2026 · 8 phút

Đừng để mất webhook nữa.

Đổi một URL. Có ngay cơ chế thử lại, nhật ký sự kiện và phát lại bằng một cú bấm. Hoặc xem tận mắt trước: đăng ký, chạy bài diễn tập sự cố 30 giây và xem một sự kiện vẫn còn nguyên khi server của bạn ngừng hoạt động. Không cần có server.