JsonMind
API 与接口12 分钟阅读更新于 2026年11月9日

API HTTP 状态码:400/401/403/422/429/5xx 怎么选和怎么排查

按客户端错误和服务端错误拆解 HTTP 状态码,讲清 400 与 422、401 与 403、409、429 重试以及 502/503/504 的排查顺序。

JM作者:JsonMind Engineering最后复核: 2026年11月9日依据权威规范与引用来源复核。
快速答案

第一位数字决定谁来处理:4xx 是客户端需要修改请求,5xx 是服务端失败且通常可以稍后重试。401 表示未认证,403 表示已认证但无权限,409 表示状态冲突,422 表示语义校验失败,429 应配合 Retry-After 表示限流。

核心要点
  • 4xx 是客户端可修正的错误;5xx 是服务端问题,通常可重试。
  • 401 是未认证,403 是已认证但无权限。
  • 400 用于请求格式错误,422 用于格式正确但语义校验失败。
  • 409 表示状态冲突,例如重复创建或版本过期。
  • 返回 200 加错误正文会破坏客户端、监控和重试逻辑。

HTTP 状态码是客户端、服务端和中间层之间的契约。状态码错了,重试、缓存、监控和用户提示都会做出错误判断。

这篇文章聚焦 API 中最容易混淆的状态码,以及最快的排查顺序。

状态码写错的代价,很少停留在“报错不好看”。它会决定缓存存什么、重试中间件做什么、监控是否叫醒值班人,以及用户下一步该干什么。线上真正昂贵的故障,往往不是冷门状态码,而是常见状态码被用在了错误的失败类型上。

五个类别

类别含义谁来处理常见误用
1xx信息协议层把 101 协议升级当成普通 JSON 响应
2xx成功无需处理返回 200 加错误正文
3xx重定向客户端跟随或缓存301 被浏览器长期缓存,迁移回滚后仍跳转
4xx客户端错误客户端修改请求缺少 Token 却返回 403
5xx服务端错误服务端修复,客户端可重试表单校验失败却返回 500

先看第一位数字。4xx 不是服务端 bug,5xx 也不是客户端 bug。

类别是路由信号,不是严重程度排名。代理、CDN 和客户端库在解析错误正文之前,就已经按类别做了决定:缓存可能把 200 存一年,重试中间件可能把 503 再发三次,统计系统可能把 404 当噪声丢掉。把校验失败写成 500,会同时造成两种伤害:客户端以为稍后重试就能成功,值班工程师却被一个用户输入问题叫醒。

2xx 也不只有 200。创建资源用 201 并带 Location;异步任务已受理但未完成用 202;成功且没有正文用 204;范围请求用 206。浏览器通常不关心这些区别,SDK 和工作流引擎却很依赖:完成和排队对应完全不同的下一步。

400 和 422

400 Bad Request:服务端无法处理请求,原因是语法错误、缺少必要参数或内容类型不正确。

422 Unprocessable Content:请求语法正确,但语义不合法,例如数量为负、结束日期早于开始日期、值超出允许范围。

// 400:JSON 格式错误
{ "quantity": }

// 422:JSON 合法,但值不合法
{ "quantity": -2 }

有些 API 统一用 400。只要错误正文解释清楚,也可以接受;但 422 能让客户端明确知道“请求解析成功,是值需要修改”。

建议把最小复现写进契约测试:发送 { "quantity": } 断言 400,再发送 { "quantity": -2 } 断言 422。如果两者都返回 400,客户端无法判断是改一个 JSON 拼写就够了,还是必须改业务取值;如果两者都返回 422,那么被截断的正文或错误的 Content-Type 会被误报成校验问题,前端表单甚至会去高亮一个根本不存在的字段。

有用的 422 会指出字段和约束,而不是把整个请求回显一遍:{"error":{"code":"validation_error","field":"quantity","message":"必须大于等于 1"}}。相邻状态码也应各有分支:415 用于正文格式完全不对,406 用于内容协商失败,413 用于在解析前就因体积被拒。把这些都塞进 400,等于把“该修什么”藏了起来。

422 也有不适合的场景。只认 400 的老式 SOAP/XML 网关、很难定义“语法正确”的表单编码接口,以及 GraphQL 这种约定用 200 加 errors 数组表达业务错误的协议,都不该硬套 422。按接口类型选择一套约定并写进文档,比悄悄混用更可靠。

401 和 403

