S19

HTTP

点击正文段落选定收藏或划线位置;未选定时使用屏幕内第一段。

课内目录

浏览器显示「打不开」,可能是名字没有解析出来,也可能是连接已经建立、服务器却回答「没有这个资源」。S18 的 DNS 帮我们找到地址,S17 的 TCP 帮我们建立可靠的字节传输;本课继续追问:送过去的字节表达什么请求,收回来的字节又代表什么结果?沿用 S13 的分层探测方法,我们把「可达」进一步拆成「可以连接」「可以安全通信」「HTTP 回答符合预期」。

本课以 TCP 上的 HTTP/1.1 为主。文中的 app.example 和 198.51.100.20 是教学用域名与文档地址,命令输出是同一组示例环境的记录,不能把它们当作公网测试服务。实际操作时应换成有权测试的域名、地址和路径。命令中的时间与状态都是用来说明证据如何衔接的具体样本,不代表所有服务都会给出同样结果。

1 HTTP 在协议栈里回答什么问题

TCP 关心字节能否按序到达,HTTP 关心这些字节在应用层表达什么。客户端可以请求读取某个页面,也可以提交表单;服务端用状态、字段和可选的正文回答。即使 TCP 完整传输了 900 字节,这 900 字节也可能是一张错误页。要判断服务是否可用,必须读懂 HTTP 层的意思。

在本课的模型中,1 次 HTTP 交互由请求和响应组成。客户端先说明要做什么、访问什么目标,并附上必要信息;服务端再说明处理结果,按需返回内容。请求可以没有正文,响应也不一定带正文。因此,不能靠「返回了多少字节」判断是否成功,也不能因为页面看起来像报错,就直接认定服务器没有收到请求。

URL 把若干信息放在同一处。例如 https://app.example:443/api/items?id=17 中,https 指定使用 HTTPS,app.example 指定主机名,443 是端口,/api/items 是路径,id=17 是查询参数。路径和查询参数参与表达访问目标;它们不要求服务器磁盘上存在同名文件,应用可以用程序生成响应。URL 末尾若有 #details 这样的片段,浏览器通常在本地使用它,不把片段作为 HTTP 请求目标发送。

把这个例子拆开看:DNS 为主机名返回地址,TCP 连接目标地址的 443 端口,TLS 为 HTTPS 建立安全通信条件,随后才传递 HTTP 请求。若 DNS 返回了错误地址,HTTP 的方法写得再正确也无济于事;若 TCP 连接超时,还没有可供分析的 HTTP 状态。反过来,已经收到 404 就说明本次交互至少到达了能够返回 HTTP 响应的某个组件,可以继续查它是否是预期组件。

HTTP 请求与响应在 TCP 之上表达应用语义
HTTP 请求与响应在 TCP 之上表达应用语义

例如,读取商品 17 的请求得到 200 和 640 字节 JSON,可以作为接口验收的候选证据;同样的连接得到 404 和 640 字节 HTML,则不能因为大小相同就判为通过。我们要同时记录目标、响应状态和内容含义。HTTP 补充了 DNS、TCP 之后的观察维度,也要求保留此前的分层证据,避免每遇到错误页都重新从「网络断了」开始猜。

还要区分连接与报文的边界。HTTP/1.1 可以在同一条 TCP 连接上先后完成多个请求,收到一次响应不必断开连接。TCP 提供连续字节流,某次读取拿到的字节可能只有半行字段,也可能包含字段和部分正文;不能把一次读取或一个 TCP 包当成完整 HTTP 响应。客户端需要按 HTTP 规则识别字段结束位置,再判断正文是否收齐。

例如响应声明 Content-Length: 640,客户端收到状态 200 后只读到 300 个正文字节,连接便被关闭。这时状态行虽然完整,正文传输却未完成,不能将截断的 JSON 当作成功结果。若响应采用 Transfer-Encoding: chunked,则按分块长度读取,直到结束块及其后的报文结束边界,不能靠短暂没有数据判断结束。相反,204 响应按协议没有正文,不能因为内容为空就判为丢包。排查时应同时确认状态、正文边界和实际接收结果;这也解释了为何日志已有 200,客户端仍可能报告传输失败。

2 请求与响应:方法、路径、状态行

HTTP/1.1 请求的开头是请求行。下面的例子表示:在指定站点上读取编号为 17 的商品。

