HTTP 基础篇的每条笔记各取一纸契约并展示单独的证据。本教程则按顺序走完一次客户端请求触达的完整路径,只用只读命令。目标是区分协议要求与某个服务器的选择,而不是要求所有端点都返回相同状态和头部。

我们全程用 www.xbcatm.com。把它换成你要检查的主机。请求使用 HTTP 安全方法,但仍可能产生访问日志或消耗速率额度。只检查你有权查询的端点。下面输出块是示意,不是当前生产测量。请在一次性工作目录运行 shell 片段,保留错误,并记录时间和客户端版本。

编号 HTTP 路径区分方法策略、元数据、HEAD 对应关系、字节范围、条件复用与编码。当前成功的 200 提供实际校验器和完整正文;匹配的 304 保留该正文。状态和长度示例不是通用承诺。
保持所选表示一致,并在各自契约内解读观察结果。本图未查询活动边缘。

1. 先确定方法可以问什么

在读响应体之前,先确定服务器允许你问什么。静态处理器通常支持 GET 和 HEAD,代理也可能实现 OPTIONS。先询问边缘对目标资源的处理:

$ curl -sS --max-time 20 -D - -o /dev/null -w 'OPTIONS -> HTTP %{http_code}\n' -X OPTIONS https://www.xbcatm.com/
HTTP/2 405
allow: GET, HEAD

OPTIONS -> HTTP 405

若服务器识别 OPTIONS、但该资源不支持它,405 Method Not Allowed 是合法答案,且 405 必须携带 Allow。成功的 200 或 204 OPTIONS 响应也可能正确。501 表示服务器未实现该方法。一次 OPTIONS 不能证明其他方法的行为。不要为了测试拒绝而向不熟悉的线上服务发送 POST:它不是安全方法,单有 200 也不能描述其副作用。

2. 读取表示行

第一行告诉你协商出来的交换;头部随后告诉你将要到达的是什么字节。像客户端看到的那样捕获它们、丢弃响应体:

$ curl -sS --max-time 20 -D - -o /dev/null https://www.xbcatm.com/
HTTP/2 200
accept-ranges: bytes
content-length: 15890
content-security-policy: default-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:
content-type: text/html; charset=utf-8
etag: "dkx84jwth7nkc9e"
last-modified: Mon, 24 Aug 2026 13:55:23 GMT
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff

三个头部值得停下来。content-type: text/html; charset=utf-8 命中了媒体类型,content-type-and-nosniff 说明为什么它必须描述你真正发送的字节。etag: "dkx84jwth7nkc9e" 交给你强验证器。x-content-type-options: nosniff 限制浏览器的 MIME 处理,尤其是脚本与样式表抓取;它不会验证正文确实是 HTML。每一个都是证据而不是装饰,合在一起是说:该响应声明了应与正文及预期策略比较的元数据。语法上为强 ETag 的值仍是服务器的声明,不是独立的内容哈希证明。

3. 对同一表示比较 HEAD 与 GET

HEAD 使用 GET 的语义,但不带响应正文。服务器应返回相同头部,不过可以省略只有生成内容时才能确定的字段。Content-Length 可省略;若在 HEAD 中发送,它必须等于相应 GET 会发送的内容大小。应在内容稳定时使用相同编码及其他选择头比较:

$ curl -sSI --max-time 20 -H 'Accept-Encoding: identity' https://www.xbcatm.com/ | grep -i content-length
content-length: 15890
$ curl -sS --max-time 20 -H 'Accept-Encoding: identity' https://www.xbcatm.com/ | wc -c
15890

两者一致:都是 15890。这就是 head-request-parity 契约。仅缺少 HEAD 长度不是协议违规。长度不符需要调查,也要排除两次请求之间发生发布、或选中不同表示的可能。没有 pipefail 的 shell 中,传给 wc 的管道可能隐藏 curl 失败;自动关卡应先确认 curl 成功保存正文,再统计文件字节。

4. 用范围请求精确的字节

因为源站知道自己的表示,它可以只服务其中一部分。范围请求要求一个字节区间,只得到那个区间:

$ curl -sS --max-time 20 -D - -o /dev/null -H 'Accept-Encoding: identity' -H 'Range: bytes=0-99' -w 'HTTP %{http_code} bytes=%{size_download}\n' https://www.xbcatm.com/
HTTP/2 206
content-range: bytes 0-99/15890
content-length: 100

HTTP 206 bytes=100

对于至少 100 字节的表示,成功的单范围响应应有 206、匹配的 Content-Range: bytes 0-99/…,以及 100 个内容字节。服务器可合法忽略 Range 并返回完整 200,这不说明它缺少校验器。无法满足的范围可能返回 416。普通 Range 请求不要求 ETag;使用 If-Range 恢复特定表示,或安全拼接部分响应时,才要遵守相关强校验器规则。范围针对所选编码后的字节,因此这里显式选择 identity。

