自动化2026年8月3日8 分钟阅读

n8n Webhook 不触发:七个原因,按出现频率排序

十次里有九次是测试 URL 的问题,它只在你盯着画布时才监听。其余原因:响应模式、原始请求体已被解析,或者事件丢失。

/webhook-test/… · 只监听一个事件你点击Listen for test1 次执行你离开页面404对所有请求永远如此/webhook/… · 只在激活期间存在工作流已保存,未激活404激活路由生效已保存 ≠ 已激活
两个看起来几乎相同的 URL,行为却完全不同。大多数时候,答案就在这里。

工作流搭好了,点击 Listen for test event(监听测试事件)时跑得很顺利,你把 URL 粘贴到 Stripe,然后就再也没有动静。没有执行记录。没有报错。平台显示投递失败;更糟的是,平台显示投递成功,你这边却还是什么都没有。

原因不多,引出的困惑却很多,主要是因为 n8n 出错时几乎没有提示,而最常见的原因又是一个看起来完全正确的 URL。下面按它们实际导致问题的频率排列。

TL;DR

  • 大多数时候,问题出在测试 URL(Test URL)和生产 URL(Production URL)用混了。测试 URL 只在你盯着它的时候监听
  • 生产 URL 只有在工作流激活之后才存在
  • 响应模式默认立即回复,对大多数平台没问题,但如果你需要返回 challenge 值,就一定会失败
  • 做任何签名验证都必须打开 Raw Body(原始请求体),否则 HMAC 永远对不上
  • 执行一旦失败,事件就搁浅了。n8n 不会重试收到的 Webhook
  • 自建部署在反向代理后面时,会多出一层请求体大小和超时限制,很容易被忽略

1. 你用的是测试 URL(多半就是它)

n8n 的 Webhook 节点会显示两个 URL,它们的行为完全不同:

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

只有在你点击了 Listen for test event、并且正看着画布时,测试 URL 才处于监听状态。它记录一次执行,把数据展示给你,然后停止。你一离开这个页面,它对所有请求都返回 404。

用来搭建工作流,这个设计完全正确;用作粘贴到 Stripe 的 URL,就完全错了。而且因为它在开发时一切正常,你心里形成的印象是“这个 Webhook 能用”,真正的原因反而更难看出来。

/webhook-test/… · 只监听一个事件你点击Listen for test1 次执行你离开页面404对所有请求永远如此/webhook/… · 只在激活期间存在工作流已保存,未激活404激活路由生效已保存 ≠ 已激活

去看看你在平台上登记的 URL。如果里面有 webhook-test,问题就在这里,后面不用再看了。

2. 工作流没有激活

生产 URL 在工作流激活(发布)时才会注册。未激活的工作流,生产 URL 返回 404,因为在服务器看来,这条路由根本不存在。

坑在于保存不等于激活。你可以整天编辑、保存、关闭一个工作流,生产路由却始终不会更新。同一类的相关问题还有:

  • 为了改东西先停用工作流,改完忘了重新激活。这段时间里的每个事件都没了
  • 复制一份工作流来测试改动。副本没有激活,接收流量的仍然是原来那个
  • 修改了 Webhook 节点的路径,URL 也就变了。平台指向的仍然是旧 URL

检查编辑器里的开关,再确认平台上的 URL 与当前路径完全一致。

3. 响应模式与平台的预期不符

Webhook 节点的 Respond(响应)设置,也就是它的响应模式,决定 n8n 何时回复、回复什么:

模式行为
Immediately(立即)事件一到就返回 200 和“Workflow was started”
When Last Node Finishes(最后一个节点完成时)保持连接直到工作流执行完毕,返回最后一个节点的输出
Using 'Respond to Webhook' node(使用 Respond to Webhook 节点)由你显式控制状态码和响应体

Immediately 是正确的默认值,你应该保持不变。它返回得快,不管工作流要跑多久,你都在每个平台的响应时限之内。

有两种情况它不适用。

验证握手。有些平台要求你在订阅时原样返回一个 challenge 值,比如一个 token 或请求体的哈希。Immediately 回复的是固定消息,订阅会被拒绝。你需要用 Respond to Webhook 节点返回平台期望的确切值。

工作流很慢,又用了 When Last Node Finishes。这时你的响应时间就是整个工作流的运行时间。三个 API 调用加一次 Google Sheets 写入,轻松就要 8 秒,而 Shopify 的响应时限是五秒。平台记下一次超时,然后重试,你的工作流从头再跑一遍。悄悄制造重复行的,就是这个模式。

如果你用的是 When Last Node Finishes,又不是非得在响应里返回输出,就切换到 Immediately。

4. 没开 Raw Body,签名验证失败

如果你在 n8n 里验证 Stripe、Shopify 或 GitHub 的签名,Webhook 节点的 Raw Body 选项必须打开。

平台对它发出的原始字节计算 HMAC。Raw Body 关闭时,n8n 会解析 JSON,之后任何重新序列化它的 Code 节点都会产生不同的字节:键的顺序不同,空白字符不同。数据完全相同,签名却不同。你会盯着一个肉眼看完全正确的请求体,验证却一直失败,这一个小时足以让人抓狂。

打开 Raw Body,然后在解析任何内容之前,对原始字符串计算 HMAC:

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

用 timingSafeEqual,不要用 ===。另外注意,长度不一致时它会抛出异常,如果传给它的是不可信的输入,要先做好防护。各平台的签名格式详见验证 Stripe、GitHub 和 Shopify 的 Webhook 签名。

