Administrator
Administrator
发布于 2026-11-04 / 3 阅读
0
0

常见 HTTP 状态码详解与排查思路:404、502、503、504 到底哪里出了问题?

适合读者:刚开始维护网站、接口和反向代理的初级运维人员。先读状态码含义表,再按排查步骤收集证据。本文的故障场景用于教学;文末实验只在本机模拟响应,不代表真实生产事故。

网站显示 404,是不是服务器坏了?接口返回 502,是不是网络断了?同样是等待很久,为什么有时看到 504,有时只有“请求超时”?

这些数字能缩小排查范围,但不能单独给出根因。理解它们时,要同时看三个问题:谁返回了这个结果、它在等待谁、同一个请求在各层日志中发生了什么。

1. 先认识 HTTP 状态码

404、502 属于 HTTP 响应状态码。HTTP/1.1 响应中可能看到这样的第一行:

HTTP/1.1 404 Not Found

HTTP/2、HTTP/3 的传输形式不同,但依然有状态码。收到一个状态码,说明某个 HTTP 响应方已经给了结果;这个响应方可能是 CDN、WAF、负载均衡、Nginx,也可能是业务应用。它不一定是最终的应用服务器。

类别 大意 日常例子
1xx 处理中、协议交互中的临时响应 100 Continue、101 Switching Protocols
2xx 请求成功处理或已接收 200、201、202、204
3xx 重定向或缓存协商等后续处理 301、302、304、307、308
4xx 当前请求存在问题或不能被满足 400、401、403、404、429
5xx 响应方处理失败,或无法完成请求 500、502、503、504

这是协议分类,不是责任划分。错误路由、权限配置或发布遗漏都可能产生 4xx;不能看到“客户端错误”就认定用户操作有问题。类别和名称可以对照 IANA HTTP 状态码登记表。

另外,DNS 解析失败、TCP 连接失败、TLS 证书校验失败,通常还没有拿到目标网站的 HTTP 响应,所以它们不等于 404 或 502。文末会专门区分这些情况。

2. 常见状态码分别是什么意思

2.1 这些状态不一定是错误

状态码 名称 通俗含义
200 OK 本次 HTTP 请求成功;业务是否成功还要看接口约定和正文
201 Created 已创建资源,例如创建订单
202 Accepted 已接受任务,不代表异步任务已经完成
204 No Content 成功,但没有响应正文;客户端不应强行解析 JSON
206 Partial Content 返回部分内容,常见于范围下载
301 / 308 Moved Permanently / Permanent Redirect 资源永久重定向;308 保留请求方法和请求体
302 / 307 Found / Temporary Redirect 临时重定向;307 保留请求方法和请求体
303 See Other 让客户端另取一个资源,通常随后用 GET 获取结果
304 Not Modified 缓存协商后沿用已有内容,不是“页面不存在”

301、302 在一些客户端中会把 POST 后续请求改成 GET。排查登录、提交表单或接口跳转时,要看每一跳的方法和 Location,不能只看最后一个 200。具体语义以 HTTP Semantics 为准。

2.2 常见 4xx

状态码 名称 通俗含义
400 Bad Request 请求本身不合要求,例如格式、语法或报文有问题
401 Unauthorized 缺少有效认证凭据;不只包括“没有登录”
403 Forbidden 服务器理解请求,但拒绝执行
404 Not Found 找不到目标资源,或不愿透露其是否存在
405 Method Not Allowed 该资源不支持当前请求方法
406 Not Acceptable 无法按客户端要求提供合适的响应表示
407 Proxy Authentication Required 需要向代理服务器认证
408 Request Timeout 服务器等待接收完整请求超时
409 Conflict 请求与资源当前状态冲突
410 Gone 资源已不可用,且很可能是永久移除
412 Precondition Failed If-Match 等请求前置条件不成立
413 Content Too Large 请求内容太大;旧资料常写 Payload Too Large
414 URI Too Long 请求 URI 太长
415 Unsupported Media Type 请求内容的媒体类型或编码不被支持
416 Range Not Satisfiable 请求的内容范围无法满足
422 Unprocessable Content 内容类型、语法可理解,但内容要求无法处理
429 Too Many Requests 触发请求频率限制
431 Request Header Fields Too Large 一个或多个请求头太大,例如 Cookie 过长

