Tự động hóa3 tháng 8, 20268 phút đọc

n8n webhook không chạy: bảy nguyên nhân, xếp theo mức độ thường gặp

Chín trên mười lần là do URL test, vốn chỉ lắng nghe khi bạn đang theo dõi. Phần còn lại: chế độ phản hồi, raw body đã bị parse hoặc sự kiện bị mất.

/webhook-test/… · CHỈ NGHE MỘT SỰ KIỆNbạn bấmListen for test1 lần chạybạn rời trang404với mọi requestmãi mãi/webhook/… · CHỈ TỒN TẠI KHI ĐANG KÍCH HOẠTworkflowđã lưu, chưa kích hoạt404kích hoạtroute hoạt độngđã lưu ≠ đã kích hoạt
Hai URL trông gần như giống hệt nhau nhưng hoạt động hoàn toàn khác nhau. Phần lớn trường hợp, đây chính là câu trả lời.

Bạn đã dựng xong workflow, nó chạy trơn tru khi bạn bấm “Listen for test event”, bạn dán URL vào Stripe, rồi chẳng có gì xảy ra. Không có lần chạy (execution) nào. Không có lỗi nào. Nhà cung cấp báo lượt gửi thất bại, hoặc tệ hơn, báo thành công mà bạn vẫn không nhận được gì.

Số nguyên nhân thì ít, nhưng sự bối rối thì kéo dài, chủ yếu vì n8n báo lỗi rất lặng lẽ và nguyên nhân phổ biến nhất lại là một URL trông hoàn toàn đúng. Dưới đây là các nguyên nhân, xếp theo tần suất chúng thực sự là thủ phạm.

TL;DR

  • URL test hay URL production: phần lớn trường hợp, câu trả lời nằm ở đây. URL test chỉ lắng nghe khi bạn đang nhìn vào nó
  • URL production chỉ tồn tại khi workflow đã được kích hoạt
  • Chế độ phản hồi mặc định là trả lời ngay, ổn với hầu hết nhà cung cấp nhưng hỏng hẳn nếu bạn cần trả về một challenge
  • Phải bật Raw Body nếu muốn xác minh chữ ký, nếu không HMAC sẽ không bao giờ khớp
  • Một lần chạy thất bại khiến sự kiện bị kẹt lại. n8n không thử lại (retry) các webhook đến
  • Tự host sau reverse proxy là thêm một lớp giới hạn kích thước body và timeout mà mọi người hay quên

1. Bạn đang dùng URL test (thường là nó)

Node Webhook của n8n hiển thị hai URL, và chúng hoạt động hoàn toàn khác nhau:

Test:        https://your-instance/webhook-test/abc-123
Production:  https://your-instance/webhook/abc-123

URL test chỉ hoạt động khi bạn đã bấm “Listen for test event” và đang xem canvas. Nó ghi nhận một lần chạy, cho bạn xem dữ liệu rồi dừng. Ngay khi bạn chuyển sang trang khác, nó trả 404 cho mọi request.

Thiết kế đó hoàn toàn đúng khi đang xây dựng, và hoàn toàn sai với URL bạn dán vào Stripe. Vì nó chạy hoàn hảo lúc phát triển, trong đầu bạn hình thành suy nghĩ “webhook chạy được rồi”, và chính điều đó khiến nguyên nhân thật khó nhìn ra.

/webhook-test/… · CHỈ NGHE MỘT SỰ KIỆNbạn bấmListen for test1 lần chạybạn rời trang404với mọi requestmãi mãi/webhook/… · CHỈ TỒN TẠI KHI ĐANG KÍCH HOẠTworkflowđã lưu, chưa kích hoạt404kích hoạtroute hoạt độngđã lưu ≠ đã kích hoạt

Mở phần cài đặt của nhà cung cấp và xem URL đã đăng ký. Nếu nó chứa webhook-test, đó chính là lỗi của bạn, và bạn có thể dừng đọc ở đây.

