做过几年后端,我越来越觉得 HTTP 响应码这套东西属于典型的“看着简单、落地全坑”。你跟前端联调时随手返回一个 200,可能让对方多写三行兼容判断;你漏掉一个 304,可能让 CDN 费用悄悄翻倍;你分不清 401 和 403,安全评审时就会被追问到说不出话。这篇就借“Http响应码分类汇总”这个题目,把五类状态码的语义边界、工程实践和踩坑记录一次讲透,希望能给正在写接口、调接口、排查线上问题的同学一份能直接抄作业的参考。
1. 先建立整体框架:状态码是两端沟通的暗号
1.1 五类状态码的划分逻辑
HTTP 响应码是服务器对客户端请求给出的结构化回答,三位数字按首位分成五大类,这个设计比你想的要严谨得多:
| 首位 | 类别 | 含义 | 典型状态码 |
|---|---|---|---|
| 1xx | 信息性 | 请求已接收,处理仍在进行 | 100 Continue、101 Switching Protocols、103 Early Hints |
| 2xx | 成功 | 请求已理解并处理成功 | 200 OK、201 Created、204 No Content、206 Partial Content |
| 3xx | 重定向 | 需要客户端做进一步操作 | 301 Moved Permanently、302 Found、304 Not Modified、307/308 |
| 4xx | 客户端错误 | 请求本身有问题,服务器拒绝 | 400、401、403、404、409、422、429 |
| 5xx | 服务端错误 | 服务器处理时出错 | 500 Internal Server Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout |
这套分类的核心逻辑是“责任划分”:1xx 和 2xx 表示服务器接收并处理了请求,3xx 表示资源位置或状态发生了变化,4xx 表明问题出在请求方,5xx 表明问题出在服务端。搞清楚这个责任边界,排查问题时思路就清晰很多——看到 4xx 先查自己的参数和权限,看到 5xx 再去翻服务端日志,而不是在两端之间来回踢皮球。
1.2 状态码在整条链路中的实际流转
单独看一个状态码意义有限,真正有用的是看清它在整条链路里的流转方式。一次典型的现代请求会经过客户端、DNS、CDN、网关、应用服务、数据库等多个节点,每一跳都可能改写状态码:
- CDN 回源失败时可能返回 502,源站根本没收到请求;
- 网关超时会返回 504,应用服务其实还在慢慢处理;
- 浏览器遇到 301/302 会主动跟进跳转,最终你看到的可能是 200。
排查线上问题时,我习惯先确认“这个状态码是谁返回的”。最简单的办法是用curl -v或浏览器开发者工具看响应头里的 Server 字段,再配合网关和 CDN 的访问日志定位。不同节点返回的相同状态码,排查方向完全不一样。
2. 2xx 成功族:不只是 200 这一种结局
2.1 不同成功码的语义边界
很多团队整个接口层只有一个 200,所有场景都往里塞。这不是不行,但会丢失语义信息。2xx 家族里有几个成员值得单独拎出来说:
- 201 Created:用于创建资源成功的场景,比 200 更精确。按规范建议,响应里应包含 Location 头指向新资源,实践中不少团队忽略了这一点。
- 204 No Content:表示请求成功但响应体为空,常用于 DELETE 操作。它的特点是浏览器不会拿响应体做解析,前端拿到 204 后不需要处理 body。
- 206 Partial Content:用于 Range 请求的部分内容返回,是断点续传、分片下载的基础。如果服务器支持 Range 而不正确处理 206,大文件下载体验会很差。
2.2 最容易被误用的 200 返回
我在联调中见过最多的反面案例,是“永远返回 200,业务错误码放在 body 里”的写法。比如用户不存在时返回:
curl -i https://api.example.com/v1/users/10086实际响应是:
HTTP/1.1 200 OK Content-Type: application/json {"code": 404, "message": "user not found"}这种做法有两个实打实的坏处。第一,所有网络层和中间件的监控、重试、缓存策略都基于 HTTP 状态码工作,200 意味着“一切正常”,错误信息全藏在 body 里,日志监控、告警规则全部失效。第二,前端处理逻辑变得别扭,每个请求都要先判断业务 code 再做分支,跟处理真实 HTTP 错误的逻辑完全割裂。我在多个项目里推动过状态码规范化,核心原则就一句话:传输层错误用 HTTP 状态码表达,业务层错误在 body 里补充细节,两者不要混在一起。
2.3 204 与 206 的实战细节
先说 204。删除接口用 204 是很标准的做法,但要注意:204 响应默认不允许包含 body,如果你既想返回 204,又在 body 里塞了 JSON,部分代理和客户端会吞掉或报错。实测下来,配置了严格内容校验的网关甚至会直接把这个响应判为非法。
206 则依赖请求头里的 Range 字段,服务器需要在响应头里正确返回Content-Range。做过视频点播或大文件下载的同学应该深有体会:客户端一上来先发一个Range: bytes=0-的探测请求,服务器返回206 Partial Content和Accept-Ranges: bytes,后续才能真正实现拖拽播放和断点续传。如果服务器不支持 Range 却返回 200 全量内容,移动端用户加载视频时就会卡到怀疑人生。
3. 3xx 重定向族:缓存与跳转的潜规则
3.1 301 与 302 的本质区别
301 表示永久重定向,302 表示临时重定向。这个区别看起来一句话就能说清,实际影响相当深远。
永久迁移的场景,比如换域名、HTTP 转 HTTPS,应该用 301。浏览器收到 301 后会缓存跳转结果,后续请求直接跳到新地址,不再回访旧地址。这里有个容易被忽略的坑:301 缓存是有时效的,取决于响应头里的Cache-Control或浏览器默认策略,一旦配错了缓存时间,想修改跳转目标时,旧客户端会顽固地守着缓存好几天。
302 适合临时场景,比如登录跳转、未登录用户被引导到登录页、A/B 实验的流量切换。用 302 的页面每次都需要重新请求原地址,服务器可以根据当时的状况动态决定跳转方向。但 302 也有个历史包袱:早期浏览器在处理 302 时,会把 POST 请求改成 GET,导致表单重复提交或丢参数。这个问题在后来的规范中通过 303 See Other、307 Temporary Redirect 做了细分。
3.2 307 与 308 的保留方法语义
307 和 308 的出现,就是为了解决 301/302 在方法转换上的历史遗留问题。
- 307 Temporary Redirect:临时重定向,且保留原始请求方法和 body,POST 还是 POST。
- 308 Permanent Redirect:永久重定向,同样保留方法。
听上去简单,但很多框架和网关在实现重定向时,默认走的是 301/302,这就会引出实际问题:一个提交表单的 POST 接口做了 302 跳转,用户在部分浏览器和代理环境中会发现请求方法变成 GET,参数丢失,甚至出现重复提交。我做接口设计时,凡是涉及表单提交、文件上传这类需要保留 body 的重定向,一律要求后端返回 307 或 308,并在文档里明确标注。
3.3 304 缓存协商:最容易省流量的环节
304 Not Modified 是我个人认为最被低估的状态码。它不携带资源内容,只告诉客户端“你的缓存还能用”,配合ETag或Last-Modified实现条件请求:
- 客户端请求时带上
If-None-Match: <etag>或If-Modified-Since: <date>; - 服务器比对后若资源未变,返回 304,body 为空;
- 客户端继续使用本地缓存,网络传输量几乎为零。
举个例子,静态资源第一次请求时完整下载,后续如果配好了 ETag,每次协商请求的流量可以忽略不计:
curl -i -H 'If-None-Match: "abc123"' https://cdn.example.com/app.js响应可能只有几行头信息:
HTTP/1.1 304 Not Modified ETag: "abc123"这事的经验之谈有两点。一是 304 必须正确配合Cache-Control使用,no-cache的意思不是不缓存,而是“使用前必须回源校验”,很多同学把 no-cache 理解成禁止缓存,导致回源压力暴增。二是动态接口也可以做条件请求,比如列表数据带上版本号,配合 304 能显著降低网关和客户端的重复计算开销。
4. 4xx 客户端错误族:接口设计的主战场
4.1 401 与 403 的边界
我把 401 和 403 放在一起讲,因为它们被混用的概率实在太高了。
- 401 Unauthorized:表示“你是谁”的问题,即身份认证失败。按规范,响应中应包含
WWW-Authenticate头,提示客户端如何进行认证。 - 403 Forbidden:表示“你是谁已知,但你不能做这件事”的问题,即权限不足。
一个典型的正确用法是:访问需要登录的资源但没带 token,返回 401;带了 token 但该用户没有管理员权限,返回 403。实际项目中常见错误包括:token 过期后返回 403,导致前端无法区分“需要重新登录”和“权限不足”;部分后端框架在认证过滤器里直接返回 401,连WWW-Authenticate头都不带,导致客户端无法触发浏览器的标准认证流程。
我排查过的一个真实问题,是移动端用户频繁被踢下线。逐个抓包后发现,网关对 token 过期的请求统一返回了 403,前端逻辑把 403 当成“无权限”处理,既不清理本地登录态,也不跳转登录页,用户就在一个“假登录”状态里反复请求失败接口。改成 401 并让前端统一处理后,问题立刻消失。这就是状态码语义错位引发的连锁故障。
4.2 422 与 400 的选择
400 Bad Request 是最常用的 4xx 状态码,但它的语义其实偏向“请求语法错误”,服务器无法理解这个请求。现实中,接口层最常遇到的是参数校验失败——格式不对、字段缺失、数值超出范围。这类问题严格来说不是“语法错误”,而是“请求无法被处理”,用 422 Unprocessable Entity 更准确。
具体选择逻辑可以参考:
- 请求 JSON 解析失败、字段类型完全对不上 → 400;
- 请求结构正确但业务校验不通过(如用户名已存在、金额超限)→ 422;
- 请求方法错误,比如只允许 POST 的接口收到 GET → 405 Method Not Allowed。
实际项目中 400 和 422 用哪个,并没有强制标准。我的建议是团队内部保持一致,同时在 response body 里统一返回字段级错误明细,比如:
{ "message": "validation failed", "errors": [ {"field": "age", "reason": "must be between 18 and 60"} ] }前端拿到这样的结构可以直接在表单上高亮报错,不用再解析一堆混乱的字符串。
4.3 429 限流的正确姿势
服务被突发流量打爆的场景,相信大家都经历过。429 Too Many Requests 是限流的标准响应,但它不只是“告诉你被限了”那么简单,有个关键响应头容易被遗漏:Retry-After。
Retry-After可以告诉客户端“多久之后再来”,单位是秒或 HTTP 日期。客户端看到 429 并读到Retry-After后,可以实现真正的平滑退避,而不是大家一起在下一秒重试,导致“限流→重试→再限流”的死循环。做 SDK 的同学尤其要注意:遇到 429 时重试是合理的,但一定要把Retry-After或指数退避策略加进去,否则你的客户端本身就是一个小型流量放大器。
4.4 404 的两面性
404 指资源不存在,本身很直白,但在安全场景下它需要额外斟酌。有的接口设计为了避免暴露资源是否存在,会在“无权访问”和“不存在”两种情况下统一返回 404,这在某些业务场景是合理的。但代价是调试复杂度上升,前端拿到 404 时无法区分是路径写错、资源被删还是权限不足。
我个人的经验是:内部接口以调试体验优先,区分 403 和 404;对外暴露的接口以安全优先,必要时混用。这个取舍没有绝对对错,关键是团队要明确场景,并在接口文档里写清楚。
5. 5xx 服务端错误族:从日志到告警的排查路径
5.1 500 与 502/504 的不同排查思路
5xx 表示服务器端出了状况,但不同状态码对应的排查方向差异很大。
- 500 Internal Server Error:应用内部异常。常规排查路径是看应用日志里的堆栈,重点确认是代码异常、依赖服务报错还是资源耗尽。
- 502 Bad Gateway:网关或代理从上游收到了无效响应。常见原因是后端进程崩溃、连接被重置、响应格式非法。
- 503 Service Unavailable:服务暂时不可用,常见于启动中、维护中或负载过高主动熔断。
- 504 Gateway Timeout:网关在等待上游响应时超时。问题多半出在上游处理过慢或网关超时时间配置过短。
一个我在生产环境踩过的例子:某个内部接口偶发 502,应用日志里却看不到任何异常。后来抓了网关日志才发现,是后端进程频繁被 OOM Killer 干掉,重启后接管的请求返回了异常响应。这时排查的重心就不在业务代码上,而在内存监控和容器资源配置上。
5.2 网关状态码的定位技巧
网关(包括 Nginx、负载均衡器、API 网关)自己会产生一批状态码,它们经常跟业务状态码混在一起。单独看前端收到的 502/504 意义不大,我的做法是逐层看链路:
- 先看浏览器/客户端收到的状态码;
- 再看 CDN 日志,确认是否命中了源站;
- 接着看网关访问日志,找到
upstream_status与upstream_response_time; - 最后回到应用日志,核对对应时间窗口的异常堆栈。
这类问题最有价值的信息往往是“时间戳对应关系”。有一次排查接口偶发 504,前后调了大量参数都没解决,最后发现是网关到后端的超时时间是 5 秒,而后端一个批量导出接口平均耗时 7 秒。单纯调大超时时间就解决了,但如果不看日志里 upstream_response_time 的分布,这个问题会一直作为“玄学故障”存在。
5.3 状态码与监控告警的联动
服务端状态码还有一个很重要的工程价值:它们是监控告警的基础数据源。稳定运行的系统里,5xx 占比应该长期趋近于 0。在监控面板上,我一般会同时关注以下指标:
- 5xx 绝对数和占比的实时曲线;
- 按接口聚合的 4xx 分布,用于发现参数错误或恶意扫描;
- 429 出现频率,用于判断限流阈值是否合理;
- 502 与 504 的比例关系,用于区分是上游问题还是超时配置问题。
告警规则也要跟着状态码分类来设计:500 激增多半是发布或依赖故障,需要立即处理和回滚;502/504 激增要考虑网关和后端的链路健康;404 激增则可能是资源被误删或爬虫在扫描路径。告警配得太粗(比如所有 5xx 一锅端)会让值班同学在无效告警里反复折腾,最后失去对告警的敏感度。
6. 实际项目中的状态码设计规范速查
6.1 后端接口响应的推荐组合
光是“理解状态码”还不够,落地到项目里需要一套可执行的规范。以下是我在多轮项目迭代中沉淀下来的推荐组合,供参考:
| 场景 | 状态码 | 说明 |
|---|---|---|
| 查询成功 | 200 | body 返回数据 |
| 创建成功 | 201 | 建议返回新资源信息及 Location |
| 更新成功 | 200 | 返回更新后的完整资源 |
| 删除成功 | 204 | 无 body 返回 |
| 异步任务已接收 | 202 | 常用于消息队列投递场景 |
| 参数格式错误 | 400 | 请求体无法解析 |
| 业务校验失败 | 422 | 返回字段级错误明细 |
| 未认证 | 401 | 带上 WWW-Authenticate 头 |
| 无权限 | 403 | 已认证但越权 |
| 资源不存在 | 404 | 路径或资源缺失 |
| 方法不允许 | 405 | 需在 Allow 头列出支持方法 |
| 资源冲突 | 409 | 如版本冲突、唯一键冲突 |
| 被限流 | 429 | 带上 Retry-After 头 |
| 服务端未知异常 | 500 | 返回通用错误信息 |
| 上游不可用 | 502/504 | 网关层需正确映射 |
这里唯一的强制要求是:不要自定义一个不在标准范围内的状态码来传递业务语义。标准状态码是两端和中间链路的共同语言,自定义数字只会让网关、监控、客户端 SDK 全部陷入困惑。
6.2 状态码语义检查清单
接口上线前,我习惯带着下面的检查清单做一次状态码语义走查:
- 是否所有成功响应都准确区分了 200/201/202/204?
- 是否还有“200 + body code 判错”的反模式?
- 401 和 403 的边界是否清晰,token 失效是否明确返回 401?
- 重定向场景是否合理选择了 301/302/307/308?
- 静态资源的缓存协商是否配置了 ETag 和 304?
- 限流接口是否返回了 429 和 Retry-After?
- 网关层是否对上游超时、断连做了正确的 504/502 映射?
这份清单看着琐碎,但每一条都对应着我在生产环境里处理过的真实故障。状态码规范本质上是接口契约的一部分,契约越清晰,前后端协作和问题排查的效率就越高。
7. 常见问题与排查技巧实录
7.1 浏览器显示状态码为 0 是怎么回事
做前端联调时经常会遇到“请求失败,状态码是 0”的情况。这个 0 并不是 HTTP 协议里的标准状态码,而是浏览器层面的异常表达。常见原因包括:
- 跨域请求被 CORS 策略拦截,请求根本没发出去;
- 请求被浏览器主动取消(如页面跳转、请求超时被 AbortController 终止);
- 请求发到了不可达地址(如断网、DNS 解析失败、证书错误被拦截);
- 广告拦截或安全软件阻止了请求。
排查时建议先看浏览器开发者工具控制台的报错信息,再确认跨域配置和网络可达性。状态码为 0 的问题经常不是因为后端,而是浏览器层,别在服务器日志里白找半天。
7.2 状态码与缓存头联动的坑
状态码和缓存头的组合是个容易被忽略但影响极大的细节。比较常见的两个坑:
第一个是 301 永久重定向默认会被浏览器缓存,如果线上配错了跳转目标,想改回旧地址时,那些已经缓存了 301 的浏览器会一直访问旧目标,只能通过给旧地址返回 200 或更换 URL 路径来规避。第二个是把 404 响应配上可缓存头,导致错误页面被缓存,用户看到的状态码和内容永久停留在错误版本上。我的经验是:动态页和错误响应统一加Cache-Control: no-store,重定向响应单独评估是否允许缓存。
7.3 排查状态码问题的抓包姿势
线上排查状态码问题,光靠代码和日志往往不够,我个人的标准流程是:
- 先用
curl -v复现请求,看到完整的状态码和响应头; - 再抓一次带
--resolve或修改 Host 的请求,确认直达源站的结果是否一致; - 对比浏览器开发者工具里的性能面板,确认是慢、是缓存、还是被拦截;
- 最后打开网关与 CDN 的访问日志,对准时间戳和
upstream_status。
这套流程能覆盖九成以上的状态码异常问题。记住一个原则:状态码问题很少是“玄学”,大多数时候是链路里的某个节点把状态码改写或吞掉了,顺着链路逐段确认,总能找到真相。
7.4 容易被忽略的 1xx 状态码
1xx 状态码在日常开发中很少被直接看到,因为浏览器和大多数 HTTP 客户端都会自动处理它们。但它们并不是毫无用处。
- 100 Continue:客户端发送大数据体前,可以先发一个带
Expect: 100-continue的请求头,服务器返回 100 后再发送正式 body,避免大文件上传被无谓拒绝。 - 103 Early Hints:服务器在最终响应前提前告知浏览器需要加载的资源,可以显著优化首屏速度。
实际项目中,直接在应用层处理 1xx 的机会不多,但理解它们的存在能帮你读懂抓包文件里的奇怪包,也能在客户端 SDK 做底层网络库时正确处理这些中间状态。
最后分享一点我个人维护这套规范的真实体会:状态码分类不是一个“背下来就行”的知识点,它在项目里的价值取决于团队是否达成了统一的接口契约。我见过太多项目因为前后端对状态码理解不一致,反复爆发“明明是成功的请求,前端却提示失败”之类的低质量 Bug。与其每次临时开会扯皮,不如把状态码使用规范固化到接口文档、代码模板和自动化测试里。花半天时间把这套语义理清楚,后面省下的联调和排障时间是以周计的。