真正会伤害你的HTTP状态码——301与302、401与403,以及5xx错误陷阱
不是字典。是一份专注于导致实际生产问题的HTTP状态码的指南——错误的重定向现在被永久缓存,401/403混淆泄露了你的认证逻辑,429错误缺少Retry-After头,以及503与504的混淆让你误入错误的调试层级。
您发送了一个重定向,但类型错误。现在所有在您之前访问过该重定向的浏览器都已永久缓存了它,唯一的解决方法是等待缓存过期,或者请求用户清除浏览器历史记录。这是 301 状态码。
这不是一个状态码字典——这类资料很多。这是在您阅读了表格后仍然发送了错误代码时需要的指南。我们正在讨论那些导致实际问题的状态码:您本想使用临时重定向却用了永久重定向,认证错误泄露了信息,未正确处理的速率限制响应,以及指向错误层级的网关超时。
301 与 302 与 307 与 308:重定向矩阵
每个人至少会犯一次的错误:您使用 301 Moved Permanently 在测试 URL 重构时。它有效。您继续进行。然后您再次重构。现在一部分用户——在第二次变更之前访问过您的用户——将被永久发送到旧的地址,且该地址已永久缓存在他们的浏览器中。
301 是永久性的,且会激进地缓存。 浏览器将重定向缓存的 TTL 设置为近乎无限,除非您设置了 Cache-Control 头信息。目前没有程序化方法可以从用户的浏览器中清除该缓存。如果您仍在确定 URL 结构,请使用 302。
方法保留问题是一个不同的问题。当浏览器跟随 301 或 302 重定向时,它 可能会 (实际上几乎总是会) 将 POST 降级为 GET。这在技术上违反了原始的 HTTP/1.0 规范,但浏览器早已将其标准化,RFC 7231 也承认这是标准行为。如果您正在重定向一个 POST 请求——例如表单提交之后——并且希望保留方法,您需要使用 307 或 308。
| 代码 | 永久性? | 保留方法? | 用于 |
|---|---|---|---|
| 301 | 是的 | 否(POST → GET) | 永久 URL 移动,GET 在重定向后即可使用 |
| 302 | 不 | 否(POST → GET) | 临时重定向,功能标志,A/B 测试 |
| 307 | 不 | 是的 | 临时重定向,且必须保留方法 |
| 308 | 是的 | 是的 | 永久重定向,且必须保留方法 |
308 的支持现在非常稳定——Chrome、Firefox、Safari 和 Edge 都能正确处理它。但要注意:一些较旧的 API 客户端和 HTTP 库仍未能正确遵循 308,因此如果您正在重定向机器对机器的流量,并且无法控制客户端及其 HTTP 库,必须明确测试。
401 与 403:认证与授权
401 表示“您尚未进行身份验证”。 规范要求它必须包含一个 WWW-Authenticate 头信息,告知客户端如何进行认证。当没有有效会话、没有令牌,或令牌已过期时,这是正确的状态码。
403 表示“我知道你是谁,但答案是‘否’”。 用户已通过认证,但缺乏权限。在令牌过期时返回 403 是错误的——这应是 401。在有效但权限不足的请求中返回 401 也是错误的,更糟糕的是,这会混淆您的认证调试:您会寻找缺失的凭据,而真正的根本问题是角色分配。
在返回 404 而不是 403 的安全论点是真实的。如果您的 API 在 返回 403,您实际上就告诉了任何攻击者该端点存在,他们需要更高权限才能访问它。返回 404 则不会泄露资源存在的信息。这是一个判断:403 在技术上是正确的,且更容易调试;404 是对敏感端点中可枚举性至关重要的安全选择。 GET /admin/users429:缺少 Retry-After 头的速率限制是无用的
没有
头的 429 响应是一个黑箱。客户端知道已被速率限制,但不知道应该等待 100 毫秒还是 24 小时。大多数客户端实现要么立即重试(导致您的速率限制器被击穿),要么直接放弃。 Retry-After Retry-After 头可以是整数(等待秒数)或 HTTP 日期(重试时间):
Retry-After 头信息并未出现在任何 RFC 中,而是源自 GitHub API 的事实标准,被广泛复制。请在
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1749254400
这 X-RateLimit-* 头信息中同时包含 Retry-After。Limit/Remaining/Reset 三者组合告诉客户端在达到限制前的状态,这比仅仅知道已触碰到限制更有用。
值得一提的一点是: Retry-After 在 503 响应中也是有效的,其含义相同——“请在若干秒后再次尝试”。如果您的服务因维护窗口暂时不可用,请发送一个带有 Retry-After 的 503 响应,而不是简单地断开连接。
503 与 504:失败发生在何处?
这两个看起来相似,但指向完全不同的故障层级,混淆它们会将您引入错误的调试方向。
503 服务不可用 表示您访问的服务器当前无法处理请求——它可能过载、处于维护模式,或后端依赖服务已宕机。服务器本身已响应,只是拒绝处理请求。
504 网关超时 表示代理或网关(如您的负载均衡器、Nginx、API 网关、CDN)尝试连接上游服务器但未在规定时间内收到响应。您访问的服务器是存活的,但其后端服务无法响应。
实际应用中:如果您在 Nginx 之后,而应用服务器(Node、Rails、Django 等)崩溃,您会收到 502 错误——Nginx 收到了响应,但内容是垃圾。如果应用服务器完全停止响应,您会收到 504。如果在应用层故意返回错误,您会收到 503。区分这一点在故障排查中至关重要:504 表示应检查上游服务、超时配置和网络。503 表示应检查应用本身。
422 与 400:验证错误有其专属代码
400 请求错误 用于服务器无法解析的请求——如格式错误的 JSON、查询字符串语法错误、缺少必需的头部。这是一个结构问题。
422 不可处理的实体 用于服务器理解了请求但因数据验证失败而无法处理的情况——如起始时间晚于结束时间、邮箱字段包含无效地址、数量字段为负值。请求在语法上是正确的,但语义上是错误的。
大多数 API 将两者都合并为 400,并将验证细节隐藏在响应体中。这虽然可行,但使客户端错误处理更困难:客户端必须解析响应体才能判断是解析错误还是验证错误。使用 422 作为验证错误代码,可以让客户端直接通过状态码判断。Rails 早已正确实现这一点;JSON:API 规范也规定 422 用于验证错误。
一个细微差别:422 定义于 WebDAV(RFC 4918),而非核心 HTTP 规范。在实践中这并不重要——所有 HTTP 客户端和服务器都能正确处理它——但您偶尔会遇到一个固执的人坚持应使用 400。他们并非完全错误。但 422 更具体且被更广泛理解。
204 与 200 带空体:DELETE 的正确响应
当 DELETE 成功时,正确的响应是 204 无内容 ——而不是 200 带空体,也不是 200 带 {"success": true}.
204 是明确表示“该操作成功且有意无响应体”的信号。200 带空体在技术上是有效的,但存在歧义——客户端不知道是本应存在但丢失了体,还是本意就是无体。204 消除了这种歧义。
同样的逻辑适用于 PUT 和 PATCH,当您不返回更新后的资源时。如果您的 API 在 PATCH 后返回更新后的对象,请使用 200;如果未返回,请使用 204。不要返回 200 带空体——那是 204 的伪装。 {} 或 null 一个常见陷阱:
204 必须不包含消息体 根据 RFC 9110。如果您返回 204 时意外包含体(某些框架允许),一些 HTTP 客户端会优雅处理,而另一些则不会。请在响应处理器中移除体,而不仅仅是意图层面。 快速参考:容易引发问题的状态码及其原因
常见错误
| 代码 | 意义 | 修复方案 | 永久重定向 |
|---|---|---|---|
| 301 | 在测试期间使用——现在已被永久缓存 | 在确认重定向为永久前使用 302 | POST 在跟随时被降级为 GET |
| 302 | 临时重定向 | 如果必须保留方法,请使用 307 | 临时重定向(方法安全) |
| 307 | 与 302 混淆 | 在临时重定向 POST/PUT/PATCH 时使用 | 永久重定向(方法安全) |
| 308 | 习惯性跳过 302 而选择 301 | 当方法重要时,使用 307 而非 301 | 请求解析错误 |
| 400 | 也用于验证错误 | 对于语义验证失败,使用 422 | 未认证 |
| 401 | 在用户缺乏权限时返回(应返回 403) | 仅在凭据缺失或已过期时返回 401 | 在安全敏感端点中,用 404 替代 403 |
| 403 | 禁忌 | 当隐藏端点存在性至关重要时,考虑使用 404 | 不可处理的实体 |
| 422 | 被合并到 400 | 用于验证失败,且请求体可解析的情况 | 速率限制 |
| 429 | 缺少 Retry-After 头 | 始终包含 Retry-After 和 X-RateLimit-* 头 | 服务不可用 |
| 503 | 与 504 混淆 | 当目标服务器无法处理请求时使用 | 网关超时 |
| 504 | 与 503 混淆 | 应调查上游服务,而非网关 | 无内容 |
| 204 | 返回 200 带空体代替 | 对于成功的 DELETE/PUT/PATCH 且无响应体,使用 204 | 如果您需要在调试时快速查找任何状态码, |
IO Tools 提供了一个 HTTP 状态码查找表 涵盖了完整范围,并附有描述和使用每个状态码的说明。 所有这些错误背后的模式
这些错误大多源于同一个地方:将状态码视为松散类别(“4xx 是客户端错误,5xx 是服务器错误,结束”)而非协议中具有特定语义的元素。语义很重要,因为客户端——浏览器、HTTP 库、CDN、监控工具——实际上会依据这些语义进行判断。CDN 不会以相同方式缓存 200 和 204。HTTP 客户端不会以相同逻辑重试 400 和 503。浏览器不会以相同 TTL 缓存 302 和 301。
使用正确的状态码。开销为零。当出现问题时,调试带来的收益是巨大的。
HTTP 状态码中真正会“咬人”的情况——301 与 302、401 与 403,以及 5xx 的陷阱 2
