状态码
这一课你将能做到
- 按 2xx / 3xx / 4xx / 5xx 四类解释「成功、重定向/协商、客户端错、服务器错」,各举出至少两个常用码
- 把 200、201、204、301、302、304、400、401、403、404、405、429、500、502 与「本站接口、Nginx 反代、登录鉴权」的真实场景对上号
- 用
curl -w "%{http_code}" 与 DevTools Status 列排错,先读状态码再读 body,并为自己的 API 写一张「状态码合同」
为什么要学
接口挂了先看 Network 红字:是你传错参数(4xx),还是上游服务炸了(5xx)?方向判断错了会在前端白改半天。REST 设计里状态码是机器可读的结果摘要——前端拦截器按它分支、监控告警按它统计、缓存按 304 决定复用、限流按 429 触发退避。
面试常问「301 和 302 的区别」「401 和 403 的区别」「502 和 500 谁的锅」,本质都在考你是否真的理解这套分类。本课建立「常用码词典」,不对着 RFC 背几百个。
先建立图像
2xx 成功:我按你的意图做完了(或已接受)
3xx 换地方 / 用本地缓存:去别的 URL(301/302),或「你本地那份还能用」(304)
4xx 客户端问题:改请求再来(没登录、路径错、参数不合法)
5xx 服务器问题:请求可能没错,服务端或网关炸了
| 码 | 人话 | 真实场景 |
|---|
| 200 | OK,body 里有东西 | GET /api/tracks 返回课表 |
| 201 | 已创建 | POST /admin/tracks 成功,常带 Location |
| 204 | 成功但无 body | DELETE 成功的标配 |
| 301 | 永久搬家 | HTTP→HTTPS 强制跳转、旧链接迁移 |
| 302 | 临时去别处 | 短链跳转、登录后回跳 |
| 304 | 未修改,用你缓存 | 协商缓存命中(缓存课细讲) |
| 400 | 请求不合法 | JSON 校验失败、参数类型错 |
| 401 | 我不认识你 | 没登录 / Token 过期 |
| 403 | 认识你但不许 | 普通用户调管理员接口 |
| 404 | 没这个资源 | 错误 slug、路由没匹配 |
| 405 | 资源在,方法错 | GET-only 接口收到 POST(方法课验证过) |
| 429 | 请求太频繁 | 网关限流,常带 Retry-After |
| 500 | 服务器内部错 | 未捕获异常、NPE |
| 502 | 网关收到无效/无响应 | Nginx 上游 Spring 挂了 |
|
跟着做
步骤 1:只打印状态码,练「先看码」的肌肉
# 成功:期望 200
curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8080/api/notes
# 不存在:期望 404
curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8080/api/notes/99999
-o /dev/null(Windows 用 -o NUL)丢弃 body,-w 自定义输出。日常巡检、监控脚本都这么写——只关心码不关心体。
步骤 2:让 Spring Boot 的状态码变「对」
给上一课的 NoteController 补上「业务失败要选对家族」的实践。加一个带校验的创建接口:
@PostMapping("/valid")
public ResponseEntity<Map<String, String>> createValid(@RequestBody Map<String, String> body) {
String title = body.get("title");
if (title == null || title.isBlank()) {
// 参数不合法 → 400,而不是 200 + {ok:false}
return ResponseEntity.badRequest().body(Map.of("error", "title 不能为空"));
}
return ResponseEntity.status(201) // 创建成功 → 201
.header("Location", "/api/notes/new") // 告诉客户端新资源在哪
.body(Map.of("title", title));
}
# 期望 400 + 错误 JSON
curl -i -X POST http://localhost:8080/api/notes/valid \
-H "Content-Type: application/json" -d '{"title":""}'
# 期望 201 + Location 头
curl -i -X POST http://localhost:8080/api/notes/valid \
-H "Content-Type: application/json" -d '{"title":"HTTP 状态码"}'
步骤 3:统一异常处理,别让 500 吞掉 4xx
真实项目用 @RestControllerAdvice 统一兜底,避免「业务异常全变 500」:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<Map<String, String>> notFound(EntityNotFoundException e) {
return ResponseEntity.status(404).body(Map.of("error", e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> badRequest(MethodArgumentNotValidException e) {
return ResponseEntity.badRequest().body(Map.of("error", "参数校验失败"));
}
// 兜底的 500 交给 Spring 默认行为即可,别把 4xx 伪装成 500
}
原则:找得到资源但业务上没有 → 404;参数错 → 400;真·程序异常 → 500。
步骤 4:401 vs 403 情景剧
场景 A:没带 Token 调 /admin/tracks
→ 401 Unauthorized(你是谁?先去登录)
场景 B:带了普通用户的 Token 调管理员删除
→ 403 Forbidden(我认得你,但你没这个权限)
前端据此分支:收到 401 跳登录页并清本地凭证;收到 403 留在当前页提示「无权限」。Spring Security 里对应「未认证」与「已认证但鉴权失败」两个阶段。
步骤 5:DevTools 按状态码筛选
- 打开站点任意页 → Network 面板
- 在筛选框输入
status-code:404(Chrome 支持),或直接点红色失败请求
- 故意访问一个错误 slug 的课程页,观察 document 与 API 各自的失败形状
4xx 在面板里标红,点开 Preview 看前端是否把 message 展示给了用户。
步骤 6:304 预习——不是错误
# 对线上或本站接口带条件头发请求(ETag 值替换为步骤中真实拿到的)
curl -i -H 'If-None-Match: "abc123"' https://coderoadmap.cn/api/tracks
若内容没变会返回 304 且无 body——这是缓存正常工作,监控里别把 304 算进错误率(缓存课展开)。
步骤 7:502 心智实验——谁的锅?
浏览器 → Nginx →(上游 Spring 进程挂了/端口不通)→ 502 Bad Gateway
模拟:把 Spring Boot 停掉再 curl 一个走 Nginx 反代的路径(或想象这一步),Nginx 自己生成 502 响应。判别口诀:看响应头里的 Server 字段——nginx 开头的 5xx 是网关层报的,查 upstream;由 Spring 生成的错误 JSON 是应用层报的,查应用日志。502 别先怪前端。
步骤 8:写一张「本 API 状态码合同」
为课表 API 在笔记里定三行合同(可直接贴进接口文档):
200 成功,body = JSON 数组或对象
404 slug 不存在,body = 统一错误形状 {"error":"..."}
5xx 留给未处理异常;业务「找不到」不许伪装成 500
管理写接口再补一行:201 创建、204 删除、401 未登录、403 无权限、400 校验失败。合同越短越好记,团队才用得起来。
讲清楚要点
- 因为状态码是契约的一部分,所以业务「失败」也要选对家族——参数错用 400,别一律 200 +
{ok:false}(除非团队明确 RPC 风格并全员文档化),否则前端拦截器、监控、网关全都失效。
- 因为 401/403 语义不同,所以前端处理分支不同——401 通常是「凭证缺失/过期」,处理动作是去登录;403 是「凭证有效但权限不足」,处理动作是提示与引导。
- 因为 204 承诺无 body,所以客户端不要对 204 调
response.json()——DELETE 成功后的经典报错点。
- 因为 301 是永久语义,浏览器和搜索引擎会缓存它,所以别把可能反复变化的跳转设成 301——临时跳转用 302/307。
- 因为 5xx 多数是环境/上游问题,所以排错顺序是:应用进程 → 网关日志 → 数据库/依赖,而不是先改前端——502/503 尤其如此。
- 因为 304 表示协商成功,所以它省的是 body 传输而不是那次往返——热点 API 想减往返,要靠 CDN/Nginx/Redis 分层缓存(缓存课与应用缓存课)。
你可能会懵的地方
-
为什么有些公司「永远返回 200」?
一些老的 JSON-RPC 风格或网关习惯,把业务码塞进 body。代价是 HTTP 生态(缓存、监控、重试、浏览器)全部失明。本站主路径按标准状态码教学;若维护遗留系统,至少在文档里写清映射表。
-
404 到底是「路径写错」还是「业务数据不存在」?
对客户端来说一样:资源不可用。服务端内部可以区分「路由没匹配」和「数据库没查到」,对外都可以 404;关键是别把「查无此人」报成 500。
-
307/308 又是什么?和 301/302 什么关系?
307/308 是「不改变方法」的重定向:POST 跳转后仍是 POST。301/302 允许客户端把 POST 改成 GET(历史遗留)。改方法可能改变语义,涉及写操作的重定向优先 307/308。
-
429 该怎么处理?
它仍是 4xx:客户端(或该 Token/IP)请求过频。响应常带 Retry-After 头,前端应指数退避重试,而不是立刻再打一轮把限流坐实。自己实现限流时在网关层(Nginx limit_req)或应用层做,并返回 429 而不是 403。
-
500 和 502 怎么快速分?
看响应是谁生成的:有 Spring 的错误 JSON / 应用堆栈特征 → 应用 500,查应用日志;Server: nginx + Nginx 默认错误页 → 网关连不上上游或上游响应非法,查 Nginx error.log 和 upstream 进程状态。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 成功失败都返回 200,body 里写 code | 监控告警、浏览器缓存、通用拦截器全部失灵 | HTTP 状态码表达大类,body 可补充业务码 |
| 把 401 当 403,提示「没有权限」 | 未登录用户被引导到「联系管理员」,体验灾难 | 未认证 401 → 引导登录;已认证无权限 403 → 提示无权 |
| DELETE 成功却返回一大段 JSON 并标 204 | 204 承诺无 body,客户端解析必炸 | 要回执就 200 + JSON;纯删除就 204 无体 |
| 把 304 统计进错误率告警 | 半夜被「缓存正常工作」叫起来 | 缓存指标单独建面板 |
| 一见 5xx 就改前端重试逻辑 | 上游挂着重试只会火上浇油 | 先查应用日志与 upstream 健康,再谈重试 |
卡点急救
- 前端拿到的全是 500,后端日志却没有业务异常:多半是请求根本没到业务代码——查 Spring Security 过滤器、参数解析失败(
HttpMessageNotReadableException);在 GlobalExceptionHandler 里临时把所有异常打出堆栈定位。
- 接口明明存在却 404:检查
@RequestMapping 拼接、server.servlet.context-path 是否多了一层前缀、包是否在组件扫描范围内;用 curl -i 看 404 的 body 是 Spring 默认的还是 Nginx 的,先分清是谁回的。
- 线上偶发 502:Nginx
error.log 找 connect() failed / upstream prematurely closed;前者查 Spring 进程和端口,后者多是上游处理超时或被 kill(OOM)——结合 dmesg、堆内存监控定位,不是重启了事。
面试常问
-
301 和 302 的区别?对 SEO 有什么影响?
301 永久重定向,浏览器会缓存、搜索引擎把权重迁移到新地址;302 临时重定向,每次都问源站、权重不迁移。网站换域名/路径重构用 301;登录回跳、A/B 短链用 302。
-
401 和 403 的区别?前端分别怎么处理?
401 未认证:凭证缺失或无效,前端清凭证并跳登录页。403 已认证但无权限:凭证有效,前端提示无权限、不跳转。服务端对应 Spring Security 的认证与授权两个阶段。
-
线上用户反馈「网站打不开」,你如何用状态码快速分层定位?
① 浏览器报 ERR_CONNECTION_REFUSED / 超时 → 网络或进程层,查服务器与端口;② 5xx:看是谁生成的响应——Nginx 502/503 查 upstream,应用 500 查异常日志;③ 4xx:404 查路由与发布版本,401/403 查登录态;④ 都正常但页面空 → 查前端资源加载与 JS 报错。
自测三问
- 写出 2xx/3xx/4xx/5xx 各一句话定义,并各举两个常用码。
GET 不存在的 slug、无 Token 调管理接口、Nginx 上游挂掉,分别最可能落在哪个码?
- 为什么「先读状态码再读 body」能加快排错?举一个 body 会误导你的例子。
小结一句话
状态码是机器可读的结果摘要:2 成功、3 换路或用缓存、4 改请求、5 查服务端——REST 语义准,监控与客户端才省心。
延伸