为什么 200 OK 不等于事情做完了
为什么 200 OK 不等于事情做完了
排查问题时最气人的一句话,常常不是“报错了”,而是“它明明返回 200 啊”。
HTTP 200 只代表“这次网络请求被对方接住了并给了一个响应”,它不保证对方真的把事情做成了。很多平台的写入、推送、鉴权接口,无论成功还是失败都会老老实实回一个 200,真正的结论藏在响应体里的业务码或某个字段里。只看状态码就写“成功”,等于把“请求成功”当成了“业务成功”。
这篇文章用我在一个多端 IM 接入项目里踩到的一类真实问题,讲清楚三件事:业务码到底怎么判定、回执怎么脱敏才安全、失败用例怎么逼出来。项目细节以实际代码为准,结论是通用的。
一、现象:传输层“成功”,业务层没完成
接入 IM 机器人时最常见的一类“假成功”是这样的:网关层或连接层已经用长连接接管了消息,你调用平台接口回写、或者给第三方服务发一个心跳/试探请求,对方返回 200,你就把状态面板打成“已连接”“测试通过”。
但用户真正发给机器人的消息,可能根本没进到会话,或者因为鉴权、权限、格式问题被静默丢弃。你看到的“通过”,其实只是“对端 HTTP 服务还活着”。
判断一个连接是否真的“能用”,不能问“对方有没有回消息”,而要从响应体的语义里去拿结论。
二、根因:没有人定义“什么叫成功”
很多时候不是没人知道要看业务码,而是从一开始就没定义清楚“成功”的标准。
在一个 IM 接入项目里,我们给本机 Harness 做一个“AI Office 连接器”,让本机主动连公网 Office。它用来探活的是这么一条链路:
- 用
POST /api/harness/connector/heartbeat完成鉴权和能力握手; - 再拉
GET /api/harness/connector/stream建立 SSE 下行。
最初的实现里,连接测试的判断就是“拿到 200 就算通过”。结果有一次网络是通的、对方服务也确实活着,但协议版本对不上、或者对方根本不是我们期待的那个 Connector,测试却依然打勾。把这个“通过”反馈给用户,等于给了个错误的结论:用户以为能用了,实际一收消息就露馅。
后来我们把成功的标准改成:心跳响应必须是真的业务级 JSON,只认 {"ok":true,"protocolVersion":"office-harness.v1"} 这种形态才算通过。也就是只要 ok 为真、协议版本也对得上,才能说明“连接测试通过”命中的是一个兼容的 Connector,而不是某个“碰巧返回 200 的网址”。
这一步的关键不是多写了一行判断,而是把“什么叫能用”这件事写进了代码,而不是留在人脑里。
三、业务码判定的几个要点
从这段经历里可以提炼出几条通用的判定规则:
- 状态码只当作“传输是否完成”,不当作“业务是否成功”。 判定成功要么看响应体里的业务码/
ok字段,要么看它是否返回了你期待的语义数据。 - 成功标准要带“协议版本”这类能力字段。 否则一个老版本服务也能骗过你。版本对不上,即便 200 也应按失败处理,或至少给出明确降级提示。
- 失败要能区分“对方没接住”和“对方接住但拒绝”。 前者是网络/超时问题,后者是业务问题,处理方式完全不同。把它们混在一个错误码里,人会无从下手。
四、回执要脱敏:成功≠把数据都倒给前端
业务码判定了,还有一件容易被忽略的事:把状态回传出去的时候,别把不该给的东西也带出去。
IM 机器人接入会涉及大量凭据:飞书的 App Secret、微信的 token、钉钉的 client_secret、企业微信 Secret、QQ 的 app_secret、Slack 的 Bot/App Token、Telegram/Discord 的 Bot Token、WhatsApp 的关联设备密钥、AI Office 的 Device Token 等等。这些都是不能回传的。
所以状态回执遵循一个原则:浏览器/前端只拿到“它能用来做判断、且不敏感”的信息,比如二维码、Manifest、脱敏后的状态;手动输入的 Secret 或 Token 只单向提交给本机 Host,任何 RPC 响应都不返回这些原始值。那种“回显一发看看能不能用”的做法,等于把密钥装到响应里发给前端,风险远大于收益。
脱敏不是把状态字段删掉,而是把“需要展示的”和“绝对不能外泄的”划清边界,只回传前者。
五、失败用例:让假通道被测试当场捉住
光改判定还不够,得有一条回归测试钉住这个行为,防止哪天改回“200 就算过”。
这个项目里有一条本地检查命令,它跑单元测试、构建 Host/Client 产物,还要验证发布包不包含凭据、也没有混入独立的渠道设置页注册。这就是把“成功标准”和“回执不泄密”这两件事,从口头约定变成了可自动执行的检查。
再往前一步,应该给“连接测试”写一个失败场景:用一个“只返回 200 但不是合法 JSON/协议不匹配”的假端点去测,断言它必须判定为失败。这样下次有人把判定条件改松,测试立刻变红。
六、可复用结论
- 200 只代表“有响应”,成功与否要看响应体的业务码或协议字段。
- 把“什么叫成功”写进代码,不要停留在常识层面。
- 成功标准尽量带上版本/能力字段,防止老版本蒙混过关。
- 回执做信息最少化,凭据类的单向提交、绝不回显。
- 给“假成功”写失败用例,用自动化把判定条件钉住。
回到项目本身:那次排查之所以花了时间,是因为我们一开始被“它返回 200 了”这个表象骗住。把“成功”的定义从状态码挪到业务语义上之后,问题才真正闭环。结果比演示重要——这个“结果”,指的是业务的真实完成,不是一次漂亮的 HTTP 握手。