多数定义见 RFC 9110 的 4xx 章节;429、431 的定义见 RFC 6585。不同框架可能对同一种业务校验选择不同代码,应同时阅读接口约定。

2.3 常见 5xx

状态码 名称 通俗含义
500 Internal Server Error 响应方遇到意外问题,无法完成处理
501 Not Implemented 不支持完成请求所需的功能
502 Bad Gateway 网关或代理未能从上游取得有效响应
503 Service Unavailable 暂时无法提供服务,例如过载或维护
504 Gateway Timeout 网关或代理没有及时等到所需的上游响应
505 HTTP Version Not Supported 不支持或拒绝请求使用的 HTTP 主版本
511 Network Authentication Required 需要先通过网络接入认证,例如某些公共网络认证入口

500—505 的标准含义见 RFC 9110 的 5xx 章节。511 用于网络接入场景,和网站账号认证的 401 不同,见 RFC 6585 第 6 节。

2.4 Nginx、CDN 中还会遇到一些扩展代码

代码与使用方 含义
Nginx 499 日志中表示客户端提前关闭连接;这里的客户端也可能是上一层代理
Nginx 444 主动关闭连接,不发送 HTTP 响应头;浏览器通常不会收到一个正常的“HTTP 444”响应
Cloudflare 520 源站响应出现未能归入其他分类的异常
Cloudflare 521 回源连接被拒绝等导致源站连接不可用
Cloudflare 522 Cloudflare 与源站连接环节超时
Cloudflare 523 源站不可达
Cloudflare 524 已建立源站连接,但回源读写未在期限内完成
Cloudflare 525 / 526 回源 TLS 握手失败 / 源站证书无效

这些属于产品约定,不能直接套到所有代理。Nginx 的 444 见 return 指令,499 可对照 Nginx 源码中的客户端关闭标记。Cloudflare 的解释见 官方 5xx 文档;521、524 的连接与超时差异另见 521、524。

3. 所有状态码都适用的排查顺序

HTTP 请求链路与响应来源:客户端、边缘入口、反向代理、应用和依赖

第一步:固定出问题的那一次请求

记录发生时间和时区、URL 路径、方法、状态码、所在网络、影响范围及最近变更。确认是所有页面失败,还是某个接口、某个账号、某个节点失败。

在浏览器开发者工具的 Network 中找到实际失败的请求。页面本身可能是 200,真正失败的是后续加载的 JS、图片或 API。前端提示“网络异常”也可能只是应用对所有失败的统一提示。

复制请求时保留排查所需的条件,但分享前移除 Token、Cookie、个人信息和签名参数。不要把真实账号凭据粘贴到公开故障记录中。

第二步:保留状态码、响应头、正文和耗时

下面示例面向 Linux Bash。app.example.test 是占位域名,要换成自己的地址;Windows PowerShell 中请使用 curl.exe,并按 PowerShell 语法调整命令。

evidence_dir=$(mktemp -d)
curl -sS --connect-timeout 3 --max-time 15 \
  -D "$evidence_dir/headers.txt" \
  -o "$evidence_dir/body.txt" \
  -w 'code=%{http_code} remote=%{remote_ip} dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s first_byte=%{time_starttransfer}s total=%{time_total}s\n' \
  'https://app.example.test/api/health'
curl_exit=$?
printf 'curl_exit=%s\n证据目录:%s\n' "$curl_exit" "$evidence_dir"

第一次先不加 -L,这样能看到入口是否返回重定向。这里使用 GET;curl -I 发的是 HEAD,请求方法不同,结果可能不同。真实故障若发生在 POST,应该在可控环境中保留原来的方法、内容类型和请求体复现。

这些时间值大多是从请求开始累计到某阶段的时间,不能把 first_byte 全部算作应用计算时间。代理、TLS、重定向和复用连接也会影响结果。参数和计时含义见 curl 官方手册。

