引言
状态码是 HTTP 里最被低估的一门「语言」:它用三位数字,把「谁的责任、能不能重试、能不能缓存、结果是什么」一次性说清。可惜大量接口把一切都塞进 200 OK,让客户端只能去解析业务码;也有人把 302 用在 PUT 上、把 403 和 401 混着用、把参数校验失败报成 500。本文从五位分层讲到具体码的语义边界,再落到幂等性、缓存头、排错套路与 API 设计取舍,帮你把状态码用成一套可依赖的契约。
前置:字节与编码、响应体与错误结构。传输层细节见 network 专题。
目录
- 1. 状态码的分层与五位语义
- 2. 2xx:成功语义的细分
- 3. 3xx:重定向与缓存协商
- 4. 4xx:客户端错误的细分
- 5. 5xx:服务端错误与可用性
- 6. 幂等性与安全性:方法与状态码搭配
- 7. 缓存头与条件请求
- 8. 常见误用与排错套路
- 9. API 设计中的状态码取舍
- 10. 速查表与一句话记忆
- 延伸阅读
1. 状态码的分层与五位语义
状态码是三位数字,首位决定大类,后两位是细分:
1xx 信息性:请求已收到,继续处理(临时响应,无响应体)
2xx 成功:请求被理解并接受
3xx 重定向:需要进一步动作才能完成
4xx 客户端错误:请求本身有问题(原样重试没用)
5xx 服务端错误:服务器处理失败(换时机可能成功)
关键心法:状态码回答的是「谁的责任」与「能否重试」——4xx 别原样重试、先改请求;5xx 才谈带退避的重试(幂等前提下);3xx 跟随 Location 但注意方法与缓存语义。中间层也会造状态码:网关、CDN、负载均衡、WAF 都可能返回自己的 502/503/504,此时响应体里往往没有你的应用错误结构——这是排错时的第一分辨点。
记忆:首位定大类——1xx 继续、2xx 成功、3xx 重定向、4xx 怪你、5xx 怪我;看到 4xx 先改请求,看到 5xx 才谈退避重试。
2. 2xx:成功语义的细分
「成功」远不止 200:
| 状态码 | 语义 | 典型场景 |
|---|---|---|
| 200 | 通用成功 | GET 返回资源、PUT/PATCH 返回更新后实体 |
| 201 | 已创建 | POST 创建资源,须带 Location 指向新资源 |
| 202 | 已接受 | 异步任务已入队,尚未完成 |
| 204 | 无内容 | 成功但无响应体(DELETE、仅确认的 PUT) |
| 206 | 部分内容 | Range 请求命中,返回片段(断点续传/拖动) |
201 的规矩:Location 头给出新资源地址
HTTP/1.1 201 Created
Location: /users/42
202 的规矩:告诉客户端"何时何地看结果"
HTTP/1.1 202 Accepted
Location: /tasks/9876 (轮询任务状态)
204 的规矩:绝不能有响应体(连非零 Content-Length 都不该给)
206 与 Range:客户端发 Range: bytes=0-1023,服务端回 206 Partial Content 并带 Content-Range: bytes 0-1023/10000。视频拖动、多线程下载都依赖它;不支持时返回 200 全量或 416。
常见误区:把「业务失败」包装成 200。这会让客户端、网关、监控全部失明——正确做法是用合适的 4xx/5xx,业务细分码放进响应体。
记忆:2xx 要分得清——创建用 201 带 Location、异步用 202 给查询地址、无体用 204、分块用 206;把失败塞进 200 是让整个链路失明的坏习惯。
3. 3xx:重定向与缓存协商
3xx 分为两族:真正的重定向(换地址)与 304 协商缓存(内容没变)。
| 状态码 | 语义 | 方法是否改变 |
|---|---|---|
| 301 | 永久重定向 | 历史上可能把 POST 变 GET |
| 308 | 永久重定向 | 保持方法不变(明确) |
| 302 | 临时重定向 | 历史上可能把 POST 变 GET |
| 303 | 见其他 | 强制改成 GET(POST 后跳结果页) |
| 307 | 临时重定向 | 保持方法不变 |
| 304 | 未修改 | 缓存协商命中,无响应体 |
永久 vs 临时:301/308 会被搜索引擎与浏览器长期缓存;302/307 不缓存
方法保持:307/308 保证 POST 还是 POST;301/302 旧实现常把 POST 降级为 GET
304 的机制:客户端带 If-None-Match: "abc"(ETag)或 If-Modified-Since,服务端发现未变就回 304,不带响应体,客户端直接用本地缓存。这是省流量的关键。陷阱:304 只能用于条件请求;301 用于 API 端点会让客户端永久缓存旧地址,很难回退——API 场景优先 308/307 或干脆用 302。
记忆:301/308 是永久、302/307 是临时,307/308 保方法、303 强制 GET、304 是协商缓存命中;给 API 做跳转别用 301,否则客户端把旧地址刻进缓存。
4. 4xx:客户端错误的细分
| 状态码 | 语义 | 记忆点 |
|---|---|---|
| 400 | 请求格式错误 | 语法错、JSON 解析失败、缺必需参数 |
| 401 | 未认证 | 缺/错凭证,应带 WWW-Authenticate |
| 403 | 已认证但无权限 | 身份明确,只是不许 |
| 404 | 资源不存在 | 也可能用于「隐藏存在性」 |
| 405 | 方法不允许 | 必须带 Allow 头列出允许的方法 |
| 409 | 冲突 | 并发写、唯一键冲突、状态机非法迁移 |
| 410 | 已永久删除 | 比 404 更明确,利于清理缓存 |
| 412 | 前置条件失败 | If-Match 版本不符(乐观锁) |
| 415 | 媒体类型不支持 | Content-Type 不对 |
| 422 | 语义错误 | 格式对但业务校验不过 |
| 429 | 请求过多 | 必须带 Retry-After |
401 与 403 的分水岭:
401 = "你是谁?"——没登录/凭证过期 → 客户端应引导登录或刷新 token
403 = "我知道你是谁,但你不能"——权限不足 → 重新登录也没用
把权限不足返回 401,会让客户端陷入"刷新 token 死循环"
405 必须带 Allow(Allow: GET, POST, DELETE);429 必须带 Retry-After 与限流维度(IP/Token/路径):
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1730000000
记忆:401 是「你没表明身份」、403 是「表明了但没权限」——混用会导致刷新凭证死循环;405 必带 Allow、429 必带 Retry-After、409/412 是并发与乐观锁的标准答案。
5. 5xx:服务端错误与可用性
| 状态码 | 语义 | 常见来源 |
|---|---|---|
| 500 | 内部错误 | 应用未捕获异常 |
| 501 | 未实现 | 网关不支持该方法/功能 |
| 502 | 网关错误 | 上游返回了非法响应或连接被重置 |
| 503 | 服务不可用 | 过载、维护、实例全部不健康 |
| 504 | 网关超时 | 上游在超时内没响应 |
排错时的分辨顺序:
502/504 → 先看网关到上游这一段:
上游进程是否存活、端口是否监听;上游是否在超时前返回(看上游访问日志)
连接池是否耗尽、keepalive 是否被中间设备切断
503 → 看实例健康与容量:是否被探针摘除、是否触发熔断/限流、是否在滚动发布
500 → 看应用日志与异常栈,通常是代码/数据问题
503 与 Retry-After:若知道恢复时间(如维护窗口),带上 Retry-After 让客户端优雅退避。别把 4xx 变 5xx:参数校验失败返回 500 会污染错误率指标、触发无谓告警、误导重试——客户端错误就该是 4xx。
记忆:500 怪应用、502/504 怪网关到上游这一段、503 怪容量与健康——排错先按「网关 → 上游 → 应用」分层看日志;把 4xx 报成 5xx 会污染指标与告警。
6. 幂等性与安全性:方法与状态码搭配
安全性(safe):不改服务端状态,可放心预取。幂等性(idempotent):执行一次与多次,服务端状态一致。
| 方法 | 安全 | 幂等 | 说明 |
|---|---|---|---|
| GET | 是 | 是 | 只读,可缓存 |
| HEAD | 是 | 是 | 只要头部 |
| OPTIONS | 是 | 是 | 能力探测/CORS 预检 |
| PUT | 否 | 是 | 全量替换,重复执行结果相同 |
| DELETE | 否 | 是 | 删除两次结果相同 |
| PATCH | 否 | 否 | 增量修改,可能叠加 |
| POST | 否 | 否 | 创建/提交,重复会重复副作用 |
幂等性决定了「能否安全重试」——这是分布式系统的生命线:
网络超时后,客户端不知道请求是否到达:
GET/PUT/DELETE → 直接重试(幂等)
POST → 重试可能重复下单/扣款
→ 解法:幂等键(Idempotency-Key 头)+ 服务端去重表
方法与状态码的搭配惯例:
POST /users → 201 + Location(或 202 异步)
GET /users/42 → 200 / 404
PUT /users/42 → 200(返回新实体)/ 204(无体)/ 201(若为新建)
PATCH /users/42 → 200 / 204 / 422(校验失败)
DELETE /users/42 → 204(成功)/ 404(不存在)
记忆:GET/HEAD/OPTIONS 安全、PUT/DELETE 幂等、POST 既不安全也不幂等——幂等性决定了能否安全重试;POST 重试必须靠幂等键去重,否则就是重复下单。
7. 缓存头与条件请求
| 响应头 | 作用 |
|---|---|
| Cache-Control | max-age/s-maxage/no-cache/no-store/private/public |
| ETag | 内容指纹(强/弱),用于 If-None-Match |
| Last-Modified | 最后修改时间,用于 If-Modified-Since |
| Vary | 告诉缓存「按哪些请求头分桶」 |
| Age | 该响应已在缓存中存活的秒数 |
Cache-Control 要点:
max-age=600 客户端可缓存 600 秒
s-maxage=600 仅共享缓存(CDN)用 600 秒
no-cache 可缓存,但每次必须回源校验(走 304)
no-store 完全不缓存(含敏感数据)
private 仅浏览器可缓存,CDN 不可
immutable 内容不会变,别校验(配合指纹文件名)
条件请求的两种校验器:强校验器用 ETag + If-None-Match(精确匹配);弱校验器用 Last-Modified + If-Modified-Since(秒级精度)。命中回 304,未命中回 200 + 新校验器。
Vary 的坑:若响应随 Accept-Encoding、Accept-Language、Authorization 变化却没写 Vary,CDN 可能把 A 用户的响应喂给 B 用户(缓存投毒/串号)。写操作的条件请求(乐观锁):
PUT /doc/1
If-Match: "v3"
→ 版本不符回 412 Precondition Failed,避免覆盖他人修改
记忆:缓存靠 Cache-Control 定策略、ETag/Last-Modified 做校验器、Vary 决定分桶;写操作用 If-Match 做乐观锁(不符回 412)——Vary 漏写会造成 CDN 串号。
8. 常见误用与排错套路
高频误用清单:
1. 一切皆 200:业务失败也回 200 + {code: 50001} → 网关/监控/重试机制全失效
2. 401 与 403 混用:权限不足回 401 → 客户端无限刷新 token
3. 302 用于 PUT/DELETE:旧实现会把方法降级为 GET,语义被破坏
4. 404 用于"无权限":把 403 伪装成 404 可隐藏存在性,但会误导排错
5. 500 掩盖 4xx:把参数校验失败报成 500,污染错误率
6. 301 用于 API 端点:客户端永久缓存旧地址,回滚困难
7. 429 不带 Retry-After:客户端只能盲目重试,加剧拥塞
8. 204 却带响应体:协议违规,部分客户端会挂
排错 SOP:
① 看大类:4xx → 检查请求(URL/方法/头/体/凭证/权限);5xx → 检查服务端与中间层
② 看响应头定位来源:Server/Via/X-Cache 揭示是哪一层回的
响应体不是你应用的错误结构 → 大概率是网关/WAF 造的
③ 502/504 分层:网关日志(连接与超时)→ 上游日志(是否收到、耗时)
上游日志无记录 → 连接根本没到上游(网络/端口/keepalive)
④ 用 curl -v 复现,对比"最小可复现请求"与"完整请求"(差异常在某个头或 Cookie)
监控价值:把 5xx 率与4xx 率分开看。5xx 飙升是故障信号;4xx 飙升常是客户端发布或攻击(429/401 激增)。混在一起就失去了告警的辨别力。
记忆:排错先分大类(4xx 改请求、5xx 查服务端),再看响应头找出是哪一层回的(网关/WAF/应用),502/504 按「网关→上游→应用」逐层看日志;监控要把 4xx 与 5xx 分开统计。
9. API 设计中的状态码取舍
REST 派用足 HTTP 语义(状态码 + Location/Allow/Retry-After 头);RPC 派一律 200、业务码放响应体,简单直观但丢失中间层可观测性。推荐折中:
- 传输层/协议层错误用真实状态码(401/403/404/429/5xx)
- 业务校验失败用 4xx(400/409/422)+ 结构化错误体
- 业务细分码放进错误体字段,不挤占状态码
结构化错误体(RFC 7807 problem+json):
{
"type": "https://example.com/probs/out-of-credit",
"title": "Insufficient credit",
"status": 403,
"detail": "Current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc"
}
Content-Type: application/problem+json
title 给人看、detail 给日志看、type 给程序分支
分页与部分成功:列表分页用 200 + Link 头(rel=next/prev)或响应体游标;批量部分失败用 207 Multi-Status 或 200 + 逐项结果。弃用:永久移除用 410 Gone,过渡期加 Deprecation/Sunset 头提示下线时间。
记忆:传输层错误用真实状态码、业务细分码放错误体(RFC 7807 problem+json)——既让网关/监控看得懂,又不被三位数限制;批量部分成功用 207 或逐项结果。
10. 速查表与一句话记忆
| 场景 | 推荐状态码 |
|---|---|
| 读取成功 | 200 |
| 创建成功 | 201 + Location |
| 异步已受理 | 202 + Location |
| 成功无内容 | 204 |
| 分块/续传 | 206 + Content-Range |
| 永久跳转 | 301 / 308(保方法) |
| 临时跳转 | 302 / 307(保方法) |
| POST 后跳结果页 | 303 |
| 协商缓存命中 | 304 |
| 请求格式错 | 400 |
| 未认证 | 401 + WWW-Authenticate |
| 无权限 | 403 |
| 不存在 | 404 / 410 |
| 方法不允许 | 405 + Allow |
| 并发冲突 | 409 / 412 |
| 校验不过 | 422 |
| 限流 | 429 + Retry-After |
| 应用异常 | 500 |
| 网关到上游失败 | 502 / 504 |
| 容量/维护 | 503 + Retry-After |
一句话记忆:状态码回答「谁的责任、能否重试、能否缓存」——首位定大类(4xx 改请求、5xx 才退避重试);成功要分细(201 带 Location、202 给查询地址、204 无体、206 分块);重定向别用 301 做 API、307/308 保方法、304 走协商缓存;401 是「没表明身份」、403 是「没权限」,混用会死循环;GET/HEAD 安全、PUT/DELETE 幂等、POST 靠幂等键去重;缓存靠 Cache-Control + ETag + Vary(漏写 Vary 会串号);排错按「网关→上游→应用」分层看日志,把 4xx 与 5xx 分开监控——把状态码当成契约来设计,整条链路都会因此受益。
延伸阅读
- 字节、编码与协议体观察
- 响应体与错误结构的序列化
- 访问日志解析与错误率监控
- Last-Modified/Date 头的时间语义
- 路由与路径匹配的直觉
- network 专题 — TCP/TLS 与网关层排查
- RFC 9110 HTTP Semantics
- MDN HTTP 状态码
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。