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

第一步:固定出问题的那一次请求
记录发生时间和时区、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。
排查顺序:
- 核对域名、端口、路径大小写、结尾斜杠和资源 ID。确认发布包中确实有目标文件或路由。
- 确认请求命中了哪个虚拟主机和
location。访问了默认站点时,也可能出现很“正常”的 404 页面。 - 对比代理实际转发的路径与应用路由;应用日志里收到的路径最有说服力。
- 若是静态文件,核对部署目录、
root/alias映射和挂载;若是 API,查版本前缀、网关规则与应用路由。 - 检查 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,先分清这四种情况

5.1 500:响应方内部处理失败
遭遇情况: 网站大多数页面正常,提交某个表单后返回 500。
优先怀疑: 未处理异常、缺失配置、数据库或其他依赖错误、磁盘写入失败、部署版本不兼容等。这些是调查方向,不是“500 必定来自数据库”。
排查实践:
- 用时间与请求标识找到返回 500 那一层的日志,确定错误是否由应用主动返回。
- 找第一条有意义的异常及依赖错误,不要只停留在“请求处理失败”的通用包装信息。
- 与正常请求比较输入;检查问题是否集中在某个版本、账号、数据或节点。
- 对照最近发布、配置、数据库结构或权限变更。
- 有证据时再做针对性修复或回滚,并验证相关数据是否需要补偿。
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 状态 / 客户端退出码 / 业务结果:
请求标识与对应日志:
响应由哪一层产生,证据是什么:
入口与上游对比结果:
根因、修复、回滚方式:
原请求与正常/异常边界的复测结果:
后续观察到的错误率、延迟与容量变化:
状态码的价值,是帮我们选择下一条证据。先确认含义,再定位响应来源,沿请求链路对照日志,最后用原请求验证修复,排查就会比反复刷新页面和重启服务更有方向。