将 payment webhook 错误处理改得「更优雅」后,错误率显示为 0%,但实际所有支付都失败了——错误被静默吞掉,监控反而显示正常。
你曾经有没有见过监控仪表盘安静下来、让人松一口气而不是心生恐惧的时刻?那是我犯的第一个错误,因为那种沉默意味着我的支付 webhook 正在对每个请求报错。我刚刚"改进"的错误处理把每一次失败都转换成了干净的 200 OK,没人能看到损害。
我们的支付 webhook 每天处理约 300 个请求,正常错误率在百分之二到三之间。在我部署了对错误处理的一个小改动之后,错误率降到了恰好为零,p99 延迟也保持平稳。这本该让人感觉像是一场胜利,但支付记录停止了增长,客户邮件询问从未出现的扣款。
下面是原来的处理器,它不够优雅但有一个优点:失败时它会大声失败。
app.post('/webhooks/payment', async (req, res) => {
const event = req.body;
await processPayment(event);
res.status(200).send('ok');
});
如果 processPayment 抛出异常,客户端收到 500,我们的错误追踪器捕获堆栈跟踪,支付提供商重试该事件。我通过 MonkeyCode 的免费模型访问让一个免费模型让这个处理器更有韧性,它生成了这个:
app.post('/webhooks/payment', async (req, res) => {
try {
const event = req.body;
await processPayment(event);
res.status(200).send('ok');
} catch (error) {
console.error('Payment processing failed:', error.message);
res.status(200).send('ok');
}
});
模型的推理并非全错,因为 webhook 提供商确实会把 200 解释为"已送达,不要重试"。问题是模型把这种逻辑应用到了每一种失败,包括本应重试的临时数据库超时。它也只记录了错误信息,没有事件 ID、没有堆栈跟踪、也没有请求上下文。
我的单元测试把 processPayment 模拟为成功解决,所以 catch 块从未执行,测试断言端点返回 200。新代码完美通过了这些测试,因为我从未为失败路径写过测试。模型的总结说"优雅地处理错误",我信任那个总结胜过信任我自己的怀疑。
麻烦的第一个迹象是一张支持工单,而不是告警,因为当每个请求都返回 200 时,不会有任何告警可以触发。我检查了日志,发现类似 Payment processing failed: Connection timed out 的行。这告诉我错误是真实的,但没有给我任何方法将它与特定事件关联起来。我查看了支付提供商的仪表盘,看到每个事件都标记为"已送达",因为我们的服务器用 200 确认了收妥。我查看了数据库,发现最后一条成功支付记录是在我部署前一天,这才终于把线索串联起来。
如果 catch 块把每一次失败都转换成虚假成功,那它有什么用?修正后的处理器区分了值得重试的失败和不值得重试的失败,并且记录了足够调试的上下文。
app.post('/webhooks/payment', async (req, res) => {
const event = req.body;
try {
await processPayment(event);
res.status(200).send('ok');
} catch (error) {
console.error('Payment processing failed', {
eventId: event.id,
customerId: event.customer,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
if (error instanceof TransientError) {
res.status(503).send('retry later');
} else {
res.status(400).send('invalid event');
}
}
});
503 告诉提供商重试,400 告诉它停止,结构化日志给了我事件 ID 来追踪失败。错误率再次上升,这次我庆祝了,因为可见的错误是唯一能被修复的错误。
建议来自于我通过 MonkeyCode 访问的一个免费模型。Bug 变得可见只是因为我把它部署到了 MonkeyCode 的免费服务器选项上,那里的数据库小到可以直接检查。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。教训不是免费模型写出了糟糕的错误处理;而是任何错误处理都应该有一个测试来证明失败保持可见。
如果你的 webhook 提供商忽略重试语义,区分 503 和 400 不会对你有帮助;你可能需要带外重试队列。如果你的监控很有限,返回非 200 只是在把问题从不可见变成吵闹,这是进步但不是修复。如果你的错误处理已经工作了,不要仅仅因为新版本看起来更干净就让模型重写它。隐藏失败的干净代码比暴露失败的丑陋代码更糟糕。
下次模型主动提出让你的错误处理更健壮时,问问捕获之后错误发生了什么。如果答案是"没有任何可见的东西",那健壮性就是幻觉。