HTTP 状态码与请求语义:从 1xx 到 5xx 的正确用法

系统梳理 HTTP 状态码与请求语义:1xx~5xx 五位分层与责任归属、2xx 成功语义细分(200/201/202/204/206)、3xx 重定向与缓存协商(301/302/303/307/308/304)、4xx 客户端错误(401 与 403、405/409/422/429)、5xx 服务端错误(500/502/503/504)、幂等性与安全性与方法与状态码搭配、缓存相关头与条件请求、常见误用与排错套路、API 设计中的状态码取舍与 RFC 7807 问题详情。

引言

状态码是 HTTP 里最被低估的一门「语言」:它用三位数字,把「谁的责任、能不能重试、能不能缓存、结果是什么」一次性说清。可惜大量接口把一切都塞进 200 OK,让客户端只能去解析业务码;也有人把 302 用在 PUT 上、把 403 和 401 混着用、把参数校验失败报成 500。本文从五位分层讲到具体码的语义边界,再落到幂等性、缓存头、排错套路与 API 设计取舍,帮你把状态码用成一套可依赖的契约。

前置:/others-binary-encoding-tools/(字节与编码)、/others-json-yaml-processing/(响应体与错误结构)。传输层细节见 network 专题。


目录


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-Controlmax-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 分开监控——把状态码当成契约来设计,整条链路都会因此受益。


延伸阅读

  • /others-binary-encoding-tools/ — 字节、编码与协议体观察
  • /others-json-yaml-processing/ — 响应体与错误结构的序列化
  • /others-log-parsing/ — 访问日志解析与错误率监控
  • /time-timezone-handling/ — Last-Modified/Date 头的时间语义
  • /others-glob-file-matching/ — 路由与路径匹配的直觉
  • network 专题 — TCP/TLS 与网关层排查
  • RFC 9110 HTTP Semantics
  • MDN HTTP 状态码

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「others」更多文章

  1. 语义化版本与依赖解析:从 SemVer 规则到依赖地狱治理
  2. 图像与媒体工具链:ImageMagick、ffmpeg 与格式选型实战
  3. 国际化与本地化处理:locale、LC_ 变量与 ICU 实践