GET /api/items/17 HTTP/1.1
Host: app.example
Accept: application/json

请求行的 3 个部分分别是方法 GET、请求目标 /api/items/17 和版本 HTTP/1.1。普通直连场景中,请求目标主要是路径及可选查询参数,主机名由 Host 字段表达。后面每行字段补充请求条件,字段结束后用空行分隔正文。这个 GET 示例没有正文;空行依然有意义,它告诉接收方字段已经结束。

GET 的基本直觉是读取资源,POST 的基本直觉是向资源提交数据。比如读取订单 17 可用 GET /orders/17,提交新订单可用 POST /orders,具体接口契约由服务定义。不要仅凭路径中出现 create 就判断方法,也不要以为 POST 的正文天然保密;明文 HTTP 中,方法、字段和正文都不因使用 POST 而自动获得加密保护。

POST /orders HTTP/1.1
Host: app.example
Content-Type: application/json
Content-Length: 11

{"item":17}

这里正文是 11 个 ASCII 字节,Content-Length: 11 描述字节长度,不能把字符数普遍当作字节数。示例 JSON 的值 17 是商品编号,不是状态码;看协议时要把数据所在位置与含义对应起来。真实客户端通常负责生成正确的长度和报文边界,排查时无需手工拼装这些内容,但应理解字段和正文为何分开。

响应以状态行开头,例如:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 23

{"id":17,"stock":"yes"}

状态行包含版本、3 位状态码和便于人阅读的原因短语。判断协议结果主要看 200,不能只匹配英文 OK。这个响应正文有 23 个 ASCII 字节,表示商品存在且有库存;它并不自动证明下单、扣款等后续动作也成功。若验收目标只是「商品查询可用」,则还应检查字段是否包含目标编号 17,而不是接受任意一个 200 页面。

幂等性的谨慎理解是:按协议语义,重复执行相同请求的预期服务端效果与执行 1 次相同;GET 应具有这种性质,POST 则不能据此假定可安全重复提交。它不承诺每次返回的正文完全相同,也不意味着请求没有日志记录。对于支付或创建订单,超时后是否重试应服从接口约定,不能因为没看到响应就推断服务端没有执行。

3 关键字段:Host、Content-Type 与为何重要

同一个 IP 地址可以承载多个站点。假设 app.example 和 docs.example 都解析到 198.51.100.20,并使用 80 端口。TCP 只看到连接的地址与端口,无法从这 2 个值分辨用户想看应用还是文档;HTTP/1.1 的 Host 把站点身份带进请求,使服务器能够选择相应的虚拟主机。对 HTTP/1.1 请求来说,Host 是必需字段,非默认端口也应包含在对应的主机信息中。

GET / HTTP/1.1
Host: app.example

这个请求可能得到应用首页;将 Host 改为 docs.example,在同一个地址上就可能得到文档首页。直接请求 http://198.51.100.20/ 时,客户端通常发送 IP 形式的 Host,服务器可能把它交给默认站点。默认站点返回 404,并不能推翻「应用域名首页返回 200」这一结果,因为 2 次请求选择的站点不同。

共享地址如何通过 Host 选择不同站点
共享地址如何通过 Host 选择不同站点

HTTP 的 Host 与 HTTPS 的证书校验还要区分:前者影响 HTTP 请求交给哪个站点,后者在发送该请求之前检查 TLS 连接身份。HTTPS 中还常用 TLS 的服务器名称指示来选择站点证书,只修改 HTTP Host 不一定能得到预期的 TLS 行为。后面使用 --resolve 时,我们会保留 URL 中的主机名,同时固定连接地址,避免把这些条件混在一起改变。

Content-Type 描述当前报文正文的媒体类型。响应中的 application/json 表示正文按 JSON 解释,text/html 表示 HTML;请求中的该字段则告诉服务端如何理解客户端提交的正文。它不直接描述 URL 后缀,也不是「这次请求一定成功」的保证。一个名为 /api/items/17 的接口,可能在异常时返回 HTML 错误页。

例如客户端预期 200 和 JSON,实际收到 200、Content-Type: text/html 以及登录页,这说明仅检查状态的探针可能误判。另一个请求把 JSON 正文标为 text/plain,服务端可能拒绝解析,返回 400 或 415,具体取决于接口约定。这时应检查提交格式与字段是否一致,而不是先改 DNS。请求中的 Accept: application/json 表达希望收到的类型,与声明已发送正文类型的 Content-Type 作用不同。

