作者为某个支付工具提了一个"修复"——用 URL+金额+钱包派生幂等 key,但被社区纠正:相同金额同一钱包但不同收款人/网络是不同支付,错误合并会导致静默返回错误收据。
我维护着几个用于实现精确一次执行(exactly-once execution)的开源库——这个问题指的是 Agent 在超时后重试工具调用,不知道第一次尝试是否成功到达,最终导致操作执行两次。一笔扣费、一条消息、一笔支付。程序不会崩溃,只是发生了两次。
部分工作是在阅读别人的 MCP server 时寻找这种 bug,昨天我就找到了一个:一个支付工具每次调用都会签名并发送一笔交易,但其 schema 中没有任何 idempotency key。我开了一个 issue,提了一个修复方案:从 URL、金额和钱包派生一个默认 key,这样重试的请求就能合并为一次结算。
有个陌生人回复说我的修复方案不对。不是那种吹毛求疵的不对,而是比我自己报告的那个 bug 还要糟糕的不对。我的 key 范围太窄了——只看了三个字段。两笔来自同一钱包的相同资源订单,价格相同,但收款人、网络或资产不同,这其实是不同的支付。我的派生方式会把它们合并为同一个 key。这意味着:一笔合法的第二次购买会静默地返回第一次购买的收据。
仔细想想这个,因为正是这部分让我停下来认真思考。双倍扣费很糟糕,但它是可见的。有人看到两个条目就会提交 dispute。一笔被吞掉的购买看起来像是成功的。买家以为付了钱也拿到了东西。没有人会去调查一笔看起来已经成功的交易。
真正的问题不在于我选的三个字段,而在于我从模板里选了这三个字段,而不是问那个唯一重要的问题:什么让两次操作成为同一操作,什么让它们不同?我套用了"金额 + 钱包 + URL"的模式,因为类似的修复通常就是这么做的。我从来没有坐下来为这个特定系统完整地列举出"一次支付"的全部形态。
这才是真正的教训,而且它比支付场景更普遍:idempotency key 是关于身份的一声声明。如果你在一个方向把身份搞错了,你会得到假的重复(烦人但安全、明显)。在另一个方向搞错,你会得到静默的数据丢失(危险、隐蔽、代价高昂)。大多数建议——包括我自己在内,直到上周——都只警告第一种情况。
我可以告诉你我优雅地更新了提案然后继续。但实际上我用更宽的 key 重写了 PR(加入了 payTo、network 和 asset),在 PR 中具名致谢指出问题的人,并且在 PR 中直说我的原始建议是错的。不是因为这样显得高尚——而是因为不这样做后果更糟。悄悄地修复然后希望没人去对比原始 issue 的 diff,这样会落得一个"防御性地犯错"的名声,而不是"优雅地犯错",而防御性地犯错是代价昂贵的那种。
PR 现在是开着的:https://github.com/CryptoAPIs-io/cryptoapis-mcp-x402-pay/pull/2。它会不会被合并和这个纠正是否正确是两个独立的问题。它是正确的。
我写了一个小扫描工具——fencescan——用来检测代码库中可能重复触发同一效果的 tool calls,只报告有证据的候选项,不做定论。无需安装:
npx fencescan
我构建这个工具部分源于这次经历。外部人员通常无法证明一次 double-fire——因为 guard 通常在 repo 调用的服务中,而且看起来像写操作的函数可能只是为别人签名构建 payload。扫描器的职责是指出值得仔细检查的地方,而不是指责任何人。这种克制不是谦虚,而是因为这个工具早期更粗糙的版本在前 7 个真实目标上错了 4 个,原因很有趣(如果你喜欢这类复盘的话——主要是:一个没有词边界的正则表达式无法匹配 camelCase,所以它指责某个 repo 有零个 idempotency guard,而实际上它内置了一整个模块)。
如果它标记了你的代码而你认为错了,值得开个 issue。这里的误报比漏报代价更高——我宁愿知道。
第一个 maintainer merge 已合并——Kibana MCP server 采纳了 retry-safety 注意事项,而 maintainer 的回复同时做了两件事:纠正了我文档字符串中的端点混淆(create-with-id 是 POST,不是 PUT——我的失误,他们发现的),然后把我的发现又延伸了一行——他们的 409 错误消息正在建议调用者做新文档警告不要做的事情。他们问我是否也想修复那个。我照做了,当天就修了。这意味着这篇文章的核心论点贯穿了整个发布过程:修正是双向流动的,双方的代码都变得更安全了。这就是重点所在。
相关项目:effectfence(Rust)、once-kernel(TypeScript,附 Python 版)、fencescan。全部采用 MIT/Apache 协议,无附加条件。
如需进一步操作,你可以考虑屏蔽此人或举报滥用行为。