401 Unauthorized:缺少认证或认证无效。客户端应该获取凭据后重试。响应应包含 WWW-Authenticate 头说明认证方式。

403 Forbidden:客户端已认证,但没有权限。用同样的凭据重试不会成功。

常见错误是为了隐藏资源是否存在,对缺少 Token 的请求返回 403。这可以作为安全策略,但会让客户端行为变得模糊,必须写进文档。

响应头也要精确。401 应带 WWW-Authenticate: Bearer realm="api", error="invalid_token",SDK 靠它判断该刷新、跳登录还是直接失败;403 不应带这个头,因为凭据已经被接受,问题在授权。该刷新时不给 401,或者 Token 过期后返回 403,都会导致刷新死循环:客户端要么永远不刷新,要么无限刷新。

一个真实故障:接口对过期 access token 返回 403。前端看到 403,认定用户没权限,提示“无权访问”,从不调用刷新接口。于是用户几乎每隔一个 Token 生命周期就被锁死一次。服务端只需在鉴权中间件里检查 exp,过期时返回 401 并带 token_expired 错误码,客户端就能区分“该刷新”和“凭据错误”。

反过来,用 404 掩盖其他租户的资源,是合理的安全选择,但会把歧义转移到监控:面板上的 404 尖峰其实是授权拒绝。服务端要记录真实原因,对外保持与真实 404 相同的正文和响应时间,并按内部原因告警,而不是按公开状态码告警。

404 和 410

404 Not Found:服务端找不到资源,可能是临时的,也可能是故意隐藏。

410 Gone:资源曾经存在,已被永久删除,告诉客户端和爬虫不要再请求。

410 要谨慎使用,只有确认资源不会恢复时才用。大多数 API 用 404 就够了。

边界情况决定选择。软删除的记录通常应该返回 404,而不是 410,因为删除可能被恢复,客户端不该永久放弃这个 URL;410 更适合已下线且生命周期有明确终点的接口。两种状态码默认都可缓存,所以对“可能回来”的资源返回 410 时,要显式设置 Cache-Control: no-store,否则资源恢复后客户端仍可能长期认为它不存在。

迁移期间尤其要小心 410。爬虫会很快丢弃 URL,内容恢复后需要从头积累权重。只要资源还有可能回来,就用 404,或者 301 到后继地址,并在迁移稳定前保留旧 URL 的站点地图记录。

409 冲突

409 Conflict 表示请求和资源当前状态冲突,例如:

  • 创建用户时邮箱已存在。
  • 用过期版本号更新记录。
  • 删除仍有依赖数据的资源。

响应里要给出足够的信息让客户端处理:

{
  "error": "conflict",
  "field": "email",
  "message": "该邮箱已注册。"
}

409 描述的是状态冲突,不是语法错误。响应要让客户端不用猜就能解决冲突:给出冲突字段、当前版本,或已有资源的链接。乐观锁场景应把 409 和 ETag、If-Match 配合使用,版本检查在 HTTP 层失败时返回 412 Precondition Failed。很多团队两种都用 409,关键是要明确客户端该按哪个分支处理。

最小复现:两个客户端都读到版本 7,然后同时写入。第一个成功,第二个绝不能静默覆盖。服务端返回 {"error":{"code":"version_conflict","currentVersion":8}},客户端重新读取、合并,再用新版本重试。如果第二次写入返回 200,就发生了一次无感知的更新丢失,日志里连错误都不会有。

幂等创建还有一条相关规则:带 Idempotency-Key 的 POST 重试应返回第一次的结果,而不是新的 409;同一个键配了不同正文时,才返回 409 或 422 并给出明确错误码,让客户端知道这不是一次合法的重放。

429 限流

429 Too Many Requests 表示客户端超过限流。应包含 Retry-After:

HTTP/1.1 429 Too Many Requests
Retry-After: 30

客户端应该使用带抖动的指数退避,并遵守 Retry-After。用 403 或 503 表示限流会让重试行为变得不可预测。

Retry-After 有两种合法写法:秒数(Retry-After: 30)或 HTTP 日期(Retry-After: Wed, 11 Nov 2026 10:00:00 GMT)。只解析其中一种的客户端,要么永远等下去,要么完全忽略提示。服务端估不出等待时间时,宁可不带这个头,也不要把值写成 0,那会立刻触发重试风暴。