重点读这些响应头:

响应头 能提供什么线索
Location 跳转到哪里,是否在 HTTP/HTTPS 或两个域名间反复跳转
WWW-Authenticate 服务希望采用的认证方式,常见于 401
Allow 405 时该资源允许的方法
Retry-After 服务建议等待多久再请求,可能是秒数或 HTTP 日期
Content-Type 错误页是 HTML,还是应用返回的 JSON
Age、Via、产品缓存头 是否经过缓存或代理;具体含义以产品为准
X-Request-ID、traceparent、CDN 请求标识 在相应日志中关联这一次请求

错误页样式、Server 头只能作为线索,代理可能保留、覆盖或重写它们。确定响应来源需要日志佐证。

第三步:对照入口与上游

在有权限的服务器上,用相同方法、预期路径和必要的 Host请求上游。假设应用本来就在本机 8080 提供 HTTP:

curl -sS -D - --connect-timeout 3 --max-time 10 \
  -H 'Host: app.example.test' \
  'http://127.0.0.1:8080/api/health'

这条命令要在代理能访问该应用的网络环境中执行。容器里的 127.0.0.1 指向当前容器;Kubernetes 中也要考虑 Pod 和 Service 的网络路径。不要把宿主机与容器的回环地址混在一起。

如果要在授权环境中对比特定 HTTPS 节点,使用保留域名与 TLS SNI 的方式:

curl -sS -D - --connect-timeout 3 --max-time 10 \
  --resolve 'app.example.test:443:192.0.2.10' \
  'https://app.example.test/api/health'

192.0.2.10 是文档地址,必须替换;这不能绕过源站访问控制。如果环境强制使用前向代理,还要确认请求是否真按预期走向指定节点。不要用关闭证书校验来代替修复证书问题。

对比结果时,先问“两个请求是否真的等价”。入口可能去掉 /api/ 前缀、注入认证头或终止 HTTPS。入口失败而直连成功,会把注意力引向入口规则和链路,但并不能仅凭一次成功就排除应用的偶发故障。

第四步:把代理日志和应用日志对上

已有日志优先使用。若 Nginx 日志缺少上游信息,可以在评估后增加下面的配置片段。log_format 放入现有 http 块,access_log 放入目标站点现有 server 块,不要另建一份替换生产配置。

# 现有 http { ... } 中
log_format http_diag escape=json
  '{"time":"$time_iso8601","rid":"$request_id",'
  '"host":"$host","method":"$request_method","uri":"$uri",'
  '"status":"$status","request_time":"$request_time",'
  '"upstream":"$upstream_addr","up_status":"$upstream_status",'
  '"up_connect":"$upstream_connect_time",'
  '"up_header":"$upstream_header_time",'
  '"up_response":"$upstream_response_time"}';

# 目标站点现有 server { ... } 中
access_log /var/log/nginx/http-diag.log http_diag;

这里没有记录 Authorization、Cookie、请求体和查询字符串;路径本身仍可能含业务信息,应限制日志访问和保留时间。$uri 可能经过内部重写。已有子级日志配置也可能影响实际写入位置。

$request_id 是当前 Nginx 生成的标识,只加这个日志字段不会自动把标识传给应用或客户端。跨层关联需按现有追踪规范传递,并让应用记录;暂时没有追踪标识时,可按精确时间、方法、路径和上游地址缩小范围。

status 是本层最终状态,up_status 是上游处理信息。重试时,上游字段可能包含多个值;- 也不能直接解读成“上游健康”。结合 error log 看连接失败、提前断开、非法响应头和超时。字段含义见 Nginx 日志模块与上游模块变量。

在确需应用日志配置变更时,先检查语法;检查通过后再按变更流程重载:

sudo nginx -t
sudo systemctl reload nginx

容器化 Nginx、Ingress Controller 和没有 systemd 的环境,应使用对应配置与发布方式。

第五步:只改变一个有证据的问题,再验证