排查时至少将「主机名、路径、方法、正文类型」作为同一条请求的条件保存。只给同事发一个 IP,或者只截取 200,不足以复现上述差别。若 2 次响应不同,先比较请求条件是否真的相同,再讨论服务是否不稳定。

4 状态码家族:2xx、3xx、4xx、5xx 怎么读

先读首位,再定位具体代码,比背诵状态码清单更能帮助排查。2xx 表示请求在 HTTP 语义上被成功处理;3xx 指示重定向等进一步处理;4xx 表示请求条件等客户端一侧的问题;5xx 表示服务端处理失败。这些家族缩小了下一步范围,但不能直接替代业务验收或责任归属判断。

200 OK 常见于成功读取资源。假设查询商品 17 返回 200,而 JSON 中的 id 是 18,HTTP 成功仍不满足本次验收。又如某接口约定在 200 的正文中提供业务结果码,调用方就必须继续读取该字段。状态码回答协议层结果,业务契约决定还需要验证哪些内容,不能把两者压缩成单一绿灯。

301 Moved Permanently 表示资源的永久重定向,通常通过 Location 字段给出新目标。例如 /old 返回 Location: /new,客户端若跟随,就再请求 /new。首个响应是 301,随后可能是 200,这已经是 2 次请求。若工具只显示最后的页面,会隐藏中间的跳转;排查地址变化时应保留每个状态和 Location,而非只看最后一行。

404 Not Found 表示响应方没有找到对应资源,或者不愿公开它是否存在。它常见于路径写错、路由未部署、选中了默认站点等情形。收到 404 的同时,DNS、TCP 和 HTTP 响应都可能完全正常。因此「404 是客户端错误家族」不等于「用户应该独自负责」,发布遗漏或网关路径改写错误同样可能制造 404。

502 Bad Gateway 通常来自扮演网关或代理的组件,表示它没有从上游获得有效响应。假设客户端连接网关的 443 端口成功,网关连接应用的 8080 端口却失败,就可能向客户端返回 502。客户端到网关和网关到应用是 2 段独立交互;前段可连接不能证明后段正常,502 也不能只凭代码确定是哪个上游或哪种故障。

从状态码家族选择下一步证据
从状态码家族选择下一步证据

用 4 个数练习分类:200 先核对正文;301 先核对 Location;404 先核对 Host、路径和方法;502 先确认响应网关及其上游证据。首位帮我们选择检查方向,后 2 位和字段、正文、日志一起帮助收敛。1xx 是信息性响应,本课不展开;读到它时应继续寻找本次交互的最终响应。

5 HTTPS 边界:TCP 已连接之后还可能失败在哪里

http:// 和 https:// 的差别涉及连接建立后的通信方式。本课的 HTTPS 路径是先建立 TCP,再进行 TLS 握手,随后通过受保护的连接交换 HTTP 报文。TCP 显示 Connected,仅说明该连接已建立;如果 TLS 失败,客户端通常还没有发送正常的应用 HTTP 请求,也就没有本次请求的 HTTP 状态可读。

假设访问 https://app.example/api,TCP 在 30 毫秒建立,随后证书校验报告名称不匹配,整个命令在 45 毫秒失败。这 45 毫秒不是「接口处理了 45 毫秒后报错」,其中根本没有进入我们预期的 HTTP 请求阶段。正确记录应是「TCP 成功,TLS 名称校验失败,未取得 HTTP 响应」,下一步检查 URL 主机名、证书覆盖的名称及入口站点配置。

TCP 成功到 HTTP 响应之间的 TLS 边界
TCP 成功到 HTTP 响应之间的 TLS 边界

证书名称不匹配、证书时间有效性问题、未知或不受信任的签发机构,都是 TLS 阶段可能出现的证据。名称错误可发生在把域名 URL 改成 IP URL 后;时间问题可能来自证书过期,也可能来自客户端时钟明显错误;未知 CA 则要检查证书链及客户端信任配置。每种情况都要保存具体报错,不能用「HTTPS 有问题」替代故障描述。

例如同一证书有效期到 10 月 31 日,客户端时钟却设成 11 月 10 日,就可能认为它已过期。这个例子说明判断依赖本地时间,但并不意味着所有过期报错都该通过改时钟解决。应比较可信时间、证书有效期和实际报错;若证书确实过期,就应更新证书。这里关注的是阶段与证据,无需展开完整的公钥基础设施课程。

