这个系列写到这里,已经到第四篇了。前几篇我们把 HTTP 协议里最基础的部分过了一遍:1xx 的临时应答、2xx 的成功语义、3xx 的重定向,以及上一篇里 4xx 家族中几个最“常见”的状态码,像 400、401、403、404、405 这类,都拆开聊了聊。今天这篇,接着把 4xx 客户端错误状态码剩下的部分讲完,重点是我在实际开发、联调、排查日志时真正遇到过、并且最能折磨人的那些。
如果你是个后端开发、运维、测试,或者是在做爬虫、对接第三方 API 的工程师,这一篇应该会对你有用。因为 4xx 这个区间太特殊了:它不像 5xx 那样是服务端炸了,也不像 2xx 那样皆大欢喜,它说的是“你的请求有问题”,但很多时候问题并不像状态码字面上写的那么直白。比如同一个 400,有时候是 JSON 格式错了,有时候是 Content-Type 没对上,有时候甚至是网关层主动拦的。你如果只盯着状态码数字猜,永远猜不中真正原因。
这一篇的定位是延续系列的深入解读,所以不打算再把每个状态码都背一遍定义。我更想把它当成一份“排障手记”来写:哪些状态码看起来像、实际完全不一样,哪些状态码虽然冷门但一出现就是大事,以及当你真的看到了它们,下一步应该怎么查。
1. 先别急着查状态码,看看4xx家族的整体逻辑
1.1 系列前情:这一篇到底继续讲什么
上一篇把 4xx 里最“出圈”的几个讲掉了,比如 400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、405 Method Not Allowed。但 4xx 区间从 400 到 499 有几十个状态码,上一篇只是开了个头。这一篇要补上的,是剩下的、在真实项目里出现频率同样不低,但很多人一看到就懵的状态码。
我先把这一篇要覆盖的列表摆出来:408 Request Timeout、409 Conflict、410 Gone、411 Length Required、412 Precondition Failed、413 Payload Too Large、414 URI Too Long、415 Unsupported Media Type、416 Range Not Satisfiable、417 Expectation Failed、418 I'm a teapot、421 Misdirected Request、422 Unprocessable Entity、423 Locked、428 Precondition Required、429 Too Many Requests、431 Request Header Fields Too Large、451 Unavailable For Legal Reasons。
看到这个列表别慌,并不是每个都需要你背下来。但有几个是“高频踩坑位”,比如 413、414、415、429 这些,在对接文件上传、URL 参数、限流策略时几乎绕不开。而像 421、422 这类,在 HTTP/2 和 RESTful API 设计里也越来越常见。剩下的属于“认识一下、遇到不慌”的冷门状态码,知道它代表什么语义就够了。
1.2 一张表看清本篇要讲的状态码
在逐个拆解之前,我先用一个表格把这一篇涉及的状态码、标准定义、以及最常见的触发场景列出来。这张表你可以直接存起来,排查问题的时候对着看:
| 状态码 | 标准定义 | 最常见触发场景 |
|---|---|---|
| 408 | Request Timeout | 客户端迟迟没把请求体发完,服务端等不及主动断开 |
| 409 | Conflict | 资源当前状态与请求冲突,常见于并发编辑、版本号不一致 |
| 410 | Gone | 资源曾存在但已被永久删除,和 404 有语义差异 |
| 411 | Length Required | 请求没带 Content-Length,但服务端必须要这个头 |
| 412 | Precondition Failed | If-Match / If-None-Match 等条件头校验失败 |
| 413 | Payload Too Large | 请求体超过服务端限制,文件上传最常见 |
| 414 | URI Too Long | URL 太长,尤其是 GET 请求拼接大量参数时 |
| 415 | Unsupported Media Type | Content-Type 不是服务端期望的格式 |
| 416 | Range Not Satisfiable | Range 请求的范围不对,断点续传经常碰到 |
| 417 | Expectation Failed | Expect 请求头里的预期无法满足 |
| 418 | I'm a teapot | 愚人节彩蛋,但协议里合法存在 |
| 421 | Misdirected Request | HTTP/2 下请求发到了错误的虚拟主机 |
| 422 | Unprocessable Entity | 请求格式正确,但业务语义校验失败 |
| 423 | Locked | 资源被锁定,常见于 WebDAV 场景 |
| 428 | Precondition Required | 服务端要求客户端必须带条件请求头 |
| 429 | Too Many Requests | 请求太频繁,触发限流 |
| 431 | Request Header Fields Too Large | 请求头太大,尤其 Cookie 过大时 |
| 451 | Unavailable For Legal Reasons | 因法律原因不可用 |
这张表只是“地图”,真正干活的时候,要结合响应头、响应体、服务端日志一起看。下面我把这里面最容易出问题的几组状态码,一个个拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频疑难4xx逐个拆解
2.1 400 Bad Request:你以为“语法错”,其实是“你没按套路出牌”
400 是上一篇可能已经讲过的状态码,但我在这一篇里还愿意再花一段强调,是因为它太特殊了。特殊在哪?它可能是 4xx 里最“万能”的状态码——后端开发者拿不准该返回什么的时候,常常随手就丢一个 400。所以当你真的收到 400,第一反应不应该是“我语法错了”,而是“我没按服务端期望的格式出牌”。
我实际遇到过的 400,原因五花八门:请求体 JSON 里多了个逗号、Content-Type 写的 application/x-www-form-urlencoded 但 body 其实是 JSON、header 里带了个服务端不认识的非法字符、multipart 请求少了 boundary、文件上传时 filename 里有未转义的特殊字符,甚至还有一次是客户端对响应做过 gzip 解压,结果把原始请求也错误地 gzip 了,服务端一解压直接炸了。
排查 400 的思路,核心就一句话:不要猜,重放请求。用 curl 把原请求的 method、URL、header、body 完整复制出来,一条条裁剪字段,看删掉哪个字段之后状态码变了,元凶就找到了。这里有个细节我特意说一下:复制请求体的时候,要特别小心那些“不可见字符”,比如首尾空格、BOM 头、Windows 换行符 \r\n。很多 JSON 解析器碰到 BOM 就会报错,但肉眼又完全看不出来。
2.2 401 Unauthorized 和 403 Forbidden:认证与授权的经典分界线
这两个状态码是 4xx 里最容易搞混的一对,也是面试和实战里都绕不开的考点。它们的区别用一句话说就是:401 是“你是谁我不知道”,403 是“我知道你是谁,但你不许进”。
举个生活中的例子,401 就像你在小区门口刷门禁,保安拦住你说“请出示业主卡”,因为你还没证明身份;403 就像你拿出业主卡进了小区,但想进某栋楼的机房,保安说“这个区域你没权限进”。在 HTTP 协议里,401 响应必须带上 WWW-Authenticate 响应头,告诉客户端“你应该用 Basic、Bearer 还是 Digest 方式重新认证”。而 403 一般不会带这个头。
但实际项目里,很多框架并不会严格遵守这个区分。比如 Spring Security 默认配置下,未登录访问受保护接口时可能返回的是 401,也可能按你的配置返回 403;Nginx 的 deny 指令直接返回 403,而一些网关在 token 过期时会返回 401,但刷新 token 后依然没有权限时又返回 403。所以排查时不要只看数字,先看响应头里有没有 WWW-Authenticate、响应体里有没有具体的错误码。我见过最坑的一次是,某个内部系统对所有未登录请求都返回 403 而不是 401,结果前端判断失误,用户永远看不到登录弹窗,还以为是自己的账号被禁了。
2.3 404 和 410:找不到资源的两种说法
404 应该是全世界最著名的 HTTP 状态码了,但它的语义其实很微妙。404 表示“服务器无法找到请求的资源”,它不区分“这个资源从来没存在过”和“这个资源以前有,现在被删了”。从设计上讲这是一种“故意模糊”——很多系统为了防止信息泄露,会把不存在的资源也统一返回 404。
与之相对的 410 Gone 就明确多了:它告诉客户端“这个资源曾经存在,但已经被永久移除了,你别再来了”。比如一篇下架的文章、一个过期活动的页面、一个被吊销的 API 版本,服务端都可以返回 410,让搜索引擎和客户端尽快把缓存里的地址清掉。可惜现实中用 410 的站点并不多,大部分时候都被粗暴地替换成了 404。
实际排查 404 时,我最大的经验是:先检查路由前缀,再检查 URL 编码,最后再怀疑资源真的不存在。比如你请求的是 /api/user/list,但服务端定义的路由是 /api/users/list,一个字母之差就是 404。又比如热词里经常出现 http%3a%2f%2f 这种带百分号编码的 URL,如果你在代码里把整个 URL 当成参数拼进去,服务端解码后的路径跟你以为的根本不是一回事,返回 404 一点都不冤。遇到这类问题,最简单的办法就是用浏览器的开发者工具复制出“实际发出的 URL”,而不是你自己在代码里写的那个。
2.4 405 和 406:方法与内容协商的“收件箱规则”
405 Method Not Allowed 的意思是:资源存在,但你用的 HTTP 方法不对。比如接口只支持 POST,你用 GET 去请求,就会收到 405。这里有个特别容易忽略的细节:符合规范的 405 响应必须携带 Allow 响应头,列出该资源实际支持的方法。所以排查 405 时,第一件事就是看响应头里的 Allow 是啥。比如 Allow: POST, OPTIONS,那就说明服务端只允许 POST 和 OPTIONS,你该改成 POST 而不是继续在 GET 上死磕。
如果你用的是 Nginx 做反向代理,405 还有一个特殊含义:当 Nginx 把请求转发给上游后收到异常,或者请求了静态文件里的某个方法不支持的资源,也可能会返回 405。我调试过的一个真实案例是:某个前端应用用静态服务器托管,前端用 GET 请求一个接口,但后端只发布了 POST 路由,结果请求直接 405。前端同事的第一反应是“接口挂了”,实际上接口压根没被调到。
406 Not Acceptable 则是另一回事,它和请求头里的 Accept 有关。Accept 表示“我能接受什么类型的响应”,比如 Accept: application/json,如果服务端只能返回 application/xml,就会返回 406。这个状态码在 REST API 里不算高频,但在内容协商做得比较严格的服务里会经常见到。排查方法也很直接:检查请求头里的 Accept、Accept-Language、Accept-Encoding,看服务端实际能产生哪种类型。
2.5 407到416:日志里常见、但你可能没细看的一组状态码
从 407 到 416 这一串,很多人在开发时很少主动触发,但一旦出现在日志里,往往意味着某种特定的配置问题。
407 Proxy Authentication Required 和 401 很像,区别是它针对的是代理服务器。响应头里会是 Proxy-Authenticate 而不是 WWW-Authenticate。如果你在代码里配了 HTTP 代理,但代理需要认证,而你又漏了认证信息,就会看到这个状态码。这个问题在真实的公司内网环境里很常见,尤其是 Java、Python 程序里配了 system proxy 但没带账号密码时。
408 Request Timeout 表示“服务端在等待客户端发送完整请求时超时了”。但老实说,这个状态码在现实中并没有那么常见,因为更多时候超时发生在更底层,客户端看到的是 connection timeout 或 502 Bad Gateway,而不是严格的 408。不过在一些运维监控里,如果你看到大量 408,就要考虑是不是客户端发请求的速度太慢、请求体太大、或者 TCP 连接半开导致的。
409 Conflict 我特别想提一下。它表示“请求与资源的当前状态冲突”,最常见的场景是并发编辑:两个人同时改同一份文档,后来者提交的版本号已经过时了,服务端就会返回 409,提示你先拉取最新版本再合并。另一个场景是幂等性校验:你用一个重复的订单号创建订单,服务端发现订单号已存在,就可能返回 409。排查这种问题,重点是看响应体里有没有给出“期望的当前状态”,比如最新的版本号、冲突字段的对比。
412 Precondition Failed 是条件请求的“失败版”,请求头里的 If-Match、If-Unmodified-Since、If-None-Match 这类前置条件没满足就会触发。它和 HTTP 缓存、乐观锁、断点续传关系密切。比如你用 If-Match: "abc" 去更新一个资源,但服务端当前的 ETag 已经是 "def",就会返回 412。这其实是设计得很精妙的一种保护机制,防止你基于一个过期的版本去覆盖新数据。
413 Payload Too Large 是文件上传场景里的大魔王。请求体超过服务端限制时就会返回它。Nginx 默认的 client_max_body_size 是 1MB,如果你传个 2MB 的图片,就会被 Nginx 拦截,返回 413。这个问题最坑的地方在于:你明明已经把后端框架的上传限制调到 100MB 了,但还是 413,因为卡在前面 Nginx 这一层。
414 URI Too Long 则和 URL 有关。GET 请求把大量参数拼在 URL 上,超过服务端限制(通常是 8KB 左右),就会收到 414。排查方法很简单:把 URL 长度缩短,或者改用 POST 把参数放到 body 里。另外要小心,有些代理服务器对 URL 长度的限制比业务服务器更严格,所以同一个 URL,直连没问题,走代理就 414。
415 Unsupported Media Type 和 406 是一对:406 是“你要的响应类型我给不了”,415 是“你发的请求类型我不要”。提交 POST/PUT 请求时,如果 Content-Type 不是服务端期望的格式,就返回 415。比如服务端只认 application/json,你发的是 text/plain,那肯定是 415。我排查过的一个案例是:前端用了 axios 默认的 application/json,但浏览器把它变成了带 charset 的形式,例如 application/json; charset=utf-8,后端框架比对字符串时没处理参数部分,结果误判为不支持的类型。
416 Range Not Satisfiable 最常出现在断点续传和多线程下载场景。客户端发了一个 Range: bytes=100-200 的请求,但服务端资源总长度只有 80 字节,无法满足这个范围,就会返回 416。排查时先看资源的实际大小,再看 Range 头里的范围值。符合规范的响应还应该带上 Content-Range 头,告诉你资源的实际范围区间。
2.6 418到451:冷门状态码与HTTP/2时代的坑
从 418 到 451 这一段,大多数属于“冷门中的冷门”,但其中有几个值得单独理解一下。
418 I'm a teapot 是 1998 年愚人节 RFC 里定义的彩蛋,说“我是茶壶,不能泡咖啡”。它不是真实业务会用到的状态码,但在很多技术博客、开源项目的测试代码里见到它,你不用惊讶,这是 HTTP 协议里少有的幽默。
421 Misdirected Request 是 HTTP/2 时代才有实际意义的状态码。它表示“请求被发到了一个无法处理该请求的服务器”。最典型的场景是连接复用:一个 TCP 连接上承载了多个虚拟主机的请求,但服务端发现某个 Host 对应的虚拟主机并不在这个连接的处理范围内,就会返回 421。热词里就有“http连接复用”,如果你在 HTTP/2 里遇到 421,多半是客户端连接复用策略配置有问题,或者服务端虚拟主机配置没匹配上,解决思路是让客户端建立新连接再重试。
422 Unprocessable Entity 在老一点的 RFC 定义里没有,但在 REST API 里非常常见,尤其是 Ruby on Rails、Spring 这类框架。它的意思是:请求格式解析成功了,但业务语义上有问题。比如你提交的用户名长度超过 20 个字符、邮箱格式不是合法的邮箱,这种校验性错误返回 422 比 400 更准确。排查时关键是看响应体里的字段级错误信息,通常是一个数组或对象,指出了具体哪个字段不合法。
423 Locked 和 428 Precondition Required 都与“资源状态”和“条件请求”有关。423 常见于 WebDAV 场景,表示资源被锁住了;428 则表示服务端希望客户端在请求里带上条件请求头,比如 If-Match,否则就拒绝处理。后者在并发安全的 API 设计里很有用,很多云存储的“条件上传”就依赖这个状态码。
429 Too Many Requests 是限流场景的主角。它表示你在一定时间窗口内发的请求太多了。排查和应对的核心是响应头里的 Retry-After,它告诉你“等多少秒之后再重试”。如果客户端无视 429 继续死循环请求,就会出现“越失败越重试,越重试越失败”的雪崩。正确的做法是结合指数退避算法:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,同时加一点随机抖动,避免所有客户端同时重试造成二次冲击。
431 Request Header Fields Too Large 和 414 很像,只是 414 针对 URL,431 针对整个请求头。最经典的发生场景是 Cookie 过大——一个站点往 Cookie 里塞了太多数据,导致后续每个请求的 header 都巨大,最终触发 431。处理方案是清理不必要的 Cookie 数据,或者把会话状态存到服务端而不是全塞 Cookie。
451 Unavailable For Legal Reasons 这个状态码比较特殊,它的含义是“因法律原因不可用”。它在协议里是真实存在的,但在实际开发里普通业务很少主动返回。作为知识了解即可,除非你的产品确实面临合规性审查,否则一般用不到。
3. 实战复盘:从真实报错场景看状态码定位
3.1 场景一:wget/curl 下载脚本遇到 404/403
在热词列表里,我注意到有好几条都涉及下载脚本、安装脚本之类的报错,比如用 wget 拉取一个脚本,结果提示 404 或者 403。这类问题我处理得非常多,而且有很强的代表性,因为它暴露了 URL 处理里的三个经典坑。
第一个坑是 URL 编码。比如你看到的 URL 是 http://example.com/install?path=http%3a%2f%2fother.com%2fdir,这种带 %3a、%2f 的写法,是浏览器或者工具对 URL 里的特殊字符做了百分号编码。%3a 是冒号,%2f 是斜杠。如果你手动去解析这种 URL,忘记解码,或者解码位置不对,就会出现“我明明看到的是 /install?path=http://other.com/dir,但服务器接收到的是另一串”的情况。排查方法是把完整 URL 原样复制出来,用 curl -v 看实际发出的请求行,确认服务端侧收到的 path 和 query 到底是什么。
第二个坑是 HTTPS 和 HTTP 混淆。热词里有一条 http: server gave http response to https client,说的是客户端用了 TLS 连接到服务端,但服务端在某个端口上返回的是纯 HTTP 明文响应。这其实不是 4xx 问题,但它和 HTTP 协议理解直接相关。遇到这种报错,先确认你访问的端口到底是 HTTP 还是 HTTPS 服务,比如 443 是 HTTPS,8080 可能是明文 HTTP,别在 HTTPS 的请求里把端口写成 8080。
第三个坑是 wget 不会自动带上 Referer 或 Cookie。很多资源下载有防盗链,服务端检查到请求里没有合法的 Referer 头,就会返回 403。你看到 403 时别急着怀疑自己的链接错了,先试着用 curl -e "http://referer.example.com" 带上 Referer 再请求一次。我遇到过不止一次,加一个 Referer 头就把 403 变 200 了。
3.2 场景二:Docker 拉镜像提示 502/400,问题却出在客户端协议
Docker 相关的报错在热词里占了很大比例,比如 error response from daemon: get "https://registry-1.docker.io/v2/": net/http...,以及 unexpected status 502 bad gateway 这类。虽然 502 属于 5xx,但排查这类问题时,我的经验是:先怀疑客户端这边有没有“自作聪明”的改动,再怀疑服务端。
举几个我见到过的真实原因:一是本地配了代理,代理指向了一个不稳定的地址,导致请求转发到 registry 时出现 502;二是 Docker 镜像加速器的配置格式写错了,导致请求走了错误的上游;三是系统时间不对,导致 TLS 证书校验失败,出现各种奇怪的 net/http 报错;四是访问的 registry 版本和客户端兼容性有问题,比如老的客户端访问新 registry,可能在协商时直接 400。
排查这类问题有个通用顺序:先看服务端返回的完整响应,再逐层检查本地的 /etc/docker/daemon.json(Linux)或 Docker Desktop 设置,把代理、镜像加速这些配置先临时去掉,用最“干净”的方式访问,看问题是否还在。如果干净环境没问题,那就是配置层的问题,逐项加回来定位即可。
3.3 场景三:接口偶发400,响应体里的错误信息才是关键
热词里有一条很典型的报错:upstream_status: http 400; cause: the 'reasoning_content' in the thinking mode must be passed back to the api. 这种报错,状态码是 400,但真正的信息藏在响应体或者日志的 error message 里。它说明服务端已经解析了请求,但发现某个必填参数或状态没有正确传递。
我把这类问题统一称为“伪 400”——它表面上是参数错误,实际上是一种业务逻辑校验。排查这类报错,最重要的不是去猜 400 从哪来,而是把响应体的 message 字段、响应头的 X-Request-Id 之类关联字段,以及服务端访问日志里的对应记录完整拉出来。很多云服务和开源网关都会在响应体里写 error 对象,里面有 code 和 message,那个 message 往往比 400 这个数字有用得多。
另外从热词里的 feign.feignexception$internalservererror: [500] during [get] to [http://item... 这类报错能看出来,很多客户端框架会把服务端返回的异常包装成自己的异常再抛出来。比如 Spring Cloud OpenFeign,当服务端返回 500 时,它默认抛出的就是 FeignException.InternalServerError。这类报错看起来是在说“500 了”,但你真正要做的是去找到被调用的那个服务,看它的日志里为什么返回 500。状态码在链路里会被层层包装,看到的那个数字不一定是根因。
4. 排查方法论:状态码只给结论,过程和细节要靠工具
4.1 三板斧:curl、DevTools、网络抓包
遇到 4xx,我最先拿出来的永远是 curl。它比任何图形化工具都适合做“最小复现”,因为你可以把请求参数原样写在命令行里,一遍遍调整、对比。常用的几个参数再提一遍:
-v:打印完整的请求和响应头,看 TLS 握手、请求行、响应状态。-i:输出响应头,方便看Allow、WWW-Authenticate、Retry-After这类关键信息。-X POST:显式指定请求方法,避免被默认的 GET 带偏。-H "Content-Type: application/json":指定请求头。-d '{"key":"value"}':指定请求体。--data-urlencode:自动对参数做 URL 编码,能避免手写编码出错。-o /dev/null -w "%{http_code}":只输出状态码,适合脚本里批量检测。
一个实际的排查 415 的流程是这样:先 curl -v http://api.example.com/login -H "Content-Type: application/json" -d '{"user":"admin"}',观察是否 415;然后改成去掉 Content-Type 再试,或者改成 Content-Type: text/plain 再试,如果状态码变化了,说明问题确实在 Content-Type 匹配上。
浏览器 DevTools 的 Network 面板适合排查前端发起的请求。重点看四个东西:Request Headers 里的 Content-Type、Accept、Authorization,以及 Response Headers 里的错误提示。点击请求还能看到完整的请求体和响应体,前端“复制为 cURL”功能也特别实用,可以直接把浏览器发出的请求转成 curl,拿到命令行里继续调试。
如果本地环境有 Charles、Fiddler 这类抓包工具,也可以用来观察应用层看不见的连接细节,比如 TCP 重传、TLS 版本、代理链路上的响应。有些 4xx 是中间链路里的某个组件返回的,只有抓包才能看到“到底是谁回了这个状态码”。
4.2 用“最小复现”把4xx逼出原形
排查一个“偶发”的 4xx,最怕的就是你对问题一无所知就上服务器看日志。我的做法是,先在客户端做“最小复现”:用一个最简单的请求,逐项恢复现场。这个流程有点像给代码做二分查找:先从一个能成功的请求开始,逐步添加 header、参数、body,直到状态码变成 4xx,最后添加的那个东西就是罪魁祸首。
比如,你怀疑是某个 header 引起的 400。可以先试一个不带头部的请求,200 OK。然后加上 Authorization,还是 200;再加上 X-Custom-Header,200;加上 Content-Type: application/json,200;最后加上 Content-Length 和 body,400。那问题就可能出在 Content-Length 和 body 的组合上。这时候再细看:body 是不是没转义、长度是不是算错了、JSON 是不是非法。
这个方法的优点是不依赖服务端日志,只要能从客户端逐步复现,基本就锁定方向了。如果始终无法在最小环境里复现,那就要考虑是不是只有特定网络环境才触发,比如公司内网网关做了某种拦截,或者是某个中间设备在特定条件下修改了请求头。
4.3 别忘了看服务端日志和网关层
有一种情况很让人抓狂:客户端看到的响应是 400,但服务端业务日志里根本没有这个请求的记录。这时候你就要怀疑,是不是这个 400 不是业务服务返回的,而是前面的网关、负载均衡器、WAF 返回的。
比如 Nginx 配置了 client_max_body_size 1m,你传了一个 2MB 的请求,Nginx 直接返回 413,压根没转发到后端;如果配置了 limit_req 限流,超限时返回的是 503;但如果配置了某些 WAF 规则,请求可能返回 406 或者 403,而且不会在上游应用日志里留下任何记录。所以排查 4xx 的顺序应该是:先确认谁是返回者。看响应头里的 Server 字段、Via 字段、X-Nginx-* 这类自定义头,能快速判断是不是经过了某个代理。
服务端日志也要分清访问日志和错误日志。Nginx 的 access.log 里有 $status、$request_time、$upstream_status 这些字段,能告诉你“客户端请求到了 Nginx,Nginx 转发给上游,上游返回了啥”。error.log 里则经常有“上游连接失败”“SSL 握手失败”这类细节。Java 后端的日志里一般会有框架自动加上的一串 requestId,把它和客户端请求头里的某个自定义 ID 关联起来,就能看完整的调用链。
5. 常见问题速查与避坑清单
5.1 4xx状态码排查速查表
以我自己的排障经验,把最常见的几种“状态码 + 症状 + 常见原因 + 下一步操作”整理成一张速查表,方便直接在排查时对照:
| 状态码 | 典型症状 | 常见原因 | 第一步排查动作 |
|---|---|---|---|
| 400 | 请求发过去了,但服务端说格式不对 | Content-Type 错误、JSON 非法、Header 含非法字符 | curl 重放,逐步裁剪字段,找最小复现 |
| 401 | 未认证或 token 过期 | Authorization 头缺失/无效、Cookie 过期 | 检查响应头 WWW-Authenticate,确认认证方式 |
| 403 | 已认证但无权访问 | 权限配置、IP 白名单、WAF 拦截 | 看响应体错误码,检查用户角色权限 |
| 404 | 路由或资源不存在 | 路由拼写错误、URL 编码错误、资源已删除 | 用浏览器复制实际 URL,检查路径和编码 |
| 405 | 方法不允许 | GET/POST 用错、路由只注册了其他方法 | 看响应头 Allow,改请求方法 |
| 406 | 响应内容类型无法满足 | Accept 头和服务端响应类型不匹配 | 检查 Accept 头,确认服务端能返回的类型 |
| 408 | 请求超时 | 请求体发送太慢、TCP 连接异常 | 检查网络链路,确认请求体大小 |
| 409 | 冲突 | 并发编辑、重复提交、版本号冲突 | 看冲突字段,拉取最新状态后再提交 |
| 412 | 前置条件失败 | If-Match / If-None-Match 校验失败 | 对比 ETag 和当前资源版本 |
| 413 | 请求体过大 | Nginx client_max_body_size 限制 | 调整 Nginx 和后端上传大小限制 |
| 414 | URL 过长 | GET 参数过多、URL 拼接了整段内容 | 改成 POST,或缩短 URL |
| 415 | 不支持的媒体类型 | Content-Type 与服务端期望不符 | 检查 Content-Type,看服务端接受的格式 |
| 416 | 范围请求不满足 | Range 头和资源实际大小不匹配 | 查看 Content-Range 响应头 |
| 421 | 请求发错主机 | HTTP/2 连接复用 + 虚拟主机配置问题 | 检查 Host 头,禁用连接复用重试 |
| 422 | 业务校验失败 | 字段格式、长度、业务规则不合法 | 看响应体里的字段级错误信息 |
| 429 | 请求过频,被限流 | 超过速率限制 | 查看 Retry-After,做指数退避重试 |
| 431 | 请求头过大 | Cookie 太大、Header 拼接太多 | 清理 Cookie,压缩请求头 |
5.2 我踩过的10个坑
最后分享一些我在实际项目里踩过、并且觉得值得写下来的坑。每一条都是真实事件改编,希望能帮你少走点弯路。
第一个坑:400 不一定是语法错。有一次我排查一个接口联调问题,对方一口咬定是“我们后端文档写错了”,结果我一抓包,发现他们的 SDK 把 JSON 请求体编码成了 GBK,后端的 UTF-8 解析器一读就乱码。表面上是 400,实际上是编码问题。
第二个坑:401 和 403 的区别,未必由业务代码决定。很多框架会在过滤器链这一层提前拦截请求,这时候你写的业务逻辑根本没执行,返回码完全取决于框架配置。排查时先搞清楚状态码是谁返回的,再决定去哪看代码。
第三个坑:404 有时候不是真的不存在。一个很经典的情况是,服务端配置了 try_files 把前端路由全部指向 index.html,但接口路径也被这个规则兜住了,导致 API 请求拿到的是 HTML 页面,状态码反而是 200 或者 404。用 curl 看响应体里的内容,比只看状态码更靠谱。
第四个坑:413 要同时调两层限制。我在一个文件上传项目里调了后端限流、网关限流,结果忘了 Nginx 的 client_max_body_size,整整折腾了一个下午。记住:链路里每一层都可能限制请求体大小,排查时一层层查。
第五个坑:429 的重试策略如果做不好,会把服务打挂。限流是为了保护系统,如果客户端收到 429 后立刻重试,大概率会继续 429,甚至触发更严格的风控。正确方案是读 Retry-After,加指数退避和随机抖动。
第六个坑:405 的 Allow 头很有用但容易被忽略。很多框架返回 405 时确实带了 Allow,但前端同学经常不看响应头,只盯着响应体里的错误消息。养成看响应头的习惯,比猜接口文档准得多。
第七个坑:406 多半是 Accept 和 Content-Type 的协商问题。有一种情况是后端接口用 @RequestMapping(produces = "application/json") 限制了响应格式,但前端在请求头里写了 Accept: text/html,结果 406。解决办法是让前后端统一 Accept 头。
第八个坑:414 经常出现在第三方回调里。支付宝、微信这类平台回调时会带一串很长的参数,如果你把这个 URL 再拼接上自己的参数,很容易超长。排查时不要把责任都推给“平台那边的问题”,先量一下 URL 长度。
第九个坑:curl 测试的时候,URL 里的特殊字符要转义。比如 & 在 shell 里会被解释成后台运行符,? 在部分环境里会被通配。写调试脚本时,记得把整个 URL 用单引号包起来,或者用 --globoff 参数关闭 curl 的通配符展开。
第十个坑,也是我最想强调的:状态码只是结论,不是原因。排查 4xx 问题时,心态要稳,不要看到数字就开始背定义。先把“谁返回的、响应头带了什么、响应体写了什么、服务端日志记了什么”这四个问题搞清楚,大方向就不会错。
最后再分享一个小技巧,是我踩过无数次坑之后养成的习惯:在调试任何 HTTP 接口之前,先把服务端返回的完整响应头存一份到本地。很多隐蔽的 4xx 问题,线索其实都在响应头里——WWW-Authenticate、Allow、Retry-After、Content-Range、Server、Via,每一个都可能成为破案的关键。状态码是那扇门,响应头才是门后面的走廊。