先保留故障证据,再选择修复。每次改动后复测原请求、正常请求和预期失败请求,并观察一段时间。比如修好合法用户的 403 后,未授权用户仍应被拒绝;解决 404 后,不存在的 API 仍应返回合理的 404。

4. 遇到 4xx,具体怎么查

4.1 404:资源、路由还是路径转发出了问题

遭遇情况: 首页能打开,/api/users 返回 404;或者单页应用点击导航正常,刷新 /users/123 却是 404。

排查顺序:

  1. 核对域名、端口、路径大小写、结尾斜杠和资源 ID。确认发布包中确实有目标文件或路由。
  2. 确认请求命中了哪个虚拟主机和 location。访问了默认站点时,也可能出现很“正常”的 404 页面。
  3. 对比代理实际转发的路径与应用路由;应用日志里收到的路径最有说服力。
  4. 若是静态文件,核对部署目录、root / alias 映射和挂载;若是 API,查版本前缀、网关规则与应用路由。
  5. 检查 CDN 或反向代理是否缓存了旧 404;先修正来源,再按范围处理缓存。

一个常见例子是 proxy_pass 的结尾斜杠。在普通前缀 location、没有其他重写和变量时:

# 请求 /api/users,上游收到 /api/users
location /api/ {
    proxy_pass http://127.0.0.1:8080;
}
# 请求 /api/users,上游收到 /users
location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}

两段是不同路径设计的备选配置,不能同时照抄。后端路由如果只有 /users,第一种转发就可能找不到资源。规则依据见 Nginx proxy_pass 文档。

单页应用刷新失败时,可能需要前端路由的入口回退;应限于前端页面位置。把 API 和所有不存在的静态文件都回退成 index.html,会把真实错误伪装成 200,甚至让接口客户端把 HTML 当 JSON 解析。

验证: 原来的有效路径恢复;不存在的资源仍返回 404;前端深层页面刷新正常,API 和静态资源的边界保持正确。

4.2 401 与 403:先区分认证失败和访问被拒绝

401 排查: 检查凭据是否携带、是否过期、签名与受众是否正确,Cookie 的域、路径和安全属性是否匹配,代理是否按设计转发认证信息。标准 401 应带认证挑战 WWW-Authenticate;有些应用实现不规范,缺少此头不能直接证明它不是认证问题。

403 排查: 检查账号角色、资源权限、IP 白名单、WAF 命中、CSRF 校验、文件与目录访问权限。403 并不保证用户已经成功认证,匿名访问也可能直接被拒绝。

如果日志明确指向静态文件权限,沿目录逐层检查运行用户是否有必要的遍历和读取权限。应按需要修正属主、组和权限,不要为了消除 403 把整站改成 777。

验证: 正确身份能访问,错误身份和无权限身份仍被拒绝。不要只测管理员账号。

4.3 400、415、422:请求格式、内容类型还是字段校验

以“创建订单”为例:

  • JSON 少了一个引号,可能是 400。
  • 接口只接收 JSON,却发了 XML 或不支持的内容编码,可能是 415。
  • JSON 语法正确,但数量填成负数,可能是 422,也可能按该接口约定返回 400。

先用一份已知合法的最小请求对比:方法、Content-Type、字符编码、必填字段和字段类型。读取错误正文中的字段提示。排查上传表单时,确认客户端是否正确生成 multipart 边界,不要随意手写一个缺少边界的 Content-Type。

406 关注客户端要求的响应格式,415 关注服务端不接受的请求内容。如果接入层拒绝了报文,应用可能根本没有对应日志,应向前查代理的解析错误。

4.4 405:URL 对了,但方法不对

读取 Allow,再查接口文档和网关方法限制。浏览器地址栏通常发 GET,不能用它验证“只支持 POST”的接口。也不要仅用 HEAD 的结果判断 GET 是否正常。

浏览器跨域请求还可能先发 OPTIONS 预检。若预检收到 405 或缺少必要的跨域响应头,浏览器可能阻止后续调用。先在 Network 中确认失败的是 OPTIONS 还是业务请求。CORS 是浏览器访问控制机制,本身不是一个新的 HTTP 错误码。

