作者用AI编码助手生成了一个Stripe webhook处理器,每行代码都正确,但当Stripe重试发送事件时会重复扣款——因为代码假设事件只发送一次。传统code review和测试都无法发现这类「假设中的bug」。
我的编程助手几周前写了一个 Stripe webhook 处理器。签名校验、事件类型校验、业务逻辑调用、返回 200。代码的 diff 看起来就像我状态好时会写出的样子。我大概花了九十秒就批准了它。
app.post("/webhooks/stripe", async (req, res) => {
const event = verifySignature(req);
if (event.type === "charge.succeeded") {
await creditCustomer(event.data.object);
}
res.sendStatus(200);
});
每一行代码都是正确的。而当 Stripe 第一次重复投递那个事件时——这是它被允许的行为,而且迟早会发生——这个处理器就会给客户加两次积分。
阅读发现不了它
这个 bug 不在任何一行代码里,所以读多少遍都找不到它。每条语句单独看都没问题。问题出在行与行之间的假设:每个事件只会到达一次。Stripe 承诺的是至少一次投递。Retries 和 duplicates 是文档记录在案的正常行为。
代码审查是根据你能想象到的流量来检查 diff。我想象的是一次干净的 charge.succeeded。助手也是这么想的——它学习的 quickstart 代码也是这么想象的。
CI 通过也是出于同样的原因
我的测试回放了我所设想的事件。fixture 文件里每个事件只有一份,因为 fixture 文件是我写的。没人会写这样的测试:同一个事件相隔九分钟到达两次。这个测试套件只是在用我的假设来检验我的假设。
我在一家非常大的支付公司的开发者平台工作了三年。这个 bug 让多少优秀的审查者都栽过,我数都数不清了。这从来不是能力问题。阅读是发现不了这类 bug 的。
真正有效的审查方式
真正发现这个 bug 的是运行它——触发失败。发送事件,再发一次,观察客户被加了两次积分。在 event ID 上添加去重,再运行一次,观察它通过了。十分钟,审查从"看起来对"变成了"证明了对"。
That loop — reproduce the failure, fix it, rerun, keep the receipt — is what I built FetchSandbox to run from your IDE over MCP. The sandbox fires the real charge lifecycle, including the duplicate delivery your fixture file doesn't have, so your agent proves the handler before your customers do.
If you'd rather judge it on real code than my word, there's a webhook-dedupe bug planted in a Stripe app in our open playground, waiting to be caught: github.com/fetchsandbox/playground.