2. Workflow chưa được kích hoạt

URL production được đăng ký khi workflow được kích hoạt (publish). URL production của một workflow chưa kích hoạt trả về 404, vì với server, route đó không tồn tại.

Cái bẫy là lưu không có nghĩa là kích hoạt. Bạn có thể sửa, lưu rồi đóng workflow cả ngày mà route production vẫn không hề cập nhật. Những lỗi cùng họ:

  • Tắt kích hoạt để sửa, rồi quên bật lại. Mọi sự kiện trong khoảng thời gian đó đều mất
  • Nhân bản workflow để thử một thay đổi. Bản sao chưa được kích hoạt, và bản gốc vẫn là bản đang nhận traffic
  • Đổi path của node webhook, tức là đổi URL. Nhà cung cấp vẫn trỏ vào URL cũ

Kiểm tra công tắc trong editor, và kiểm tra URL bên nhà cung cấp khớp chính xác với path hiện tại.

3. Chế độ phản hồi không khớp với điều nhà cung cấp mong đợi

Tùy chọn Respond của node Webhook, tức chế độ phản hồi của nó, quyết định n8n trả lời khi nào và trả lời gì:

Chế độCách hoạt động
ImmediatelyTrả 200 kèm “Workflow was started” ngay khi sự kiện đến
When Last Node FinishesGiữ kết nối đến khi workflow chạy xong, trả về output của node cuối
Using 'Respond to Webhook' nodeBạn tự quyết định status và body

Immediately là mặc định đúng và bạn nên giữ nguyên. Nó trả lời nhanh, nên bạn luôn nằm trong timeout của mọi nhà cung cấp, bất kể workflow chạy bao lâu.

Có hai trường hợp nó sai.

Handshake xác minh. Một số nhà cung cấp yêu cầu bạn trả lại một giá trị challenge khi đăng ký, chẳng hạn một token hoặc giá trị băm của body. “Immediately” trả lời bằng một thông điệp cố định nên không đăng ký được. Bạn cần node Respond to Webhook để trả về đúng giá trị được mong đợi.

When Last Node Finishes, với một workflow chậm. Lúc này thời gian phản hồi chính là toàn bộ thời gian chạy của workflow. Ba lệnh gọi API cộng một lần ghi Google Sheets dễ dàng mất 8 giây, trong khi thời hạn phản hồi của Shopify là năm giây. Nhà cung cấp ghi nhận timeout, thử lại, và workflow của bạn chạy lần thứ hai từ đầu. Đây là chế độ âm thầm tạo ra các dòng trùng lặp.

Nếu bạn đang dùng “When Last Node Finishes” và không thực sự cần output trong response, hãy chuyển sang Immediately.

4. Không xác minh được chữ ký vì Raw Body đang tắt

Nếu bạn xác minh chữ ký của Stripe, Shopify hoặc GitHub ngay trong n8n, tùy chọn Raw Body của node Webhook là bắt buộc.

Nhà cung cấp tính HMAC trên đúng các byte họ đã gửi. Khi Raw Body tắt, n8n parse JSON, và bất kỳ node Code nào serialize lại nó cũng tạo ra các byte khác: thứ tự key khác, khoảng trắng khác. Dữ liệu giống hệt nhưng chữ ký thì không. Bạn sẽ nhìn chằm chằm vào một payload rõ ràng là đúng trong khi bước xác minh vẫn thất bại, và đó là một giờ đồng hồ thật sự phát điên.

Bật Raw Body, rồi tính HMAC trên chuỗi thô trước khi parse bất cứ thứ gì:

const crypto = require('crypto');
const raw = $input.first().binary
  ? Buffer.from($input.first().binary.data.data, 'base64').toString('utf8')
  : $input.first().json.body;

const expected = crypto
  .createHmac('sha256', $env.SHOPIFY_SECRET)
  .update(raw, 'utf8')
  .digest('base64');

const received = $input.first().json.headers['x-shopify-hmac-sha256'];
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
  throw new Error('invalid signature');
}
return { json: JSON.parse(raw) };