验证时同时检查允许与不允许的方法,并确认预检与业务请求分别符合接口策略。

4.5 413、414、431:先弄清“哪里太大”

表现 先量什么 再对照什么
大文件上传 413,小文件正常 请求体大小 CDN、Ingress、Nginx、框架、应用的上传限制
搜索 URL 越来越长后 414 路径与查询字符串长度 请求行、网关 URI 限制,接口是否应改用请求体
某些老用户 431,无痕窗口正常 Cookie 和其他请求头大小 重复 Cookie、过长 Token、各层请求头限制

以 Nginx 为例,请求体限制可查 client_max_body_size,但只改这一层不一定有效,外层 CDN 或应用本身仍可能拒绝。不同产品也可能对同一超限情况使用其他状态码,以具体日志为准。

先修复异常增长或不合理请求,再按业务需要设置有边界的限额,并考虑内存、临时文件、存储空间与超时。验证应覆盖小于、接近和超过限额的请求。Nginx 的相关指令见 HTTP 核心模块。

4.6 409、412、416:请求与当前资源不匹配

409 常见于重复创建、状态流转冲突、并发更新;412 常见于版本条件不匹配。先重新读取资源和版本,再按业务规则处理冲突,避免反复重放同一个写请求。

416 常见于断点续传所需的字节范围超出当前文件。检查 Range、文件长度和资源是否已经变化;不要把旧文件的下载偏移直接用于一份新文件。

410 则应确认资源确实被永久移除,以及调用方是否仍使用旧链接。修复方向可能是更新链接或迁移策略,而不是重启服务。

4.7 429:先找限流规则,再决定如何重试

确认是哪一层限流,限制对象是账号、IP、Token、接口,还是全站。多个用户经过同一个出口 IP 时,IP 限流可能误伤;拿不到真实客户端地址时,也可能把所有请求算在代理身上。

读取 Retry-After,按服务约定降低并发和频率。需要重试时设置等待、抖动、次数上限和总期限,避免所有客户端同时再次冲击。对于创建订单、付款等写操作,还要确认幂等机制和原请求的处理结果。

“收到 429 就立即重试”往往会放大故障。限流对象与等待提示的协议说明见 RFC 6585 第 4 节。

5. 遇到 5xx,先分清这四种情况

500、502、503、504 的区别及优先调查方向

5.1 500:响应方内部处理失败

遭遇情况: 网站大多数页面正常,提交某个表单后返回 500。

优先怀疑: 未处理异常、缺失配置、数据库或其他依赖错误、磁盘写入失败、部署版本不兼容等。这些是调查方向,不是“500 必定来自数据库”。

排查实践:

  1. 用时间与请求标识找到返回 500 那一层的日志,确定错误是否由应用主动返回。
  2. 找第一条有意义的异常及依赖错误,不要只停留在“请求处理失败”的通用包装信息。
  3. 与正常请求比较输入;检查问题是否集中在某个版本、账号、数据或节点。
  4. 对照最近发布、配置、数据库结构或权限变更。
  5. 有证据时再做针对性修复或回滚,并验证相关数据是否需要补偿。

systemd 管理的应用可参考以下只读命令,服务名和时间要替换:

sudo systemctl status myapp --no-pager
sudo journalctl -u myapp --since '15 minutes ago' --no-pager
df -h
df -i

容器应用改查相应容器日志、退出原因和事件。5xx 不一定都是应用生成,代理也可能内部失败,因此仍要先定位响应来源。

5.2 502:重点查代理怎样连接并读取上游

遭遇情况: Nginx 可访问,但代理到应用的请求返回 502。

常见线索包括上游地址或端口错误、进程退出、HTTP/HTTPS 协议配置不一致、连接被重置、上游提前断开、响应头不合法或超限。具体如何映射为状态码取决于代理产品与配置。

在代理所在环境先检查目标和请求:

sudo ss -lntp
curl -sS -D - --connect-timeout 3 --max-time 10 \
  -H 'Host: app.example.test' \
  'http://127.0.0.1:8080/api/health'
sudo tail -n 100 /var/log/nginx/error.log