curl 的 -k 会跳过证书校验,因而可能让原先失败的请求继续到 HTTP。即使此时拿到 200,也只能说明在放宽校验的条件下应用回答了请求,不能作为 HTTPS 身份验证通过的验收证据。正式验收应使用正常校验,并记录证书名称是否匹配。排查命令改变了什么条件,必须与它得出的结论一起说明。

把明文 HTTP 发到预期 TLS 的入口,或者把 HTTPS 发到只接受明文的端口,也可能产生协议错误或连接被关闭,不能把它们统称为 404。先核对 scheme 与端口的服务约定,再读后续输出。HTTP/2 以及更后的版本也存在,其中 HTTP/3 的传输方式不同;本课用 --http1.1 固定观察范围,避免把本课顺序当成所有版本的唯一实现。

6 看一次交互:用 curl 读阶段与正文

curl 的 -v 会输出连接和协议过程。示例先固定 HTTP/1.1,正文另存到文件,使终端中的阶段记录更容易阅读。> 标出发出的 HTTP 行,< 标出收到的 HTTP 行,* 通常是 curl 的过程说明;这些标记是工具的展示方式,不属于线上报文正文。

curl --http1.1 -v -o /tmp/s19-body.json https://app.example/api
echo $?
* Trying 198.51.100.20:443...
* Connected to app.example (198.51.100.20) port 443
* SSL connection using TLSv1.3
* SSL certificate verify ok.
> GET /api HTTP/1.1
> Host: app.example
< HTTP/1.1 404 Not Found
< Content-Type: application/json
0

这里省略了握手细节。Trying 表示正在尝试某个地址,不等于已经连上;Connected 是 TCP 已连接的证据;TLS 版本和证书校验成功说明该阶段通过;最后的状态行才是 HTTP 回答。输出格式会随 curl 版本和 TLS 后端变化,应识别阶段含义,不要把某一句英文当作所有系统必有的固定文本。

末尾的 0 来自紧随 curl 的 echo $?,表示这个 curl 命令按默认规则完成了传输。curl 默认不会把 404 或 502 自动当作命令执行失败,因此「退出码 0」和「HTTP 200」不是同一个判断。退出码应立刻保存或显示,因为后续执行的其他命令会覆盖 shell 中的 $?。

curl --http1.1 --fail -v -o /tmp/s19-fail-body.json https://app.example/api
echo $?
< HTTP/1.1 404 Not Found
curl: (22) The requested URL returned error: 404
22

对于这里的 404,--fail 让 curl 返回 22;本课后面的 502 也有相同对照。这个选项通常还会抑制 HTTP 错误响应正文,若正文包含诊断信息,应在默认请求中单独留存。22 不能区分 404 和 502,脚本仍须保存状态码,不能把退出码本身当成业务结果码。

若怀疑 DNS 指到了旧入口,可以在保持 URL 名称的同时固定地址:

curl --http1.1 -v --resolve app.example:443:198.51.100.20 https://app.example/health

--resolve 为指定主机与端口提供本次命令使用的地址,连接去往 198.51.100.20,URL 名称、HTTP Host 和 TLS 使用的服务器名称仍围绕 app.example。这比把 URL 改成 IP 更适合隔离入口地址差异。它绕过了本次目标的正常解析选择,所以该请求成功不能反过来证明 DNS 配置正确;应把普通访问与固定地址访问的结果配对记录。

用 curl 将连接阶段与响应结果分别记录
用 curl 将连接阶段与响应结果分别记录

若要比较耗时,可以使用 -w 输出累计时间。例如观测到 DNS 完成于 0.010 秒、TCP 完成于 0.040 秒、TLS 完成于 0.090 秒、首字节到达于 0.210 秒、总计 0.230 秒。在这次无复用、无跳转的简单请求中,TCP 阶段约为 30 毫秒,TLS 增量约为 50 毫秒,TLS 完成到首字节约为 120 毫秒。最后这段仍包括请求传送和等待等因素,不能直接命名为应用 CPU 时间。

7 常见坑:把传输成功当业务成功、乱猜状态码、忽略 Host

