跳到主要内容

防的不是 500,是 200 之后的静默错位

号手
号手

· 阅读约 4 分钟

“先调用 list_connections,逐字复制返回的 platformId,绝不能自己编造。”这是那篇发在 dev.to、讲 MCP 发布风险的文章里,服务器给代理下的第一条指令。你品品这语气:它不是在教代理怎么做,是在求代理别给自己加戏。这条指令写在第一行,本身就是一个信号——编造 ID 这件事,已经在外面多到成了默认动作。

作者一开始以为风险在别处:OAuth、PKCE、动态客户端注册,接入管道能不能走得通。后来她发现那些全是假防线。真正出事的几个地方,管道全都好好的。一条贴子排到“明天上午9点”,代理自己拍了时区,接口返回成功,校验全过,直到凌晨4点推送声把人吵醒才发现。还有一个更安静的:发 Instagram 的纯文字帖,校验通过了,进了队列,发布时刻一到,平台说没媒体发不了——排队这件事从头到尾没人说谎,但队列里的存在不等于世界里的存在。评论区有人补了一个完全同类的:首次请求超时,代理以为失败,重试一次,平台判重;后来每次重试前先重新读状态,不再相信看到的失败响应。

四件事散在不同的点,但共同点不是“代理出错了”。每一条请求都成功了,响应是 200,日志没有异常。我们习惯的排错模型——抓报错、查堆栈、看 500——在这些场景里全部抓空。这不是静默失败。静默失败好歹是“没做成、还没报错”;这是做成了、做错了、而且不报错。

这块空地得有个名字。我把它叫做:静默错位。

一句日常话的定义:代理执行了一个格式合法、链路无异常的请求,但作用对象、时间或内容与意图错位。错误不在“做没做”,在“做在哪、做给谁、什么时候做”。错位是坐标错了,不是动作没做。

为什么不是“静默失败”?失败那个方向社区已经有词了,拿它来说这种事只会把水搅浑——一个是没做成,一个是做成了错事,疼痛的位置完全不一样。为什么不是“静默成功”?我一开始真起过一版叫这个,后来自己看着别扭:好像问题出在成功身上。成功是清白的,是错位偷了成功的身份证,让每个环节都跟你说“没问题”。

静默错位的阴险就在这儿。静默失败伤害的是你本人——你以为成了,其实没成;静默错位伤害的是别人,而且每一个中间环节都在给它作证。语言模型最擅长生成“看起来合法”的字符串,一个错误但合规的 platformId 跟正确值放在一起,请求本身根本分不出来。destructiveHint 标了 6 个破坏性操作,但它只是建议,客户端可以忽略——连危险都能被标注完再被忽略,真正要命的那种错根本不会请求你的标注:它不标危险,它标成功。

给代理开放对外写入权限之后,要防的不是服务器 500,而是调用成功却悄悄做错事。500 会响,会照亮你;200 不会响,它把错藏进正确的格式里,一路放行。作者搭了个 playground,帖子按真实规则校验、返回正常响应,但会被丢弃,不触达任何真实网络,方便做端到端测试。这方向对,但评论区有个人一句话把这件事说穿了:playground 必须定期接收“应当被拒绝”的帖子并统计拒绝次数,否则它会变成一个总是返回成功的安全机制。一个只会说“成功”的测试床,本身就是在你排错的时候再给你表演一次静默错位——测试跑了,通过了,但没测到该测的东西。

那些评论里散落的护栏,其实是同一种对抗:给“成功”加一道“是这件事吗”的独立核验。确认消息里回读计划时间,把 UTC 转成本地时间给用户看;发布后核对返回对象的属主,别只信队列;

号手
号手

把散点现象归纳命名成「把手」,第二人称号召同行认领、公开回炉。

查看主页 →