结合 error log 判断下一步:

典型线索 排查方向
connect() failed / Connection refused 是否连对 IP、端口;监听、服务退出、显式拒绝策略
上游提前关闭连接 应用崩溃、被终止、主动断开、协议不匹配
上游响应头不合法或过大 应用响应格式、异常 Cookie、合理的响应头大小与代理限制
容器本机请求成功,代理侧失败 容器监听地址、发布端口、服务发现与两者网络路径

ss 只能看到当前网络命名空间中的监听;它不能替代从代理发起的实际连接测试。用 HTTPS 访问只说 HTTP 的端口,或反过来,也要先修正协议,而不是盲目扩大超时。

验证: 从代理侧和入口侧访问同一路径,确认所有上游节点都正常;再观察重启、滚动发布期间是否还有短暂 502。

5.3 503:目前无法服务,找出是维护、容量还是可用后端问题

遭遇情况: 高峰期大面积 503,或者发布后入口一直 503。

先检查维护开关、应用就绪状态、负载均衡健康检查、是否有可用上游,再看 CPU、内存、连接池、队列、线程和下游依赖。某些代理会用 503 表示没有可选后端;其他产品可能返回 502,因此不能只靠数字判断“后端数量为零”。

如果应用进程存在但健康检查失败,要核对健康检查的路径、Host、端口、预期状态和启动时间。在 Kubernetes 环境中,继续查看 Service 选择器、EndpointSlice 和 Pod 就绪状态;先确认这些对象属于正确的命名空间。

503 可能带 Retry-After。可以按业务设计做降级、限流、扩容或回滚;扩容前先识别瓶颈,避免让更多应用实例同时挤压已经满载的数据库。

验证: 可用节点数量恢复、健康检查稳定、队列和错误率下降,实际业务请求持续成功。只看到进程“running”不算恢复。

5.4 504:查等待发生在哪一段,以及是谁先到期

遭遇情况: 某接口总在接近固定时间后返回 504,而应用日志稍后才记录处理完成。

先画出调用链,例如:客户端 → CDN → Nginx → 应用 → 数据库。分别列出各层的连接、读写、请求总期限和依赖调用期限。对比代理的上游连接时间、响应头时间、总响应时间,以及应用的慢请求和数据库慢查询。

重点调查连接排队、线程或连接池耗尽、锁等待、慢查询、外部 API 卡住等。请求耗时接近某个配置值是线索,但不能单凭“正好 60 秒”证明是哪一层。

Nginx 的 proxy_connect_timeout 关注连接上游;proxy_read_timeout 是两次连续读取之间允许等待的时间,不是整个请求的总时长限制。流式响应持续输出时,行为会与“等待第一个字节”不同,见 Nginx 代理超时说明。

修复应针对慢点、排队和容量,并让内部依赖调用有界、失败能向外传递。确需调整超时时,要结合业务耗时与整条链路安排;单独把所有超时改大,可能只是让更多连接长期占用。

写请求尤其要注意: 网关超时不代表应用一定停止处理。先确认订单、付款或任务是否已执行,再按幂等设计重试,否则可能重复扣款或创建。

验证: 原请求的成功率与延迟恢复;故障注入时能在预期时间内失败;没有后台任务堆积和重复业务操作。

5.5 501、505 与 511 怎么办

501 要对照请求方法、协议能力和产品支持范围;同一资源不允许某方法通常先想到 405,而非把所有方法错误都当作 501。

505 要核对客户端、前向代理和服务端之间的 HTTP 版本兼容情况,保留握手与响应证据。HTTP/2 协商失败、TLS 握手失败或浏览器显示协议错误,都不保证一定收到 505。

511 则先确认是否处于需要联网认证的环境;它不能通过重置业务网站密码解决。407 也要找对认证对象:它针对代理,而不是源站账号。

6. 都叫“超时”,408、499、504 和 curl 28 有什么区别

