调试2026年4月18日7 分钟阅读

在生产环境排查失败的 Webhook,不用搭上整个周末

排查 Webhook 投递问题:集成停止工作时,请求头、状态码、耗时和请求体里该看什么,以及 4xx、5xx 和超时分别意味着什么。

平台控制台记录了什么平台投递日志2xx到达了,返回响应之后被丢弃4xx你拒绝了它:签名、WAF、URL 错误5xx你的处理程序恰好在那个时间点抛出了异常超时处理程序太慢;把工作改成异步
从平台自己的投递日志开始。它记录的状态会告诉你接下来该查哪一层,省下大约三十分钟的瞎猜。

一位客户发来邮件:“我的 Stripe Webhook 坏了。”你打开 Stripe 控制台,事件都在正常触发。你查服务器日志,什么都没有。Sentry,也什么都没有。事件确实去了某个地方,只是不在你以为的地方。

这篇文章给出一套排查生产环境 Webhook 投递问题的分步框架,依据的是最常出现的那些故障模式。

TL;DR

  • 从平台控制台开始:事件有没有触发,它收到的是什么状态码?
  • 4xx 说明是你在拒收它(签名、中间件、URL 错误)
  • 5xx 说明你的处理程序崩溃了。去错误监控里查那个确切的时间点
  • 超时说明你的处理程序太慢。把繁重的工作改成异步,尽快返回 200
  • 要解决“排查花了 2 个小时”,就需要一个能把接收、转发、重试和重放放在一起看的地方,这通常意味着一个 Webhook 转发服务

决策树

从这里开始。它能帮你省下 30 分钟的瞎猜。

Q1:平台真的在发送这个事件吗?

查看平台的控制台:Stripe 在 Developers → Events(开发者 → 事件),GitHub 在 Settings → Webhooks → Recent Deliveries(设置 → Webhook → 最近交付),Shopify 在 Dev Dashboard → Apps → 你的应用 → Logs。

如果事件不在那里,说明触发条件没有生效,去排查本该生成它的上游状态。如果在,进入 Q2。

Q2:平台收到 2xx 了吗?

在同一个控制台里,查看你的端点返回的响应状态码。

  • 2xx:平台认为投递成功了。如果你的应用从没见过这个事件,那它是到达了,却在响应之后被丢掉了。检查你的异步队列、数据库写入和处理程序逻辑。
  • 4xx:是你在拒收它。最常见的是签名验证失败、缺少某个请求头,或者中间件(WAF、限流器、认证层)拦下了请求。
  • 5xx:你的端点崩溃了。去查错误监控,不要只看“最近一小时”,而要看这次投递的确切时间点。
  • 超时:你的端点太慢。见 Q4。
  • 连接被拒绝或 DNS 错误:你的端点无法访问,这是另一类问题,出在网络、DNS 或防火墙的某个环节。

Q3:如果是 4xx,具体是哪一个?

  • 400,错误请求。通常是请求头不匹配,或者你的签名检查在返回明确错误之前就抛了异常。检查请求体格式错误或缺少签名时,你的处理程序返回的是什么。
  • 401 / 403,你把请求当作未授权拒收了。是签名验证失败?还是中间件在一个本不需要用户会话的端点上检查了用户会话?
  • 404,URL 错了。你是不是部署了一次路由变更,把 Webhook 端点挪走了?
  • 413,请求体过大。调高你的 body-parser 上限。
  • 429,触发了限流。不管是什么在对你的 Webhook 路由限流,都不应该这样做。大多数 Webhook 平台都从一组有限的 IP 发出请求,你的限流器可能把它们全当成了同一个攻击者。

Q4:如果是超时,慢在哪里?

GitHub 给你 10 秒。Shopify 给 5 秒。Slack 和 Discord 给 3 秒。Stripe 从没公布过具体数字;它的文档只说要在任何耗时逻辑之前返回 2xx。完整的表格(每一行都附出处)见 Webhook 平台参考。