5. 执行一旦失败,事件就搁浅了

这是真正会造成金钱损失的一种,而且它不是 bug。这个模型本来就是这样工作的。

Respond 设为 Immediately 时,n8n 在工作流运行之前就已经返回了 200。平台认为事件已经投递,永远不会再发。如果第 4 个节点随后因为 token 过期、限流或字段格式错误而失败,这次执行会被标记为失败,而这个事件除了那条失败的执行记录,在哪里都不存在。

收到的 Webhook 没有自动重试。它前面也没有队列。平台不会重发,因为在它看来,一切正常。

应对办法:

  • 在工作流设置里配置 Error Workflow(错误工作流)。它至少要能通知你。静默失败是默认行为,而且不是一个好的默认行为
  • 把保存原始请求体作为第 1 个节点。在做任何转换之前,先把请求体写进 Postgres、Airtable、Google Sheet,任何持久化的存储都行。这样失败的执行可以从你自己的存储里重放,而不是就此丢失
  • 只在允许部分成功的节点上有意识地使用 continueOnFail,不要用在会悄悄吞掉你需要看到的失败的节点上
  • 检查保留期。自建的 n8n 会定期清理执行数据,一旦被清理,连失败执行的请求体也没了

6. 自建部署:反向代理挡在中间

如果你是自建部署,放在 nginx、Traefik 或 Caddy 后面,有几层可能在 n8n 看到请求之前就把它吞掉:

  • 请求体大小限制。nginx 的 client_max_body_size 默认是 1 MB,超过就返回 413。Shopify 的批量请求体或 base64 编码的附件都会超过这个值
  • 代理超时。如果 proxy_read_timeout 比你的工作流运行时间短,代理会关闭连接,即使 n8n 已经执行完毕,平台也会记一次失败
  • 没有设置 WEBHOOK_URL。n8n 用它生成界面上显示的 URL。在代理后面,它会显示内网地址,在你的笔记本上能访问,从公网却访问不了
  • 队列模式下的 worker。队列模式中,Webhook 必须到达主实例,路由到 worker 会得到 404

最快的诊断方法:从你的网络外部对生产 URL 执行 curl -X POST。404 说明是路由或激活问题。413 说明是请求体大小。请求一直挂着不返回,说明是超时。返回 200 但 Executions(执行记录)里什么都没有,说明这次运行没有被保存:去工作流设置里检查保存生产执行记录的选项。

7. 请求到了,但被过滤条件丢掉了

这种情况不太常见,但值得排除:执行记录存在,却好像什么都没做。对照真实请求体的结构,检查 IF 或 Switch 节点的条件,因为平台的嵌套方式往往和你搭建时用的测试请求体不同。如果根本没有执行记录,检查节点上的 HTTP Method,它默认是 GET,平台向只接受 GET 的 Webhook 发送 POST,会得到 404,看起来和路由问题完全一样。

AnyHook 能做什么

原因 5 是结构性的。n8n 负责执行工作流。它不会先把事件保存下来,它返回 200 的那一刻,事件唯一的另一份副本在平台手里。

在前面加一层 AnyHook,“已投递”的含义就变了。你把平台指向 in.anyhook.net/you/app,再把 AnyHook 指向 n8n 的生产 URL,只改两个 URL,不用重写工作流。之后:

  • 事件在调用 n8n 之前就已持久化,执行失败的事件可以从日志里重放
  • 对你的 n8n 实例按指数退避重试,升级时实例重启,之后会自动恢复
  • 签名验证在边缘节点完成。平台的密钥只需配置一次,你收到的是统一的 AnyHook-Signature 请求头,原因 4 里围绕 Raw Body 的那套折腾,就变成一个 HMAC 校验,写一次,所有平台通用
  • 连续失败 1 次、5 次和 20 次时发送失败告警,并自动暂停,避免出错的工作流引发重试风暴

实际中最要紧的是这个场景:一次 n8n 升级让实例停机十分钟,这十分钟里的每个事件都可能丢掉。前面有一层转发服务的话,代价只是点一下重放。

设置过程不需要写代码:

  1. 在 AnyHook 里创建一个 App。如果发送方在来源列表里(Shopify 就在),选它作为来源;不在列表里的(比如 Typeform)选 Generic。把转发目标设为工作流的生产 Webhook URL。
  2. 在发送方那边,把 Webhook URL 换成这个 App 的接收 URL。在 Shopify 后台是 Settings → Notifications → Webhooks;在 Typeform 里是 Connect → Webhooks。
  3. 如果工作流以 n8n 自带的 Shopify Trigger 或 Typeform Trigger 节点开头,把它换成普通的 Webhook 节点。Trigger 节点会自己向平台注册 Webhook,这样就绕过了 AnyHook。
  4. 宕机过后,在 AnyHook 里打开这个 App,从失败事件横幅里重放。

一个 App 对应一个发送方,Free 套餐只有一个 App,所以第二个发送方就需要 Pro 套餐。

总结

先检查 URL。webhook-test 还是 webhook,这个答案出现的频率远高于其他任何原因,激活问题紧随其后。能正常接收之后,趁还没忘,先把持久化补上:第一个节点就写入原始请求体,再配置一个错误工作流。n8n 不会重发它丢掉的东西,平台也不会。

全部文章2026年8月3日 · 8 分钟

别再丢 Webhook 了。

只改一个 URL。获得重试、事件日志和一键重放。也可以先亲眼看看:注册后跑一次 30 秒的故障演练,看一个事件如何挺过你的服务器宕机。不需要服务器。