<?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>笔记 · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/notes/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/notes/index.xml" rel="self" type="application/rss+xml"/><item><title>维护定时器应让其最近结果可见</title><link>https://hk.xbcatm.com/zh/notes/a-maintenance-timer-should-leave-its-last-result-visible/</link><pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-maintenance-timer-should-leave-its-last-result-visible/</guid><description>用一个 oneshot 服务、一个定时器、有界运行时间与日志证据搭配使用。</description><content:encoded>&lt;p&gt;cron 式语法并不是维护任务契约的全部。命令需要一个清晰的服务身份、一个有界的运行时间，以及能区分&amp;quot;未调度&amp;quot;与&amp;quot;运行但失败&amp;quot;的证据。一个 systemd 定时器与 oneshot 服务能把这些关注点分开。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/maintenance-timer.detail.zh.svg" alt="每小时调度、随机延迟、oneshot 激活与结果证据是时间线上的独立阶段。错过的日历事件合并为一次补跑，仍受随机延迟限制。两分钟激活超时还需考虑停止处理与工具自身的有界请求。成功可以回到非活动状态；必须对应 Result、退出状态和新的日志时间戳。服务仍活动时，重复定时器不会再启动实例。" width="720" height="3270"&gt;&lt;figcaption&gt;定时器触发只证明发生了调度事件。用服务结果、退出状态和新的时间戳判断本次运行。&lt;/figcaption&gt;&lt;/figure&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-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Read-only public site check&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;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sitecheck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/check-public-site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;TimeoutStartSec&lt;/code&gt; 限制 oneshot 的激活过程，其中包括命令运行时间；oneshot 默认没有启动超时。超时处理之后还可能需要停止超时，且不能立即结束不可中断的内核 I/O，因此工具本身也需要有界的网络请求。当契约失败时，helper 应返回非零退出码，并且不得修改它所检查的服务。&lt;/p&gt;
&lt;h2 id="调度该服务"&gt;调度该服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /etc/systemd/system/site-check.timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;每小时运行公共站点检查&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;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;OnCalendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5min&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site-check.service&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;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若至少错过一次日历事件，&lt;code&gt;Persistent=true&lt;/code&gt; 会在定时器重新激活时触发一次补跑；它不会逐小时重放所有错过的运行。补跑仍受随机延迟限制。&lt;code&gt;RandomizedDelaySec&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;systemd-analyze verify /etc/systemd/system/site-check.service &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; /etc/systemd/system/site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemd-analyze calendar hourly
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl start site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status site-check.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u site-check.service -n &lt;span class="m"&gt;50&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl show site-check.service -p Result -p ExecMainCode -p ExecMainStatus
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; --now site-check.timer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl list-timers site-check.timer
&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;定时器触发并不等于成功。要监控 oneshot 服务的退出状态或日志结果。保持 stdout 简洁，调试需要的详细制品写入一个有界的路径。&lt;/p&gt;
&lt;p&gt;成功的 oneshot 通常回到 &lt;code&gt;inactive (dead)&lt;/code&gt;；这本身不是失败，&lt;code&gt;systemctl status&lt;/code&gt; 对非活动服务可能返回非零。应将 &lt;code&gt;Result&lt;/code&gt;、退出状态与新的日志时间戳对应，避免把旧运行的成功误认成本次结果。不要为了显示绿色而添加 &lt;code&gt;RemainAfterExit=yes&lt;/code&gt;：服务仍处于活动状态时，重复定时器不会启动一个新实例。日历时间默认使用主机时区，而且常规定时器精度会在随机延迟之外增加调度余量。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.timer.5.html"&gt;systemd.timer(5)&lt;/a&gt;，日历定时器、持久化与延迟行为，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/systemd.time.7.html"&gt;systemd.time(7)&lt;/a&gt;，日历表达式语法，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;，oneshot 服务语义，已核验 2026-08-04。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml"&gt;systemd.service upstream manual (v257)&lt;/a&gt;，Type=exec/oneshot、就绪、激活超时与重启语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.timer.xml"&gt;systemd.timer upstream manual (v257)&lt;/a&gt;，持久化会合并错过的运行；活动单元、随机延迟与精度，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>重启策略需要一个速率限制</title><link>https://hk.xbcatm.com/zh/notes/restart-policies-need-a-rate-limit/</link><pubDate>Mon, 03 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/restart-policies-need-a-rate-limit/</guid><description>让 systemd 恢复瞬时故障，同时不掩盖永久性的配置错误。</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/restart-loop.detail.zh.svg" alt="程序执行、应用就绪和响应健康是三项独立检查。图中示例配置允许 60 秒内启动五次，快速失败后延迟五秒。再次尝试达到限制时，自动重启停止。较慢失败可能始终低于速率限制。单纯等到间隔结束不会排定新尝试；修复原因后应有意识地启动，或先清除计数器再启动。保存第一次失败日志，并核验就绪状态和健康响应。" width="720" height="3410"&gt;&lt;figcaption&gt;按配置窗口统计启动次数。启动速率限制可以停止快速反复重启，但不能证明健康，也不限制生命周期重试总数。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="捕获启动失败"&gt;捕获启动失败&lt;/h2&gt;
&lt;p&gt;对长驻进程，&lt;code&gt;Type=exec&lt;/code&gt; 能在启动时暴露配置程序执行失败，而不是把一次 fork 当成功。把它与显式的 &lt;code&gt;ExecStart&lt;/code&gt; 以及能完成工作所需最小权限的 &lt;code&gt;User&lt;/code&gt; 搭配使用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Example foreground site service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;60s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;StartLimitBurst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&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;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/srv/site/bin/server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Type=exec&lt;/code&gt; 确认程序已执行，并不确认应用已就绪。程序应保持前台运行；应用支持显式就绪通知时可用 &lt;code&gt;Type=notify&lt;/code&gt;。&lt;code&gt;Restart=on-failure&lt;/code&gt; 是恢复策略，不是健康检查。如果进程在提供错误响应的同时保持存活，systemd 不会知道站点坏了；外部冒烟测试仍有其职责。&lt;/p&gt;
&lt;h2 id="限制循环"&gt;限制循环&lt;/h2&gt;
&lt;p&gt;Systemd 通过 &lt;code&gt;StartLimitIntervalSec=&lt;/code&gt; 与 &lt;code&gt;StartLimitBurst=&lt;/code&gt; 应用启动速率限制。这两个指令放在 &lt;code&gt;[Unit]&lt;/code&gt;，&lt;code&gt;RestartSec&lt;/code&gt; 放在 &lt;code&gt;[Service]&lt;/code&gt;。示例允许 60 秒内启动五次；快速失败之间只有五秒重启延迟，因此可能达到限制。更长延迟可能让循环一直低于该速率：这并不是整个生命周期的重试次数预算。应按预期故障模式选择限制。当达到限制时，日志里应包含区分&amp;quot;重启成功&amp;quot;与&amp;quot;重复失败后放弃&amp;quot;所需的证据。&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;systemd-analyze verify /etc/systemd/system/example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl cat example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl show example.service -p Restart -p RestartUSec &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -p StartLimitIntervalUSec -p StartLimitBurst
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl restart example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl status --no-pager example.service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;journalctl -u example.service -n &lt;span class="m"&gt;80&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不要用重启策略掩盖缺失文件、无效权限或糟糕的环境。先修复第一次失败，再测试恢复路径。&lt;/p&gt;
&lt;p&gt;启动限制停止自动重启后，单纯等到间隔结束不会自动排定下一次尝试。修复原因后，可等待间隔结束再启动，或有意识地用 &lt;code&gt;systemctl reset-failed example.service&lt;/code&gt; 清除计数器后启动。在隔离服务中测试可执行文件缺失与快速崩溃，并保存第一次失败日志，不要只留下最后的“达到启动限制”消息。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.service.5.html"&gt;systemd.service&lt;/a&gt;，服务类型、重启与启动速率限制器，已核验 2026-08-03。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/systemd.unit.5.html"&gt;systemd.unit&lt;/a&gt;，通用单元段与 drop-in 语义，已核验 2026-08-03。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.service.xml"&gt;systemd.service upstream manual (v257)&lt;/a&gt;，Type=exec/oneshot、就绪、激活超时与重启语义，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raw.githubusercontent.com/systemd/systemd/v257/man/systemd.unit.xml"&gt;systemd.unit upstream manual (v257)&lt;/a&gt;，启动限制所属配置段、速率窗口与 reset-failed 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>静态网站的发布检查清单</title><link>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</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/release-checklist.detail.zh.svg" alt="四个编号阶段分别构建固定生成器版本的干净制品，要求 index.html、404.html、robots.txt、sitemap.xml 非空并复核链接，在待发布和公共路由测试确切制品，最后记录提交、生成器版本和下载校验和。Hugo 源码预览位于独立分支，会重新构建；HEAD 检查元数据，GET 支持正文比较。" width="720" height="2570"&gt;&lt;figcaption&gt;检查对象应是生成制品。干净输出不等于可复现；占位符搜索只是复核线索，真正的链接门禁应基于解析器。发布前服务确切制品，发布后比较两种语言的公共 GET 正文，并保存提交、生成器版本和下载校验和。&lt;/figcaption&gt;&lt;/figure&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;&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;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --cleanDestinationDir --gc --minify --destination public
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--gc&lt;/code&gt; 回收未使用的构建缓存资源，不会清理陈旧的已发布页面。&lt;code&gt;--cleanDestinationDir&lt;/code&gt; 会移除目标目录中的陈旧文件，因此只能用于一次性制品目录，不能指向源码或其他数据所在的目录。把打印出的版本与项目固定版本比较；打印版本本身不会强制版本一致。记录构建输入并验证生成的制品。干净的目标目录不能证明逐字节可复现：时间戳与其他非确定性输入需要单独控制和比较。不要在构建之后手改生成的 HTML。&lt;/p&gt;
&lt;h2 id="检查制品而不只是源码"&gt;检查制品，而不只是源码&lt;/h2&gt;
&lt;p&gt;检视生成的树，确认期望的入口点存在。把下面的断言块作为独立 shell 脚本运行；任何必需文件缺失或为空时，都应在打印清单之前停止：&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="nb"&gt;test&lt;/span&gt; -s public/index.html
&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; -s public/404.html
&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; -s public/robots.txt
&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; -s public/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;rg -l &lt;span class="s1"&gt;&amp;#39;localhost|127\.0\.0\.1|TODO|example\.com|/Users/&amp;#39;&lt;/span&gt; public --glob &lt;span class="s1"&gt;&amp;#39;*.html&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这只是复核线索，不是自动发布门禁：本清单这类示例本来就会命中，翻译页中也一样。这项仅检查 HTML 的扫描不会覆盖 XML 订阅或 JSON 搜索输出。&lt;code&gt;rg&lt;/code&gt; 无匹配时退出 1，发生错误时退出 2。逐个复核列出的文件，区分围栏示例与有效链接或属性，并用基于解析器的链接检查作为真正门禁。公开链接中的私有路径属于发布错误。&lt;/p&gt;
&lt;h2 id="验证公共路由"&gt;验证公共路由&lt;/h2&gt;
&lt;p&gt;让隔离的类生产服务器直接指向这份制品，才能进行更可靠的发布前路由检查。Hugo 开发服务器会重新构建内容；下面是独立的源码预览，不能证明它服务的就是已经检查过的 &lt;code&gt;public/&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;hugo server --bind 127.0.0.1 --port &lt;span class="m"&gt;1313&lt;/span&gt; --baseURL http://127.0.0.1:1313/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --disableFastRender --disableLiveReload --renderToMemory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/notes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/resources/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/missing-page/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;HEAD 检查不比较正文。对待发布服务器再运行 GET，发布后将公共正文与制品比较，并检查两种语言。确认缺失路由在生产服务器上返回 404 状态。一个外观精美但状态 200 的错误页，仍然会破坏监控与缓存。&lt;/p&gt;
&lt;h2 id="记录结果"&gt;记录结果&lt;/h2&gt;
&lt;p&gt;在发布记录中保留源码提交、生成器版本与每个可下载资源的校验和。这样无需依赖记忆，就能把线上文件与产生它的源码进行比较。&lt;/p&gt;
&lt;p&gt;这份检查清单的可下载版本在&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;资源&lt;/a&gt;一节中。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;，干净生产构建的生成器标志，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，HTTP 验证与响应头检查选项，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，可下载的配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo server command reference&lt;/a&gt;，开发服务器重新构建、内存渲染与实时刷新控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>SSH 主机密钥是部署记录的一部分</title><link>https://hk.xbcatm.com/zh/notes/ssh-host-keys-are-part-of-the-deployment-record/</link><pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/ssh-host-keys-are-part-of-the-deployment-record/</guid><description>让首次接触保持显式，并拒绝未预期的主机密钥变化。</description><content:encoded>&lt;p&gt;在公钥认证中，客户端证明自己持有私钥，但它并不告诉客户端到达的是哪台服务器。服务器的主机密钥与本地 &lt;code&gt;known_hosts&lt;/code&gt; 数据库，提供身份检查的后一半。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/ssh-host-key.detail.zh.svg" alt="客户端私钥认证与服务器主机身份核验是不同检查。安装前，将候选指纹与独立可信的所有者或控制台指纹比较。yes 拒绝未知或变化的密钥；accept-new 接受首次见到的密钥但没有独立身份保证，仍拒绝变化。复核轮换、保存私有运维记录，ssh -G 不等于握手测试。" width="720" height="2300"&gt;&lt;figcaption&gt;SSH 服务器身份依赖观察到的主机密钥与独立可信参考之间的比较。严格检查拒绝未知或变化的密钥。首次使用时信任与网络密钥扫描，都不能独立认证首次接触的服务器。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把首次接触与轮换分开"&gt;把首次接触与轮换分开&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;StrictHostKeyChecking=yes&lt;/code&gt; 下，SSH 拒绝未知主机与变化的密钥。对于一个固定生产端点，只要期望密钥通过可信渠道安装，这就是最安全的默认值。&lt;code&gt;accept-new&lt;/code&gt; 是面向预期会出现新主机的、更窄的方便选项：它接受一个从未见过的密钥，但仍拒绝被更改的密钥。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host production-site
HostName www.example.test
User deploy
IdentityFile ~/.ssh/site_ed25519
StrictHostKeyChecking yes
UserKnownHostsFile ~/.ssh/known_hosts
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;不要通过给部署脚本加 &lt;code&gt;StrictHostKeyChecking=no&lt;/code&gt; 来&amp;quot;修复&amp;quot;警告。它会在客户端限制条件下允许连接到密钥已变化的主机；警告可能仍被打印，但原本遇到身份问题就拒绝的检查已被削弱。如果主机被重建，请记录替换后的指纹，并复核旧密钥为何不再有效。&lt;/p&gt;
&lt;h2 id="让指纹可复核"&gt;让指纹可复核&lt;/h2&gt;
&lt;p&gt;在首次连接之前，从主机所有者或控制台获取指纹，再与客户端看到的值比较。密钥变更应有工单、维护说明或其他他人可检视的解释。&lt;/p&gt;
&lt;p&gt;同一规则适用于自动化：known-hosts 文件是带有安全后果的配置，不是可丢弃的缓存数据。把它与部署记录一起备份，但不要把私钥或复制的凭据随站点制品一起公开。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accept-new&lt;/code&gt; 是首次使用时信任，并不能独立证明第一次连接的服务器真实可信。&lt;code&gt;ssh-keyscan&lt;/code&gt; 能采集候选公钥，但通过同一条不可信网络取回它，并不能认证它。安装前先将指纹与独立可信渠道取得的值比较。&lt;/p&gt;
&lt;p&gt;把上面的配置单独保存为 &lt;code&gt;deploy.conf&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;ssh -G -F ./deploy.conf production-site &lt;span class="p"&gt;|&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; grep -E &lt;span class="s1"&gt;&amp;#39;^(hostname|user|stricthostkeychecking|userknownhostsfile) &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结果应显示配置中的主机、用户、known-hosts 路径与严格检查（客户端输出可能为 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;yes&lt;/code&gt;）。这只解析配置，并不测试真实握手。非标准端口的 known-hosts 条目通常采用 &lt;code&gt;[hostname]:port&lt;/code&gt;。将主机公钥指纹保存在私有运维发布记录中，不要随公共网站发布；即使主机公钥不是秘密，主机名也可能暴露内部拓扑。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh_config"&gt;OpenBSD &lt;code&gt;ssh_config&lt;/code&gt;&lt;/a&gt;，&lt;code&gt;StrictHostKeyChecking&lt;/code&gt; 与 &lt;code&gt;UserKnownHostsFile&lt;/code&gt;，已核验 2026-07-28。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keygen"&gt;OpenSSH &lt;code&gt;ssh-keygen&lt;/code&gt; 手册&lt;/a&gt;，指纹检视，已核验 2026-07-28。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh-keyscan"&gt;OpenSSH ssh-keyscan&lt;/a&gt;，未经核验采集的密钥不能防止中间人攻击，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.openbsd.org/ssh"&gt;OpenSSH ssh&lt;/a&gt;，-G 不建立会话，打印求值后的客户端配置，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>小系统的优势</title><link>https://hk.xbcatm.com/zh/notes/the-advantage-of-small-systems/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/the-advantage-of-small-systems/</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/small-system.detail.zh.svg" alt="DNS 查询向客户端返回地址，不承载 HTTP。请求进入预期的 TLS 边缘与一个已知输出目录。文件判断分流到必要内容或由站点处理的真实 404。职责字段需要明确 DNS、边缘与发布负责人；文件来源和恢复要求仍需记录。独立备份与恢复演练应对单点故障。" width="720" height="3030"&gt;&lt;figcaption&gt;明确每个请求边界、相关证据与负责人。组件更少只会减少需要理解的路径；恢复承诺仍靠备份与恢复演练支撑。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="可见性是一种特性"&gt;可见性是一种特性&lt;/h2&gt;
&lt;p&gt;当运维人员能说出每个边界时，他们也能指出缺少哪份证据。日志变得更有用，告警更具体，例行维护风险更低。&lt;/p&gt;
&lt;p&gt;对静态网站而言，重要的边界通常很容易列出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DNS 把主机名指向预期的边缘。&lt;/li&gt;
&lt;li&gt;TLS 在预期的代理处终结。&lt;/li&gt;
&lt;li&gt;代理只服务一个已知的输出目录。&lt;/li&gt;
&lt;li&gt;输出只包含站点需要的文件。&lt;/li&gt;
&lt;li&gt;404 响应由站点本身处理，而不是意外的上游。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这些写下来，比加上一块回答不了任何上述问题的仪表盘更有价值。&lt;/p&gt;
&lt;h2 id="复杂性要用实力取得位置"&gt;复杂性要用实力取得位置&lt;/h2&gt;
&lt;p&gt;当新的一层消除的不确定性多于它引入的，它才值得。缓存重要时，给资源加指纹的构建步骤会有用；内容需要事务时，数据库才有用。但对一个小而多为静态的站点，这两者都不必然是改进。&lt;/p&gt;
&lt;p&gt;对客户端 JavaScript 也适用同一规则。如果页面确实需要客户端脚本，应选择满足需求的最小实现，并有意识地审查内容安全策略。对本站而言，这首先需要重新评估已接受的零脚本架构及其验证契约，并不是当前策略的例外。如果页面是一份文档，纯 HTML 往往更容易检视、更有韧性。&lt;/p&gt;
&lt;h2 id="让所有权明确"&gt;让所有权明确&lt;/h2&gt;
&lt;p&gt;每个文件都应有存在的理由与明确的更新方式。vendored 主题应记录其上游版本与许可。生成目录应标记为生成物。资源下载应说明它包含什么、何时更新、校验和如何产生。&lt;/p&gt;
&lt;p&gt;这些简短声明能防止未来的运维者把偶然产生的制品当作受支持的接口。&lt;/p&gt;
&lt;h2 id="为下一次变更留出空间"&gt;为下一次变更留出空间&lt;/h2&gt;
&lt;p&gt;最好的小系统不是行数最少的那个，而是下一次变更看得见去处的那一个。笔记站可以从 Markdown、静态生成器与一个 Web 服务器起步；如果以后需要搜索或评论，可以独立评估这个新需求，而不是一开始就假设它存在。&lt;/p&gt;
&lt;p&gt;规模小并不自动带来容错。一个边缘、磁盘或运维者仍可能成为单点故障。明确可接受的恢复时间与数据损失，保留独立备份并演练恢复。组件更少的好处是需要理解的故障路径更少，而不是不会停机。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/getting-started/directory-structure/"&gt;Hugo directory structure&lt;/a&gt;，内容与资源的目录约定，已核验 2026-07-27。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt; 指令&lt;/a&gt;，设计中引用的静态文件服务默认值，已核验 2026-07-27。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>安全响应头应描述实际页面</title><link>https://hk.xbcatm.com/zh/notes/security-headers-should-describe-the-actual-page/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/security-headers-should-describe-the-actual-page/</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/security-layers.detail.zh.svg" alt="HTML、本地样式表与 favicon 决定 CSP 资源许可。Content-Security-Policy 强制执行；Report-Only 揭示违规且报告需另行配置。CSP 应通过响应头发送：frame-ancestors 在 meta 中被忽略且不回退到 default-src，form-action 需要自己的指令，style-src self 本身不允许内联样式。HSTS 从有效 HTTPS 响应学习；www.example.test 的 includeSubDomains 覆盖该主机与后代，不覆盖 example.test 或 api.example.test。应检查正常页、缺失页与资源响应，并在真实浏览器验证执行。本图不声称已经测试。" width="720" height="3460"&gt;&lt;figcaption&gt;把响应头视为部署契约：匹配实际资源，区分强制执行与仅报告策略，再验证准确主机范围和浏览器行为。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="从资源图开始"&gt;从资源图开始&lt;/h2&gt;
&lt;p&gt;如果站点只提供 HTML、本地样式表、一个 favicon、没有任何可执行 JavaScript，那么一条窄策略是可理解的：&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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;script-src 'none'&lt;/code&gt; 只有在站点不需要可执行脚本时才是正确的选择。加入搜索、评论或统计标签会改变资源图，并应触发一次有意识的策略复核。&lt;/p&gt;
&lt;h2 id="增加传输与嵌套保护"&gt;增加传输与嵌套保护&lt;/h2&gt;
&lt;p&gt;对仅 HTTPS 的站点，下列响应头是常见起点：&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;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;span class="line"&gt;&lt;span class="cl"&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Referrer-Policy: strict-origin-when-cross-origin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;只有当每个子域名都准备好提供 HTTPS 时才启用带 includeSubDomains 的 HSTS。响应头是与未来部署的契约，而不只是扫描器里的一个分数。&lt;/p&gt;
&lt;h2 id="检查行为而不只是存在"&gt;检查行为，而不只是存在&lt;/h2&gt;
&lt;p&gt;请求一个正常页面、一个缺失页面与一个静态资源。错误响应应携带与成功响应同样重要的安全响应头。还要确认压缩与缓存不会改变内容类型，也不会暴露内部路径。&lt;/p&gt;
&lt;p&gt;本站的&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;Caddy 示例&lt;/a&gt;只是一个起点，不是普适策略。请根据你自己的部署调整主机名、证书处理与应用路由。&lt;/p&gt;
&lt;p&gt;通过响应头发送 CSP。meta 策略中的 &lt;code&gt;frame-ancestors&lt;/code&gt; 会被忽略，而且它不会继承 &lt;code&gt;default-src&lt;/code&gt;；&lt;code&gt;form-action&lt;/code&gt; 也需要自己的指令。&lt;code&gt;style-src 'self'&lt;/code&gt; 本身不允许内联样式，因此应测试生成的主题，包括深色模式。只有页面确实需要 data 图片时，才在 &lt;code&gt;img-src&lt;/code&gt; 中保留 &lt;code&gt;data:&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; 可在上线过程中揭示违规，但不会强制执行策略；报告端点还需单独配置。除了查看响应头，也要在真实浏览器中验证执行效果。HSTS 从有效 HTTPS 响应中学习；带有 &lt;code&gt;includeSubDomains&lt;/code&gt; 时，它还覆盖设置策略的主机的后代：&lt;code&gt;www.example.test&lt;/code&gt; 的策略不覆盖父域 &lt;code&gt;example.test&lt;/code&gt; 或同级 &lt;code&gt;api.example.test&lt;/code&gt;。确认这个准确范围后，再逐步延长有效期。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;，CSP 指令名称与语法，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security"&gt;MDN: &lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/a&gt;，HSTS 指令与部署注意事项，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/header"&gt;Caddy &lt;code&gt;header&lt;/code&gt; 指令&lt;/a&gt;，响应头操作指令，已核验 2026-07-23。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.w3.org/TR/CSP3/"&gt;W3C Content Security Policy Level 3&lt;/a&gt;，响应头交付、指令回退、嵌套与仅报告策略；当前为工作草案，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6797.html"&gt;RFC 6797 — HTTP Strict Transport Security&lt;/a&gt;，第 8.1–8.3 节的有效 HTTPS 交付与主机及后代匹配，已核验 2026-10-15。&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>可靠的默认行为胜过聪明的恢复</title><link>https://hk.xbcatm.com/zh/notes/reliable-defaults-beat-clever-recovery/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/reliable-defaults-beat-clever-recovery/</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/reliable-path.detail.zh.svg" alt="常见路径使用固定工具构建，检查完整目录，发布并验证公共响应，保留上一版目录。配置、运行时与文档一致指向一个选中发布。故障后先判断是否瞬时且可安全重复，仅符合条件的操作可使用次数与耗时预算重试，否则停止并修正。权限错误不进入重试；响应丢失时首次操作可能已成功。恢复显式回到保留的已知良好目录并记录结果。" width="720" height="3620"&gt;&lt;figcaption&gt;让常见路径可见。先判断并限制重试，再恢复到保留的已知良好状态，避免选择半成品副本。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="让常见路径变得无聊"&gt;让常见路径变得无聊&lt;/h2&gt;
&lt;p&gt;一个有用的默认行为是明确的、易于检视的、且可安全重复的。对静态站点，可以简单到：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建自固定的工具链。&lt;/li&gt;
&lt;li&gt;检查生成的 HTML、资源与完整的待发布目录。&lt;/li&gt;
&lt;li&gt;发布该目录，再把公共响应与它核对。&lt;/li&gt;
&lt;li&gt;保留上一个目录以便回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这段序列不如多阶段部署系统炫目，但它的状态可见。当某处失败时，运维者能回答：问题在源码、构建、复制还是 Web 服务器。&lt;/p&gt;
&lt;h2 id="只对可限定范围的操作重试"&gt;只对可限定范围的操作重试&lt;/h2&gt;
&lt;p&gt;当故障可能是瞬时的、且重复操作不会改变其预期效果时，重试才有用。幂等不等于每次响应或附带日志都相同；响应丢失后，第一次尝试可能早已成功。从源站下载可能可安全重试；数据库迁移则未必。把每个错误都看作可重试的，会掩盖需要关注的边界。&lt;/p&gt;
&lt;p&gt;当重试适当时，记录尝试次数与最终原因。类似&amp;quot;发布失败：校验和不匹配&amp;quot;的日志条目，比一串静默重试后跟一个笼统超时更有可操作性。&lt;/p&gt;
&lt;h2 id="偏好单一权威状态"&gt;偏好单一权威状态&lt;/h2&gt;
&lt;p&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;release: 2026-07-20
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;source: git commit &amp;lt;commit-id&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;builder: Hugo 0.165.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;output: public/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;verification: links + headers + checksums
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;占位符 commit ID 应按设计在发布时解析，而不是在文档里杜撰。&lt;/p&gt;
&lt;h2 id="恢复应回到已知状态"&gt;恢复应回到已知状态&lt;/h2&gt;
&lt;p&gt;当恢复把服务带回最后一个已知良好目录，而不是在几个半成品替代品之间选择时，恢复最容易。保留上一次输出直到新输出通过检查，再按保留策略移除旧副本。&lt;/p&gt;
&lt;p&gt;目标不是消除失败，而是把失败缩小到能解释、把恢复缩小到能信任。&lt;/p&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;，9.2.2 节关于幂等方法与重试，已核验 2026-07-20。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/"&gt;AWS Builders’ Library: Timeouts, retries, and backoff with jitter&lt;/a&gt;，重试放大、有界重试预算、退避与随机抖动，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布清单应点名每一个已发布的字节</title><link>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</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/release-manifest.detail.zh.svg" alt="冻结的常规文件制品被传输到目标端，两端分别生成位于制品树外的有序清单。目标清单中多出示意的 old.js 记录；两份清单汇入完整记录比较。另一条分支对源端清单做哈希以生成发布标识。逐项校验不能发现额外文件，字节一致也不能证明作者身份。" width="720" height="1990"&gt;&lt;figcaption&gt;测量前停止写入、拒绝符号链接和特殊文件，并将清单保存在制品树外。逐项校验发现缺失或变化的字节；重新生成并比较完整清单还可发现多余文件。对可信源端清单做哈希可得到紧凑标识，真实性与元数据仍需独立控制。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="生成稳定的列表"&gt;生成稳定的列表&lt;/h2&gt;
&lt;p&gt;从包含 &lt;code&gt;public/&lt;/code&gt; 的目录，在 Bash 中使用 GNU findutils 与 coreutils 运行以下命令。清单应保存在被测目录之外。发布只能包含目录和常规文件：先检查并拒绝符号链接或特殊文件，因为它们不在该清单的契约内。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&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;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&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; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&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; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&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="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&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;先排序文件名，再计算哈希。工具之间用 NUL 分隔路径，可保留空格、制表符与内嵌换行；GNU &lt;code&gt;sha256sum&lt;/code&gt; 会在普通可校验输出中转义特殊文件名。不要用 &lt;code&gt;awk '{print $2}'&lt;/code&gt; 解析该输出：文件名中可以合法包含空白。单独赋值会保留 &lt;code&gt;find&lt;/code&gt; 失败时的退出状态；只检查其输出，可能把扫描失败误认成空结果。扫描或管道一旦失败，就应把清单视为不完整，不得发布。测量期间停止写入；这些命令不会创建文件系统快照。&lt;/p&gt;
&lt;h2 id="传输后验证"&gt;传输后验证&lt;/h2&gt;
&lt;p&gt;在目标端的同一个可丢弃 Bash 会话中，按顺序运行后续片段，使路径变量与 &lt;code&gt;set&lt;/code&gt; 选项保持生效。将清单与制品路径设为实际位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&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; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&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="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;检查列出的文件能发现缺失或改变的字节，却不能发现多余文件。因此按相同流程重新生成目标端清单，并比较完整记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;observed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.observed.XXXXXX&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;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -f &amp;#34;$observed&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&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;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&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; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&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; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$observed&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;diff -u &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&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;$observed&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;/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;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&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;/p&gt;
&lt;h2 id="不要把完整性等同于真实性"&gt;不要把完整性等同于真实性&lt;/h2&gt;
&lt;p&gt;SHA-256 摘要一致，是字节一致的强实用证据，并不能证明作者身份。攻击者若能同时替换制品与清单，也能让检查通过。签名与可信分发是独立控制；只应校验通过预期可信渠道取得的清单。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html"&gt;GNU Coreutils: sha2 utilities&lt;/a&gt;，校验和生成与验证；通过下列上游手册源码核对，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/coreutils/coreutils/master/doc/coreutils.texi"&gt;GNU Coreutils 手册源码&lt;/a&gt;，非 tagged 校验和的转义、&lt;code&gt;--check&lt;/code&gt; 与 NUL 分隔排序，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man1/sha256sum.1.html"&gt;sha256sum(1)&lt;/a&gt;，命令行为与输出格式，已核验 2026-07-16。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Simple-Command-Expansion.html"&gt;GNU Bash: Simple Command Expansion&lt;/a&gt;，纯赋值命令保留命令替换的退出状态，已核验 2026-10-18。&lt;/p&gt;
&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>用两条命令读取公共边缘</title><link>https://hk.xbcatm.com/zh/notes/read-the-public-edge-in-two-commands/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/read-the-public-edge-in-two-commands/</guid><description>读取响应头与证书日期，同时区分到期、身份与字节一致性的验证。</description><content:encoded>&lt;p&gt;部署完成意味着公共响应符合发布契约，而不是最后一条上传命令退出码为零。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;操作套件&lt;/a&gt;里的两个只读工具能提供有用证据，但不能证明整个制品都已正确发布。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/field-output.detail.zh.svg" alt="源文中的示意片段展示 HTTP_STATUS 200 与 FINAL_URL，以及完整的 2026 年证书到期时间和 30 天到期检查结果。注释区分命令成功与 HTTP 状态，以及到期与 notBefore、主机名和信任检查；正文比较与 SAN 身份策略另行验证。" width="720" height="1970"&gt;&lt;figcaption&gt;这些片段仅为示意。分别判断 HTTP 状态与到期结果；主机名、信任与正文一致性需要对应的独立检查。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="检查响应头"&gt;检查响应头&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-http-headers.sh&lt;/code&gt; 发出 GET、丢弃正文、跟随重定向，打印每一跳的响应头以及最终状态和 URL：&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;check-http-headers.sh https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;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-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;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;permissions-policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;referrer-policy: strict-origin-when-cross-origin
&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;span class="line"&gt;&lt;span class="cl"&gt;x-frame-options: DENY
&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_STATUS 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FINAL_URL https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核对预期状态、最终主机与路径，以及最终响应上的策略。工具没有使用 curl 的 &lt;code&gt;--fail&lt;/code&gt;：HTTP 404 或 500 仍可能使 shell 命令成功结束。必须判断它打印的状态。响应头不一致需要调查服务器、代理或缓存配置；单凭这种不一致，不能证明活动输出目录选错了。&lt;/p&gt;
&lt;h2 id="检查证书日期"&gt;检查证书日期&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;check-tls-expiry.sh HOST [DAYS]&lt;/code&gt; 提取使用 SNI 时服务器呈现的证书，并检查距离到期是否至少还有指定天数对应的秒数：&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;check-tls-expiry.sh www.xbcatm.com &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;subject=CN=www.xbcatm.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;issuer=C=US, O=Let&amp;#39;s Encrypt, CN=YE1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notBefore=Aug 21 08:37:13 2026 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;notAfter=Nov 19 08:37:12 2026 GMT
&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;certificate remains valid for at least 30 day(s)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;工具打印主体、签发者、日期与 SHA-256 指纹，不打印剩余天数。成功消息只描述到期检查：脚本没有强制验证主机名或信任链，&lt;code&gt;-checkend&lt;/code&gt; 也不验证 &lt;code&gt;notBefore&lt;/code&gt;。主体 CN 不足以证明主机身份；服务身份应与证书的主体备用名称匹配。有效的通配符也可能合法覆盖该主机。&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;openssl s_client -connect www.xbcatm.com:443 -servername www.xbcatm.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -verify_hostname www.xbcatm.com -verify_return_error &amp;lt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这使用已配置的信任库，并在 OpenSSL 匹配规则下的证书验证出错时失败。在 OpenSSL 3.0 中，没有合适的 DNS 主题备用名称时，主机名验证可能回退到主题 CN。因此，命令成功本身不能证明符合 RFC 9525 仅使用 SAN 的身份策略：还要检查 SAN 扩展，并单独验证目标客户端的策略。TLS 工具本身没有网络截止时间，应在任务层设置超时。证书检查与自动续期配置仍是两项独立检查。&lt;/p&gt;
&lt;h2 id="在发布前后使用两者"&gt;在发布前后使用两者&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;发布前：保存预期策略、最终 URL、验证器与证书日期，并注明 UTC 采集时间。&lt;/li&gt;
&lt;li&gt;部署已经验证的制品。&lt;/li&gt;
&lt;li&gt;发布后：重复探测，并下载代表性正文与该制品比较。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;响应头与证书日期不能证明字节一致。&lt;code&gt;Last-Modified&lt;/code&gt; 可能不变，也可能在复制中被重写。&lt;a href="https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/"&gt;发布冒烟测试&lt;/a&gt;保存正文并直接比较；完整清单可将检查扩展到单个页面之外。浏览器行为、不同地区边缘的差异，以及只在负载下出现的故障，需要另行测试。记录实际检查过的路径与观察点。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，GET、重定向、响应头输出与 HTTP 失败行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，SNI、显式主机名检查与验证错误时失败，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-verification-options/"&gt;OpenSSL verification options&lt;/a&gt;，主机名匹配与主题 CN 回退，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-x509/"&gt;OpenSSL &lt;code&gt;x509&lt;/code&gt;&lt;/a&gt;，证书显示与只检查到期的 &lt;code&gt;-checkend&lt;/code&gt; 行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9525.html"&gt;RFC 9525 — Service Identity in TLS&lt;/a&gt;，主体备用名称与通配符身份匹配，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy Automatic HTTPS&lt;/a&gt;，自动证书管理与续期配置，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP"&gt;MDN: Content Security Policy guide&lt;/a&gt;，所示策略的背景说明，已核验 2026-07-13。&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;，响应元数据与验证器不能证明完整制品匹配，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布在公共边缘一致之前不算完成</title><link>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>把生成制品与访问者实际能收到的响应进行比较。</description><content:encoded>&lt;p&gt;静态发布消除了应用运行时的复杂度，但不会消除生成目录与公共响应之间的边界。发布检查应同时覆盖两边。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/diagrams/release-path.svg" alt="一条小的静态发布路径" /&gt;
&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-edge.detail.zh.svg" alt="先在使用预期配置的待发布服务器上检查确切制品，切换目录后在公共边缘重复检查。将 index.html、下载资源和 404.html 与解码后的 GET 正文配对。字节比较与预期状态、类型、缓存、安全头分别检查。重复中英文样本，并在新目录通过检查前保留上一个已知良好目录。" width="720" height="2200"&gt;&lt;figcaption&gt;切换前后使用同一份制品。下载行对应文中的 public/downloads/check-http-headers.sh。cmp 比较前会解码受支持的内容编码；服务器转换需有明确比较规则。状态和响应头另行检查，并保留前一目录供主动回滚。&lt;/figcaption&gt;&lt;/figure&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;&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;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件列表是证据，不是装饰。如果某条路由消失，diff 应能说明是该来源被移除，还是生成器未能产出它。&lt;/p&gt;
&lt;h2 id="演练三类响应"&gt;演练三类响应&lt;/h2&gt;
&lt;p&gt;发布前，用预期服务器配置测试待发布制品。切换目录后，在公共边缘重复检查；探测仍在线的旧站不能验证新发布。请求一个 HTML 页面、一个静态资源与一个缺失路径。检查状态码、内容类型、缓存策略与安全头：&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 --compressed -fsS -D page.headers -o page.body https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D asset.headers -o asset.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; https://www.example.test/downloads/check-http-headers.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -sS -D missing.headers -o missing.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; https://www.example.test/does-not-exist
&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;cmp public/index.html page.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/downloads/check-http-headers.sh asset.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/404.html missing.body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把每个片段作为独立 shell 脚本运行，或放在可丢弃的 shell 会话中执行。&lt;code&gt;set -eu&lt;/code&gt; 让这个顺序脚本在构建、传输或比较失败后停止，避免后面的成功命令掩盖先前失败。缺失路径的请求有意不用 &lt;code&gt;-f&lt;/code&gt;：404 是预期结果，仍应被捕获。检查各响应头文件的最终状态：依次为 &lt;code&gt;200&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;404&lt;/code&gt;；字节比较不会自动完成状态码检查。这里不跟随重定向。&lt;code&gt;--compressed&lt;/code&gt; 在比较前解码受支持的内容编码；若服务器有意转换 HTML，需要另行说明比较规则。&lt;code&gt;cmp&lt;/code&gt; 相等时不输出，有差异时返回非零。在 &lt;code&gt;/zh/&lt;/code&gt; 及其错误页上重复检查，不能直接假设两种语言一致。&lt;/p&gt;
&lt;h2 id="让回滚保持简单"&gt;让回滚保持简单&lt;/h2&gt;
&lt;p&gt;在新目录通过检查之前，不要删除上一个目录。回滚应选择一个已知良好目录，而不是在事故中凭记忆重新构造一个。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;发布检查清单&lt;/a&gt;与&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;HTTP 响应头检查&lt;/a&gt;都刻意做得足够小，能在干净的 shell 里运行。&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;，状态与表示语义，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，响应检查选项，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>事故复盘应留下一份事实记录</title><link>https://hk.xbcatm.com/zh/notes/a-postmortem-should-leave-a-fact-record/</link><pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-postmortem-should-leave-a-fact-record/</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/postmortem.detail.zh.svg" alt="竖直事故时间线包含四项：检测连接采集结果，界定范围连接已测边界，修复连接配置差异与预期结果，验证连接重复检查的证据。每项都保留来源、采集时间和上下文。虚线不确定项区域与带负责人、到期日和验收检查的预防动作分开。" width="720" height="2860"&gt;&lt;figcaption&gt;每个阶段都要连接真实证据。修复后的采样成功仍是有范围的观察；原因、不确定项和有负责人的预防动作仍需明确记录。&lt;/figcaption&gt;&lt;/figure&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-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ check-http-headers.sh 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;content-security-policy: ... script-src &amp;#39;none&amp;#39; ...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 11:20:54 GMT
&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;p&gt;用证据而不是形容词陈述影响范围。先界定能够检视的边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些主机与路径出现症状？记录每次探测的 URL 与观察时间；一次成功请求不能代表所有边缘或访问者的状态。&lt;/li&gt;
&lt;li&gt;缓存与源站是否不同？使用相同请求头，比较两条路径的实际响应、缓存元数据与验证器。&lt;code&gt;Vary&lt;/code&gt; 描述选择输入，不能证明响应来自缓存。&lt;/li&gt;
&lt;li&gt;什么因素拖延恢复？DNS 变更需记录旧 TTL 与解析器观察结果；HTTP 缓存需另记新鲜度与重新验证策略。DNS TTL 不是通用的回滚期限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="修复变更及其预期效果"&gt;修复：变更及其预期效果&lt;/h2&gt;
&lt;p&gt;记录实际配置差异与部署标识，让修复动作可理解，并在安全时可重放。下面的响应头片段是预期观察结果，不是 shell 命令，也不是完整策略。优先选择恢复契约的最小变更，并说明修复后运行应显示什么：&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;Content-Security-Policy: script-src &amp;#39;none&amp;#39;
&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;/p&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;，响应元数据与条件请求，已核验 2026-07-07。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;制品 HTTP 响应头检查&lt;/a&gt;，贯穿本记录使用的只读检查。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sre.google/sre-book/postmortem-culture/"&gt;Google SRE: Postmortem Culture&lt;/a&gt;，影响、促成因素、证据复核与预防动作，已核验 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;，缓存选择与新鲜度不同于 DNS 缓存寿命，已核验 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>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>压缩会改变所选中的 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/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>HSTS 应带着恢复计划来灰度发布</title><link>https://hk.xbcatm.com/zh/notes/hsts-should-be-rolled-out-with-a-recovery-plan/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/hsts-should-be-rolled-out-with-a-recovery-plan/</guid><description>只有在 HTTPS、重定向与每个被子域名覆盖的域名都就绪之后，才增大 max-age。</description><content:encoded>&lt;p&gt;HTTP Strict Transport Security 告诉支持的浏览器，在一个缓存周期内对该主机强制使用 HTTPS。这种持久性既是特性也是风险：移除响应头并不会立刻抹掉客户端已存储的策略。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/hsts-rollout.detail.zh.svg" alt="发布轨道要求检查 HTTPS 与重定向、采用短时长示例，并在观察站点与续期后选择时长。域名树将 includeSubDomains 限于发送主机的后代；独立的预加载和恢复面板说明已存策略的限制。" width="720" height="2530"&gt;&lt;figcaption&gt;观察后再增加 HSTS 时长，独立决定后代覆盖与预加载。恢复需要有效 HTTPS，也不能立刻抹除所有已存或预加载策略。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="先验证-https-路径"&gt;先验证 HTTPS 路径&lt;/h2&gt;
&lt;p&gt;在发送 HSTS 之前，确认规范主机名提供有效证书，并且 HTTP 在不依赖用户输入的情况下重定向到预期的 HTTPS URL：&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 --max-redirs &lt;span class="m"&gt;0&lt;/span&gt; http://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 https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;检查有代表性的深层路由与错误页。HSTS 无法修复证书错误，因为浏览器在接受该策略之前要求有效的 HTTPS 连接。&lt;/p&gt;
&lt;h2 id="从一个短的-max-age-开始"&gt;从一个短的 max-age 开始&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;Strict-Transport-Security: max-age=300
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;观察站点与续期路径之后，再有意识地提高时长。记录日期、所选值与回滚条件。最终策略可能用一年，但这个数字是一种承诺，而不是扫描器分数。&lt;/p&gt;
&lt;h2 id="把-includesubdomains-当作独立的决定"&gt;把 includeSubDomains 当作独立的决定&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;includeSubDomains&lt;/code&gt; 把策略扩展到每个子域名。先盘点名称，包括旧服务、委派区，以及只在内部网络使用的主机名。即使只有一个仅 HTTP 的后代，也足以让该选项对收到父级策略的客户端产生破坏。&lt;/p&gt;
&lt;h2 id="预加载不是普通的响应头配置"&gt;预加载不是普通的响应头配置&lt;/h2&gt;
&lt;p&gt;浏览器预加载程序可以在正常响应周期之外分发策略，其要求与移除延迟比设置一个响应头更严格。不要仅仅因为扫描器推荐就申请预加载；请先确认对域名及所有被覆盖名称的长期所有权。&lt;/p&gt;
&lt;h2 id="验证公共响应"&gt;验证公共响应&lt;/h2&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 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 &lt;span class="s1"&gt;&amp;#39;tolower($1) == &amp;#34;strict-transport-security:&amp;#34; { print }&amp;#39;&lt;/span&gt;
&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;p&gt;通过有效 HTTPS 响应发送 &lt;code&gt;max-age=0&lt;/code&gt;，只会让收到该响应的客户端清除该主机已学习的策略。移除响应头或通过 HTTP 发送它，都不能清除已缓存策略。子域无法退出父域仍有效的 &lt;code&gt;includeSubDomains&lt;/code&gt;；清除父域策略，也不会删除客户端独立从子域学习到的策略。&lt;/p&gt;
&lt;p&gt;作用域由发送响应头的主机决定：&lt;code&gt;www.example.test&lt;/code&gt; 的策略覆盖自身，加上 &lt;code&gt;includeSubDomains&lt;/code&gt; 后覆盖其后代，但不覆盖同级的 &lt;code&gt;api.example.test&lt;/code&gt; 或父级 &lt;code&gt;example.test&lt;/code&gt;。恢复期间必须保持 HTTPS 可用。移除预加载是独立的浏览器更新过程，不是响应头的即时回滚。&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/rfc6797.html"&gt;RFC 6797 — HTTP Strict Transport Security&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/Strict-Transport-Security"&gt;MDN: &lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/a&gt;，指令与部署注意事项，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hstspreload.org/"&gt;HSTS 预加载项目&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></channel></rss>