页面显示“保存失败”,浏览器网络面板里却是一排 200;另一个接口返回 504,用户重试后又生成了两笔订单。两种问题都与 HTTP 状态有关,但只记住数字含义还不够。你需要知道响应由谁产生、业务执行到了哪里,以及客户端接下来可以做什么。
下面按创建任务、编辑资源、缓存和代理几个场景说明。示例响应是协议设计示意,不来自某个实际服务的抓包。标准语义以 RFC 9110为基础,限流和错误正文分别参考后文列出的规范。
先认出是谁返回了这个状态
一次请求可以经过 CDN、负载均衡、反向代理,再到应用。客户端收到 403,可能是应用拒绝权限,也可能是请求根本没有通过边缘规则。500 也不自动等于数据库故障,它只说明生成该响应的服务器遇到了意外情况。
因此先保存请求时间、URL、方法、状态、响应头和请求 ID。再沿各层日志寻找同一次请求。不要仅凭某个响应头就断定来源,因为代理可能保留或改写它;把应用日志和代理日志对上,证据才完整。
如果没有任何 HTTP 响应,例如 DNS 解析失败或 TLS 握手失败,就不该强行记成 500。监控最好把连接失败、超时和收到的 HTTP 错误分开统计,否则一次证书问题可能被误报为应用异常。
保存成功、创建成功和进入队列,是三件事
以报告导出为例。用户提交筛选条件后,服务器建立了一个导出任务,但文件还没生成。这时 202 Accepted 比“导出成功”准确;响应还应提供任务 ID 和查询位置,让用户知道之后去哪里看结果。
如果接口同步创建了一个可以立即访问的资源,可以返回 201 Created,并用 Location 给出资源地址。修改成功且需要返回更新结果时,200 很常见;删除或保存成功但不返回正文时,可以使用 204,客户端不应继续强行解析 JSON。
真正影响产品体验的是这些状态是否与界面一致。返回 202 后立即显示“文件已经生成”,会让用户点进不存在的下载地址。返回 204 后仍执行 JSON 解析,又会把一次成功操作显示成异常。服务端和前端应一起约定响应体,而不是各自理解数字。
接口如果为了历史兼容始终返回 200,需要明确迁移方案。突然把全部业务错误改成 4xx,可能使老客户端走入没有处理过的分支。可以先补充契约测试,再按版本或端点逐步调整,同时修正监控的成功率计算。
用户需要登录,还是没有权限
401 表示当前请求缺少有效身份凭据,并要求服务器提供适用的 WWW-Authenticate 挑战。403 表示服务器理解请求但拒绝执行,原因可以是权限或其他策略;不能把它限定成“用户已经登录”。具体说明可参见 MDN 的 401 与 403 文档。
对浏览器应用,登录态失效可以引导重新认证;已登录用户没有某篇私有文档的权限,再弹登录框通常没有帮助。需要避免泄露资源存在性时,服务也可能选择 404。这种策略应在同类接口上保持一致,避免用户通过不同错误文案枚举资源。
排查认证问题时,用同一个请求分别检查浏览器是否带了 Cookie、代理是否转发必要信息、服务器是否接受凭据。不要把用户给出的截图当作完整请求。密码和令牌不能复制进普通工单,定位通常只需要脱敏后的头部结构和关联 ID。
对于上传,先区分 413 请求内容过大与 415 媒体类型不支持。前者可能来自代理限制,后者可能来自应用要求。只在前端放宽文件大小,无法改变代理配置;只修改文件扩展名,也不会让不支持的内容格式变得可用。
两个人同时编辑,应该告诉后提交的人什么
假设甲和乙同时打开版本 7 的文章。甲保存后变成版本 8,乙随后提交自己的旧内容。如果服务器直接覆盖,甲的修改就丢了。客户端需要一个能辨认冲突的结果,而不是笼统提示“网络错误,请重试”。
使用 HTTP 条件请求时,客户端可以带 If-Match,服务器在条件不满足时返回 412 Precondition Failed。若服务器要求条件头但请求没带,可以用 428 Precondition Required。后者及其避免丢失更新的用途见 RFC 6585。
业务系统也常用自己的版本字段,并以 409 Conflict 表示当前资源状态不允许这次修改。选择哪种方式,应与接口契约保持一致。前端拿到冲突后,可以保留用户输入,展示最新版本,让用户比较后再提交。自动重试同一份旧内容,不会消除冲突。
400、409 和 422 也不宜只靠一句口诀划分。请求格式有问题、资源状态冲突、语法正确但指令无法处理,是不同判断;具体校验结果应放进结构化正文,让客户端定位到字段或动作。最有用的设计是让调用者知道怎样修正请求。
跳转和缓存会改变你看到的请求
接口从旧域名迁到新域名时,要检查跳转是否保留方法和请求体。307 与 308 明确保留方法;301、302 对 POST 的历史处理可能改变方法。提交表单后跳到结果页面,则可以用 303 表达获取另一个资源。不要只用浏览器访问首页一次,就认为所有 API 跳转都正确。
缓存场景中的 304 也不是“服务器忘记返回正文”。客户端发送条件请求后,服务器表示已有副本仍可使用,缓存据此更新相关信息。排查时将 ETag、If-None-Match 和缓存策略一起看,具体规则见 RFC 9111。
若内容已经修改而用户仍看到旧版,需要确认条件判断、代理缓存和浏览器缓存分别如何工作。刷新一次看到正确内容,不能证明其他节点已更新。反过来,频繁得到 304 也不自动说明缓存配置失效,它可能恰好说明条件请求在正常运行。
站点删除文章时,真实状态与页面内容应一致。页面写着“文章不存在”却返回 200,搜索系统可能把它识别为 soft 404。Google 对抓取处理有单独说明。友好的错误页面完全可以同时返回正确的 404,不必在用户体验和状态语义之间二选一。
502 和 504 都要继续向上游查
502 表示网关从上游收到无效响应,504 表示网关没有及时等到所需上游响应。它们帮助你确定观察位置,却不能直接指出是哪一行代码有问题。MDN 的 502 与 504 文档区分了这两种情况。
遇到超时,按时间线检查连接建立、排队、应用处理和依赖调用。若数据库锁等待已经持续很久,把代理超时加倍只会让用户多等一段时间。若上游处理完成,但响应在返回途中中断,客户端也可能看到失败。
这个差异影响重试。用户提交订单后收到 504,订单可能已经创建。再次点击之前,应通过业务请求标识查询结果;创建接口则需要可靠的幂等设计。状态码不具备撤销已经发生的业务操作的能力。
429 表示限流,503 表示服务暂时无法处理。客户端可以利用 Retry-After,同时采用有上限的退避和随机抖动。不要让大量客户端在同一个固定时刻一起重试,也不要把所有 5xx 都视为适合无条件重放的信号。
错误正文给机器分类,也给用户出路
RFC 9457定义了 application/problem+json。它让接口用 type 表达问题类型,用 title 和 detail 提供说明,并可携带定位信息。下面只是一种示意约定:
{
"type": "https://example.com/problems/edit-conflict",
"title": "文章已有新版本",
"status": 409,
"detail": "请比较最新内容后再次保存。",
"instance": "/requests/request-123"
}
正文中的状态应与实际 HTTP 状态相符。不要把 SQL、文件路径和内部堆栈直接返回给用户;内部日志可以通过请求标识关联。客户端也应依赖稳定的问题类型,而不是匹配一句随时可能改写的中文文案。
测试接口时,除了检查数字,还要检查调用者的下一步:创建后能否找到资源,异步任务能否查询,冲突时输入是否保留,限流是否停止密集重试,错误页是否仍返回错误状态。做到这些,状态码才能真正参与排障和产品流程。
原文来自 zhihu.ee,本文保留原发布时间。











