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 层:能到达服务器吗?认证通过吗?资源存在吗?——由状态码直接回答。
- 业务层:操作成功了吗?业务规则允许吗?——由 body 中的业务码(如 code: 1001)回答。
- 数据层:返回的数据是什么?——由 body 中的 data 字段回答。
这三层不要混。举个例子:用户注册时用户名已存在,HTTP 层应该返回 409 Conflict(表示资源冲突),body 里放业务码 1020,data 里给个 error_fields 列表。如果你返回 200 然后在 body 里写「error: true」,前端就得写一堆 if 来判断,真没必要。
注意:不要把 500 当作万能兜底来处理业务异常。比如数据库唯一索引冲突,你应该返回 409,而不是让系统抛异常变成 500。我曾经见过一个项目,所有错误都转成 500:包括参数校验、权限不足、资源不存在……前端看到 500 就直接弹「系统异常」,用户体验极差。正确的做法是:每个错误场景对号入座,不图省事。
嗯,状态码本身不难,难的是坚持用对。下次你设计接口时,可以对照这张表选码,时间久了自然形成肌肉记忆。