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 内部处理失败 |
| 404 | endpoint URL 错或没部署 |
| 401/403 | 签名验证 / auth 拦了 |
| 500 | endpoint 抛异常 |
| Timeout | endpoint 处理太慢 |
| 无尝试 | 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 复杂度)