Webhook 收不到:3 个原因 + 修复路径

提供方说发了,你的端点没收到——通常是端点访问性 / URL / 状态码。

Stripe 后台显示 “Delivery succeeded”,但你的 server 日志里没看到任何 webhook 请求;或者 GitHub Webhooks “Recent Deliveries” 全是红色 X;或者 Shopify 一直在 retry。同步业务依赖 webhook(支付成功后给用户加权限、PR 合并后发部署),webhook 不到 = 业务挂。

诊断流程的核心是:先从 provider 那一侧看它说发出去了没、收到啥状态码——绝大多数 case provider 后台都有完整投递日志。然后再看你的 endpoint 是不是真的 reachable。

常见原因

按命中率从高到低:

1. 端点返回非 2xx,provider 重试用尽

provider 看到 4xx/5xx 会按指数退避 retry 几次(Stripe 默认 3 天内 retry),全失败后放弃。你看到的是”最后一次失败的状态”。

如何判断:provider dashboard 显示 “failed delivery”,附带 HTTP status code。

2. webhook URL 拼错

✅ https://api.yourdomain.com/webhooks/stripe
❌ https://api.yourdomain.com/webhook/stripe   # 少了 s
❌ https://yourdomain.com/webhooks/stripe       # 缺了 api 子域

如何判断:把 dashboard 里的 URL 复制到浏览器或 curl,看是否能 hit。

3. 端点只本地能访问(localhost / 内网)

dev 时配了 http://localhost:3000,但 webhook 来自公网,根本到不了你电脑。

如何判断:URL 含 localhost / 127.0.0.1 / 私有 IP(10.x、192.168.x)。

4. 端点处理慢,超时被判失败

webhook 投递方等响应有超时(Stripe 默认 30 秒,但很多 provider 5-10 秒)。如果你 handler 同步处理 LLM / 邮件 / 长 query,超时被判失败 → retry。

如何判断:endpoint 处理时间 > 5s 且 provider 报 timeout。

5. 签名验证拦了

provider 要求 webhook header 带签名(Stripe 的 stripe-signature、GitHub 的 x-hub-signature-256),你 endpoint 校验失败直接 401。

如何判断:endpoint 返回 401/403,body 含 “signature mismatch”。

6. 防火墙 / CDN 拦了 unusual user-agent

Cloudflare / WAF 看到 Stripe/1.0 这种非浏览器 UA 判定为机器人拒了。

如何判断:Cloudflare logs → 看是否 block 了 provider IP / UA。

7. handler 抛异常 server 返回 500

代码 bug 让 handler 抛 unhandled error → server 框架返 500 → provider retry。

如何判断:server 日志显示 stack trace。

最短修复路径

Step 1:到 provider 看投递日志

Stripe:    Developers → Webhooks → 点 endpoint → "Recent events"
GitHub:    Repo Settings → Webhooks → 点 → "Recent Deliveries"
Shopify:   Settings → Notifications → Webhooks → 看 attempts
Linear:    Settings → API → Webhooks → Deliveries
Slack:     api.slack.com/apps → Event Subscriptions → Recent

每条 attempt 显示:

  • 时间戳
  • HTTP status code(如 200 / 404 / 500)
  • response body(前几 KB)
  • request body(payload)

直接从 status code 判断症状:

Status意味着
200-299投递成功,你 endpoint 收到了 → 是 endpoint 内部处理失败
404endpoint URL 错或没部署
401/403签名验证 / auth 拦了
500endpoint 抛异常
Timeoutendpoint 处理太慢
无尝试webhook config 没启用 / event 没订阅

Step 2:用 curl 测端点可达性

# 从外网(手机热点 / 服务器)跑,不要用公司 wifi
curl -i -X POST 'https://api.yourdomain.com/webhooks/stripe' \
  -H 'Content-Type: application/json' \
  -d '{"test": true}'

# 应该 5 秒内返 2xx 或带具体错误的 4xx

curl 失败 → endpoint 本身访问性问题。curl 成功 → provider 那边配置错。

Step 3:dev 端点用 ngrok 暴露公网

# 启 server
npm run dev   # http://localhost:3000

# 另一个 terminal
ngrok http 3000
# 输出:Forwarding https://abc123.ngrok.io -> http://localhost:3000

# 把 https://abc123.ngrok.io/webhooks/stripe 配到 provider
# webhook 进来后能命中你的本地代码

或用 Stripe CLI:

stripe listen --forward-to localhost:3000/webhooks/stripe
# 它会模拟 webhook 投递到本地

Step 4:handler 改成”立刻返 200,异步处理”

// ❌ 同步处理(慢 → 超时)
app.post('/webhooks/stripe', async (req, res) => {
  await processEvent(req.body);  // 30 秒
  res.json({ received: true });
});

// ✅ 立刻 ack,异步处理
app.post('/webhooks/stripe', async (req, res) => {
  // 1. 验签
  const valid = verifySignature(req.headers['stripe-signature'], req.rawBody);
  if (!valid) return res.status(401).end();

  // 2. enqueue 异步处理
  await enqueue({ task: 'process-stripe-event', payload: req.body });

  // 3. 立刻返 200(< 1 秒)
  res.json({ received: true });
});

provider 觉得 OK,你慢慢处理。

Step 5:签名验证正确实现

// Stripe 示例
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_KEY!);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),  // 必须 raw body 才能验签
  async (req, res) => {
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        req.headers['stripe-signature']!,
        endpointSecret
      );
    } catch (err) {
      console.error('Signature verify failed:', err);
      return res.status(400).send(`Webhook Error`);
    }

    // 处理 event ...
    res.json({ received: true });
  }
);

注意:必须用 raw body(不要 JSON parse 后验签),且 endpointSecret 要跟 provider 后台显示的对齐。

Step 6:CDN / WAF 加 allow rule

Cloudflare:
  WAF → Custom Rules → Skip:
    URI Path equals /webhooks/stripe
    AND IP in [Stripe IP range]

Stripe / GitHub / Shopify 都发布过 IP 段,加白名单。

Step 7:endpoint 有 server log

每个 webhook handler 第一行就 log:

app.post('/webhooks/stripe', async (req, res) => {
  console.log('webhook received:', {
    event_type: req.body.type,
    id: req.body.id,
    timestamp: Date.now(),
  });
  // ...
});

没 log 但 provider 显示投递了 → 没真到达你 server,看 CDN / 路由。

预防建议

  • 配置 webhook 时第一件事用 webhook.site 验证 URL 可达
  • handler 模板永远”立刻 ack + 异步处理”,超过 2 秒就不安全
  • 验签放在 enqueue 之前,但都在响应之前;不要先返 200 才验签(攻击者就能用任意 payload 入队列)
  • 端点写 idempotent(用 event_id 去重),provider retry 不会双重处理
  • 所有 webhook endpoint 加监控:投递成功率、处理耗时、429 / 500 计数
  • dev 用 ngrok 或 provider 自己的 CLI(Stripe CLI / GitHub webhook tester)模拟
  • provider 后台 webhook secret 跟生产 env 对齐,rotate 时同步改
  • 多个 service 共用同一个 webhook secret 用同一个 endpoint,分别路由(reduce config 复杂度)

相关阅读

标签: #后端 #排查 #排查