Dùng timingSafeEqual, không dùng ===. Lưu ý hàm này ném lỗi khi độ dài không khớp, nên cần chặn trường hợp đó nếu đầu vào không đáng tin. Định dạng của từng nhà cung cấp có trong bài Xác minh chữ ký webhook của Stripe, GitHub và Shopify.

5. Một lần chạy thất bại khiến sự kiện bị kẹt lại

Đây là nguyên nhân gây thiệt hại bằng tiền thật, và nó không phải bug. Mô hình hoạt động vốn là như vậy.

Khi Respond đặt là Immediately, n8n đã trả 200 trước khi workflow chạy. Nhà cung cấp coi sự kiện đã được gửi thành công và sẽ không bao giờ gửi nó thêm lần nào nữa. Nếu sau đó node 4 lỗi vì token hết hạn, vì giới hạn tốc độ (rate limit) hoặc vì một trường sai định dạng, lần chạy đó bị đánh dấu thất bại và sự kiện không còn tồn tại ở đâu ngoài bản ghi của chính lần chạy thất bại đó.

Không có cơ chế tự động thử lại webhook đến. Không có hàng đợi nào phía trước nó. Nhà cung cấp cũng không thử lại, vì với họ mọi thứ đều ổn.

Cách xử lý:

  • Đặt một Error Workflow trong phần cài đặt workflow. Tối thiểu, nó phải báo cho bạn biết. Lỗi âm thầm là mặc định, và đó không phải một mặc định tốt
  • Lưu payload thô ở node 1. Trước mọi bước biến đổi, ghi body vào Postgres, Airtable, Google Sheet, bất cứ nơi nào bền vững. Khi đó một lần chạy thất bại có thể phát lại (replay) từ kho của chính bạn thay vì mất hẳn
  • Dùng continueOnFail có chủ đích trên những node chấp nhận thành công một phần, và không dùng trên những node mà nó sẽ âm thầm nuốt mất lỗi bạn cần thấy
  • Kiểm tra thời gian lưu trữ. n8n tự host dọn dữ liệu lần chạy theo lịch, và khi đã bị dọn thì ngay cả payload của lần chạy thất bại cũng mất

6. Tự host: reverse proxy chắn đường

Nếu bạn tự host sau nginx, Traefik hoặc Caddy, vài lớp có thể nuốt request trước khi n8n kịp thấy:

  • Giới hạn kích thước body. nginx mặc định client_max_body_size là 1 MB và trả 413 cho mọi thứ lớn hơn. Một payload bulk của Shopify hay một tệp đính kèm base64 sẽ vượt mức đó
  • Timeout của proxy. Nếu proxy_read_timeout ngắn hơn thời gian chạy workflow, proxy đóng kết nối và nhà cung cấp ghi nhận thất bại dù n8n đã chạy xong
  • Chưa đặt WEBHOOK_URL. n8n dựa vào biến này để tạo các URL nó hiển thị. Sau proxy, nó sẽ hiển thị địa chỉ nội bộ, dùng được từ laptop của bạn nhưng không dùng được từ internet
  • Worker ở queue mode. Ở queue mode, webhook phải tới được instance chính; route nó tới một worker sẽ nhận 404

Cách chẩn đoán nhanh: chạy curl -X POST tới URL production từ bên ngoài mạng của bạn. 404 nghĩa là lỗi routing hoặc chưa kích hoạt. 413 nghĩa là kích thước body. Treo nghĩa là timeout. 200 mà Executions trống trơn nghĩa là lần chạy không được lưu lại: kiểm tra các tùy chọn lưu lần chạy production trong phần cài đặt của workflow.

7. Sự kiện đã đến, nhưng một bộ lọc đã bỏ qua nó