第 1 个坑是看见 Connected 就宣布服务正常。一次探测可能依次得到 TCP 成功、TLS 成功和 HTTP 502,这 3 条记录并不矛盾。它们分别回答不同问题。验收报告应写出「哪个 URL、什么方法、预期什么状态、实际什么状态」,而不是只贴上连接成功截图。若目标要求返回商品 17,再多加一项正文断言,才能防止登录页或维护页以 200 混过检查。

第 2 个坑是把代码当成根因。404 提示资源选择方向,却不自动说明磁盘文件不存在;502 提示网关与上游方向,却不证明应用进程一定已停止。假设 /api 得到 404,/health 得到 200,可以缩小到路径、方法或路由差异,但还不能排除 2 条路径被分发给不同服务。要把「已证实的现象」与「待验证的原因」写成不同句子。

第 3 个坑是忽略 Host。运维人员测 IP 首页得到默认站点的 404,用户用域名却能访问,双方可能都测对了自己的请求,只是请求并不相同。比较结果时应列出完整 URL、Host、实际连接地址及端口。对于 HTTPS 还要保留 TLS 名称,避免为了测试 Host 而引入新的证书不匹配问题。

第 4 个坑是用浏览器表现替代原始响应。浏览器可能自动跟随 301,最终显示 200,也可能展示先前缓存的页面;缓存或 CDN 还可能让相同 URL 在不同时间、不同入口得到不同响应。若问题是跳转,先不跟随跳转观察 Location;若问题只在部分用户出现,就比较实际入口与响应字段,不要在证据缺失时断言「服务器随机报错」。

第 5 个坑是只报告「慢」。假设 2 次请求都耗时 2 秒,前者卡在 TCP 连接,后者 40 毫秒已连上、随后等待首字节,两者的检查方向显然不同。报告应写清哪个阶段耗时,以及是否发生了重试或跳转。跟随 3 次跳转的总时间不能直接与无跳转请求的应用等待时间比较。

还有一种误判发生在健康检查范围上。/health 返回 200 可能只表示入口进程能回答,不代表订单、数据库和支付链路都正常。假设健康路径由网关直接返回,而 /api/orders 需要访问上游,那么健康 200 与订单 502 可以同时出现。健康路径证明了什么必须由服务约定,不能把轻量探针结果扩大成全部业务保证。

8 案例:同个 URL「打不开」的3种 HTTP 侧原因

现在把用户入口固定为 https://app.example/work。以下是 3 次独立故障中的证据包,不是同一时刻连续出现的状态。它们都可能被用户描述成「页面打不开」,也都能在 TCP 和 TLS 成功后发生。我们的目标是从响应中选择首个检查动作,而不是从这句口头描述直接选负责人。

情形 A 是重定向配置形成环路。先请求入口,得到以下响应:

HTTP/1.1 301 Moved Permanently
Location: https://app.example/login

继续请求 /login,又得到:

HTTP/1.1 301 Moved Permanently
Location: https://app.example/work

2 个地址互相指向,浏览器可能最终报告跳转过多。用 curl --http1.1 -v -L --max-redirs 5 https://app.example/work 能保留每跳状态与 Location;在本例中达到重定向上限时,curl 返回退出码 47。这个 47 是客户端因跳转上限停止的结果,不能当成 HTTP 状态码。首个检查应是比较入口与登录跳转规则,定位谁把已经跳转过的请求送回原址。

情形 B 是身份或权限条件不满足。未带所需凭据时,入口可能回答:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="app"
Content-Type: application/json

401 提示需要满足身份验证条件;已有身份却不允许访问时,常见响应是 HTTP/1.1 403 Forbidden,但实际选择仍以服务契约为准。证据包应记录测试账号的角色、凭据是否过期、请求时间和状态,避免在共享输出中暴露令牌。首个检查是本次请求是否具有约定的身份条件,再核对该身份的权限;反复改 DNS 不会为请求增加权限。

情形 C 是入口网关无法取得有效的上游响应:

HTTP/1.1 502 Bad Gateway
Content-Type: text/html
X-Request-ID: s19-work-052

此时保留请求标识 s19-work-052 与时间,交给网关侧查询对应路由、上游地址和连接结果。若日志显示网关访问 10.0.2.17:8080 时连接被拒绝,就有了继续检查该实例监听与进程的依据;若只有 502 状态,便不能擅自补出这条结论。响应中的服务器名称字段可以提供线索,但不应被当成不可伪造的组件身份证明。