观察到的结果 谁在描述什么 下一步看什么
HTTP 408 响应方等待接收完整请求超时 上传速度、请求头/体接收、前置代理与接收超时
Nginx 日志 499 本层的客户端提前断开 浏览器取消、上一层代理或客户端先超时、慢请求
HTTP 504 网关没有及时等到上游 连接与上游读写、应用处理、依赖等待和期限
curl 退出码 28 curl 自己的某个超时条件触发 -v 最后停在哪、配置了什么期限、是否已收到响应头
页面“请求超时”,没有 HTTP 状态 浏览器或应用自己的提示 开发者工具原始错误、前端超时逻辑、服务端是否仍在处理

例如客户端只愿意等 5 秒,Nginx 愿意等 15 秒,应用处理要 10 秒。客户端可能先断开,Nginx 日志出现 499;这不能证明应用没有问题,也不表示 Nginx 把“499 错误页”成功送给了用户。

再区分一组容易混淆的数字:

curl 退出码或输出 含义
6 目标主机名解析失败
7 连接目标失败;需要结合错误文字判断拒绝、地址或其他原因
28 超时,不只可能发生在连接阶段
35 TLS/SSL 握手环节出错
60 对端证书校验失败
22 启用 --fail 或 --fail-with-body 后,HTTP 错误导致失败退出
%{http_code} 显示 000 没有取得可报告的 HTTP 响应状态;它不是标准 HTTP 状态码

curl 默认拿到 404、500 后,若传输正常结束,仍可能退出为 0。反过来,已经收到 200 响应头但下载正文超时,也可能是 HTTP 200 + curl 28。因此脚本应同时判断传输结果、HTTP 状态和必要的业务结果。退出码定义见 curl 官方手册。

没有拿到 HTTP 响应时,先沿 DNS、连接、TLS 分层排查,可结合本站文章《Ping 通却打不开网站:沿 DNS、TCP、TLS、HTTP 分层排查》阅读。

7. 本机动手:亲眼看 404、302、429、503 与超时

下面的小程序使用 Python 3 标准库,仅监听 127.0.0.1:18088。它主动返回指定状态码用于观察,不会启动真实网关;请求 /status/502 得到 502,并不代表发生了真实回源故障。

将代码保存为 http_status_lab.py:

"""Local HTTP response lab. Synthetic status codes, not a real reverse proxy."""

import argparse
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import time
from urllib.parse import urlsplit


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        path = urlsplit(self.path).path
        headers = {}
        code = 404
        allowed = {200, 400, 401, 403, 404, 405, 408, 409, 413,
                   414, 415, 422, 429, 431, 500, 501, 502, 503, 504}
        if path == "/slow":
            time.sleep(3)
            code = 200
        elif path == "/jump":
            code = 302
            headers["Location"] = "/status/200"
        elif path.startswith("/status/"):
            value = path[len("/status/"):]
            if value.isascii() and value.isdecimal() and len(value) == 3:
                candidate = int(value)
                if candidate in allowed:
                    code = candidate
        if code == 401:
            headers["WWW-Authenticate"] = 'Basic realm="local-lab"'
        if code == 405:
            headers["Allow"] = "POST"
        if code in (429, 503):
            headers["Retry-After"] = "5"
        data = json.dumps({"status": code, "meaning": HTTPStatus(code).phrase,
                           "synthetic": True}).encode("utf-8")
        try:
            self.send_response(code)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(data)))
            self.send_header("Cache-Control", "no-store")
            for name, value in headers.items():
                self.send_header(name, value)
            self.end_headers()
            self.wfile.write(data)
        except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
            pass  # The client may leave before /slow sends its response.

    def do_POST(self):
        # An allowed method is implemented separately from the synthetic 405.
        self.send_response(204)
        self.send_header("Content-Length", "0")
        self.send_header("Connection", "close")
        self.end_headers()
        self.close_connection = True


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--port", type=int, default=18088)
    args = parser.parse_args()
    with ThreadingHTTPServer(("127.0.0.1", args.port), Handler) as server:
        print(f"Local lab: http://127.0.0.1:{server.server_port}", flush=True)
        try:
            server.serve_forever()
        except KeyboardInterrupt:
            pass