能给出限流元数据时就给出:RateLimit-Limit、RateLimit-Remaining、RateLimit-Reset,或者旧式的 X-RateLimit-*。行为良好的客户端会在被拒之前主动降速。常见故障是移动端捕获 429 后立刻重试三次,结果换来更长的封禁。带完整抖动的指数退避——在 0 到 min(cap, base * 2^attempt) 之间取随机等待——能避免故障恢复时成千上万个客户端同时重试。

429 也有用错的时候。因为容量原因只限制单条连接或单个接口时,503 加 Retry-After 更准确;请求被 WAF 按策略拒绝时,那是 403。两者的区别决定了客户端应该等待,还是修改请求。

500、502、503、504

这些都是服务端问题,但含义不同:

状态码含义典型原因
500内部服务器错误应用未捕获异常
502网关错误上游返回无效响应
503服务不可用过载或维护
504网关超时上游未在超时内响应

排查方向不同:

  • 500:看应用日志和堆栈。
  • 502:看代理、负载均衡和上游健康状态。
  • 503:看容量、发布状态和维护开关。
  • 504:看上游延迟、超时和连接池。

502 或 504 往往意味着应用根本没收到请求。不要一上来就改业务代码。

日志里的请求 ID 和耗时能快速区分这几种情况。如果客户端拿到的请求 ID 在应用日志里完全查不到,请求死在了代理层或根本没到达,应从 502/504 查起;如果日志显示处理器已经开始但一直没结束,重点看下游超时或工作线程阻塞;如果处理器留下了堆栈,那才是真的 500。

一个具体故障:CDN 在 30 秒时返回 504,源站却继续处理,45 秒后完成了支付。客户端重试,用户被扣款两次。网关超时不会取消源站工作,所以任何可能超时的非幂等操作都要有幂等键或状态查询接口。看到 504 就无脑重试同样危险。

代理生成的 502、503 经常返回 HTML 错误页,而不是你的 JSON 错误结构。只认 JSON 的客户端会抛解析异常,连状态码上下文都丢了。要测试经过代理的路径,而不只是应用本身,并让网关对 API 路由统一返回 JSON。

3xx 和重定向

301、308 是永久重定向;302、303、307 是临时重定向。API 中频繁出现重定向通常是配置问题。如果每个请求都要跟随重定向,应该修基础 URL。

注意 307 和 308 会保留方法和正文,303 会把方法改成 GET。一个把 POST 变成 GET 的重定向可能静默丢数据。

301 会被浏览器缓存得很久,有时超过响应头声明的期限,所以只用于不可逆的永久迁移;非 GET 接口的永久重定向,308 更安全,因为它保留方法。POST 之后让客户端去 GET 结果页,正确状态码是 303。

跨域重定向会让大多数 HTTP 客户端丢掉 Authorization 头。API 基础地址跳到另一个主机后,即使 Token 有效,调用也会变成 401;这时该修基础地址,而不是鉴权代码。客户端不缓存重定向时,每个请求还会多一次往返,所以 API 里的重定向通常是配置问题,不是功能。

重试语义

只在操作可以安全重复时重试:

  • GET、HEAD、PUT、DELETE 按定义是幂等的。
  • POST 默认不幂等,除非使用幂等键。

可重试状态码:

408 Request Timeout
425 Too Early
429 Too Many Requests
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout

使用带抖动的指数退避,限制最大尝试次数,并遵守 Retry-After。非幂等请求使用幂等键,避免重试造成重复创建。

重试预算比状态码清单更重要。一个服务把每个 503 都重试三次,等于在故障期间把负载放大四倍,而这正是后端最扛不住的时候。要同时限制单请求和单客户端的重试次数,并加入抖动,避免重试同步。幂等写入优先用带完整表示的 PUT 或条件请求;POST 则发送 Idempotency-Key: <uuid>,服务端至少在整个重试窗口内保存键和响应。

425 Too Early 服务于重放保护,不是通用重试信号:它表示请求可能已经被重放,客户端应在 TLS 握手或 early data 窗口稳定后重试。没弄懂原因之前,不要把它加进通用重试列表。

错误正文设计

有用的错误正文包含:

  • 稳定的机器可读错误码。
  • 人类可读的说明。
  • 出错的字段或参数。
  • 请求 ID,便于支持排查。
  • 可选的文档链接。
{
  "error": {
    "code": "validation_error",
    "message": "结束日期必须晚于开始日期。",
    "field": "endDate",
    "requestId": "req_01JCH6X8Y4Q2V7M3N9K5R1T6B8"
  }
}