waitUntil · 这个 200 是你兑现不了的承诺验证返回 200waitUntil(work)只在内存中被回收 → 丢失,不会再重发先持久化 · 这个 200 是真的验证持久化写入返回 200处理可重试
不管真正的工作有多慢,这种结构都能让你守住每个平台的超时时限。

大多数超时都来自在处理程序里做了太多事。最常见的原因有:往已经饱和的连接池里同步写数据库,在处理程序里调用另一个服务(Stripe、Salesforce、Notion、你自己的 API),同步发送邮件或 Slack 消息,以及在日志采集端正在施加背压时,把整个请求体打印到 stdout。

修复模式是:验证签名,把事件放进队列,返回 200。所有业务逻辑都在下游异步进行。

通常真正坏掉的是什么

看了几年这类问题,下面是 80/20 法则里最常见的那些情况:

  1. 签名验证时重新序列化了请求体。见签名验证指南
  2. 代理或 WAF 在 Webhook 到达处理程序之前就把它吞了。去 Cloudflare、Vercel 或你所用平台的请求日志里查
  3. 触发了限流。Cloudflare 的 Bot Management 可能会把正常的 Webhook 误判为机器人流量
  4. 一次部署改了端点 URL,却没人去平台那里更新
  5. 生产环境缺了某个环境变量(签名密钥、数据库 URL),处理程序静默地返回 500
  6. 后台任务在 Webhook 返回 200 之后崩溃了。平台以为一切正常,但什么都没有被处理

第 6 条在 Serverless 平台上非常普遍,因为安排在响应之后的工作完全没有持久性保证:见在 Vercel 上接收 Webhook,不让事件丢失。而如果症状是重复而不是缺失,那通常是设计如此:见同一个 Webhook 收到了两次。

真正有用的工具

平台控制台适合一次看一个事件,不适合看规律。如果你想看“过去 24 小时内所有失败的事件”,就只能手动翻页。

ngrok 和 webhook.site 是给本地开发用的。在生产环境里帮不上你。

如果你记录了对的东西,你自己的日志就有用。如果你在签名检查之前记录了“webhook received for event evt_xxx”,至少能知道它到达过。大多数人是在之后记录的,这让签名失败的事件完全不可见。

像 AnyHook 这样的 Webhook 转发服务位于你的处理程序前面,记录请求头、请求体、耗时、响应状态码和尝试次数。排查流程就变成:看事件流,点开失败的那个,检查原始请求体,重放它。

AnyHook 的排查流程

当有用户给 AnyHook 支持发邮件说“我的 Webhook 不工作了”,控制台通常不到一分钟就能让原因变得很明显:

  1. 打开事件流,筛选 status=failed
  2. 点开失败的事件
  3. 查看接收到的请求头(签名在不在?)、接收到的请求体(结构和你预期的一样吗?)、转发后的响应(你的服务器说了什么?),以及耗时拆解(边缘节点 → 队列 → 投递)
  4. 如果只是偶发问题,比如一次糟糕的部署或网络抖动,点击 Replay(重放),看着它成功
  5. 如果每个事件都在失败,你现在就确切知道该修什么了;修的期间,你的事件会一直保存,在你的套餐的重放窗口内都可以重放(Free 套餐 3 天,Pro 30 天,Scale 90 天)

能把 2 小时的排查缩短到 2 分钟的,是把平台实际发出的请求、你的服务器实际返回的响应,以及两者之间的时间戳,全都放在一个地方。没有这些,你就得从三个控制台和自己的日志里拼凑还原。

想在注册任何东西之前先看看效果,一条命令就能返回一个可用的接收 URL,不需要账号:

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

响应里有一个 inbound_url,用来填到平台那里;一个 api_key,用来通过 API 读取事件日志;还有一个 claim_url。这个端点是临时的:除非你打开认领 URL 并登录,否则 7 天后它和它的事件都会被删除;认领之后 URL 保持不变。

总结

如果你的周日下午太常耗在这上面,AnyHook 能把其中大部分变成控制台上的一次点击。

全部文章2026年4月18日 · 7 分钟

别再丢 Webhook 了。

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