5. 用验证器让缓存重新验证

现在把方法、表示与字节选择通过缓存结合。新鲜的已存响应可能无需检查校验器就可复用。测试重新验证时,应抓取当前响应,并用相同表示选择重放其实际 ETag,不要复制文章里的旧标签:

set -eu
url=https://www.xbcatm.com/
work=$(mktemp -d)
status=$(curl -sS --max-time 20 -H 'Accept-Encoding: identity' \
  -D "$work/headers" -o "$work/body" -w '%{http_code}' "$url")
test "$status" = 200
etag=$(awk 'tolower($1)=="etag:" {sub(/\r$/, ""); sub(/^[^:]*:[ \t]*/, ""); print; exit}' "$work/headers")
test -n "$etag" || { echo 'No ETag: inspect Last-Modified instead' >&2; exit 1; }
curl -sS --max-time 20 -H 'Accept-Encoding: identity' \
  -H "If-None-Match: $etag" -D - -o /dev/null \
  -w 'HTTP %{http_code}\n' "$url"

把此片段作为独立 shell 脚本运行。只有初次传输完整成功且状态为 200,才有可用的已存响应;错误表示也可能有 ETag。若所选表示仍匹配且正常处理成功,预期得到 304,并用更新后的元数据复用这份完整正文。表示变化时,200 完全可能是正确结果。检查结果期间保留临时目录,完成后仅删除此处生成的目录。If-None-Match 使用弱比较,所以 304 不一定证明字节完全一致。新鲜度寿命与当前年龄决定能否直接复用;校验器回答的是另一问题,本身不设定寿命。

6. 协商更小的表示

同样的内容在客户端表明能解压时可以传递更少字节。请求 gzip 并观察正文缩水而语义不变:

$ curl -sS --max-time 20 -H 'Accept-Encoding: identity' -o /dev/null -w 'no-enc: %{size_download} bytes\n' https://www.xbcatm.com/
$ curl -sS --max-time 20 -o /dev/null -H 'Accept-Encoding: gzip' -w 'gzip:   %{size_download} bytes\n' https://www.xbcatm.com/
no-enc: 15890 bytes
gzip:   4368 bytes

示例大小展示一种可能的缩减;服务器也可以选择 identity。这是 http-compression-negotiation 笔记的核心:改变选中的表示会改变线上字节,元数据(content-length、encoding)也随之改变。请求压缩的客户端与存储编码形式的缓存必须就选了哪种表示达成一致,否则缓存可能交付不匹配结果。除字节数外,还应检查 Content-Encoding 与 Vary: Accept-Encoding。手工设置 Accept-Encoding 不会让 curl 自动解压;--compressed 才会请求受支持编码并解码响应。应比较解码后的正文来确认内容相同,也不要假定不同编码共用同一个强 ETag。

7. 跟随 URL 变化时的故事

身份变化时,响应应说明故事在哪里继续,而不去猜正文:

$ curl -sS --max-time 20 -o /dev/null -w '%{http_code} -> %{redirect_url}\n' -I http://www.xbcatm.com/ | head -1
308 -> https://www.xbcatm.com/

308 Permanent Redirect 保持方法与故事,正是 redirects-and-status-codes 关心的。无论是永久移动的 301/308,还是由站点处理的 404,状态行都是故事的第一句,应在读正文前先读。

8. 最后让爬虫读地图

边缘也会被根本不请求 HTML 的机器读取。发现机制回答两个不同的问题,robots-and-sitemaps 有说明。确认发现机制指向真实 sitemap:

$ curl -sS --max-time 20 https://www.xbcatm.com/robots.txt
User-agent: *
Disallow:
Sitemap: https://www.xbcatm.com/sitemap.xml

也应抓取 sitemap URL,核对规范主机名与语言 URL。robots.txt 是爬虫指引,不是访问控制;被列入 sitemap 不保证抓取或索引。

所有检查一览

检查应检查什么笔记
OPTIONS 状态 + Allow对该资源观察到的方法策略http-method-semantics
content-type + 正文 + nosniff声明的类型与 MIME 处理策略content-type-and-nosniff
HEAD/GET 元数据对应表示,允许规定的字段省略head-parity
Range + Content-Range观察到的部分传输支持http-range-requests
抓取的 ETag -> 304条件表示匹配conditional-requests
308 保留方法重定向保留故事redirects-and-status-codes
robots -> sitemap发现答案不同robots-and-sitemaps

每一行原来是一条笔记;现在是你可对任意边缘运行的命令。需要细节时读链接的笔记,把这个表留作再次检查时的速查。

来源