<?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>Semantics · XB Field Notes</title><link>https://www.xbcatm.com/tags/semantics/</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://www.xbcatm.com/tags/semantics/index.xml" rel="self" type="application/rss+xml"/><item><title>HTTP methods are a promise about side effects</title><link>https://www.xbcatm.com/notes/http-methods-are-a-promise-about-side-effects/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.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 is a promise about side effects, not just a verb you pass to a client. When the origin is a static file tree, most of that promise is already settled before the first request: the tree has no transactions, so methods that would mutate state have nothing safe to mutate.&lt;/p&gt;
&lt;h2 id="the-method-is-part-of-the-contract"&gt;The method is part of the contract&lt;/h2&gt;
&lt;p&gt;HTTP method semantics are defined in &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110&lt;/a&gt;. GET and HEAD must be safe: they promise a client that the request does not change server state. PUT, POST, and DELETE are not safe by default; they may change state. A server that cannot honor the promise should say so with an explicit status, not with a guessed body.&lt;/p&gt;
&lt;p&gt;For a static site the useful distinctions are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GET&lt;/strong&gt; returns the representation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HEAD&lt;/strong&gt; returns the same response metadata without the body.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPTIONS&lt;/strong&gt; discloses what the origin will accept.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POST / PUT / DELETE / PATCH&lt;/strong&gt; have no meaning for an immutable file tree and should be rejected.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-a-caddy-static-origin-actually-answers"&gt;What a Caddy static origin actually answers&lt;/h2&gt;
&lt;p&gt;Point &lt;code&gt;curl&lt;/code&gt; at the public edge and the method boundary is easy to read. An &lt;code&gt;OPTIONS&lt;/code&gt; request against a site that only serves files returns &lt;code&gt;405 Method Not Allowed&lt;/code&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;A &lt;code&gt;405&lt;/code&gt; with an &lt;code&gt;Allow&lt;/code&gt; header naming the supported methods is the correct, explicit reject. Compare this with a dynamic origin that happens to ignore the body: an imported handler can turn a universal non-promise into silent success, which hides the actual contract.&lt;/p&gt;
&lt;h2 id="head-should-equal-get-except-for-the-body"&gt;HEAD should equal GET except for the body&lt;/h2&gt;
&lt;p&gt;Because HEAD is safe and body-free, its metadata should match the equivalent GET. &lt;code&gt;Content-Length&lt;/code&gt; still describes the bytes a GET would return. On this site both agree:&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;If a proxy strips or rewrites &lt;code&gt;Content-Length&lt;/code&gt; on HEAD, a checker that compares the two will catch it. This is the parity the &lt;code&gt;head-request-parity&lt;/code&gt; note and the &lt;code&gt;check-http-headers.sh&lt;/code&gt; helper both rely on.&lt;/p&gt;
&lt;h2 id="what-to-do-when-a-client-sends-a-mutating-method"&gt;What to do when a client sends a mutating method&lt;/h2&gt;
&lt;p&gt;Answer explicitly. Return &lt;code&gt;405 Method Not Allowed&lt;/code&gt; with an &lt;code&gt;Allow&lt;/code&gt; header listing the safe methods, rather than falling through to a page that looks successful. The HTTP foundations path depends on a single obvious answer at the edge: every method is either supported with its documented semantics or rejected with a status that says so.&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;, method definitions, safe/idempotent methods, and &lt;code&gt;405&lt;/code&gt;, checked 2026-07-02.&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 semantics and &lt;code&gt;405&lt;/code&gt; behavior, checked 2026-07-02.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;, &lt;code&gt;-X&lt;/code&gt;, &lt;code&gt;-o&lt;/code&gt;, and &lt;code&gt;-w&lt;/code&gt; inspection options, checked 2026-07-02.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>