作为后端开发,我几乎每天都会在日志里见到一堆 4xx 状态码,但发现很多同事对它们的理解一直停留在“大概就是客户端错了”这个层面。尤其是每次联调前端报“你又给我返回 400 了”,后端一查其实是不带参数名提示的 JSON 解析失败,根本不知道从哪下手;或者前端明明登录了,接口却一直 403,两边来回拉扯半天。HTTP 状态码里的客户端错误(4xx),本质上就是服务器在用 HTTP 协议告诉调用方“你这次请求哪里不对”,读懂了它们,整个联调和排障效率会提高非常多。
这是 HTTP 状态码系列的第四篇,前一篇已经把 400、401、403、404 这几个高频状态码讲得比较透了,这一篇集中把 4xx 里面剩下的“硬骨头”全部啃完:405、406、408、409、410、412、413、414、415、416、417、428、429、431,同时我会把 401、403、407 这三兄弟的边界也再撕开一次,顺便结合之前项目里真实踩过的坑来讲解。适合后端、前端、运维、以及所有要写接口文档的人,争取看完之后,再遇到 4xx 报错都能直接定位到原因。
1. 请求本身没写对:从 400 到 417 的一串“格式问题”
1.1 400 Bad Request:框架替你挡下的“格式炸弹”
400 在语义上是“服务器无法理解请求的语法”,翻译成人话就是:服务端虽然收到了你的 HTTP 请求,但报文本身没法按约定解析。实际工作中 400 最常出现的三种场景,我逐个拆一下。
第一种是 JSON 反序列化失败。比如接口要求接收 {"name": "张三", "age": 30},结果调用方传了 {"name": "张三", "age": "三十"},服务端在把字符串转成 int 的时候直接抛异常,框架默认返回 400。Spring Boot 里遇到的 HttpMessageNotReadableException、FastAPI 里的 RequestValidationError,基本都是这个套路。这时候响应体里通常会带一句“JSON parse error”,但对排障真正有用的是它后面紧跟的 field 和 reason。很多团队只看状态码不看响应体,这是我最想纠正的毛病。
第二种是参数绑定失败。这种经常出在 GET 请求的 query 参数上,接口要求 ?page=1&size=10,结果调用方传了 ?page=abc,后端转 Integer 失败,也是 400。第三种是请求体在传输层就带着非法字符,比如没有正确做 URL 编码的非 ASCII 字符、Content-Type 和实际 body 格式不匹配,网关或应用容器直接拒绝。
在旧项目里我还遇到过一种情况:前端把表单里所有字段名从 snake_case 改成了 camelCase,后端用的是老接口没做兼容,结果整个页面全是 400。后端日志里赫然写着“JSON parse error: Unrecognized field”,但前端页面上只有一个笼统的“系统繁忙”。这件事给我的教训是:后端在返回 400 的时候,响应体里必须写清楚“哪个字段、什么格式、期望的值是什么”,而不是只甩一个数字。比如说:
json复制{
"code": 40001,
"message": "参数校验失败:age 字段必须是 18-60 之间的整数",
"request_id": "c1a2b3d4e5f6",
"detail": {
"field": "age",
"reason": "expected integer, got string"
}
}
还有一个值得注意的点:很多 AI 模型服务、第三方开放平台返回的 400,响应体里已经把原因写得非常明确了。我见过一个调用大模型接口的报错,上游返回 400,里面直接写着“the reasoning_content in the thinking mode must be passed back to the api.”,意思是开启思考模式后,需要把上下文里的 reasoning_content 一起回传给 API,否则请求不合法。这个例子说明:遇到 400 先看响应体,它已经把钥匙递到你手里了,不要一上来就怀疑网关或网络。
1.2 405 Method Not Allowed:路由正确,方法不对
405 的语义是“请求方法不被允许”。最常见的场景是你明明访问了一个存在的接口路径,但用的 HTTP 方法不对。例如服务端只实现了 GET /api/v1/users,你偏偏发了一个 DELETE /api/v1/users,路由匹配上了,但方法不匹配,服务端返回 405。
这里有一个非常容易被忽视的细节:规范要求 405 响应必须带 Allow 头,告诉客户端这个资源支持哪些方法。比如:
http复制HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
对排障的工程师来说,Allow 头比状态码本身信息量大得多。你可以直接看出是应该改成 GET 还是 POST,而不是对着代码猜。用 curl 测一下就能看到:
bash复制curl -i -X DELETE http://localhost:8080/api/v1/users
如果响应里出现 Allow: GET, HEAD, OPTIONS,那答案已经写在响应头里了。
还有一个隐蔽的坑和跨域有关。浏览器发跨域请求时,如果请求不是简单请求,会先发一个 OPTIONS 预检请求。很多后端框架没有显式处理 OPTIONS,或者接口方法上只写了 @PostMapping,结果预检请求直接命中“无方法匹配”的逻辑,返回 405,前端控制台只会显示“CORS error”。排查这类问题时,先用 curl -i -X OPTIONS 复现,看看是不是 405,如果确实是渠道被拦,通常需要在网关层或拦截器里统一放行 OPTIONS。
1.3 内容协商失败:406 与 415 的左右手
406 Not Acceptable 和 415 Unsupported Media Type 经常被弄混,其实一个管“输出”一个管“输入”。
406 发生在内容协商阶段,核心看 Accept 请求头。客户端在 Accept 里声明了自己能接受哪些响应格式,比如 Accept: text/html,但服务端这个接口只输出 application/json,两边谈不拢,服务器只能回 406,意思是“你想要的我给不了”。实际工作中,406 不像 404 那么常见,但一旦遇到,排查方向非常明确:看 Accept,看服务端能产生什么 Content-Type。Spring MVC 里面如果配置了 ContentNegotiationManager,对这类情况通常会有默认行为,但不同版本表现略有差异,测试的时候要注意。
415 则相反,它关心的是“你发的 body 我解析不了”。最经典的场景就是调试接口时写 curl,curl -d '{"name":"test"}' http://localhost:8080/api,但忘记加 Content-Type: application/json 头。此时服务端看到的是 text/plain 的请求体,但接口方法签名上写的是 @RequestBody User,框架不知道如何把纯文本解析成对象,于是返回 415。同理,上传文件时忘记把 Content-Type 设成 multipart/form-data,或者 multipart 的 boundary 写错,也会得到 415。
这里我建议所有开发者养成一个习惯:在 curl 命令里把 -H "Content-Type: application/json" 写得明明白白,不要依赖 curl 自动推断。
bas复制