在一个终端启动,实验结束按 Ctrl+C 停止。端口被占用时更换端口,并同步替换后续 URL:

python3 http_status_lab.py --port 18088

另开一个 Bash 终端依次实验。命令中 --noproxy '*' 只用于本机实验,避免本机请求被环境代理转发。

实验一:HTTP 404 不等于 curl 失败退出

curl --noproxy '*' -sS -o /dev/null \
  -w 'http=%{http_code}\n' 'http://127.0.0.1:18088/status/404'
printf 'curl_exit=%s\n' "$?"

预期 http=404、curl_exit=0。加入 --fail 后再试:

curl --noproxy '*' -sS --fail -o /dev/null \
  -w 'http=%{http_code}\n' 'http://127.0.0.1:18088/status/404'
printf 'curl_exit=%s\n' "$?"

预期仍能报告 http=404,退出码变成 22,并在错误输出中提示 HTTP 错误。

实验二:看认证、方法和稍后重试提示

for code in 401 405 429 503; do
  curl --noproxy '*' -sS -i --max-time 5 \
    "http://127.0.0.1:18088/status/$code"
done

依次观察 401 的 WWW-Authenticate、405 的 Allow、429 和 503 的 Retry-After: 5。这些是模拟响应;程序没有真正实现登录、限流或维护判断。

实验三:不跟随与跟随重定向

curl --noproxy '*' -sS -D - -o /dev/null \
  'http://127.0.0.1:18088/jump'
curl --noproxy '*' -sS -L --max-redirs 3 -D - -o /dev/null \
  'http://127.0.0.1:18088/jump'

第一条看到 302 和 Location: /status/200;第二条能看到 302、200 两组响应头。如果只打印最终状态,会遗漏中间这一跳。

实验四:客户端超时,不会自动变成 HTTP 504

curl --noproxy '*' -sS --max-time 1 -o /dev/null \
  -w 'http=%{http_code}\n' 'http://127.0.0.1:18088/slow'
printf 'curl_exit=%s\n' "$?"

服务器要等 3 秒才发送响应,curl 只等 1 秒,因此这里预期 http=000、curl_exit=28。把总期限改为 5 秒再试:

curl --noproxy '*' -sS --max-time 5 -o /dev/null \
  -w 'http=%{http_code}\n' 'http://127.0.0.1:18088/slow'
printf 'curl_exit=%s\n' "$?"

预期 http=200、curl_exit=0。同一个服务并没有返回过 504;变化的是客户端是否愿意继续等待。

验证范围:本文配套程序与上述本机响应、重定向、超时和退出码实验已实际验证;Bash 示例经过语法检查。Nginx 配置为按官方文档整理的片段,没有在真实 Nginx、CDN 或生产集群上执行完整故障演练。

8. 最后把排查收敛成一张工作表

遇到什么 第一轮优先调查
没有 HTTP 状态 DNS、连接、TLS、客户端期限
404 哪个站点、哪个路由、转发后什么路径、资源是否存在
401 / 403 认证凭据与访问策略,究竟哪一层拒绝
400 / 415 / 422 报文格式、内容类型、字段与接口约定
405 方法、Allow、浏览器 OPTIONS 预检
413 / 414 / 431 请求体、URI、请求头各自大小及各层限额
429 限流来源、计数对象、等待与有界重试
500 产生响应那一层的异常与最近变更
502 上游目标、连接、协议、断开与响应格式
503 维护、就绪、可用节点、容量与队列
504 上游与依赖耗时、排队、整条链路的期限

一份有用的故障记录至少写清:

发生时间与时区:
影响范围、接口与方法:
HTTP 状态 / 客户端退出码 / 业务结果:
请求标识与对应日志:
响应由哪一层产生,证据是什么:
入口与上游对比结果:
根因、修复、回滚方式:
原请求与正常/异常边界的复测结果:
后续观察到的错误率、延迟与容量变化:

状态码的价值,是帮我们选择下一条证据。先确认含义,再定位响应来源,沿请求链路对照日志,最后用原请求验证修复,排查就会比反复刷新页面和重启服务更有方向。


评论