不要在响应里放堆栈、密钥或内部主机名。

统一媒体类型。RFC 9457 定义了 application/problem+json,字段包括 type、title、status、detail 和 instance;直接采用它,客户端和工具链就有稳定结构,不必自己发明一套。如果保留自定义信封,机器可读错误码要在版本间保持稳定,因为客户端会对它做分支;message 则应当视为可能变化、可能本地化的人类可读文本。

请求 ID 要同时出现在正文和 X-Request-ID 响应头里。用户报障时,支持人员可以用它搜索日志,而不是让用户截图。错误正文经常被第三方日志系统和浏览器开发者工具记录,不适合放任何敏感内容。

排查顺序

  1. 看状态码类别。
  2. 看响应正文和响应头。
  3. 用 curl 或 HTTP 请求工作台 复现。
  4. 4xx 检查请求:方法、URL、头、正文、认证。
  5. 5xx 检查服务端日志和上游健康状态。
  6. 检查代理、CDN 或网关是否修改了响应。
  7. 必要时检查 Retry-After、WWW-Authenticate 和 CORS 头。

HTTP 状态码查询工具 提供标准含义和常见原因。HTTP 头解析工具 可以查看经常解释故障原因的响应头。

在看状态码之前,先确认自己拿到的是正确的响应。浏览器开发者工具对 DNS、TLS 和 CORS 失败显示状态 0,此时根本没有 HTTP 响应,也就没有状态码。curl -v 能看出是代理应答、TLS 是否成功、重定向是否被跟随。耗时也能提供线索:很快返回的 502 多半是上游连接被拒,很慢的 504 是超时,两毫秒就返回的 500 更像路由或中间件 bug。

任何状态码处理改动,都应过一遍检查清单:客户端只在 401 时刷新、不在 403 时刷新;重试中间件只处理清单内的状态码并遵守 Retry-After;监控把 4xx 和 5xx 分开;缓存不会误存错误响应;错误正文经过代理后仍能按 JSON 解析;请求 ID 能在服务端日志里查到。

一致比完美更重要

不同 API 对 400/422、401/403 的划分不完全一样。真正重要的是内部一致、文档清楚。客户端应该能从失败类型预测状态码类别,运维应该能判断重试是否有用。

选一套约定,写进文档,并让错误正文有用。状态码是第一个信号,正文才是修复的起点。

用一张决策矩阵让新接口保持一致:语法错误用 400;语法正确但取值非法用 422;缺少或无效凭据用 401;已认证但无权限用 403;状态冲突用 409;触发限流用 429;未预期的服务端失败用 500;上游响应无效用 502;过载或维护用 503;上游超时用 504。把矩阵放进 API 风格指南,并为每个分支写契约测试,因为约定往往是一个处理器一个处理器地走样的。

当 REST 纯粹性和客户端行为冲突时,优先照顾客户端。只有一个内部客户端的接口,用点不常见的状态码还能接受;一旦出现第二个客户端、代理或重试层,歧义就会变贵。GraphQL 和部分 RPC 框架有意用 200 表达业务错误,那是另一套契约,不是 REST 接口也可以 200 加错误的理由。每个 API 面选一种模型,写清楚,并且测试失败路径,而不只是 happy path。

常见问题

400 和 422 有什么区别?

400 Bad Request 表示服务端无法理解请求语法或缺少必要参数。422 Unprocessable Content 表示语法正确,但内容语义不合法,例如数量为负、结束日期早于开始日期。

什么时候返回 401,什么时候返回 403?

缺少认证或认证无效时返回 401,客户端应该去获取凭据。已经认证但没有权限时返回 403。不要用 403 来掩盖缺少认证。

资源不存在时一定要返回 404 吗?

通常是的。资源曾经存在但已被永久删除时可以用 410 Gone;不希望暴露资源是否存在时用 404。同一个 API 内部要保持一致。

哪些状态码应该重试?

408、425、429、500、502、503、504 可以配合指数退避和抖动重试。非幂等请求不要盲目重试;服务端提供 Retry-After 时要遵守。

为什么返回 200 加错误正文是个问题?

客户端、代理、监控和重试逻辑都依赖状态码判断结果。200 会让除业务代码之外的所有层都认为成功,从而隐藏故障并破坏可观测性。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南