<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>HTTP foundations · XB Field Notes</title><link>https://hk.xbcatm.com/series/http-foundations/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/series/http-foundations/index.xml" rel="self" type="application/rss+xml"/><item><title>Content-Type should describe the bytes you actually send</title><link>https://hk.xbcatm.com/notes/content-type-should-describe-the-bytes-you-actually-send/</link><pubDate>Thu, 11 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/content-type-should-describe-the-bytes-you-actually-send/</guid><description>Check media types at the public edge and use nosniff to make mismatches fail visibly.</description><content:encoded>&lt;p&gt;A static server can return the correct bytes with the wrong &lt;code&gt;Content-Type&lt;/code&gt;. The page may still look acceptable in one browser, while a download, stylesheet, feed reader, or security policy treats the same response differently. The public response header is the contract; the filename is only an input to server configuration.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/content-type.detail.en.svg" alt="A CSS pathname leads to public-response inspection of status, actual bytes, and Content-Type. Correct CSS and an HTML error page are separate cases. The browser script/style destination boundary applies nosniff; failures require fixing responses, and public CDN/proxy headers must be checked independently of origin headers." width="720" height="2080"&gt;&lt;figcaption&gt;A suffix cannot prove what arrived. Read the public response status, bytes, and media type together; an HTML error page at a CSS path is not a mapping test. Nosniff makes script and style type mismatches visible.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="inspect-representative-formats"&gt;Inspect representative formats&lt;/h2&gt;
&lt;p&gt;Check one HTML document, stylesheet, SVG, feed, and downloadable script. Include headers but discard the body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; path in / /assets/site.css /favicon.svg /index.xml /downloads/check-http-headers.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -sS -D - -o /dev/null &lt;span class="s2"&gt;&amp;#34;https://www.example.test&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; awk -v &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; BEGIN { print &amp;#34;== &amp;#34; path }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; /^HTTP\// || tolower($1) == &amp;#34;content-type:&amp;#34; || tolower($1) == &amp;#34;x-content-type-options:&amp;#34; { print }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; &amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Replace the example asset paths with files that actually exist in the release; an HTML 404 response at an invented CSS path is not a MIME-mapping test. Expected media types depend on the resource, but they should be specific and stable. HTML should normally include a character set. CSS and SVG should not be served as generic binary data. A shell helper offered for download should not be advertised as HTML.&lt;/p&gt;
&lt;p&gt;This loop displays headers; it does not assert success, inspect the discarded bytes, or exercise browser blocking. Confirm the resource against the release artifact and inspect an actual browser load as a stylesheet or script. Opening the same URL as a document tests a different request destination.&lt;/p&gt;
&lt;h2 id="keep-nosniff-consistent"&gt;Keep nosniff consistent&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; asks supporting browsers to respect the declared type for script and style destinations instead of guessing. For that Fetch check, a script-like destination needs a JavaScript MIME type, and a style destination needs &lt;code&gt;text/css&lt;/code&gt;; a missing or unusable type also fails. This turns a mismatch into an observable failure rather than browser-dependent interpretation.&lt;/p&gt;
&lt;p&gt;The header does not repair a wrong type or make arbitrary content safe. Check the inventory while deploying it, and fix any rejected script/style response rather than removing the protection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;X-Content-Type-Options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="check-the-server-mapping"&gt;Check the server mapping&lt;/h2&gt;
&lt;p&gt;Static servers usually derive types from extensions. If a new extension is served as &lt;code&gt;application/octet-stream&lt;/code&gt;, decide whether that is intentional before adding a global override. A narrow mapping is easier to review than a rule that changes every response.&lt;/p&gt;
&lt;p&gt;Repeat the checks at the public hostname. A CDN or reverse proxy can replace a correct origin header, and a friendly error page can return HTML for a path whose suffix looks like CSS or JavaScript.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, representation metadata and &lt;code&gt;Content-Type&lt;/code&gt;, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type"&gt;MDN: &lt;code&gt;Content-Type&lt;/code&gt;&lt;/a&gt;, media types and charset examples, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options"&gt;MDN: &lt;code&gt;X-Content-Type-Options&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;nosniff&lt;/code&gt; behavior, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff"&gt;WHATWG Fetch Standard&lt;/a&gt;, script-like and style destination blocking, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HEAD should describe the same representation as GET</title><link>https://hk.xbcatm.com/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/notes/head-should-describe-the-same-representation-as-get/</guid><description>Compare HEAD and GET metadata without confusing a missing body with a different resource.</description><content:encoded>&lt;p&gt;The &lt;code&gt;HEAD&lt;/code&gt; method asks for the response a server would send to &lt;code&gt;GET&lt;/code&gt;, without transferring the response content. It is useful for checks that need status, validators, type, or length, but only when the metadata remains aligned with the real &lt;code&gt;GET&lt;/code&gt; path.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/head-parity.detail.en.svg" alt="Matched request context, including Accept-Encoding, feeds GET and HEAD metadata cards. A content/checksum arrow exists only under GET. An absent HEAD Content-Length is allowed; a present value describes corresponding GET content. A small GET checks payload, and method-specific or redirect differences need investigation." width="720" height="2240"&gt;&lt;figcaption&gt;Match the URL, request headers, release, and content coding before comparing HEAD with GET. Permitted omissions are not automatic failures. Keep a small GET body or checksum check, because plausible metadata cannot prove that the payload is intact.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="compare-a-normalized-header-set"&gt;Compare a normalized header set&lt;/h2&gt;
&lt;p&gt;Dynamic headers such as &lt;code&gt;Date&lt;/code&gt; will differ between requests. Compare the fields that describe the selected representation:&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;Run this in a disposable working directory; the two header files are overwritten. Match the URL, request headers, and release version. Compression negotiation belongs in the comparison: send the same &lt;code&gt;Accept-Encoding&lt;/code&gt; value to both requests.&lt;/p&gt;
&lt;p&gt;HEAD should describe the corresponding GET, but RFC 9110 permits omission of fields determined only while generating content. Missing &lt;code&gt;Content-Length&lt;/code&gt; alone is not a failure. If present on HEAD, it must be the length the corresponding GET content would have, not zero just because HEAD has no body. Compare encoded lengths consistently and investigate changed validators or types rather than requiring byte-identical header lists.&lt;/p&gt;
&lt;h2 id="do-not-use-head-as-the-only-release-check"&gt;Do not use HEAD as the only release check&lt;/h2&gt;
&lt;p&gt;A server can synthesize plausible headers while the file is missing, unreadable, or truncated. Keep one small &lt;code&gt;GET&lt;/code&gt; in the smoke test and verify its body or checksum. &lt;code&gt;HEAD&lt;/code&gt; reduces transfer cost; it does not prove the payload.&lt;/p&gt;
&lt;h2 id="check-redirects-explicitly"&gt;Check redirects explicitly&lt;/h2&gt;
&lt;p&gt;Without &lt;code&gt;--location&lt;/code&gt;, &lt;code&gt;curl -I&lt;/code&gt; reports the first response. That is useful when testing redirect policy. With &lt;code&gt;--location&lt;/code&gt;, it prints header blocks for the followed responses, including intermediate redirects and the final destination. Record which question the command answers:&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="treat-method-specific-failures-as-configuration-defects"&gt;Treat method-specific failures as configuration defects&lt;/h2&gt;
&lt;p&gt;A static page that returns &lt;code&gt;200&lt;/code&gt; to &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;405&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt; to &lt;code&gt;HEAD&lt;/code&gt; usually exposes a proxy or routing inconsistency. Fix the method handling instead of teaching monitors to ignore it, unless the endpoint contract deliberately disallows &lt;code&gt;HEAD&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 method semantics, checked 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;, and redirect options, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Compression changes the selected HTTP representation</title><link>https://hk.xbcatm.com/notes/compression-changes-the-selected-http-representation/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/compression-changes-the-selected-http-representation/</guid><description>Treat content encoding, validators, and Vary as one observable response contract.</description><content:encoded>&lt;p&gt;Compression saves transfer bytes, but it also creates another representation of a resource. A client announces acceptable content codings with &lt;code&gt;Accept-Encoding&lt;/code&gt;; the server describes the applied coding with &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.en.svg" alt="A negotiation fork selects identity bytes A or encoded bytes B, requiring different strong tags. Vary controls matching; weak tags have a qualified alternative. A separate pipeline keeps encoded Content-Length in saved response headers while curl decodes the body before an artifact-checksum comparison." width="720" height="2320"&gt;&lt;figcaption&gt;Compression selects representation bytes, so negotiation, cache matching, and validators must agree. Distinct coded bytes need distinct strong tags. Compare the decoded download with the artifact; an encoded Content-Length need not equal the decoded file size.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="make-negotiation-visible"&gt;Make negotiation visible&lt;/h2&gt;
&lt;p&gt;When a response can change according to &lt;code&gt;Accept-Encoding&lt;/code&gt;, include that request field in &lt;code&gt;Vary&lt;/code&gt;. This tells shared caches that a compressed response and an identity response are not interchangeable cache entries.&lt;/p&gt;
&lt;p&gt;Compare the two paths instead of checking only a browser request:&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;Inspect &lt;code&gt;Content-Encoding&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt; when present, and the validator. Strong entity tags must differ when content coding changes the representation bytes. A shared weak tag is possible for semantically equivalent variants, but it cannot support strong comparison. Check conditionals against the same negotiated representation.&lt;/p&gt;
&lt;h2 id="compress-appropriate-content"&gt;Compress appropriate content&lt;/h2&gt;
&lt;p&gt;Text formats such as HTML, CSS, JSON, XML, and SVG usually benefit from compression. Formats that are already compressed might not. Caddy&amp;rsquo;s &lt;code&gt;encode&lt;/code&gt; directive negotiates supported encodings and normally avoids encoding a response without an appropriate content type or minimum size.&lt;/p&gt;
&lt;p&gt;Do not add compression solely to satisfy a score. Measure a representative response, confirm that the edge emits the intended headers, and verify that downloadable files retain the correct content type and checksum after transfer decoding.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;--compressed&lt;/code&gt;, curl decodes the body automatically but leaves saved response headers unchanged. A reported &lt;code&gt;Content-Length&lt;/code&gt;, when present, describes encoded content, so it need not equal the size of the decoded download. Compare a checksum with the artifact only after content decoding; HTTP transfer framing is a separate layer. The option also does not guarantee compression: inspect the actual response, especially for small or already-compressed files.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, content codings and &lt;code&gt;Vary&lt;/code&gt;, checked 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; directive&lt;/a&gt;, content encoding selection and negotiation, checked 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;, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Range requests should prove which bytes were selected</title><link>https://hk.xbcatm.com/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/notes/range-requests-should-prove-which-bytes-were-selected/</guid><description>Verify partial responses, Content-Range, and the behavior of unsatisfiable byte ranges.</description><content:encoded>&lt;p&gt;Range requests let a client ask for part of a representation. Media players, resumable download tools, and diagnostic clients use them, but a server that advertises or returns ranges incorrectly can produce silent corruption instead of a clean failure.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/range-request.detail.en.svg" alt="A stable encoded representation of N bytes supplies the reference size. A ruler selects inclusive offsets 0 through 15, exactly 16 bytes. Outcomes distinguish matching 206 bytes and Content-Range, complete 200 when Range is ignored, and rejected unsatisfiable ranges starting at offset N with 416 and bytes */N. Strong If-Range validators prevent splicing different versions. Weak ETags are forbidden; a strong HTTP date qualifies only when no ETag exists. Changed representation requires a complete current body." width="720" height="3050"&gt;&lt;figcaption&gt;Keep the representation and content coding stable. Match interval, total size and actual body bytes; a complete 200 can be valid when Range is ignored. Resume only with an appropriate strong validator.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="establish-the-complete-size"&gt;Establish the complete size&lt;/h2&gt;
&lt;p&gt;Use an immutable test download of at least 16 bytes. Run the fragments in the same disposable shell session so &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt; remain available. Their explicit &lt;code&gt;set -eu&lt;/code&gt; stops after a failed transfer, size check, or comparison; inspect the HTTP status and range fields separately. Save the complete body, headers, and validator in that temporary working directory:&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;Check that the full response is &lt;code&gt;200&lt;/code&gt; and has no unexpected content coding. The commands request the identity representation and do not use automatic decompression; byte ranges apply to the selected encoded representation, so changing encoding invalidates the comparison. &lt;code&gt;Content-Length&lt;/code&gt; may be absent, which is why the complete body supplies the size.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Accept-Ranges: bytes&lt;/code&gt; is an explicit signal, but absence of the header does not by itself prove that range requests are unsupported. Exercise the request.&lt;/p&gt;
&lt;h2 id="request-a-known-interval"&gt;Request a known interval&lt;/h2&gt;
&lt;p&gt;Ask for the first 16 bytes and keep both headers and body:&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;A successful byte-range response uses &lt;code&gt;206 Partial Content&lt;/code&gt;. &lt;code&gt;Content-Range&lt;/code&gt; should identify &lt;code&gt;0-15&lt;/code&gt; and the complete representation length, while the body should contain exactly 16 bytes.&lt;/p&gt;
&lt;p&gt;The byte comparison deliberately tests the partial-body case. A valid full &lt;code&gt;200&lt;/code&gt; larger than 16 bytes will fail it and stop this shell session; inspect the saved headers before calling that corruption. Conversely, a complete 16-byte file can pass &lt;code&gt;cmp&lt;/code&gt; even with &lt;code&gt;200&lt;/code&gt;, so status and &lt;code&gt;Content-Range&lt;/code&gt; still need checking.&lt;/p&gt;
&lt;h2 id="test-an-impossible-range"&gt;Test an impossible range&lt;/h2&gt;
&lt;p&gt;Start at the complete byte count: for a file of size N, the last valid offset is N−1. Unlike a fixed large constant, this is outside the known representation:&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;When the server rejects an unsatisfiable byte range with &lt;code&gt;416 Range Not Satisfiable&lt;/code&gt;, it should include &lt;code&gt;Content-Range&lt;/code&gt;, for example &lt;code&gt;bytes */12345&lt;/code&gt;, to report the current size. A &lt;code&gt;200&lt;/code&gt; response may be valid when the server ignores the range, but it must return the complete representation rather than a mislabeled fragment.&lt;/p&gt;
&lt;h2 id="keep-validators-in-the-story"&gt;Keep validators in the story&lt;/h2&gt;
&lt;p&gt;Resumed downloads can use &lt;code&gt;If-Range&lt;/code&gt; with a strong ETag. A weak ETag is forbidden; an HTTP date is allowed only when no ETag is available and the date qualifies as a strong validator. If the representation changed, the safe response is the full current representation, not bytes spliced from two versions. Test this behavior before calling a download resumable.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, range units, partial responses, and &lt;code&gt;Content-Range&lt;/code&gt;, checked 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; and header options, checked 2026-07-06.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Cache headers are a contract with two clocks</title><link>https://hk.xbcatm.com/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/notes/cache-headers-are-a-contract-with-two-clocks/</guid><description>Choose freshness and revalidation rules from how often a representation can change.</description><content:encoded>&lt;p&gt;Caching is not a single switch. A response has a freshness decision made by a cache and a validation conversation with the origin. Mixing those two ideas produces either stale content or needless requests.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/cache-clocks.detail.en.svg" alt="A storage and reuse matrix is followed by stable and fingerprinted URL policies. A conditional GET sends the real ETag in If-None-Match: a weak match can return 304, while a changed validator normally returns a new 200. Weak equivalence need not mean equal bytes. Age, Vary, freshness-limited immutable, and previously cached lifetimes constrain reuse." width="720" height="2800"&gt;&lt;figcaption&gt;Storage, freshness, and validation answer separate questions. Match the policy to how the URL changes, preserve validators exactly, and check shared-cache variants.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="pick-the-change-model-first"&gt;Pick the change model first&lt;/h2&gt;
&lt;p&gt;For an HTML document that may change after publication, &lt;code&gt;Cache-Control: no-cache&lt;/code&gt; is usually a more accurate starting point than &lt;code&gt;no-store&lt;/code&gt;. &lt;code&gt;no-cache&lt;/code&gt; permits storage but requires a cache to validate before reuse; &lt;code&gt;no-store&lt;/code&gt; asks caches not to store the response at all. The distinction matters when you want conditional requests and a cheap &lt;code&gt;304 Not Modified&lt;/code&gt; response.&lt;/p&gt;
&lt;p&gt;For a fingerprinted asset such as &lt;code&gt;app.4f2c.js&lt;/code&gt;, the URL changes when the bytes change. A long freshness lifetime can then be reasonable:&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;That policy is unsafe for a stable URL whose contents can be replaced in place. If the URL is stable, prefer a shorter lifetime and a validator such as an entity tag (&lt;code&gt;ETag&lt;/code&gt;) or a meaningful &lt;code&gt;Last-Modified&lt;/code&gt; value.&lt;/p&gt;
&lt;p&gt;Choose who may store the response as well as how long it stays fresh. Use the unqualified &lt;code&gt;private&lt;/code&gt; directive when shared caches must not store user-specific content; a browser’s private cache can still store it. &lt;code&gt;no-cache&lt;/code&gt; alone does not provide that restriction. The &lt;code&gt;public&lt;/code&gt; example above is for assets safe to share across users, not a default for personalized responses.&lt;/p&gt;
&lt;h2 id="inspect-both-paths"&gt;Inspect both paths&lt;/h2&gt;
&lt;p&gt;Make one ordinary request and one conditional request. The exact server flags vary, but the evidence you want is stable:&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;Do not paste a made-up validator into a runbook. Copy the value from the first response or use a test fixture. For this conditional GET, a matching &lt;code&gt;If-None-Match&lt;/code&gt; can yield &lt;code&gt;304&lt;/code&gt; using weak comparison: it permits semantic equivalence, not necessarily byte identity. A changed validator normally produces a new &lt;code&gt;200&lt;/code&gt; response. Keep the ETag’s quotes and any &lt;code&gt;W/&lt;/code&gt; prefix intact.&lt;/p&gt;
&lt;h2 id="keep-intermediaries-visible"&gt;Keep intermediaries visible&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Age&lt;/code&gt;, &lt;code&gt;Vary&lt;/code&gt;, and the cache-control directives explain why two clients can receive different results. If a response varies by &lt;code&gt;Accept-Encoding&lt;/code&gt;, language, or another request field, that dimension belongs in &lt;code&gt;Vary&lt;/code&gt;; otherwise a shared cache can reuse the wrong representation.&lt;/p&gt;
&lt;p&gt;The useful release check is not “the scanner found a cache header”. It is “the header matches the replacement policy, and a conditional request behaves as documented”.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;immutable&lt;/code&gt; applies during the advertised freshness lifetime; it is not a permanent ban on revalidation. Keep versioned URLs immutable and older assets reachable while old HTML can still reference them. Changing today’s cache header does not retroactively shorten a previously cached response’s lifetime, so a cache-policy mistake may require a new URL or an explicitly supported cache purge.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, sections 4–5, checked 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;, sections 8 and 13, checked 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;, directive semantics and defaults, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8246.html"&gt;RFC 8246 — Immutable Responses&lt;/a&gt;, freshness-limited immutable semantics, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Redirects and error codes should preserve the story</title><link>https://hk.xbcatm.com/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/notes/redirects-and-error-codes-should-preserve-the-story/</guid><description>Use permanent redirects and absence codes to describe what happened, not to hide a broken route.</description><content:encoded>&lt;p&gt;An HTTP status is part of the interface. A browser may render a friendly page, but a monitor, crawler, cache, and API client also need the status code and headers to tell the same story.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/redirect-map.detail.en.svg" alt="An old endpoint receives POST and returns 308 with Location; after resolving Location the client can send POST to the new endpoint. 307 and 308 preserve method while 301 and 302 may change POST to GET. Separate absence branches show 404 for temporary or unknown absence and 410 for likely permanent unavailability; neither explains the cause and a friendly page must retain the correct non-200 status. Relative Location is valid when resolved against the original URI. 308 caching depends on method and cache controls; a cached move can outlast configuration. Curl without --location does not follow." width="720" height="2960"&gt;&lt;figcaption&gt;Choose relocation or absence deliberately. Preserve the required method, resolve Location, account for caching, and keep error-page status truthful.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="permanent-moves-are-not-all-interchangeable"&gt;Permanent moves are not all interchangeable&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; communicates that the resource has a new permanent URI while preserving the request method. That is important for a client that sends &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, or another non-&lt;code&gt;GET&lt;/code&gt; method. A redirect from an old page to a new page is easy to test with &lt;code&gt;GET&lt;/code&gt;; a migration of an endpoint must also be tested with the method that real callers use.&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;Without &lt;code&gt;--location&lt;/code&gt;, curl leaves the redirect unfollowed. Resolve &lt;code&gt;Location&lt;/code&gt; against the original URI: an absolute URI, root-relative path, or other relative reference can be valid. Check the resolved host, path, and scheme. &lt;code&gt;301&lt;/code&gt;/&lt;code&gt;302&lt;/code&gt; permit changing POST to GET; &lt;code&gt;307&lt;/code&gt;/&lt;code&gt;308&lt;/code&gt; preserve the method. Test state-changing methods only against an isolated endpoint. &lt;code&gt;308&lt;/code&gt; is heuristically cacheable, subject to method and cache controls, so a cached move can outlive a server configuration change.&lt;/p&gt;
&lt;p&gt;If the intent is to show a result after a completed action, &lt;code&gt;303 See Other&lt;/code&gt; points to a separate resource that the client can retrieve with GET or HEAD. It does not ask the client to replay the original write at a new address; choose that behavior deliberately rather than treating every redirect as a relocation.&lt;/p&gt;
&lt;h2 id="distinguish-missing-from-removed"&gt;Distinguish missing from removed&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;404 Not Found&lt;/code&gt; when the server cannot find a current representation and the absence may be temporary or unknown. &lt;code&gt;410 Gone&lt;/code&gt; is stronger: it says access is no longer available and the condition is likely permanent; it does not establish why the resource disappeared. Both should have a body that helps a human, but neither should return &lt;code&gt;200&lt;/code&gt; just because a custom error page rendered successfully.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;404&lt;/code&gt; can also conceal whether a forbidden resource exists. Both &lt;code&gt;404&lt;/code&gt; and &lt;code&gt;410&lt;/code&gt; are heuristically cacheable, subject to method and cache controls. Choose an explicit policy for error responses: a cached missing-route result can remain visible after the route is repaired.&lt;/p&gt;
&lt;p&gt;The check belongs at the edge:&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;Keep the response headers on the error path. Security headers, a useful content type, and a correct status are more valuable than a decorative error animation.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, sections 15 and 13, checked 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;, section 3, checked 2026-07-14.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, status and redirect inspection flags, checked 2026-07-14.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>robots.txt and sitemaps answer different questions</title><link>https://hk.xbcatm.com/notes/robots.txt-and-sitemaps-answer-different-questions/</link><pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/robots.txt-and-sitemaps-answer-different-questions/</guid><description>Keep crawler preferences, URL discovery, and access control separate.</description><content:encoded>&lt;p&gt;Two small files are often treated as if they were a security boundary. They are not. A &lt;code&gt;robots.txt&lt;/code&gt; file describes crawler preferences, while a sitemap gives search systems a list of URLs that belong to the site.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/discovery-map.detail.en.svg" alt="robots.txt at the origin root is a UTF-8 preference file for cooperating crawlers, not authentication or reliable de-indexing. Inspect the sitemap root: follow sitemapindex children such as language sitemaps, or read a direct urlset. Use canonical absolute URLs from one host. XML syntax alone is insufficient: check namespace, count, host and page existence; each sitemap is limited to 50,000 URLs and 52,428,800 uncompressed bytes, unchanged by gzip. Private routes need server authorization or a network boundary. RFC 9309 permits crawling for unavailable robots with 4xx; network/server errors initially require complete disallow. A reachable 200 HTML fallback is not proof of a correct robots file." width="720" height="3560"&gt;&lt;figcaption&gt;Discovery aids do not grant or deny access. Follow every listed sitemap, validate more than XML syntax, and handle robots error responses according to their actual class.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="what-robotstxt-can-and-cannot-do"&gt;What robots.txt can and cannot do&lt;/h2&gt;
&lt;p&gt;The rules are published at the origin&amp;rsquo;s &lt;code&gt;/robots.txt&lt;/code&gt; path and are read by cooperating crawlers. They do not protect a private directory, remove a URL from the network, or replace authentication. A route that must not be public needs server authorization or a network boundary.&lt;/p&gt;
&lt;p&gt;Keep the file boring and test its actual location:&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;The file should be UTF-8. If a deployment redirects it, verify the crawler behavior you intend rather than assuming every client follows the same chain.&lt;/p&gt;
&lt;h2 id="what-a-sitemap-adds"&gt;What a sitemap adds&lt;/h2&gt;
&lt;p&gt;A sitemap is an inventory, not a permission list. Use absolute URLs from one host, encode XML values correctly, and include only canonical pages that you are prepared to publish. Do not list a URL merely because it exists in a build directory.&lt;/p&gt;
&lt;p&gt;Each sitemap may contain at most 50,000 URLs and 50 MB (52,428,800 bytes) uncompressed; gzip does not raise that limit. An index is optional for a small site, but multilingual generators may legitimately produce one pointing to separate language sitemaps. Inspect the root element and follow each listed sitemap rather than assuming &lt;code&gt;/sitemap.xml&lt;/code&gt; is a URL set. Validate it as XML and request it through the same HTTPS edge as a normal page.&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;Separate download and parsing so a failed fetch cannot be hidden by a successful parser. Well-formed XML is only a syntax check: also validate the namespace, URL count, canonical hosts, and whether listed pages exist. If &lt;code&gt;xmllint&lt;/code&gt; is not installed, parse the XML in the same CI environment that publishes it. The important part is that the check runs before the file is replaced at the edge.&lt;/p&gt;
&lt;p&gt;A robots &lt;code&gt;Disallow&lt;/code&gt; is not a reliable de-indexing instruction. Also test error responses: RFC 9309 permits crawling when robots is unavailable with a 4xx response, while a network/server error initially requires complete disallow. Accidentally serving the HTML fallback with status 200 can therefore change crawler behavior even though the URL appears reachable.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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 rules, file location, and user-agent matching, checked 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 structure and discovery, checked 2026-07-21.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>HTTP methods are a promise about side effects</title><link>https://hk.xbcatm.com/notes/http-methods-are-a-promise-about-side-effects/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/http-methods-are-a-promise-about-side-effects/</guid><description>The HTTP method names what a request may change; a static origin can answer most of them without pretending to have a stateful contract.</description><content:encoded>&lt;p&gt;A request method tells the server what the client is asking it to do. A read-only file origin has a narrow contract, but “static site” alone does not prove that proxies or imported handlers expose no write operations. Check the route actually serving the resource.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/http-methods.detail.en.svg" alt="A semantic matrix separates safe GET and HEAD from unsafe but idempotent PUT and DELETE. A scoped Caddy v2.10.2 file route accepts GET and HEAD and rejects other methods with 405 and Allow, while earlier handlers may differ. Safe methods can still produce incidental logs; idempotency does not promise identical statuses or authorize every retry. Any supplied HEAD Content-Length describes the corresponding GET with the same coding; omission may be legal. Method support is separate from CORS permission." width="720" height="2900"&gt;&lt;figcaption&gt;Separate requested state change from intended repeat effect, then inspect the actual route. The file-handler boundary is scoped; method support is separate from CORS permission.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="safe-and-idempotent-answer-different-questions"&gt;Safe and idempotent answer different questions&lt;/h2&gt;
&lt;p&gt;GET and HEAD are safe: the client does not request a state-changing action. Incidental logging, metrics, or accounting can still change server state. PUT and DELETE are idempotent in their defined semantics, but are not safe: repeating the same request is intended to have the same effect as performing it once. That does not require identical response statuses, and it does not make every retry after a network failure appropriate.&lt;/p&gt;
&lt;p&gt;For a read-only static origin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GET&lt;/strong&gt; retrieves a representation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HEAD&lt;/strong&gt; describes the corresponding GET without response content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPTIONS&lt;/strong&gt; asks about the target’s communication options; a file handler may reject it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POST / PUT / DELETE / PATCH&lt;/strong&gt; should not fall through to successful file serving unless a separate, intended handler implements their semantics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-caddy-file-handlers-method-boundary"&gt;A Caddy file handler’s method boundary&lt;/h2&gt;
&lt;p&gt;Caddy v2.10.2’s file-serving path accepts GET and HEAD and rejects other methods with &lt;code&gt;405&lt;/code&gt; and &lt;code&gt;Allow: GET, HEAD&lt;/code&gt;. Redirects, missing paths, error handlers, and other configured routes can answer earlier or differently, so this is not a universal response for every static-site URL.&lt;/p&gt;
&lt;p&gt;The earlier note recorded:&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;The original response log is not retained here, and this review did not reproduce that live result. The command reports only status; it does not establish that the required &lt;code&gt;Allow&lt;/code&gt; field was present. Inspect that field separately. &lt;code&gt;405&lt;/code&gt; means a known method is not supported for this target; &lt;code&gt;501 Not Implemented&lt;/code&gt; is the usual distinction when the server does not recognize or implement a method at all.&lt;/p&gt;
&lt;h2 id="head-parity-allows-documented-omissions"&gt;HEAD parity allows documented omissions&lt;/h2&gt;
&lt;p&gt;HEAD should describe the same selected representation as GET, while omitting content. Fields calculated only while generating the content may be omitted. If &lt;code&gt;Content-Length&lt;/code&gt; is present, it must describe the corresponding GET content, using the same content coding.&lt;/p&gt;
&lt;p&gt;The earlier note reported this comparison:&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;Those historical numbers are not a fixed expected size or a current verification result. A missing HEAD length alone is legal; a wrong supplied length needs investigation. Follow the &lt;code&gt;head-request-parity&lt;/code&gt; note for paired probes. The downloadable &lt;code&gt;check-http-headers.sh&lt;/code&gt; helper inspects a GET response and does not perform this comparison automatically.&lt;/p&gt;
&lt;h2 id="reject-unintended-writes-without-hiding-the-contract"&gt;Reject unintended writes without hiding the contract&lt;/h2&gt;
&lt;p&gt;For a method recognized but disallowed on the target, return &lt;code&gt;405&lt;/code&gt; with &lt;code&gt;Allow&lt;/code&gt; listing the methods actually supported. The list is not necessarily restricted to safe methods on an application route. Do not return a plausible &lt;code&gt;200&lt;/code&gt; page for an ignored write request.&lt;/p&gt;
&lt;p&gt;If browser clients need CORS preflight, define that OPTIONS behavior explicitly; method support and CORS permission are separate decisions. Test write rejection only against an authorized disposable fixture rather than assuming a public endpoint is harmless because the site looks static.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, safe/idempotent methods and HEAD/405/501 semantics, checked 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 request methods&lt;/a&gt;, method overview, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, method and response-inspection options, checked 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 file-server source&lt;/a&gt;, method rejection after file selection, checked 2026-10-15.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/#cors-preflight-fetch"&gt;WHATWG Fetch Standard&lt;/a&gt;, CORS preflight processing, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Validators decide whether a cached copy is still the answer</title><link>https://hk.xbcatm.com/notes/validators-decide-whether-a-cached-copy-is-still-the-answer/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/notes/validators-decide-whether-a-cached-copy-is-still-the-answer/</guid><description>ETags and Last-Modified let caches check whether a stored copy is current and avoid transferring the body when revalidation confirms it is unchanged.</description><content:encoded>&lt;p&gt;A cache can ask whether its stored copy is still usable without always downloading the body again. It sends a validator; a matching conditional GET can receive &lt;code&gt;304 Not Modified&lt;/code&gt;, while a changed representation normally needs a new &lt;code&gt;200&lt;/code&gt; response. Validation saves bytes on a hit, not on every request.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/etag-validation.detail.en.svg" alt='The cache sends the exact weak ETag W/"release-a" in If-None-Match. Weak comparison matches the origin tag "release-a", giving a body-free 304; the cache updates metadata and reuses stored body A. Alternatively, origin tag "release-b" produces 200 and new body B for GET. HEAD uses the same status test but returns no body. Weak matching implies neither byte identity nor freshness.' width="720" height="1750"&gt;&lt;figcaption&gt;Illustrative tags; otherwise successful requests. GET/HEAD use weak comparison. HEAD has no body. Matching is neither byte identity nor freshness. Other methods normally return 412; If-Range cannot use weak ETags.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="validator-type-and-strength-are-different-questions"&gt;Validator type and strength are different questions&lt;/h2&gt;
&lt;p&gt;An &lt;code&gt;ETag&lt;/code&gt; is an opaque, quoted value. It can be strong (&lt;code&gt;&amp;quot;release-a&amp;quot;&lt;/code&gt;) or explicitly weak (&lt;code&gt;W/&amp;quot;release-a&amp;quot;&lt;/code&gt;). Strong comparison requires both tags to be strong and equal; weak comparison compares the opaque values regardless of the &lt;code&gt;W/&lt;/code&gt; marker. Weak tags can describe semantic equivalence even when bytes differ.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Last-Modified&lt;/code&gt; is a timestamp with one-second HTTP-date resolution. It is usually weak unless the conditions in RFC 9110 section 8.8.2.2 establish it as strong. Do not classify every ETag as strong or every timestamp as unconditionally weak.&lt;/p&gt;
&lt;p&gt;The earlier site observation recorded:&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;These are historical values; the original response log is not retained here and the live result was not rechecked in this review. Caddy’s file server normally sets both fields, subject to file metadata and configuration.&lt;/p&gt;
&lt;h2 id="conditional-get-uses-weak-comparison"&gt;Conditional GET uses weak comparison&lt;/h2&gt;
&lt;p&gt;Send the ETag exactly as received in &lt;code&gt;If-None-Match&lt;/code&gt;, including its quotes and any &lt;code&gt;W/&lt;/code&gt;. The original note reported &lt;code&gt;HTTP 304&lt;/code&gt; for the value above but printed an unquoted request value. That request text was not valid entity-tag syntax and should not be copied as a working probe. Obtain a current validator before testing a live endpoint.&lt;/p&gt;
&lt;p&gt;This socket-free exercise illustrates the comparison for individual, already-parsed tags; it is not a complete HTTP header parser:&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;For an otherwise successful GET or HEAD, a matching &lt;code&gt;If-None-Match&lt;/code&gt; produces a body-free &lt;code&gt;304&lt;/code&gt;. The same false precondition on another method normally produces &lt;code&gt;412 Precondition Failed&lt;/code&gt;. If both &lt;code&gt;If-None-Match&lt;/code&gt; and &lt;code&gt;If-Modified-Since&lt;/code&gt; are present, the ETag condition takes precedence. Byte-range validation with &lt;code&gt;If-Range&lt;/code&gt; cannot use a weak ETag.&lt;/p&gt;
&lt;h2 id="republish-without-preserving-a-misleading-validator"&gt;Republish without preserving a misleading validator&lt;/h2&gt;
&lt;p&gt;An ETag is not necessarily a content hash. Caddy v2.10.2’s default file tag is derived from modification time and size; replacing bytes while preserving both can preserve the tag. Inspect the deployed version and any configured overrides, then make the release process update the validator’s inputs or use content-based versioning.&lt;/p&gt;
&lt;p&gt;Keep byte checksums in the release manifest and test both unchanged and changed responses. A checksum does not automatically become the server’s ETag. A &lt;code&gt;304&lt;/code&gt; also needs the applicable cache metadata so a cache can update its stored response; it is not merely an empty success status.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, sections 8.8 and 13 on validator strength and preconditions, checked 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;, section 4.3 on validation and updating stored responses, checked 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 conditional requests&lt;/a&gt;, conditional-request overview, checked 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 file-server source&lt;/a&gt;, &lt;code&gt;calculateEtag&lt;/code&gt; inputs and default response fields, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Read a live edge the way a client does</title><link>https://hk.xbcatm.com/tutorials/http-edge-walkthrough/</link><pubDate>Fri, 07 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/tutorials/http-edge-walkthrough/</guid><description>Inspect methods, representation metadata, ranges, and caching with curl, separating protocol requirements from endpoint-specific choices.</description><content:encoded>&lt;p&gt;The HTTP-foundations notes each take one contract and show single pieces of evidence. This tutorial walks the whole path a single client request touches, in order, using only read-only commands. The aim is to distinguish protocol requirements from choices made by a particular server, rather than demand the same status and headers from every endpoint.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;www.xbcatm.com&lt;/code&gt; throughout. Replace it with the host you are inspecting. The requests use safe HTTP methods; they can still create access logs or consume rate limits. Inspect only endpoints you are permitted to query. Output blocks below are illustrative, not current production measurements. Run shell snippets in a disposable working directory, keep failures visible, and record time and client version.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-http.detail.en.svg" alt="A numbered HTTP journey separates method choices, metadata, HEAD parity, byte ranges, conditional reuse and encoding. The current successful 200 supplies an actual validator and complete body; matching 304 retains that body. Status and length examples are not universal promises." width="720" height="3750"&gt;&lt;figcaption&gt;Keep the selected representation consistent, and interpret each observed response within its own contract. No live edge was queried for this illustration.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-start-with-what-a-method-may-ask"&gt;1. Start with what a method may ask&lt;/h2&gt;
&lt;p&gt;Before you read a response body, pin down what the server will allow you to ask. A static handler often supports GET and HEAD, while a proxy may also implement OPTIONS. Ask the edge about the target resource:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -w &amp;#39;OPTIONS -&amp;gt; HTTP %{http_code}\n&amp;#39; -X OPTIONS https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 405
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;allow: GET, HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OPTIONS -&amp;gt; HTTP 405
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;405 Method Not Allowed&lt;/code&gt; is valid when OPTIONS is recognized but not supported for this resource, and a 405 must include &lt;code&gt;Allow&lt;/code&gt;. A successful 200 or 204 OPTIONS response can also be correct. &lt;code&gt;501&lt;/code&gt; means the method is not implemented by the server. An OPTIONS result is not proof that every other method behaves correctly. Do not send POST merely to test rejection on an unfamiliar live service: it is not a safe method, and a 200 alone does not describe its side effects.&lt;/p&gt;
&lt;h2 id="2-read-the-representation-line"&gt;2. Read the representation line&lt;/h2&gt;
&lt;p&gt;The first line tells you the negotiated exchange; the headers then tell you what kind of bytes are coming. Capture them exactly as a client sees them, discarding the body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 200
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept-ranges: bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-security-policy: default-src &amp;#39;self&amp;#39;; base-uri &amp;#39;self&amp;#39;; form-action &amp;#39;none&amp;#39;; frame-ancestors &amp;#39;none&amp;#39;; object-src &amp;#39;none&amp;#39;; script-src &amp;#39;none&amp;#39;; style-src &amp;#39;self&amp;#39;; img-src &amp;#39;self&amp;#39; data:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-type: text/html; charset=utf-8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;etag: &amp;#34;dkx84jwth7nkc9e&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;last-modified: Mon, 24 Aug 2026 13:55:23 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;strict-transport-security: max-age=31536000; includeSubDomains
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-content-type-options: nosniff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three headers deserve a pause. &lt;code&gt;content-type: text/html; charset=utf-8&lt;/code&gt; names the media type, and &lt;code&gt;content-type-and-nosniff&lt;/code&gt; shows why it must describe the actual bytes you send. &lt;code&gt;etag: &amp;quot;dkx84jwth7nkc9e&amp;quot;&lt;/code&gt; hands you a strong validator. &lt;code&gt;x-content-type-options: nosniff&lt;/code&gt; constrains browser MIME handling, notably for script and stylesheet fetches; it does not validate the body as HTML. Each is evidence, not decoration, and together they say: this response advertises metadata to compare with its body and intended policy. A syntactically strong ETag is a server assertion, not independent proof of a content hash.&lt;/p&gt;
&lt;h2 id="3-compare-head-and-get-for-the-same-representation"&gt;3. Compare HEAD and GET for the same representation&lt;/h2&gt;
&lt;p&gt;HEAD has GET semantics without a response body. Servers should return the same header fields but may omit fields whose values are determined only while generating content. &lt;code&gt;Content-Length&lt;/code&gt; is optional; if sent on HEAD, it must equal the size of the content a corresponding GET would send. Compare stable requests with the same encoding and other selection headers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sSI --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | grep -i content-length
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; https://www.xbcatm.com/ | wc -c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;15890
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;They match: 15890 on both. That is the &lt;code&gt;head-request-parity&lt;/code&gt; contract. An absent HEAD length alone is not a protocol violation. A mismatched value needs investigation, including a deployment between the requests or different selected representations. A pipeline to &lt;code&gt;wc&lt;/code&gt; can hide curl failure in shells without &lt;code&gt;pipefail&lt;/code&gt;; for an automated gate, save the body only after curl succeeds and count that file.&lt;/p&gt;
&lt;h2 id="4-ask-for-precise-bytes-with-a-range"&gt;4. Ask for precise bytes with a range&lt;/h2&gt;
&lt;p&gt;Because the origin knows its representation, it can serve part of it. A range request asks for a byte span and gets only that span:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -D - -o /dev/null -H &amp;#39;Accept-Encoding: identity&amp;#39; -H &amp;#39;Range: bytes=0-99&amp;#39; -w &amp;#39;HTTP %{http_code} bytes=%{size_download}\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/2 206
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-range: bytes 0-99/15890
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content-length: 100
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP 206 bytes=100
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For a representation at least 100 bytes long, a successful single-range response should have &lt;code&gt;206&lt;/code&gt;, a matching &lt;code&gt;Content-Range: bytes 0-99/…&lt;/code&gt;, and 100 content bytes. A server may legally ignore Range and return a full 200; that does not imply it lacks validators. An unsatisfiable range can return 416. A plain Range request does not require an ETag. Resuming a particular representation with &lt;code&gt;If-Range&lt;/code&gt;, or safely combining partial responses, needs the relevant strong-validator rules. Ranges refer to the selected encoded bytes, hence the explicit identity encoding here.&lt;/p&gt;
&lt;h2 id="5-let-the-cache-revalidate-with-a-validator"&gt;5. Let the cache revalidate with a validator&lt;/h2&gt;
&lt;p&gt;Now combine method, representation, and byte selection through caching. A fresh stored response may be reusable without checking a validator. To test revalidation, capture the current response and replay its actual ETag using the same representation selection. Do not copy an old tag from an article:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt; -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/body&amp;#34;&lt;/span&gt; -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;awk &lt;span class="s1"&gt;&amp;#39;tolower($1)==&amp;#34;etag:&amp;#34; {sub(/\r$/, &amp;#34;&amp;#34;); sub(/^[^:]*:[ \t]*/, &amp;#34;&amp;#34;); print; exit}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$work&lt;/span&gt;&lt;span class="s2"&gt;/headers&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;No ETag: inspect Last-Modified instead&amp;#39;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$etag&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -D - -o /dev/null &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -w &lt;span class="s1"&gt;&amp;#39;HTTP %{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run this fragment as a standalone shell script. It must finish the initial transfer and require status 200 before storing a usable response; an error representation can also have an ETag. If the selected representation still matches and normal processing succeeds, expect 304 and reuse that complete stored body with updated metadata. A changed representation can correctly return 200. Keep the temporary directory while inspecting the result, then remove only that generated directory when finished. &lt;code&gt;If-None-Match&lt;/code&gt; uses weak comparison, so a 304 need not prove byte identity. Freshness lifetime and current age decide reuse without validation; a validator answers a different question and does not itself set that lifetime.&lt;/p&gt;
&lt;h2 id="6-negotiate-a-smaller-representation-when-you-can"&gt;6. Negotiate a smaller representation when you can&lt;/h2&gt;
&lt;p&gt;The same content can travel in fewer bytes when the client says it can decompress. Offer gzip and watch the body shrink while the meaning stays identical:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -H &amp;#39;Accept-Encoding: identity&amp;#39; -o /dev/null -w &amp;#39;no-enc: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -H &amp;#39;Accept-Encoding: gzip&amp;#39; -w &amp;#39;gzip: %{size_download} bytes\n&amp;#39; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;no-enc: 15890 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gzip: 4368 bytes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These example sizes illustrate a possible reduction; the server may choose identity instead. That is the &lt;code&gt;http-compression-negotiation&lt;/code&gt; note&amp;rsquo;s core idea: changing the selected representation changes the bytes on the wire, and the metadata (&lt;code&gt;content-length&lt;/code&gt;, &lt;code&gt;encoding&lt;/code&gt;) changes with it. A client that requests compression and a cache that stores the encoded form must agree on which representation they selected, otherwise the cache can serve a mismatch. Inspect &lt;code&gt;Content-Encoding&lt;/code&gt; and &lt;code&gt;Vary: Accept-Encoding&lt;/code&gt;, not just the byte count. A manually supplied &lt;code&gt;Accept-Encoding&lt;/code&gt; does not make curl automatically decompress; &lt;code&gt;--compressed&lt;/code&gt; requests supported encodings and decodes the response. Compare decoded bodies to establish equal content, and do not assume strong ETags remain identical across encodings.&lt;/p&gt;
&lt;h2 id="7-follow-the-story-when-a-url-changes"&gt;7. Follow the story when a URL changes&lt;/h2&gt;
&lt;p&gt;When identity changes, the response should say where the story continues rather than guess the body:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 -o /dev/null -w &amp;#39;%{http_code} -&amp;gt; %{redirect_url}\n&amp;#39; -I http://www.xbcatm.com/ | head -1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;308 -&amp;gt; https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;308 Permanent Redirect&lt;/code&gt; keeps the method and preserves the story, which is the concern of &lt;code&gt;redirects-and-status-codes&lt;/code&gt;. Whether it is a 301/308 for a permanent move, or a 404 handled by the site, the status line is the first sentence of the story and should be read before the body.&lt;/p&gt;
&lt;h2 id="8-let-the-crawler-read-the-map-last"&gt;8. Let the crawler read the map last&lt;/h2&gt;
&lt;p&gt;The edge is also read by machines that do not request HTML at all. Discovery answers two different questions, as &lt;code&gt;robots-and-sitemaps&lt;/code&gt; explains. Confirm discovery points at the real sitemap:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS --max-time 20 https://www.xbcatm.com/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User-agent: *
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disallow:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sitemap: https://www.xbcatm.com/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fetch the sitemap URL too and check canonical host and language URLs. &lt;code&gt;robots.txt&lt;/code&gt; is crawler guidance, not access control; listing a URL in a sitemap does not guarantee crawling or indexing.&lt;/p&gt;
&lt;h2 id="the-whole-path-in-one-glance"&gt;The whole path in one glance&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OPTIONS&lt;/code&gt; status + Allow&lt;/td&gt;
&lt;td&gt;observed method policy for this resource&lt;/td&gt;
&lt;td&gt;http-method-semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content-type&lt;/code&gt; + body + &lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;advertised type and MIME-handling policy&lt;/td&gt;
&lt;td&gt;content-type-and-nosniff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HEAD/GET metadata&lt;/td&gt;
&lt;td&gt;corresponding representation, with allowed omissions&lt;/td&gt;
&lt;td&gt;head-parity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range + Content-Range&lt;/td&gt;
&lt;td&gt;observed partial-transfer support&lt;/td&gt;
&lt;td&gt;http-range-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;captured &lt;code&gt;ETag&lt;/code&gt; -&amp;gt; 304&lt;/td&gt;
&lt;td&gt;conditional representation match&lt;/td&gt;
&lt;td&gt;conditional-requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308 follows method&lt;/td&gt;
&lt;td&gt;redirects preserve the story&lt;/td&gt;
&lt;td&gt;redirects-and-status-codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;robots -&amp;gt; sitemap&lt;/td&gt;
&lt;td&gt;discovery answers differ&lt;/td&gt;
&lt;td&gt;robots-and-sitemaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each row was a note; now it is a command you can run against any edge. Read the linked note when you need the details, and keep this table as your re-check before you trust an endpoint.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fetch.spec.whatwg.org/#x-content-type-options-header"&gt;WHATWG Fetch Standard&lt;/a&gt;, nosniff processing and request destinations, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;, methods, validators, and status codes, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9111.html"&gt;RFC 9111 — HTTP Caching&lt;/a&gt;, freshness and revalidation, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7538.html"&gt;RFC 7538 — 308 Permanent Redirect&lt;/a&gt;, method-preserving redirect, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, read-only inspection flags, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>