<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>HTTP 基础 · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/series/http-foundations/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/series/http-foundations/index.xml" rel="self" type="application/rss+xml"/><item><title>Content-Type 应描述你实际发送的字节</title><link>https://hk.xbcatm.com/zh/notes/content-type-should-describe-the-bytes-you-actually-send/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/content-type-should-describe-the-bytes-you-actually-send/</guid><description>在公开边缘核对媒体类型，并用 nosniff 让不匹配以可见的方式失败。</description><content:encoded>&lt;p&gt;静态服务器可能返回正确的字节，却带上错误的 &lt;code&gt;Content-Type&lt;/code&gt;。某个浏览器里页面看起来或许还正常，但下载、样式表、订阅阅读器或安全策略会把同一响应理解成别的东西。公开响应头才是契约；文件名只是服务器配置的输入。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/content-type.detail.zh.svg" alt="CSS 路径之后检查公开响应的状态、实际字节和 Content-Type。正确 CSS 与 HTML 错误页分列。浏览器脚本和样式目标使用 nosniff，失败时应修复响应；CDN 或代理的公开响应头需要单独核对。" width="720" height="2080"&gt;&lt;figcaption&gt;后缀不能证明实际收到的内容。应同时检查公开响应的状态、字节与媒体类型；CSS 路径上的 HTML 错误页不能用来测试 MIME 映射。nosniff 让脚本和样式目标上的类型不匹配以可见方式失败。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="检查有代表性的格式"&gt;检查有代表性的格式&lt;/h2&gt;
&lt;p&gt;检查一份 HTML 文档、样式表、SVG、feed 与可下载脚本。包含响应头但丢弃正文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; path in / /assets/site.css /favicon.svg /index.xml /downloads/check-http-headers.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -sS -D - -o /dev/null &lt;span class="s2"&gt;&amp;#34;https://www.example.test&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk -v &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; BEGIN { print &amp;#34;== &amp;#34; path }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; /^HTTP\// || tolower($1) == &amp;#34;content-type:&amp;#34; || tolower($1) == &amp;#34;x-content-type-options:&amp;#34; { print }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把示例资源路径换成当前发布中确实存在的文件；虚构 CSS 路径返回 HTML 404，并不能测试 MIME 映射。期望的媒体类型因资源而异，但应该具体且稳定。HTML 通常应包含字符集。CSS 与 SVG 不应被当作泛型二进制数据提供服务。用于下载的 shell 脚本不应被声明为 HTML。&lt;/p&gt;
&lt;p&gt;该循环只是展示响应头，不会自动断言成功、检视被丢弃的字节，也不会执行浏览器的阻止逻辑。应将资源与发布制品核对，并在浏览器中检查它作为样式表或脚本的实际加载结果。把相同 URL 直接当作文档打开，测试的是另一种请求目标。&lt;/p&gt;
&lt;h2 id="保持-nosniff-一致"&gt;保持 nosniff 一致&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; 请求支持它的浏览器在脚本与样式目标上尊重声明的类型，而不是猜测。在这项 Fetch 检查中，脚本类目标需要 JavaScript MIME 类型，样式目标需要 &lt;code&gt;text/css&lt;/code&gt;；类型缺失或无法使用也会失败。这样，不匹配会变成可见的失败，而不是因浏览器而异的解读。&lt;/p&gt;
&lt;p&gt;该响应头不能修复错误类型，也不能让任意内容变安全。部署时核对资源清单；若脚本或样式被拒绝，应修复响应，而不是移除保护：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="检查服务器映射"&gt;检查服务器映射&lt;/h2&gt;
&lt;p&gt;静态服务器通常根据扩展名推导类型。如果某个新扩展名被当作 &lt;code&gt;application/octet-stream&lt;/code&gt; 提供服务，先判断这是否有意为之，再决定是否加入全局覆盖。一个窄映射比一条会改变所有响应的规则更容易复核。&lt;/p&gt;
&lt;p&gt;在公开主机名上重复这些检查。CDN 或反向代理可能替换掉正确的源站响应头；而一个友好的错误页可能为后缀像 CSS 或 JavaScript 的路径返回 HTML。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，表示元数据与 &lt;code&gt;Content-Type&lt;/code&gt;，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type"&gt;MDN: &lt;code&gt;Content-Type&lt;/code&gt;&lt;/a&gt;，媒体类型与字符集示例，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options"&gt;MDN: &lt;code&gt;X-Content-Type-Options&lt;/code&gt;&lt;/a&gt;，&lt;code&gt;nosniff&lt;/code&gt; 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff"&gt;WHATWG Fetch 标准&lt;/a&gt;，脚本类与样式目标的阻止规则，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HEAD 应描述与 GET 相同的表示</title><link>https://hk.xbcatm.com/zh/notes/head-should-describe-the-same-representation-as-get/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/head-should-describe-the-same-representation-as-get/</guid><description>比较 HEAD 与 GET 的元数据，不要把"缺少正文"误当成"资源不同"。</description><content:encoded>&lt;p&gt;&lt;code&gt;HEAD&lt;/code&gt; 方法请求服务器对 &lt;code&gt;GET&lt;/code&gt; 会返回的响应，但不会传输响应内容。它适合需要状态、验证器、类型或长度的检查，但前提是元数据与真实的 &lt;code&gt;GET&lt;/code&gt; 路径保持一致。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/head-parity.detail.zh.svg" alt="包括 Accept-Encoding 在内的相同请求条件进入 GET 与 HEAD 元数据卡片。只有 GET 连到正文和校验和。HEAD 缺少 Content-Length 不等于失败，提供时应描述对应 GET 内容。小 GET 检查有效载荷，并调查方法分歧与重定向比较范围。" width="720" height="2240"&gt;&lt;figcaption&gt;比较 HEAD 与 GET 之前，先对齐 URL、请求头、发布版本与内容编码。允许省略的字段不能自动判为失败。应保留小 GET 的正文或校验和检查，因为看似合理的元数据不能证明有效载荷完好。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="比较规范化后的响应头集合"&gt;比较规范化后的响应头集合&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Date&lt;/code&gt; 这类动态响应头会随请求不同而变化。只比较描述所选表示的那些字段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.example.test/downloads/manual.pdf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -I &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt; head.headers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D get.headers -o /dev/null &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; file in head.headers get.headers&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;== %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; awk &lt;span class="s1"&gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; /^HTTP\// ||
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; tolower($1) ~ /^(content-type:|content-length:|content-encoding:|cache-control:|etag:|last-modified:|vary:)$/ { print }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在可丢弃的工作目录运行，因为两个头部文件会被覆盖。对齐 URL、请求头与发布版本；压缩协商也应纳入比较，给两个请求发送相同的 &lt;code&gt;Accept-Encoding&lt;/code&gt; 值。&lt;/p&gt;
&lt;p&gt;HEAD 应描述对应的 GET，但 RFC 9110 允许省略只有在生成内容时才能确定的字段。单独缺少 &lt;code&gt;Content-Length&lt;/code&gt; 不代表失败；如果 HEAD 携带它，其值必须是对应 GET 内容的长度，不能因为 HEAD 没有正文就填零。比较时应统一内容编码，调查变化的验证器与类型，而不是要求两份头部逐字节相同。&lt;/p&gt;
&lt;h2 id="不要把-head-当作唯一的发布检查"&gt;不要把 HEAD 当作唯一的发布检查&lt;/h2&gt;
&lt;p&gt;服务器可能在文件缺失、不可读或被截断时仍合成看似合理的响应头。在冒烟测试里保留一个小的 &lt;code&gt;GET&lt;/code&gt; 并核验其正文或校验和。&lt;code&gt;HEAD&lt;/code&gt; 降低了传输成本，但并不能证明有效载荷存在。&lt;/p&gt;
&lt;h2 id="显式检查重定向"&gt;显式检查重定向&lt;/h2&gt;
&lt;p&gt;不使用 &lt;code&gt;--location&lt;/code&gt; 时，&lt;code&gt;curl -I&lt;/code&gt; 报告第一个响应，适合测试重定向策略；使用 &lt;code&gt;--location&lt;/code&gt; 时，会打印跟随过程中的多个响应头块，包括中间重定向与最终目标。请记录这条命令回答的是哪个问题：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -I --max-redirs &lt;span class="m"&gt;0&lt;/span&gt; https://old.example.test/manual
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -I --location https://old.example.test/manual
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="把方法相关的失败视为配置缺陷"&gt;把方法相关的失败视为配置缺陷&lt;/h2&gt;
&lt;p&gt;一个对 &lt;code&gt;GET&lt;/code&gt; 返回 &lt;code&gt;200&lt;/code&gt;、对 &lt;code&gt;HEAD&lt;/code&gt; 返回 &lt;code&gt;405&lt;/code&gt; 或 &lt;code&gt;404&lt;/code&gt; 的静态页面，通常暴露出代理或路由的不一致。应当修复方法处理，而不是教监控系统忽略它——除非端点契约刻意不允许 &lt;code&gt;HEAD&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-head"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，HEAD 方法语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，&lt;code&gt;--head&lt;/code&gt;、&lt;code&gt;--include&lt;/code&gt; 与重定向选项，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>压缩会改变所选中的 HTTP 表示</title><link>https://hk.xbcatm.com/zh/notes/compression-changes-the-selected-http-representation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/compression-changes-the-selected-http-representation/</guid><description>把内容编码、验证器与 Vary 视为同一个可观察的响应契约。</description><content:encoded>&lt;p&gt;压缩节省传输字节，但也制造了资源的另一种表示。客户端用 &lt;code&gt;Accept-Encoding&lt;/code&gt; 声明可接受的内容编码；服务器用 &lt;code&gt;Content-Encoding&lt;/code&gt; 描述实际应用的编码。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/compression.detail.zh.svg" alt="协商分支选择 identity 字节 A 或编码后字节 B，因此强标签不同。Vary 控制匹配，弱标签有语义等价的条件。另一路径说明响应头保留编码后 Content-Length，curl 解码正文后才与制品校验和比较。" width="720" height="2320"&gt;&lt;figcaption&gt;压缩选择的是表示字节，协商、缓存匹配与验证器必须一致。编码后字节不同就需要不同强标签。下载正文解码后再与制品比较；编码后 Content-Length 不必等于解码文件大小。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="让协商可见"&gt;让协商可见&lt;/h2&gt;
&lt;p&gt;当响应可能随 &lt;code&gt;Accept-Encoding&lt;/code&gt; 变化时，把该请求字段写进 &lt;code&gt;Vary&lt;/code&gt;。这告诉共享缓存：压缩响应与 identity 响应不是可互换的缓存条目。&lt;/p&gt;
&lt;p&gt;不要只检查一个浏览器请求，而是比较两条路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --compressed -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;检查 &lt;code&gt;Content-Encoding&lt;/code&gt;、&lt;code&gt;Vary&lt;/code&gt;、&lt;code&gt;Content-Length&lt;/code&gt;（如存在）与验证器。当内容编码改变表示字节时，强实体标签必须不同。语义等价的变体可以共享弱标签，但它不能用于强比较。条件请求必须针对同一个协商出的表示检查。&lt;/p&gt;
&lt;h2 id="压缩合适的内容"&gt;压缩合适的内容&lt;/h2&gt;
&lt;p&gt;HTML、CSS、JSON、XML、SVG 等文本格式通常适合压缩；已经压缩过的格式未必适合。Caddy 的 &lt;code&gt;encode&lt;/code&gt; 指令会协商支持的编码，并且通常对没有合适内容类型或体积过小的响应不做压缩。&lt;/p&gt;
&lt;p&gt;不要为了满足某个评分而加压缩。测量一个有代表性的响应，确认边缘返回预期的头，并核验可下载文件在传输解码后仍保留正确的内容类型与校验和。&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;--compressed&lt;/code&gt; 时，curl 自动解码正文，但保存的响应头不会随之改写。若存在 &lt;code&gt;Content-Length&lt;/code&gt;，它描述的是编码后内容，未必等于解码后下载文件的大小。应在内容解码后才把校验和与制品比较；HTTP 传输分帧属于另一层。此外，该选项不保证服务器一定压缩，应查看真实响应，尤其是小文件与已经压缩过的文件。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，内容编码与 &lt;code&gt;Vary&lt;/code&gt;，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/encode"&gt;Caddy &lt;code&gt;encode&lt;/code&gt; 指令&lt;/a&gt;，内容编码选择与协商，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，&lt;code&gt;--compressed&lt;/code&gt;，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>范围请求应证明选中了哪些字节</title><link>https://hk.xbcatm.com/zh/notes/range-requests-should-prove-which-bytes-were-selected/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/range-requests-should-prove-which-bytes-were-selected/</guid><description>验证部分响应、Content-Range，以及无法满足的字节范围的行为。</description><content:encoded>&lt;p&gt;范围请求让客户端请求表示的一部分。媒体播放器、可断点续传的下载工具与诊断客户端会用到它，但服务器如果错误地声明或返回范围，就可能产生静默损坏，而不是干净的失败。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/range-request.detail.zh.svg" alt="稳定的编码后表示共 N 字节，提供参考大小。标尺选中含端点的 0 到 15，恰好 16 字节。分支区分匹配正文与 Content-Range 的 206、忽略 Range 时的完整 200，以及从 N 开始的越界请求被拒绝时的 416 和 bytes */N。强 If-Range 验证器防止拼接不同版本。禁止弱 ETag；只有无 ETag 且日期满足强验证器条件时才能使用 HTTP 日期。表示改变时须返回完整的最新正文。" width="720" height="3050"&gt;&lt;figcaption&gt;保持表示与内容编码一致，同时核对区间、总大小与实际正文字节。忽略 Range 时，完整的 200 可以合法；续传需要合适的强验证器。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id="建立完整大小"&gt;建立完整大小&lt;/h3&gt;
&lt;p&gt;选择至少 16 字节、测试期间不变的下载文件。在同一个一次性 shell 会话和临时工作目录中依次运行以下片段，保留 &lt;code&gt;url&lt;/code&gt; 与 &lt;code&gt;size&lt;/code&gt;。显式的 &lt;code&gt;set -eu&lt;/code&gt; 会让顺序执行的传输与字节断言失败时停止；HTTP 状态与范围响应头仍需按下文人工核对：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.example.test/downloads/archive.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsS -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; -D full.headers -o full.body &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;wc -c &amp;lt; full.body&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$size&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -ge &lt;span class="m"&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1) ~ /^(content-length:|content-encoding:|etag:|last-modified:|accept-ranges:)$/ { print }&amp;#39;&lt;/span&gt; full.headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;确认完整响应为 &lt;code&gt;200&lt;/code&gt;，且没有意外的内容编码。这些命令请求 identity 表示，不做自动解压；字节范围针对所选的编码后表示，因此改变编码会使比较失效。&lt;code&gt;Content-Length&lt;/code&gt; 可能缺失，所以用完整正文确定大小。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; 是显式信号，但缺少这个头并不能证明范围请求不受支持。去实际请求一次。&lt;/p&gt;
&lt;h3 id="请求一个已知区间"&gt;请求一个已知区间&lt;/h3&gt;
&lt;p&gt;请求开头 16 字节，同时保留响应头与正文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D range.headers -o range.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Range: bytes=0-15&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wc -c range.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;head -c &lt;span class="m"&gt;16&lt;/span&gt; full.body &amp;gt; expected.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp expected.body range.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1) ~ /^(content-range:|content-length:|etag:)$/ || /^HTTP\// { print }&amp;#39;&lt;/span&gt; range.headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;成功的字节范围响应应使用 &lt;code&gt;206 Partial Content&lt;/code&gt;。&lt;code&gt;Content-Range&lt;/code&gt; 应指出 &lt;code&gt;0-15&lt;/code&gt; 与完整表示长度，而正文应正好包含 16 个字节。&lt;/p&gt;
&lt;p&gt;这里的字节比较专门检查部分正文。合法但大于 16 字节的完整 &lt;code&gt;200&lt;/code&gt; 会使比较失败并停止当前 shell 会话；先查看已保存的响应头，再判断是否发生损坏。反过来，完整文件恰好为 16 字节时，即便返回 &lt;code&gt;200&lt;/code&gt; 也可能通过 &lt;code&gt;cmp&lt;/code&gt;，所以仍需核对状态码与 &lt;code&gt;Content-Range&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="测试一个不可能的区间"&gt;测试一个不可能的区间&lt;/h3&gt;
&lt;p&gt;从完整字节数开始请求：大小为 N 的文件，最后一个有效偏移量是 N−1。与任意写一个大常量不同，这确实超出了已知表示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; -H &lt;span class="s2"&gt;&amp;#34;Range: bytes=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;服务器用 &lt;code&gt;416 Range Not Satisfiable&lt;/code&gt; 拒绝无法满足的字节范围时，应附带 &lt;code&gt;Content-Range&lt;/code&gt;（例如 &lt;code&gt;bytes */12345&lt;/code&gt;）报告当前大小。当服务器忽略 Range 时返回 &lt;code&gt;200&lt;/code&gt; 也可能合法，但它必须返回完整表示，而不是一个标签错误的片段。&lt;/p&gt;
&lt;h3 id="让验证器留在故事里"&gt;让验证器留在故事里&lt;/h3&gt;
&lt;p&gt;续传工具可以用 &lt;code&gt;If-Range&lt;/code&gt; 配合强 ETag。不能使用弱 ETag；只有在没有 ETag、且日期符合强验证器条件时，才能使用 HTTP 日期。如果表示已改变，安全响应是完整的最新表示，而不是从两个版本拼接出的字节。在宣称某个下载&amp;quot;可续传&amp;quot;之前，先测试这一行为。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-range-requests"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，范围单位、部分响应与 &lt;code&gt;Content-Range&lt;/code&gt;，已核验 2026-07-06。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，&lt;code&gt;--range&lt;/code&gt; 与响应头选项，已核验 2026-07-06。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>缓存头是两套时钟的契约</title><link>https://hk.xbcatm.com/zh/notes/cache-headers-are-a-contract-with-two-clocks/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/cache-headers-are-a-contract-with-two-clocks/</guid><description>根据表示内容可能变化的频率，选择新鲜度与重校验规则。</description><content:encoded>&lt;p&gt;缓存不是一个单一开关。一个响应既有一个由缓存作出的新鲜度决策，也有与源站之间的校验对话。把这两种想法混在一起，会得到陈旧内容或多余请求。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/cache-clocks.detail.zh.svg" alt="存储与复用矩阵之后，分别说明稳定 URL 与带指纹 URL 的策略。条件 GET 在 If-None-Match 中发送真实 ETag：弱匹配可返回 304，验证器变化时通常返回新的 200。语义等价不保证字节一致。Age、Vary、仅在新鲜期内生效的 immutable 与既有缓存寿命，共同约束复用。" width="720" height="2800"&gt;&lt;figcaption&gt;存储、新鲜度与校验是不同问题。根据 URL 的变更方式选择策略，完整保留验证器，并检查共享缓存的变体。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="先选变更模型"&gt;先选变更模型&lt;/h2&gt;
&lt;p&gt;对于发布之后可能变化的 HTML 文档，&lt;code&gt;Cache-Control: no-cache&lt;/code&gt; 通常比 &lt;code&gt;no-store&lt;/code&gt; 更接近正确起点。&lt;code&gt;no-cache&lt;/code&gt; 允许存储，但要求缓存复用前先校验；&lt;code&gt;no-store&lt;/code&gt; 则要求缓存完全不存储。当你想要条件请求与廉价的 &lt;code&gt;304 Not Modified&lt;/code&gt; 时，这个区别很关键。&lt;/p&gt;
&lt;p&gt;对类似 &lt;code&gt;app.4f2c.js&lt;/code&gt; 这种带指纹的资源，URL 在字节变化时也随之改变，此时一个较长的新鲜度生命周期是合理的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但对一个内容可能被原地替换的稳定 URL，该策略不安全。如果 URL 稳定，应优先使用较短的生命周期，并搭配实体标签（&lt;code&gt;ETag&lt;/code&gt;）或有意义的 &lt;code&gt;Last-Modified&lt;/code&gt; 值。&lt;/p&gt;
&lt;p&gt;除了新鲜期，还要决定谁可以存储响应。如果共享缓存不能保存面向特定用户的内容，应使用不带参数的 &lt;code&gt;private&lt;/code&gt; 指令；浏览器的私有缓存仍可存储。单独使用 &lt;code&gt;no-cache&lt;/code&gt; 不提供这项限制。上面的 &lt;code&gt;public&lt;/code&gt; 示例适用于可以在用户之间共享的资源，不是个性化响应的默认策略。&lt;/p&gt;
&lt;h2 id="同时检查两条路径"&gt;同时检查两条路径&lt;/h2&gt;
&lt;p&gt;发一次普通请求，再发一次条件请求。各服务器的具体标志可能不同，但你想要的证据是稳定的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;If-None-Match: &amp;#34;copy-a-real-etag-from-the-first-response&amp;#34;&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不要在 runbook 里粘贴编造的验证器。从第一次响应里复制该值，或使用测试夹具。对这里的条件 GET，&lt;code&gt;If-None-Match&lt;/code&gt; 匹配时可以通过弱比较返回 &lt;code&gt;304&lt;/code&gt;：它允许语义等价，并不一定保证字节完全相同。验证器变化时通常返回新的 &lt;code&gt;200&lt;/code&gt; 响应。保留 ETag 的双引号与可能存在的 &lt;code&gt;W/&lt;/code&gt; 前缀。&lt;/p&gt;
&lt;h2 id="让中间层可见"&gt;让中间层可见&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Age&lt;/code&gt;、&lt;code&gt;Vary&lt;/code&gt; 与 cache-control 指令说明了为什么两个客户端可能得到不同结果。如果响应随 &lt;code&gt;Accept-Encoding&lt;/code&gt;、语言或其他请求字段变化，该维度应进入 &lt;code&gt;Vary&lt;/code&gt;；否则共享缓存可能复用错误的表示。&lt;/p&gt;
&lt;p&gt;有意义的发布检查不是&amp;quot;扫描器找到了缓存头&amp;quot;，而是&amp;quot;该头与替换策略匹配，且条件请求的行为符合文档说明&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;immutable&lt;/code&gt; 只在声明的新鲜期内生效，并非永久禁止重校验。版本化 URL 的内容应保持不变，只要旧 HTML 仍可能引用旧资源，就应让它们继续可访问。今天修改响应头不会追溯缩短既有缓存响应的寿命；缓存策略出错时，可能需要新 URL 或缓存系统明确支持的清除操作。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;，第 4–5 节，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，第 8 与 13 节，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control"&gt;MDN: &lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt;，指令语义与默认值，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8246.html"&gt;RFC 8246 — 不可变响应&lt;/a&gt;，immutable 仅在新鲜期内生效，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>重定向与错误码应保留故事的完整性</title><link>https://hk.xbcatm.com/zh/notes/redirects-and-error-codes-should-preserve-the-story/</link><pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/redirects-and-error-codes-should-preserve-the-story/</guid><description>用永久重定向与缺失类状态码描述发生了什么，而不是隐藏一条坏路由。</description><content:encoded>&lt;p&gt;HTTP 状态码是接口的一部分。浏览器可能渲染出一个友好的页面，但监控、爬虫、缓存与 API 客户端也需要状态码与响应头来讲述同一个故事。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/redirect-map.detail.zh.svg" alt="旧端点收到 POST 后返回带 Location 的 308；解析 Location 后，客户端可向新端点发送 POST。307 与 308 保留方法，301 与 302 允许把 POST 改为 GET。另设缺失分支，404 表示暂时或未知的缺失，410 表示很可能永久不可访问；两者都不说明原因，友好错误页也须保留正确的非 200 状态。Location 可为相对引用，须以原 URI 为基准解析。308 缓存受方法与缓存控制限制，缓存可能比配置存续更久。curl 不加 --location 不会跟随。" width="720" height="2960"&gt;&lt;figcaption&gt;有意选择迁移或缺失语义。保留所需方法，解析 Location，考虑缓存，并让错误页状态与事实一致。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id="永久移动并不都等价"&gt;永久移动并不都等价&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; 在保留请求方法的同时，传达资源有了新的永久 URI。这对发送 &lt;code&gt;POST&lt;/code&gt;、&lt;code&gt;PUT&lt;/code&gt; 或其他非 &lt;code&gt;GET&lt;/code&gt; 方法的客户端很重要。旧页面到新页面的重定向用 &lt;code&gt;GET&lt;/code&gt; 很容易测试；端点的迁移则必须用真实调用方实际使用的方法来测试。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D - -o /dev/null https://old.example.test/api/item
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不带 &lt;code&gt;--location&lt;/code&gt; 时，curl 不会跟随重定向。相对原 URI 解析 &lt;code&gt;Location&lt;/code&gt;：绝对 URI、根相对路径或其他相对引用都可能有效。检查解析后的主机、路径与协议。&lt;code&gt;301&lt;/code&gt;/&lt;code&gt;302&lt;/code&gt; 允许将 POST 改为 GET；&lt;code&gt;307&lt;/code&gt;/&lt;code&gt;308&lt;/code&gt; 保留方法。改变状态的方法只能在隔离端点测试。&lt;code&gt;308&lt;/code&gt; 可以启发式缓存，但仍受方法与缓存控制限制，因此缓存的迁移结果可能在服务器配置变化后继续存在。&lt;/p&gt;
&lt;p&gt;如果目的是在完成操作后展示结果，&lt;code&gt;303 See Other&lt;/code&gt; 会指向客户端可用 GET 或 HEAD 获取的另一资源。它不是要求客户端在新地址重放原来的写入操作；应有意选择这种行为，而不是把所有重定向都当作迁移。&lt;/p&gt;
&lt;h3 id="区分缺失与被移除"&gt;区分&amp;quot;缺失&amp;quot;与&amp;quot;被移除&amp;quot;&lt;/h3&gt;
&lt;p&gt;当服务器找不到当前表示且缺失可能是暂时或未知时，使用 &lt;code&gt;404 Not Found&lt;/code&gt;。&lt;code&gt;410 Gone&lt;/code&gt; 更强：它表示资源已不可访问，且这种情况很可能是永久的；它并不能说明资源消失的原因。两者都应有帮助人类的正文，但都不应因为自定义错误页成功渲染就返回 &lt;code&gt;200&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;404&lt;/code&gt; 也可以用于隐藏某个禁止访问的资源是否存在。&lt;code&gt;404&lt;/code&gt; 与 &lt;code&gt;410&lt;/code&gt; 都可以启发式缓存，但仍受方法与缓存控制约束。应为错误响应明确选择缓存策略：路由修复后，已缓存的缺失结果仍可能继续显示。&lt;/p&gt;
&lt;p&gt;这条检查应放在边缘：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -D missing.headers -o missing.html -w &lt;span class="s1"&gt;&amp;#39;%{http_code} %{url_effective}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/a-route-that-does-not-exist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在错误路径上也应保留安全响应头。安全头、有用的内容类型与正确的状态码，比一个装饰性的错误动画更有价值。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，第 15 与 13 节，已核验 2026-07-14。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7538.html"&gt;RFC 7538 — 308 Permanent Redirect&lt;/a&gt;，第 3 节，已核验 2026-07-14。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，状态与重定向检查标志，已核验 2026-07-14。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>robots.txt 与 sitemap 回答不同的问题</title><link>https://hk.xbcatm.com/zh/notes/robots.txt-and-sitemaps-answer-different-questions/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/robots.txt-and-sitemaps-answer-different-questions/</guid><description>把爬虫偏好、URL 发现与访问控制分开。</description><content:encoded>&lt;p&gt;两个小文件常被当成安全边界使用，但它们不是。&lt;code&gt;robots.txt&lt;/code&gt; 描述爬虫偏好，sitemap 则给搜索系统一份属于该站点的 URL 清单。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/discovery-map.detail.zh.svg" alt="源站根路径上的 UTF-8 robots.txt 是配合爬虫的偏好文件，不是认证或可靠的搜索索引删除机制。检查 sitemap 根元素：跟随 sitemapindex 中的语言 sitemap，或读取直接的 urlset。使用单一主机的规范绝对 URL。XML 语法正确不足以验证：还需检查命名空间、数量、主机与页面存在性。每份 sitemap 最多 50,000 个 URL、未压缩 52,428,800 字节，gzip 不提高限制。私有路由需要服务器授权或网络边界。RFC 9309 对 4xx 的 robots 不可用允许抓取；网络或服务器错误最初要求全部禁止。可达的 200 HTML 兜底页不能证明 robots 正确。" width="720" height="3560"&gt;&lt;figcaption&gt;发现工具不会授予或拒绝访问。继续检查索引列出的每份 sitemap，验证不止 XML 语法，并按真实错误类别处理 robots 响应。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id="robotstxt-能做什么不能做什么"&gt;robots.txt 能做什么、不能做什么&lt;/h3&gt;
&lt;p&gt;规则发布于源站根路径 &lt;code&gt;/robots.txt&lt;/code&gt;，由配合的爬虫读取。它不会保护私有目录、不会从网络上移除 URL，也不能替代认证。不应公开的路由需要服务器授权或网络边界。&lt;/p&gt;
&lt;p&gt;让文件保持平实，并测试它的实际位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User-agent: *
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disallow:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sitemap: https://www.example.test/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件应为 UTF-8。如果部署会重定向它，请核验你想要的爬虫行为，而不是假设每个客户端都遵循同一条链。&lt;/p&gt;
&lt;h3 id="sitemap-增加了什么"&gt;sitemap 增加了什么&lt;/h3&gt;
&lt;p&gt;sitemap 是一份清单，而不是权限列表。使用单一主机的绝对 URL、正确做 XML 转义，并且只列出你准备发布的规范页面。不要因为某个 URL 存在于构建目录就把它列出。&lt;/p&gt;
&lt;p&gt;每份 sitemap 最多包含 50,000 个 URL，未压缩大小不超过 50 MB（52,428,800 字节）；gzip 不会提高该限制。小站可以不用 index，但多语言生成器也可以合理地生成指向各语言 sitemap 的索引。应检查根元素并继续检查列出的各份 sitemap，而不是假设 &lt;code&gt;/sitemap.xml&lt;/code&gt; 一定是 URL 集合。把它的 XML 校验通过，并像普通页面一样通过同一 HTTPS 边缘请求它。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsS https://www.example.test/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -fsS -o sitemap.xml https://www.example.test/sitemap.xml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; xmllint --noout sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把下载与解析分开，避免下载失败被解析成功掩盖。XML 格式正确只代表语法通过；还要验证命名空间、URL 数量、规范主机名，以及列出的页面是否存在。若未安装 &lt;code&gt;xmllint&lt;/code&gt;，请在发布它的同一个 CI 环境里解析该 XML。重要的是检查要在文件被替换到边缘之前运行。&lt;/p&gt;
&lt;p&gt;robots 的 &lt;code&gt;Disallow&lt;/code&gt; 不是可靠的搜索索引删除指令。还要测试错误响应：RFC 9309 在 robots 返回 4xx、表示文件不可用时允许抓取，而网络或服务器错误在最初要求全部禁止。意外返回状态为 200 的 HTML 兜底页，因此可能改变爬虫行为，即使 URL 看起来仍然可访问。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9309.html"&gt;RFC 9309 — Robots Exclusion Protocol&lt;/a&gt;，robots 规则、文件位置与 user-agent 匹配，已核验 2026-07-21。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sitemaps.org/protocol.html"&gt;Sitemaps Protocol&lt;/a&gt;，sitemap XML 结构与发现，已核验 2026-07-21。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HTTP 方法是对副作用做出的承诺</title><link>https://hk.xbcatm.com/zh/notes/http-method-semantics/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/http-method-semantics/</guid><description>HTTP 方法说明了请求可能改变什么；静态源站可以在不假装有状态契约的情况下应答其中大多数方法。</description><content:encoded>&lt;p&gt;请求方法说明客户端要求服务器做什么。只读文件源站的契约很窄，但仅凭“静态站点”并不能证明代理或导入的处理器没有暴露写操作。应检查真正服务该资源的路由。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/http-methods.detail.zh.svg" alt="语义矩阵区分安全的 GET、HEAD 与不安全但幂等的 PUT、DELETE。限定的 Caddy v2.10.2 文件路径接受 GET、HEAD，以 405 和 Allow 拒绝其他方法，更早应答的处理器可能不同。安全方法仍可产生附带日志；幂等不保证状态码相同，也不意味着每次失败后都适合重试。HEAD 的 Content-Length 若提供须对应同编码 GET，省略可能合法。方法支持与 CORS 权限分开判断。" width="720" height="2900"&gt;&lt;figcaption&gt;先区分是否请求改变状态与重复请求的预期效果，再检查实际路由。文件处理器边界有明确适用范围，方法支持与 CORS 权限分开判断。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="安全与幂等回答不同的问题"&gt;安全与幂等回答不同的问题&lt;/h2&gt;
&lt;p&gt;GET 与 HEAD 是安全方法：客户端没有请求改变状态的操作。但附带的日志、指标或计费仍可能改变服务器状态。PUT 与 DELETE 按其定义具有幂等语义，却并不安全：重复相同请求的预期效果与执行一次相同。这不要求响应状态码相同，也不意味着每次网络失败后都适合重试。&lt;/p&gt;
&lt;p&gt;对只读静态源站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GET&lt;/strong&gt; 获取表示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HEAD&lt;/strong&gt; 描述对应的 GET，但不携带响应内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPTIONS&lt;/strong&gt; 询问目标的通信选项；文件处理器可以拒绝它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POST / PUT / DELETE / PATCH&lt;/strong&gt; 不应直接落入成功的文件响应，除非另有明确设计的处理器实现其语义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="caddy-文件处理器的方法边界"&gt;Caddy 文件处理器的方法边界&lt;/h2&gt;
&lt;p&gt;Caddy v2.10.2 的文件服务路径接受 GET 与 HEAD，并以 &lt;code&gt;405&lt;/code&gt; 和 &lt;code&gt;Allow: GET, HEAD&lt;/code&gt; 拒绝其他方法。重定向、缺失路径、错误处理器与其他配置路由可能更早应答或采取不同处理，因此这不是每个静态网站 URL 的统一响应。&lt;/p&gt;
&lt;p&gt;原笔记记录过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -s -o /dev/null -w &amp;#39;OPTIONS -&amp;gt; HTTP %{http_code}\n&amp;#39; -X OPTIONS https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OPTIONS -&amp;gt; HTTP 405
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里没有保留原始响应日志，本轮审阅也未重现该在线结果。命令只报告状态码，不能证明必需的 &lt;code&gt;Allow&lt;/code&gt; 字段存在，需另行检查。&lt;code&gt;405&lt;/code&gt; 表示已知方法不被该目标支持；如果服务器根本不识别或未实现该方法，通常应区分为 &lt;code&gt;501 Not Implemented&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="head-一致性允许规范规定的省略"&gt;HEAD 一致性允许规范规定的省略&lt;/h2&gt;
&lt;p&gt;HEAD 应描述与 GET 相同的所选表示，同时省略内容。只有生成内容时才能确定的字段可以省略。如果存在 &lt;code&gt;Content-Length&lt;/code&gt;，它必须描述采用相同内容编码的对应 GET 内容长度。&lt;/p&gt;
&lt;p&gt;原笔记报告过以下比较：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HEAD content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET bytes read : 15890
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些历史数字不是固定的期望大小，也不是当前验证结果。仅缺少 HEAD 长度是合法的，提供了错误长度才需要调查。成对探针可参考 &lt;code&gt;head-request-parity&lt;/code&gt; 笔记；可下载的 &lt;code&gt;check-http-headers.sh&lt;/code&gt; 只检视 GET 响应，并不会自动执行这项比较。&lt;/p&gt;
&lt;h2 id="拒绝意外写入同时说明真实契约"&gt;拒绝意外写入，同时说明真实契约&lt;/h2&gt;
&lt;p&gt;对已识别、但目标不允许的方法，返回 &lt;code&gt;405&lt;/code&gt;，并以 &lt;code&gt;Allow&lt;/code&gt; 列出实际支持的方法。应用路由的这个列表不一定只包含安全方法。不要为被忽略的写请求返回看似合理的 &lt;code&gt;200&lt;/code&gt; 页面。&lt;/p&gt;
&lt;p&gt;如果浏览器客户端需要 CORS 预检，应明确配置相应 OPTIONS 行为；方法支持与 CORS 权限是两项不同决策。写入拒绝测试应在获准使用的可丢弃夹具上进行，不要因为公共端点看起来静态，就假设向它发送写请求没有影响。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，安全/幂等方法与 HEAD、405、501 语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"&gt;MDN: HTTP 请求方法&lt;/a&gt;，方法概览，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，方法与响应检视选项，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/caddyserver/caddy/v2.10.2/modules/caddyhttp/fileserver/staticfiles.go"&gt;Caddy v2.10.2 文件服务器源码&lt;/a&gt;，文件选取后的方法拒绝，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#cors-preflight-fetch"&gt;WHATWG Fetch 标准&lt;/a&gt;，CORS 预检处理，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>验证器决定缓存副本是否仍是答案</title><link>https://hk.xbcatm.com/zh/notes/http-conditional-requests/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/http-conditional-requests/</guid><description>ETag 与 Last-Modified 让缓存判断副本是否仍然有效，并在重新验证确认未变化时省下正文传输。</description><content:encoded>&lt;p&gt;缓存可以询问已存储的副本是否仍可使用，而不必每次重新下载正文。它发送验证器；匹配的条件 GET 可以收到 &lt;code&gt;304 Not Modified&lt;/code&gt;，表示变化时通常需要新的 &lt;code&gt;200&lt;/code&gt; 响应。验证是在命中时省下正文，并非每次请求都省下。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/etag-validation.detail.zh.svg" alt='缓存把弱 ETag W/"release-a" 原样放入 If-None-Match。它与源站标签 "release-a" 弱比较匹配，源站返回无正文的 304；缓存更新元数据并复用正文 A。另一种结果是源站标签 "release-b" 导致 GET 返回 200 与新正文 B。HEAD 使用相同状态判断，但不返回正文。弱匹配既不证明字节相同，也不证明新鲜度。' width="720" height="1750"&gt;&lt;figcaption&gt;标签仅为示意；假定请求原本成功。GET/HEAD 使用弱比较；HEAD 无正文。匹配既不证明字节相同，也不证明新鲜度。其他方法通常返回 412；If-Range 不能使用弱 ETag。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="验证器类型与强度是两回事"&gt;验证器类型与强度是两回事&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; 是带双引号的不透明值，可以是强标签（&lt;code&gt;&amp;quot;release-a&amp;quot;&lt;/code&gt;），也可以明确标记为弱标签（&lt;code&gt;W/&amp;quot;release-a&amp;quot;&lt;/code&gt;）。强比较要求两者都为强标签且值相等；弱比较忽略 &lt;code&gt;W/&lt;/code&gt; 标记，只比较不透明值。即使字节不同，弱标签也可以描述语义等价。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Last-Modified&lt;/code&gt; 是 HTTP 日期中精确到秒的时间戳。通常它是弱验证器，除非满足 RFC 9110 第 8.8.2.2 节的条件、可认定为强验证器。不能把所有 ETag 都归为强验证器，也不能无条件把所有时间戳归为弱验证器。&lt;/p&gt;
&lt;p&gt;先前的站点观察记录了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etag: &amp;#34;dkx84jwth7nkc9e&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 13:55:23 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些是历史值；这里未保留原始响应日志，本轮审阅也未重新检查在线结果。Caddy 文件服务器通常会设置这两个字段，但仍取决于文件元数据与配置。&lt;/p&gt;
&lt;h2 id="条件-get-使用弱比较"&gt;条件 GET 使用弱比较&lt;/h2&gt;
&lt;p&gt;把收到的 ETag 原样放进 &lt;code&gt;If-None-Match&lt;/code&gt;，保留双引号与可能存在的 &lt;code&gt;W/&lt;/code&gt;。原笔记针对上述值报告过 &lt;code&gt;HTTP 304&lt;/code&gt;，但展示的请求值没有双引号；该请求文本不符合实体标签语法，不应当作可工作的探针照抄。测试在线端点前应取得当前验证器。&lt;/p&gt;
&lt;p&gt;下面无需套接字的练习，展示对单个、已解析标签的比较；它不是完整的 HTTP 头部解析器：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;weak_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;W/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strong_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;W/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;W/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;W/&amp;#34;release-a&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#34;release-a&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;weak_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;weak_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;strong_equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;weak_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;weak_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;release-b&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;weak match: True; strong match: False; changed tag: no match&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对原本会成功的 GET 或 HEAD，&lt;code&gt;If-None-Match&lt;/code&gt; 匹配时返回无正文的 &lt;code&gt;304&lt;/code&gt;。其他方法上的同一前置条件为假时，通常返回 &lt;code&gt;412 Precondition Failed&lt;/code&gt;。若同时存在 &lt;code&gt;If-None-Match&lt;/code&gt; 与 &lt;code&gt;If-Modified-Since&lt;/code&gt;，ETag 条件优先。通过 &lt;code&gt;If-Range&lt;/code&gt; 做字节范围验证时，不能使用弱 ETag。&lt;/p&gt;
&lt;h2 id="重新发布时不要保留误导性的验证器"&gt;重新发布时不要保留误导性的验证器&lt;/h2&gt;
&lt;p&gt;ETag 不一定是内容哈希。Caddy v2.10.2 的默认文件标签由修改时间与大小生成；替换字节时若两者都保持不变，标签也可能不变。检查实际部署版本与覆盖配置，再让发布流程更新验证器依赖的输入，或采用基于内容的版本标识。&lt;/p&gt;
&lt;p&gt;在发布清单中保留字节校验和，并测试未变化与已变化两种响应。校验和不会自动成为服务器的 ETag。&lt;code&gt;304&lt;/code&gt; 还需要适用的缓存元数据，让缓存更新已存储的响应；它不只是一个空的成功状态。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，第 8.8 与 13 节的验证器强度及前置条件，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;，第 4.3 节的验证与已存储响应更新，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Conditional_requests"&gt;MDN: HTTP 条件请求&lt;/a&gt;，条件请求概览，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/caddyserver/caddy/v2.10.2/modules/caddyhttp/fileserver/staticfiles.go"&gt;Caddy v2.10.2 文件服务器源码&lt;/a&gt;，&lt;code&gt;calculateEtag&lt;/code&gt; 的输入与默认响应字段，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>像客户端一样读取活动边缘</title><link>https://hk.xbcatm.com/zh/tutorials/http-edge-walkthrough/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/tutorials/http-edge-walkthrough/</guid><description>用 curl 检查方法、表示元数据、范围与缓存，区分协议要求和端点的具体选择。</description><content:encoded>&lt;p&gt;HTTP 基础篇的每条笔记各取一纸契约并展示单独的证据。本教程则按顺序走完一次客户端请求触达的完整路径，只用只读命令。目标是区分协议要求与某个服务器的选择，而不是要求所有端点都返回相同状态和头部。&lt;/p&gt;
&lt;p&gt;我们全程用 &lt;code&gt;www.xbcatm.com&lt;/code&gt;。把它换成你要检查的主机。请求使用 HTTP 安全方法，但仍可能产生访问日志或消耗速率额度。只检查你有权查询的端点。下面输出块是示意，不是当前生产测量。请在一次性工作目录运行 shell 片段，保留错误，并记录时间和客户端版本。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-http.detail.zh.svg" alt="编号 HTTP 路径区分方法策略、元数据、HEAD 对应关系、字节范围、条件复用与编码。当前成功的 200 提供实际校验器和完整正文；匹配的 304 保留该正文。状态和长度示例不是通用承诺。" width="720" height="3750"&gt;&lt;figcaption&gt;保持所选表示一致，并在各自契约内解读观察结果。本图未查询活动边缘。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-先确定方法可以问什么"&gt;1. 先确定方法可以问什么&lt;/h2&gt;
&lt;p&gt;在读响应体之前，先确定服务器允许你问什么。静态处理器通常支持 GET 和 HEAD，代理也可能实现 OPTIONS。先询问边缘对目标资源的处理：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -w &amp;#39;OPTIONS -&amp;gt; HTTP %{http_code}\n&amp;#39; -X OPTIONS https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 405
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;allow: GET, HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OPTIONS -&amp;gt; HTTP 405
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若服务器识别 OPTIONS、但该资源不支持它，&lt;code&gt;405 Method Not Allowed&lt;/code&gt; 是合法答案，且 405 必须携带 &lt;code&gt;Allow&lt;/code&gt;。成功的 200 或 204 OPTIONS 响应也可能正确。&lt;code&gt;501&lt;/code&gt; 表示服务器未实现该方法。一次 OPTIONS 不能证明其他方法的行为。不要为了测试拒绝而向不熟悉的线上服务发送 POST：它不是安全方法，单有 200 也不能描述其副作用。&lt;/p&gt;
&lt;h2 id="2-读取表示行"&gt;2. 读取表示行&lt;/h2&gt;
&lt;p&gt;第一行告诉你协商出来的交换；头部随后告诉你将要到达的是什么字节。像客户端看到的那样捕获它们、丢弃响应体：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept-ranges: bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-type: text/html; charset=utf-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etag: &amp;#34;dkx84jwth7nkc9e&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 13:55:23 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;strict-transport-security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-content-type-options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三个头部值得停下来。&lt;code&gt;content-type: text/html; charset=utf-8&lt;/code&gt; 命中了媒体类型，&lt;code&gt;content-type-and-nosniff&lt;/code&gt; 说明为什么它必须描述你真正发送的字节。&lt;code&gt;etag: &amp;quot;dkx84jwth7nkc9e&amp;quot;&lt;/code&gt; 交给你强验证器。&lt;code&gt;x-content-type-options: nosniff&lt;/code&gt; 限制浏览器的 MIME 处理，尤其是脚本与样式表抓取；它不会验证正文确实是 HTML。每一个都是证据而不是装饰，合在一起是说：该响应声明了应与正文及预期策略比较的元数据。语法上为强 ETag 的值仍是服务器的声明，不是独立的内容哈希证明。&lt;/p&gt;
&lt;h2 id="3-对同一表示比较-head-与-get"&gt;3. 对同一表示比较 HEAD 与 GET&lt;/h2&gt;
&lt;p&gt;HEAD 使用 GET 的语义，但不带响应正文。服务器应返回相同头部，不过可以省略只有生成内容时才能确定的字段。&lt;code&gt;Content-Length&lt;/code&gt; 可省略；若在 HEAD 中发送，它必须等于相应 GET 会发送的内容大小。应在内容稳定时使用相同编码及其他选择头比较：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sSI --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | grep -i content-length
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | wc -c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;15890
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两者一致：都是 15890。这就是 &lt;code&gt;head-request-parity&lt;/code&gt; 契约。仅缺少 HEAD 长度不是协议违规。长度不符需要调查，也要排除两次请求之间发生发布、或选中不同表示的可能。没有 &lt;code&gt;pipefail&lt;/code&gt; 的 shell 中，传给 &lt;code&gt;wc&lt;/code&gt; 的管道可能隐藏 curl 失败；自动关卡应先确认 curl 成功保存正文，再统计文件字节。&lt;/p&gt;
&lt;h2 id="4-用范围请求精确的字节"&gt;4. 用范围请求精确的字节&lt;/h2&gt;
&lt;p&gt;因为源站知道自己的表示，它可以只服务其中一部分。范围请求要求一个字节区间，只得到那个区间：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -H &amp;#39;Accept-Encoding: identity&amp;#39; -H &amp;#39;Range: bytes=0-99&amp;#39; -w &amp;#39;HTTP %{http_code} bytes=%{size_download}\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 206
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-range: bytes 0-99/15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 100
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP 206 bytes=100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对于至少 100 字节的表示，成功的单范围响应应有 &lt;code&gt;206&lt;/code&gt;、匹配的 &lt;code&gt;Content-Range: bytes 0-99/…&lt;/code&gt;，以及 100 个内容字节。服务器可合法忽略 Range 并返回完整 200，这不说明它缺少校验器。无法满足的范围可能返回 416。普通 Range 请求不要求 ETag；使用 &lt;code&gt;If-Range&lt;/code&gt; 恢复特定表示，或安全拼接部分响应时，才要遵守相关强校验器规则。范围针对所选编码后的字节，因此这里显式选择 identity。&lt;/p&gt;
&lt;h2 id="5-用验证器让缓存重新验证"&gt;5. 用验证器让缓存重新验证&lt;/h2&gt;
&lt;p&gt;现在把方法、表示与字节选择通过缓存结合。新鲜的已存响应可能无需检查校验器就可复用。测试重新验证时，应抓取当前响应，并用相同表示选择重放其实际 ETag，不要复制文章里的旧标签：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/body&amp;#34;&lt;/span&gt; -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1)==&amp;#34;etag:&amp;#34; {sub(/\r$/, &amp;#34;&amp;#34;); sub(/^[^:]*:[ \t]*/, &amp;#34;&amp;#34;); print; exit}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;No ETag: inspect Last-Modified instead&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -w &lt;span class="s1"&gt;&amp;#39;HTTP %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把此片段作为独立 shell 脚本运行。只有初次传输完整成功且状态为 200，才有可用的已存响应；错误表示也可能有 ETag。若所选表示仍匹配且正常处理成功，预期得到 304，并用更新后的元数据复用这份完整正文。表示变化时，200 完全可能是正确结果。检查结果期间保留临时目录，完成后仅删除此处生成的目录。&lt;code&gt;If-None-Match&lt;/code&gt; 使用弱比较，所以 304 不一定证明字节完全一致。新鲜度寿命与当前年龄决定能否直接复用；校验器回答的是另一问题，本身不设定寿命。&lt;/p&gt;
&lt;h2 id="6-协商更小的表示"&gt;6. 协商更小的表示&lt;/h2&gt;
&lt;p&gt;同样的内容在客户端表明能解压时可以传递更少字节。请求 gzip 并观察正文缩水而语义不变：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; -o /dev/null -w &amp;#39;no-enc: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -H &amp;#39;Accept-Encoding: gzip&amp;#39; -w &amp;#39;gzip: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;no-enc: 15890 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gzip: 4368 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;示例大小展示一种可能的缩减；服务器也可以选择 identity。这是 &lt;code&gt;http-compression-negotiation&lt;/code&gt; 笔记的核心：改变选中的表示会改变线上字节，元数据（&lt;code&gt;content-length&lt;/code&gt;、&lt;code&gt;encoding&lt;/code&gt;）也随之改变。请求压缩的客户端与存储编码形式的缓存必须就选了哪种表示达成一致，否则缓存可能交付不匹配结果。除字节数外，还应检查 &lt;code&gt;Content-Encoding&lt;/code&gt; 与 &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;。手工设置 &lt;code&gt;Accept-Encoding&lt;/code&gt; 不会让 curl 自动解压；&lt;code&gt;--compressed&lt;/code&gt; 才会请求受支持编码并解码响应。应比较解码后的正文来确认内容相同，也不要假定不同编码共用同一个强 ETag。&lt;/p&gt;
&lt;h2 id="7-跟随-url-变化时的故事"&gt;7. 跟随 URL 变化时的故事&lt;/h2&gt;
&lt;p&gt;身份变化时，响应应说明故事在哪里继续，而不去猜正文：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -w &amp;#39;%{http_code} -&amp;gt; %{redirect_url}\n&amp;#39; -I http://www.xbcatm.com/ | head -1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;308 -&amp;gt; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; 保持方法与故事，正是 &lt;code&gt;redirects-and-status-codes&lt;/code&gt; 关心的。无论是永久移动的 301/308，还是由站点处理的 404，状态行都是故事的第一句，应在读正文前先读。&lt;/p&gt;
&lt;h2 id="8-最后让爬虫读地图"&gt;8. 最后让爬虫读地图&lt;/h2&gt;
&lt;p&gt;边缘也会被根本不请求 HTML 的机器读取。发现机制回答两个不同的问题，&lt;code&gt;robots-and-sitemaps&lt;/code&gt; 有说明。确认发现机制指向真实 sitemap：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 https://www.xbcatm.com/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User-agent: *
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disallow:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sitemap: https://www.xbcatm.com/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也应抓取 sitemap URL，核对规范主机名与语言 URL。&lt;code&gt;robots.txt&lt;/code&gt; 是爬虫指引，不是访问控制；被列入 sitemap 不保证抓取或索引。&lt;/p&gt;
&lt;h2 id="所有检查一览"&gt;所有检查一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;检查&lt;/th&gt;
&lt;th&gt;应检查什么&lt;/th&gt;
&lt;th&gt;笔记&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPTIONS&lt;/code&gt; 状态 + Allow&lt;/td&gt;
&lt;td&gt;对该资源观察到的方法策略&lt;/td&gt;
&lt;td&gt;http-method-semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content-type&lt;/code&gt; + 正文 + &lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;声明的类型与 MIME 处理策略&lt;/td&gt;
&lt;td&gt;content-type-and-nosniff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEAD/GET 元数据&lt;/td&gt;
&lt;td&gt;对应表示，允许规定的字段省略&lt;/td&gt;
&lt;td&gt;head-parity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range + Content-Range&lt;/td&gt;
&lt;td&gt;观察到的部分传输支持&lt;/td&gt;
&lt;td&gt;http-range-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;抓取的 &lt;code&gt;ETag&lt;/code&gt; -&amp;gt; 304&lt;/td&gt;
&lt;td&gt;条件表示匹配&lt;/td&gt;
&lt;td&gt;conditional-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308 保留方法&lt;/td&gt;
&lt;td&gt;重定向保留故事&lt;/td&gt;
&lt;td&gt;redirects-and-status-codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robots -&amp;gt; sitemap&lt;/td&gt;
&lt;td&gt;发现答案不同&lt;/td&gt;
&lt;td&gt;robots-and-sitemaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每一行原来是一条笔记；现在是你可对任意边缘运行的命令。需要细节时读链接的笔记，把这个表留作再次检查时的速查。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fetch.spec.whatwg.org/#x-content-type-options-header"&gt;WHATWG Fetch Standard&lt;/a&gt;，nosniff 处理与请求目标，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，方法、验证器与状态码，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;，新鲜度与重新验证，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7538.html"&gt;RFC 7538 — 308 Permanent Redirect&lt;/a&gt;，保留方法的重定向，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，只读检查选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>