Debug18 tháng 4, 20267 phút đọc

Debug webhook thất bại trên production mà không mất cả cuối tuần

Debug lượt gửi webhook: cần xem gì trong header, mã trạng thái, thời gian và payload khi tích hợp của bạn ngừng hoạt động.

NHỮNG GÌ DASHBOARD CỦA NHÀ CUNG CẤP GHI LẠInhà cung cấpnhật ký gửi2xxđã tới nơi, rồi mất sau khi đã phản hồi4xxbạn đã từ chối nó: chữ ký, WAF, sai URL5xxhandler của bạn ném lỗi đúng vào thời điểm đótimeouthandler quá chậm; chuyển phần việc sang bất đồng bộ
Bắt đầu từ nhật ký gửi của chính nhà cung cấp. Mã trạng thái được ghi ở đó cho bạn biết cần mở lớp nào tiếp theo, và giúp bạn bỏ qua khoảng ba mươi phút đoán mò.

Một khách hàng gửi email: “webhook Stripe của tôi bị hỏng”. Bạn kiểm tra dashboard của Stripe và các sự kiện vẫn đang được bắn đi. Bạn kiểm tra log server và chẳng có gì. Sentry, cũng không. Sự kiện đang đi đâu đó, chỉ là không phải chỗ bạn nghĩ.

Bài này là khung từng bước để debug các vấn đề gửi webhook trên production, dựa trên những kiểu hỏng hay gặp nhất.

TL;DR

  • Bắt đầu từ dashboard của nhà cung cấp: sự kiện có được bắn đi không, và nó nhận về mã trạng thái gì?
  • 4xx nghĩa là bạn đang từ chối nó (chữ ký, middleware, sai URL)
  • 5xx nghĩa là handler (đoạn code xử lý webhook trên server của bạn) bị crash. Kiểm tra hệ thống theo dõi lỗi đúng vào thời điểm đó
  • Timeout nghĩa là handler quá chậm. Chuyển phần việc nặng sang xử lý bất đồng bộ và trả về 200 thật nhanh
  • Cách chữa cho chuyện “mất 2 giờ để debug” là một nơi duy nhất hiển thị cùng lúc request đến, request đi, các lần thử lại (retry) và phát lại (replay), mà thường thì đó là một dịch vụ trung chuyển webhook (webhook relay)

Cây quyết định

Bắt đầu từ đây. Nó sẽ giúp bạn bớt 30 phút đoán mò.

Q1: Nhà cung cấp có thực sự gửi sự kiện không?

Kiểm tra dashboard của nhà cung cấp (Stripe: Developers → Events; GitHub: Settings → Webhooks → Recent Deliveries; Shopify: Dev Dashboard → Apps → app của bạn → Logs).

Nếu sự kiện không có ở đó, trigger không chạy, nên hãy đi debug trạng thái phía upstream lẽ ra phải tạo ra nó. Nếu có, chuyển sang Q2.

Q2: Nhà cung cấp có nhận lại mã 2xx không?

Trong cùng dashboard đó, xem mã trạng thái phản hồi từ endpoint của bạn.

  • 2xx: nhà cung cấp cho rằng đã thành công. Nếu app của bạn chưa bao giờ thấy sự kiện, thì nó đã tới và bị rơi mất sau khi phản hồi. Kiểm tra hàng đợi bất đồng bộ, lệnh ghi DB và logic của handler.
  • 4xx: bạn đang từ chối nó. Thường gặp nhất là xác minh chữ ký thất bại, thiếu header hoặc một middleware chặn request: WAF, bộ giới hạn tốc độ (rate limiter), lớp xác thực.
  • 5xx: endpoint của bạn bị crash. Kiểm tra hệ thống theo dõi lỗi, không chỉ “một giờ vừa qua” mà đúng thời điểm của lượt gửi này.
  • Timeout: endpoint của bạn quá chậm. Xem Q4.
  • Connection refused hoặc lỗi DNS: không kết nối được tới endpoint, một vấn đề khác hẳn, nằm đâu đó ở mạng, DNS hoặc firewall.

Q3: Nếu là 4xx, mã nào?

  • 400, bad request. Thường là header không khớp, hoặc bước kiểm tra chữ ký ném lỗi trước khi kịp trả về một lỗi rõ ràng. Kiểm tra xem handler trả về gì khi body sai định dạng hoặc thiếu chữ ký.
  • 401 / 403, bạn đang từ chối request vì không được phép. Xác minh chữ ký thất bại? Middleware kiểm tra session người dùng trên một endpoint lẽ ra không cần?
  • 404, URL sai. Bạn có deploy một thay đổi route làm endpoint webhook đổi chỗ không?
  • 413, payload quá lớn. Nâng giới hạn của body-parser.
  • 429, bộ giới hạn tốc độ đã bị kích hoạt. Thứ gì đang giới hạn tốc độ route webhook của bạn thì không nên làm vậy. Phần lớn nhà cung cấp webhook gửi từ một tập IP hữu hạn, và bộ giới hạn của bạn có thể đang coi tất cả chúng là một kẻ tấn công.

Q4: Nếu timeout, cái gì đang chậm?

GitHub cho bạn 10 giây. Shopify cho 5 giây. Slack và Discord cho 3 giây. Stripe chưa bao giờ công bố con số; tài liệu của họ chỉ nói trả về 2xx trước mọi logic chậm. Bảng đầy đủ, mỗi hàng kèm nguồn, nằm trong trang tra cứu nhà cung cấp webhook.

