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

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

**快速答案:** 第一位数字决定谁来处理：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`：请求语法正确，但语义不合法，例如数量为负、结束日期早于开始日期、值超出允许范围。

```json
// 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` 表示请求和资源当前状态冲突，例如：

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

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

```json
{
  "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
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` 默认不幂等，除非使用幂等键。

可重试状态码：

```text
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，便于支持排查。
- 可选的文档链接。

```json
{
  "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 请求工作台](/tools/http-request) 复现。
4. 4xx 检查请求：方法、URL、头、正文、认证。
5. 5xx 检查服务端日志和上游健康状态。
6. 检查代理、CDN 或网关是否修改了响应。
7. 必要时检查 `Retry-After`、`WWW-Authenticate` 和 CORS 头。

[HTTP 状态码查询工具](/tools/http-status) 提供标准含义和常见原因。[HTTP 头解析工具](/tools/http-headers) 可以查看经常解释故障原因的响应头。

在看状态码之前，先确认自己拿到的是正确的响应。浏览器开发者工具对 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 会让除业务代码之外的所有层都认为成功，从而隐藏故障并破坏可观测性。

## 参考资料

- [RFC 9110 - HTTP 语义](https://www.rfc-editor.org/rfc/rfc9110)
- [MDN - HTTP 响应状态码](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status)
- [RFC 6585 - 额外 HTTP 状态码](https://www.rfc-editor.org/rfc/rfc6585)
- [RFC 9111 - HTTP 缓存](https://www.rfc-editor.org/rfc/rfc9111)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/http-status-codes-for-apis
- Locale: zh-CN
- Published: 2026-11-09
- Updated: 2026-11-09
- Tools: http-status, http-request, http-headers, cors-checker