接了个老项目,所有接口不管成功失败都返回 200,错误信息塞在 body 里的 code 字段。前端同事写了一堆 if (res.code === 0) 的判断,看起来没问题。直到有一天接了个第三方监控告警系统,它只认 HTTP 状态码——结果服务器炸了,监控一直绿油油的,因为业务层全返回 200。
这个坑的根源,是把状态码当成了「给前端看的标记」,而不是「给整个 HTTP 生态看的契约」。
上一篇讲了 method 表达「我要干什么」,这一篇讲它的对偶:状态码表达「干得怎么样」。两者合起来,才是 HTTP 的完整意图层。状态码不只是一串数字,它带语义,而这套语义被监控、网关、重试、缓存全部依赖。
五大类:看首位数字就够了
状态码第一位数字是类别,这个上一篇的骨架图立过了。这里往深挖几个被误用最多的类别。
容易被忽视的是 2xx 不只有 200。201 Created 配合 Location 头告诉客户端「新资源建好了,在 Location 指向的 URL」;204 No Content 表示「成功但没有 body 要返回」,DELETE 接口常用它;206 Partial Content 是范围请求(断点续传)的标志。这些状态码不是装饰,监控和客户端可以靠它们做精确判断——比如看到 201 就知道该跳转,看到 204 就知道不用解析 body。
301 vs 302 vs 307 vs 308:重定向里藏着 method 的命运
3xx 重定向是最容易踩坑的一类。看起来都是「跳到新地址」,但它们对请求 method 的处理完全不同:
关键差别在一个历史包袱上:
- 301 永久重定向:规范允许把后续请求的 method 从 POST 改成 GET(实际上大多数浏览器都这么干)。这是历史遗留——早期实现图省事,301 统一转成 GET。
- 302 临时重定向:同样,规范虽然说不该改 method,但浏览器普遍把 POST 改成 GET。
- 307 临时重定向:明确禁止改变 method。POST 重定向后还是 POST,body 保留。这是为了修正 302 的歧义引入的。
- 308 永久重定向:明确禁止改变 method,是 301 的「严格遵守版」。
这个差别什么时候要命?当你提交表单(POST),服务器返回重定向,你希望用户的提交数据被原样带到新地址——这时候必须用 307/308。如果用 302,浏览器可能把 POST 改成 GET,body 丢了,用户数据没了。
实战建议:现代应用优先用 307/308,而不是 301/302。除非你明确想降级成 GET(比如把旧 API 永久指向新文档页),否则保留 method 更安全。
401 vs 403:认证和授权不是一回事
这两个状态码经常被混用,但它们语义不同:
- 401 Unauthorized:名字有误导性,它真正表达的是「未认证——我不知道你是谁」。正确的响应是让客户端提供凭证(登录、带 token)。响应里通常会带
WWW-Authenticate: Bearer告诉客户端该怎么认证。 - 403 Forbidden:表达「未授权——我知道你是谁,但你没权限做这件事」。这时候让客户端重新登录没用,因为问题不在身份,在权限。
举个场景:用户 A 登录后想访问用户 B 的订单。如果没带 token,返回 401(先登录);带了 token 但那是 A 的身份,A 不能看 B 的订单,返回 403(你知道我是谁,但我没权限)。把这两个搞反,前端的登录流程就会乱——该提示「无权限」的时候却弹登录框,用户体验很糟。
顺带一提 429 Too Many Requests:限流专用状态码,配合 Retry-After 头告诉客户端「多久后再试」。很多团队限流时返回 500 或 200+业务码,客户端不知道该退避多久,结果持续重试把服务器打得更狠。429 + Retry-After 是限流的正确姿势。
全用 200:为什么这是个坏习惯
回到开头那个案例。把所有响应都设成 200,错误塞进 body,看起来「前端处理方便」,但它破坏了 HTTP 语义契约:
- 监控告警失效:APM、网关、负载均衡都按 HTTP 状态码判断健康。全返回 200,它们以为一切正常,服务器着火了告警还是绿的。
- 重试逻辑失效:SDK 和中间件的重试通常按 5xx 和超时触发。业务错误被包成 200,重试机制完全不工作。
- 缓存行为失效:CDN、反向代理按状态码决定缓存。错误的 200 可能被缓存,下次请求拿到的是上次的错误响应。
不是说业务码完全不能用——有些团队喜欢在 body 里带 code 做更细的业务分类(比如区分「余额不足」和「商品下架」),这本身没毛病。但 HTTP 状态码应该是第一道语义层:成功就是 2xx,客户端错误是 4xx,服务端错误是 5xx。细分的业务码可以放 body 里作为补充,但不能替代状态码。
取舍与边界
状态码这套体系有几个不完美的地方:
- 418 I'm a teapot 是个彩蛋(RFC 2324 超文本咖啡壶控制协议的玩笑),虽然保留在注册表里,但别在生产用。
- 状态码不够用是真实问题。比如「请求格式正确但语义冲突」(创建已存在的资源)该用什么?有人用 409 Conflict,有人用 422 Unprocessable Entity。这类边界没有标准答案,团队内统一就行。
- 代理会篡改状态码:某些错误网关会把上游的真实状态码替换成 502/504,排查时看到的不是原始信息,这点在排障时要警惕。
收束:状态码是给生态看的,不只是给你看的
method 和状态码是 HTTP 意图层的阴阳两面。method 说「我要干什么」,状态码说「干得怎么样」。这套契约的价值,在于它让无数你没见过的中间件——监控、缓存、重试、网关——能按统一规则工作。
下一篇讲 body 怎么编码。状态码是「结果的语义」,body 是「结果的内容」,两者配合才是一个完整的响应。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