waitUntil · MÃ 200 LÀ LỜI HỨA BẠN KHÔNG GIỮ ĐƯỢCxác minhtrả về 200waitUntil(work)chỉ nằm trong bộ nhớbị thu hồi → mất, không gửi lạiLƯU TRƯỚC · MÃ 200 LÀ THẬTxác minhghi bền vữngtrả về 200xử lýthử lại được
Cấu trúc giúp bạn luôn nằm trong timeout của mọi nhà cung cấp, bất kể phần việc thật chậm đến đâu.

Phần lớn timeout đến từ việc làm quá nhiều thứ trong handler. Thủ phạm thường gặp là ghi database đồng bộ vào một pool đã bão hòa, gọi ra một dịch vụ khác (Stripe, Salesforce, Notion, API của chính bạn) từ bên trong handler, gửi email hay tin nhắn Slack đồng bộ và ghi toàn bộ body ra stdout trong khi hệ thống thu log đang bị backpressure.

Khuôn mẫu khắc phục: xác minh chữ ký, đưa sự kiện vào hàng đợi, trả về 200. Toàn bộ business logic diễn ra ở downstream, bất đồng bộ.

Thứ thường thực sự bị hỏng

Sau vài năm theo dõi chuyện này, đây là phần 80/20:

  1. Bước xác minh chữ ký đang serialize lại body. Xem hướng dẫn xác minh chữ ký
  2. Một proxy hoặc WAF đang nuốt webhook trước khi nó tới handler. Kiểm tra log request của Cloudflare, Vercel hoặc nền tảng bạn đang dùng
  3. Giới hạn tốc độ đang bị kích hoạt. Bot Management của Cloudflare có thể nhận nhầm webhook hợp lệ là bot
  4. URL của endpoint đã đổi trong một lần deploy và không ai cập nhật lại bên nhà cung cấp
  5. Thiếu một biến môi trường trên production, như signing secret (khóa bí mật dùng để ký) hay DB URL, và handler lặng lẽ trả về 500
  6. Một background job bị crash sau khi webhook đã trả về 200. Nhà cung cấp tưởng mọi thứ ổn, nhưng không có gì được xử lý

Số 6 là bệnh kinh niên trên serverless, nơi phần việc được lên lịch sau khi phản hồi hoàn toàn không có bảo đảm bền vững nào: Nhận webhook trên Vercel mà không bị mất. Còn nếu triệu chứng là bản trùng chứ không phải bị lỡ, thì thường là hệ thống đang chạy đúng thiết kế: Bạn nhận cùng một webhook hai lần.

Công cụ thực sự có ích

Dashboard của nhà cung cấp tốt khi xem từng sự kiện một và tệ khi cần nhìn ra quy luật. Nếu bạn muốn “mọi sự kiện thất bại trong 24 giờ qua”, bạn sẽ phải cuộn tay.

ngrok và webhook.site dành cho phát triển trên máy local. Chúng không giúp được gì trên production.

Log của chính bạn có ích nếu bạn đã ghi đúng thứ. Nếu bạn ghi “webhook received for event evt_xxx” trước bước kiểm tra chữ ký, ít nhất bạn biết nó đã tới. Phần lớn mọi người ghi log sau, khiến các sự kiện có chữ ký không hợp lệ trở nên vô hình.

Một dịch vụ trung chuyển webhook như AnyHook đứng trước handler và ghi lại header, body, thời gian, mã trạng thái phản hồi và số lần gửi. Vòng debug trở thành: xem luồng sự kiện, bấm vào sự kiện lỗi, xem raw payload, phát lại nó.

Vòng debug với AnyHook

Khi một người dùng gửi email cho bộ phận hỗ trợ của AnyHook với nội dung “webhook của tôi không chạy”, dashboard thường cho thấy rõ nguyên nhân trong chưa tới một phút:

  1. Mở luồng sự kiện, lọc theo status=failed
  2. Bấm vào sự kiện lỗi
  3. Xem header đầu vào (có chữ ký không?), body đầu vào (có đúng cấu trúc bạn mong đợi không?), phản hồi đầu ra (server của bạn đã trả lời gì?) và bảng thời gian theo từng chặng: edge (lớp máy chủ biên của Cloudflare) → hàng đợi → gửi đi
  4. Nếu đó là sự cố một lần, như một lần deploy lỗi hay mạng chập chờn, bấm Replay và xem nó thành công
  5. Nếu mọi sự kiện đều thất bại, giờ bạn biết chính xác cần sửa gì, và trong lúc bạn sửa, các sự kiện vẫn được lưu và phát lại được trong thời hạn phát lại của gói (3 ngày trên gói Free, 30 ngày trên gói Pro, 90 ngày trên gói Scale)

Điều biến một buổi debug 2 giờ thành 2 phút là có được request mà nhà cung cấp thực sự đã gửi, phản hồi mà server của bạn thực sự đã trả về và các mốc thời gian giữa chúng, tất cả ở một chỗ. Không có những thứ đó, bạn phải dựng lại mọi chuyện từ ba dashboard và log của chính mình.

Để xem thử trước khi đăng ký bất cứ thứ gì, một lệnh duy nhất trả về một URL nhận (inbound URL) đang hoạt động, không cần tài khoản:

curl -X POST https://anyhook.net/api/v1/quickstart

Phản hồi có một inbound_url để trỏ nhà cung cấp tới, một api_key để đọc nhật ký sự kiện qua API và một claim_url. Endpoint này là tạm thời: nó và các sự kiện của nó bị xóa sau 7 ngày, trừ khi bạn mở claim URL và đăng nhập để nhận về tài khoản, khi đó URL vẫn giữ nguyên.

Điều cần nhớ

Nếu những buổi chiều Chủ nhật như thế này đến với bạn thường xuyên hơn bạn muốn, AnyHook biến phần lớn công việc đó thành một cú bấm trên dashboard.

Tất cả bài viết18 tháng 4, 2026 · 7 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.