3 个证据包的差别很具体:A 有重复 Location 链,B 有身份或权限相关响应,C 有网关失败及可关联的请求标识。相同的用户入口不保证相同的处理路径;重定向规则、登录状态和上游健康都能改变结果。报告「同一 URL 失败」时,要再补上时间、请求条件和首个异常响应,才能让后续排查从可验证的位置开始。

9 排查案例:从 DNS/TCP 正常走到 HTTP 404 与 502

值班人员收到报告:「应用 API 不可用,可能网络断了。」本例预期 GET /api 返回 200 和接口描述,GET /health 返回 200 和 ok。我们先验证前序阶段,再比较同一域名的路径。下列片段来自控制了请求条件的教学环境,每条命令后立即读取退出码;实际记录还应写上采样时间,以免把不同时刻的状态误当成并存事实。

dig +short app.example A
198.51.100.20

这证明本次 DNS 查询返回了该地址,尚不能单凭这个输出认定地址正确。与方案中的预期入口 198.51.100.20 对照一致后,继续测试端口:

nc -vz 198.51.100.20 443
echo $?
Connection to 198.51.100.20 443 port [tcp/https] succeeded!
0

现在有了 DNS 返回预期地址、该地址 443 端口可建立 TCP 的证据。它们还没有证明证书正确,也没有证明 /api 存在。若域名有多个地址,应逐个核对相关入口;本例为便于比较,只配置 1 个 A 地址,随后 curl 的 Connected 行也确认连接到了它。

curl --http1.1 -v https://app.example/api
echo $?
* Trying 198.51.100.20:443...
* Connected to app.example (198.51.100.20) port 443
* SSL certificate verify ok.
> GET /api HTTP/1.1
> Host: app.example
< HTTP/1.1 404 Not Found
< Content-Type: application/json
{"error":"route_not_found"}
0

这次请求通过了 TCP 和 TLS,收到明确的 HTTP 404,curl 默认退出码为 0。排查重点转向「这个站点上的 GET /api 是否应存在」。先按相同 scheme、域名和端口请求约定健康路径:

curl --http1.1 -v https://app.example/health
echo $?
* Connected to app.example (198.51.100.20) port 443
* SSL certificate verify ok.
> GET /health HTTP/1.1
> Host: app.example
< HTTP/1.1 200 OK
< Content-Type: text/plain
ok
0

两次只改变路径,健康请求能返回预期内容,因此「所有 HTTP 请求都到不了入口」与现有证据不符。下一步核对路由清单、接口版本和发布记录:是否实际部署的是 /api/v1,是否网关误删了 /api 前缀,是否该路径只支持其他方法。不要为了追求成功而任意换方法;应与接口约定核对后,再实施有目的的比较。

若还怀疑虚拟主机或入口选择,可以用 --resolve app.example:443:198.51.100.20 重复上述请求,并保留 Host: app.example。如果固定地址后 /api 仍为 404,就继续查询该入口的站点路由。若换到另一个已知入口结果改变,才进一步比较入口配置。直接访问 IP 得到默认页,会同时改变名称条件,不适合作为仅隔离地址的对照。

接着测试另一条约定路径 /api/orders。本例中它已配置到订单上游,但该上游当前不可用:

curl --http1.1 -v https://app.example/api/orders
echo $?
* Connected to app.example (198.51.100.20) port 443
* SSL certificate verify ok.
> GET /api/orders HTTP/1.1
> Host: app.example
< HTTP/1.1 502 Bad Gateway
< X-Request-ID: s19-orders-081
< Content-Type: text/html
<html><body>Bad Gateway</body></html>
0

502 与刚才的 404 都证明有 HTTP 组件回答了本次请求,却提示不同方向。404 先查目标选择和路由是否存在;502 先查已接收请求的网关如何访问上游。本例继续关联 s19-orders-081 的网关日志,发现它连接订单实例 8080 端口被拒绝,才将故障收敛到对应实例的监听与服务状态。若没有这条日志,结论应停在「需要核查网关到上游」,不能跳到重启应用。

404 与 502 在同一个入口后的不同排查方向
404 与 502 在同一个入口后的不同排查方向

最后让命令退出码与验收脚本的意图对齐:

