HTTP 状态码:帮你一眼看懂服务器的「回话」

每次请求发出去,服务器都会返回一个三位数的状态码。这三位数,说白了就是服务器在跟你说:「你刚才的操作,结果是什么」。做接口设计这些年,我最怕的不是状态码多,而是乱用。200 表示一切正常,有人把参数校验失败也返回 200,然后 body 里塞个「error」字段——这种设计,前端得先解析 body 才能知道到底成功没,你说坑不坑?

我个人习惯,先背熟每个分类的含义,再记常用编号。状态码分五大类,数字开头就告诉你方向:

分类含义常见场景
1xx信息性协议升级、继续发送
2xx成功获取、创建、更新、删除成功
3xx重定向资源移动、临时/永久跳转
4xx客户端错误参数错误、身份验证失败、资源不存在
5xx服务端错误内部异常、服务不可用、网关超时

核心原则:状态码描述的是 HTTP 层面的事情,业务逻辑的错误不要滥用 4xx/5xx,你得用业务码(比如 body 里的 code 字段)来区分。

1xx:握手和预热

1xx 在日常开发中很少遇到。我记得刚学 HTTP 时,看到 101 Switching Protocols 还以为是报错,其实是 WebSocket 升级握手成功。平时做 REST API,基本不用 1xx 系列。如果你搞长轮询或流式响应,可能会碰到 100 Continue——告诉客户端「继续发请求体吧」。

2xx:你最想看到的那一串

2xx 是「一切 OK」的家族。最常用的是这几个:

  • 200 OK:GET、PUT、PATCH 成功后返回。我见过有人把所有成功都返回 200,包括创建资源——其实应该用 201。
  • 201 Created:POST 创建成功后返回,通常附带 Location 头指向新资源。我在一个项目中因为没返回 201,前端用 body 里的 id 判断创建是否成功,结果服务器崩溃时 body 是空的,前端直接崩溃……从那以后,我坚持用 201 表示创建。
  • 202 Accepted:异步任务接受成功,但还没处理完。适合消息推送、批量导出等场景。
  • 204 No Content:DELETE 成功,或者更新操作不需要返回 body 时。省流量,也干净。

小技巧:204 响应不要带 body,连 Content-Type 都不需要。带 body 的 204 会让人困惑——你到底有没有内容?

3xx:资源搬家了

3xx 在 API 设计里用得不多,但理解它很重要,尤其是做网关或短链接服务时。

  • 301 Moved Permanently:永久重定向。搜索引擎会把权重转移到新地址。API 接口换域名时用这个,记得通知前端更新缓存。
  • 302 Found:临时重定向。上次我做一个登录态跳转,用户未登录时 302 到登录页,浏览器会自动跟随,体验还行。
  • 304 Not Modified:配合条件请求(If-None-Match / If-Modified-Since),告诉客户端缓存还能用。我建议所有 GET 接口都支持 ETag 和 Last-Modified,能省下大量带宽。
  • 307 Temporary Redirect:和 302 类似,但保证请求方法不变。POSt 重定向时用 307 比 302 安全。

曾经踩过的坑:我试过用 302 做 POST 后的重定向,结果浏览器把 POST 改成了 GET,丢掉了 form 数据。后来换成 307 才解决。记住:302 可能改变请求方法,307/308 不会。

4xx:你那边有问题

4xx 是接口设计里最常用的错误分类。用对 4xx,前端能快速定位问题,不用猜。

状态码含义建议使用场景
400 Bad Request请求格式不对,参数校验失败缺少必填字段、JSON 格式错误
401 Unauthorized未认证或认证失败需要登录、 过期
403 Forbidden权限不足,但认证通过普通用户尝试删除管理员资源
404 Not Found资源不存在ID 错误、URL 路径错误
405 Method Not Allowed请求方法不对只支持 GET,你发了 DELETE
409 Conflict资源冲突唯一索引重复、并发编辑
422 Unprocessable Entity语义错误(非格式问题)用户名太短、年龄不在范围
429 Too Many Requests限流触发调用频率超限

我个人习惯,400 只做格式校验(比如 JSON 解析失败),业务校验走 422。为什么?你想想看:前端可以根据不同状态码展示不同错误提示,混在一起反而不利于排查。

避坑:不要把敏感信息通过 404 暴露出来。比如你根据用户 ID 查询订单,如果订单不存在返回 404,攻击者就能遍历 ID 知道哪些订单不存在。我一直的做法是:所有不存在的资源都返回 404,但提示信息统一为「资源未找到」,不透露具体 ID。

5xx:我这边背锅了

5xx 代表服务器出了问题,千万别让用户看到具体异常栈。

  • 500 Internal Server Error:万能兜底。我建议把所有未捕获的异常都用 500,但日志要记全。
  • 502 Bad Gateway:网关层,比如 Nginx 后端服务挂了。通常不是业务代码的问题。
  • 503 Service Unavailable:服务暂时过载或维护。配合 Retry-After 头,告诉客户端多久后重试。
  • 504 Gateway Timeout:后端响应超时。如果你的接口涉及慢查询,记得调成异步 + 轮询,而不是让网关直接超时。

实践建议:所有 5xx 都应该触发告警。我维护过一个老系统,400 和 500 混着记,结果数据库连接池满了时,前端只看到 500,运维才发现告警没配……现在我会把 5xx 单独打到健康监测通道,实时通知。

一张图看懂状态码家族

HTTP 状态码家族图谱 1xx 信息性 100 Continue 2xx 成功 200 201 204 3xx 重定向 301 302 304 4xx 客户端错 400 401 404 5xx 服务端错 500 503 各分类核心职责 1xx:协议层握手 接口设计最常用 2xx / 4xx 占据日常 90% 注意配合 body 中的业务码 推荐实践清单 ✅ 创建资源用 201,不要用 200+body ✅ 业务校验错误用 422,格式错误用 400 ✅ 所有 5xx 自动告警,5xx body 只给 traceId ✅ GET 接口支持条件请求(304)

状态码语义归纳:三个层次

做接口设计久了,我总结出思考状态码的三层模型:

  1. HTTP 层:能到达服务器吗?认证通过吗?资源存在吗?——由状态码直接回答。
  2. 业务层:操作成功了吗?业务规则允许吗?——由 body 中的业务码(如 code: 1001)回答。
  3. 数据层:返回的数据是什么?——由 body 中的 data 字段回答。

这三层不要混。举个例子:用户注册时用户名已存在,HTTP 层应该返回 409 Conflict(表示资源冲突),body 里放业务码 1020,data 里给个 error_fields 列表。如果你返回 200 然后在 body 里写「error: true」,前端就得写一堆 if 来判断,真没必要。

注意:不要把 500 当作万能兜底来处理业务异常。比如数据库唯一索引冲突,你应该返回 409,而不是让系统抛异常变成 500。我曾经见过一个项目,所有错误都转成 500:包括参数校验、权限不足、资源不存在……前端看到 500 就直接弹「系统异常」,用户体验极差。正确的做法是:每个错误场景对号入座,不图省事。

嗯,状态码本身不难,难的是坚持用对。下次你设计接口时,可以对照这张表选码,时间久了自然形成肌肉记忆。