Ít gặp hơn, nhưng đáng loại trừ: lần chạy có tồn tại nhưng có vẻ chẳng làm gì. Kiểm tra điều kiện của các node IF hoặc Switch với cấu trúc payload thật, vì nhà cung cấp lồng dữ liệu khác với payload test mà bạn dựa vào lúc xây dựng. Nếu hoàn toàn không có lần chạy nào, kiểm tra HTTP Method trên node, mặc định là GET, vì nhà cung cấp gửi POST tới một webhook chỉ nhận GET sẽ nhận 404, trông y hệt một lỗi routing.

AnyHook giúp được gì

Nguyên nhân 5 là vấn đề mang tính cấu trúc. n8n chạy workflow. Nó không lưu sự kiện trước, và ngay khi nó trả 200, ngoài nó ra thì chỉ nhà cung cấp từng giữ một bản của sự kiện.

Đặt AnyHook phía trước sẽ thay đổi ý nghĩa của “đã gửi thành công”. Bạn trỏ nhà cung cấp tới in.anyhook.net/you/app và trỏ AnyHook tới URL production của n8n: hai lần đổi URL, không phải viết lại workflow. Từ đó:

  • Sự kiện được lưu trước khi n8n được gọi, nên một lần chạy thất bại có thể phát lại từ nhật ký sự kiện
  • Các lần thử lại chạy theo exponential backoff (khoảng chờ tăng theo cấp số nhân) tới instance n8n của bạn, nên một lần khởi động lại trong lúc nâng cấp sẽ tự hồi phục
  • Xác minh chữ ký diễn ra tại edge (lớp máy chủ biên của Cloudflare). Cấu hình secret của nhà cung cấp một lần và nhận về một header AnyHook-Signature duy nhất, nên màn xoay xở với Raw Body ở nguyên nhân 4 trở thành một bước kiểm tra HMAC bạn viết một lần cho mọi nhà cung cấp
  • Cảnh báo lỗi được gửi khi có 1, 5 và 20 lần thất bại liên tiếp, kèm tự động tạm dừng để một workflow hỏng không bị thử lại dồn dập

Điều quan trọng trong thực tế: một lần nâng cấp n8n làm instance ngừng hoạt động mười phút có thể khiến bạn mất mọi sự kiện trong mười phút đó. Có một dịch vụ trung chuyển webhook (webhook relay) phía trước, cái giá chỉ là một cú bấm phát lại.

Thiết lập không cần viết code:

  1. Trong AnyHook, tạo một App (một URL nhận cho một nguồn webhook, ví dụ “Stripe production”). Chọn bên gửi làm nguồn nếu nó có trong danh sách (Shopify có), hoặc chọn Generic nếu không có, chẳng hạn Typeform. Đặt đích là URL webhook Production của workflow.
  2. Ở bên gửi, thay URL webhook bằng URL nhận (inbound URL) của App. Trong Shopify admin, đó là Settings → Notifications → Webhooks; trong Typeform là Connect → Webhooks.
  3. Nếu workflow bắt đầu bằng node Shopify Trigger hoặc Typeform Trigger có sẵn của n8n, thay nó bằng một node Webhook thường. Các node Trigger tự đăng ký webhook riêng với nhà cung cấp, tức là đi vòng qua AnyHook.
  4. Sau một sự cố ngừng hoạt động, mở App trong AnyHook và phát lại từ banner các sự kiện thất bại.

Mỗi App phục vụ một bên gửi, và gói Free có một App, nên bên gửi thứ hai đồng nghĩa với gói Pro.

Điều cần nhớ

Kiểm tra URL trước tiên. webhook-test hay webhook là câu trả lời thường gặp hơn hẳn mọi thứ khác, và việc kích hoạt đứng thứ hai, sát nút. Khi đã nhận được sự kiện, điều cần sửa trước khi bạn quên là lưu trữ bền vững: ghi payload thô ở node đầu tiên, và đặt một error workflow. n8n sẽ không tự lấy lại những gì nó đã đánh rơi, và nhà cung cấp cũng sẽ không gửi thêm lần nữa.

Tất cả bài viết3 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.