curl --http1.1 --fail -v https://app.example/api
echo $?
curl --http1.1 --fail -v https://app.example/api/orders
echo $?
< HTTP/1.1 404 Not Found
curl: (22) The requested URL returned error: 404
22
< HTTP/1.1 502 Bad Gateway
curl: (22) The requested URL returned error: 502
22

默认请求中 404 与 502 都返回退出码 0,加 --fail 后这里都返回 22;健康请求在两种模式下均为 200、退出码 0。若验收明确要求 200,还应直接比较状态是否为 200,因为 --fail 不能替代精确的状态断言,例如未跟随的 301 通常不会因此变成失败。

这次交接可写为:「预期入口解析一致,443 TCP 可连接,TLS 校验通过;GET /api 返回 404,GET /health 返回 200 与 ok,GET /api/orders 返回 502。请应用路由负责人核对 /api 发布情况,网关负责人按请求标识核对订单上游。」这种表述让每条结论都有对应证据,也明确哪些事项仍需查证。

放进方案里

HTTP 服务方案应把「可访问」改写成可重复的验收条件。先写完整 URL、方法、端口和预期状态,例如 GET https://app.example/api/items/17 应返回 200,正文的 id 应为 17,Content-Type 应为 JSON。若允许重定向,写清允许的目标与最大跳数,不能让测试人员自行猜测 301 是否算通过。受保护接口还应说明测试身份和预期权限,不在方案中保存真实凭据。

把入口条件写在同一处:域名应解析到哪个入口,HTTP Host 应是什么,TLS 证书应覆盖哪个名称。需要固定地址测试时,提供保留域名的 --resolve 示例,并说明该结果只验证指定入口。若共有 2 个入口,分别列出地址与验收结果,避免只测中其中 1 个便宣布全部可用。正常证书校验是验收条件,跳过校验的结果应单独标注。

单列健康路径及其证明范围,例如 /health 返回 200 和 ok 只证明入口能够响应;订单功能另测 /api/orders。记录状态、正文关键断言、请求时间、实际连接地址及 curl 退出码,默认模式和失败判定规则都要明确。对于慢请求,保存 DNS、连接、TLS 与首字节阶段时间,让后续人员知道先查哪段。

责任分工应落到证据入口:404 由谁核对站点路由与发布,401/403 由谁核对身份权限,301 环路由谁检查跳转规则,502 由谁先查网关日志、再联系对应上游负责人。502 交接至少带 URL、时间与请求标识,不要求用户凭状态码判断具体实例。另写连接超时与首字节超时的上限秒数,以及超时时应保留的 curl -v 片段,避免把传输层等待误记成 HTTP 业务失败。

验收时请第 2 位同事独立执行:面对「Connected 后失败」「健康 200 但接口 404」「订单 502」这 3 种症状,能否各自指出首个 HTTP 侧检查并找到负责人。能完成这项映射,方案才具备交接价值。

把症状映射到可执行的首个检查
把症状映射到可执行的首个检查

10 小结:走向解决方案(P1)

现在可以带着证据叙述浏览器访问的过程:DNS 为主机名找地址,TCP 建立连接,HTTPS 场景中 TLS 校验并建立安全通信条件,HTTP 再表达方法、目标与处理结果。每个阶段都能成功或失败,后续阶段的结论不能只靠前序阶段推测。

自测可以沿用本课记录:为什么 Connected 后还会没有 HTTP 状态?为什么 404 可以伴随退出码 0?为什么同一 IP 下修改 Host 会得到不同站点?为什么健康 200 不能排除订单 502?回答时各指出对应的输出位置和下一步检查,比背诵代码名称更能说明你掌握了分层判断。

阶段 1 的收束是能够把现象、证据和结论连起来。下一阶段进入云与非功能需求时,还会讨论可用性、性能和责任边界;本课留下的 URL 验收条件、阶段耗时和故障交接记录,将成为那些讨论可操作的起点。

下一课转向 P1 什么是解决方案。

把本课的 URL 分层验收、阶段耗时与故障交接记录带走:下一课把这些证据写进「解决方案」的问题定义与验收口径,并检查云与非功能需求讨论是否仍能量化到可观察的 HTTP 结果。

作业:附上本课 1 组 curl 阶段耗时与 3 类症状→首个检查映射,作为 P1 练习的输入。

同阶段相关课

AI 答疑

默认关闭。启用后,只发送本课标题、最近阅读的屏幕内正文摘录(最多 6000 字)与本次问题。

配置自己的服务