<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="atom-style.xsl"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">
  <title>Writings from Timo Furrer</title>
  <subtitle>I rarely write something, but here is the feed of my writings. Enjoy.</subtitle>
  <id>https://furrer.life/~timo/feed.xml</id>
  <link rel="alternate" type="text/html" href="https://furrer.life/~timo/"/>
  <link rel="self" type="application/atom+xml" href="https://furrer.life/~timo/feed.xml"/>
  <updated>2026-05-19T12:47:26+02:00</updated>
  <author>
    <name>Timo Furrer</name>
    <email>timo@furrer.life</email>
  </author>
  <rights>Copyright 2025, Timo Furrer</rights>
  <category term="Weblog"/>
  <generator>Timo's XSLT</generator>
  <icon>https://furrer.life/~timo/favicon.png</icon>
  <entry>
    <title>Python pass vs. ellipsis</title>
    <id>https://furrer.life/~timo/writings/2021/11/05/python-pass-vs-ellipsis</id>
    <link rel="alternate" type="text/html" href="https://furrer.life/~timo/writings/2021/11/05/python-pass-vs-ellipsis"/>
    <published>2021-11-05T00:00:00+01:00</published>
    <updated>2026-05-18T20:26:28+02:00</updated>
    <author>
      <name>Timo Furrer</name>
      <email>timo@furrer.life</email>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">I’ve recently had a nerdy discussion with a
colleague during a code review about when to use Pythons
<code>pass</code> statement vs. the ellipsis literal
<code>...</code>. You’ve probably seen the <code>pass</code>
statement in Python code as a means to indicate that a block is
intentionally left empty to avoid a <code>SyntaxError</code>. Like
in the following <code>except</code> block where we expect that an
exception might be raised but just want to ignore it:
<pre><code>try:
    client.get(id=42)
except ApiException:
    pass</code></pre>
Somehow the ellipsis literal <code>...</code> has gotten some hype
lately and some people started doing this:
<pre><code>try:
    client.get(id=42)
except ApiException:
    ...</code></pre>
While this is syntactically valid Python code the semantics are
weird to me. To me, the <code>pass</code> statement effectively
communicates that we should just pass this block without running
any code at all. While the <code>...</code> are more like <i>"there
is something to be expected here in the future”</i>. Therefore, the
<code>...</code> can be thought of a placeholder. Whenever I see
the <code>...</code> my head auto-plays a <em>Dun Dun Dun</em>
sound effect - to emphasize the suspension of code. It would be a
pity if it’s a suspension of code until all eternity if you use at
as a <code>pass</code> replacement.
<h2>Where should I use the <code>...</code>?</h2>
As I’ve already mentioned you should use the <code>...</code> as a
placeholder where eventually some could should appear, but you
don’t necessarily want to <code>raise NotImplementedError</code>. A
similar situation where I use it is with Pythons Abstract Base
Classes to indicate that the function body needs to be implemented
by a subclass.
<pre><code>from abc import abstractmethod

class AbstractEventLoop:
    @abstractmethod
    def run(self, ...):
        ...</code></pre>
The next place where I use it is in Python stub files to indicate
an ignored function body:
<pre><code>def get(id: int) -&gt; Model: ...</code></pre>
The fourth place is in extended slicing with custom container
types, for what the <code>...</code> was originally introduced for.
For example, the <code>numpy</code> array indexing supports it:
<pre><code>from numpy import arange

a = arange(16).reshape(2, 2, 2, 2)
flat_a = a[..., 0].flatten()</code></pre>
<h2>But what is the <code>...</code> literal ?</h2>
The ellipsis literal <code>...</code> is syntactic sugar for the
<code>Ellipsis</code> object, which is the sole instance of the
<code>types.EllipsisType</code> type. You can use it like every
other object in Python, except that you can’t create a new one.
Thus, when you override it (yes, that’s possible:
<code>__builtins__.Ellipsis = 42</code>) you can’t really get it
back - thus, don’t do it.</div>
    </content>
  </entry>
  <entry>
    <title>Migadu.com CalDav and CardDav
auto-discovery</title>
    <id>https://furrer.life/~timo/writings/2025/02/18/migadu-caldav-carddav-auto-discovery</id>
    <link rel="alternate" type="text/html" href="https://furrer.life/~timo/writings/2025/02/18/migadu-caldav-carddav-auto-discovery"/>
    <published>2025-02-18T12:54:28+01:00</published>
    <updated>2026-05-18T20:26:33+02:00</updated>
    <author>
      <name>Timo Furrer</name>
      <email>timo@furrer.life</email>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>I started, what turns out to be, a long running journey to
migrate away from Google. Part of that journey is my personal email
address, calendar and address book.</p>
<p>After a lengthy back and forth and changing the provider
multiple times, I finally (hopefully?!) settled with <a href="https://migadu.com" title="Migadu.com">Migadu.com</a>. I will
probably write another post about why Migadu and how to actually go
about such a migration but this one is just about how to properly
setup auto-discovery for their calendar and address book
offerings.</p>
<p>Migadu doesn't really advertise that they even support calendar
and contacts. The only thing you'll find on their website is
this:</p>
<figure>
<blockquote cite="https://www.migadu.com/procon/#basic-calendar">
<p>We make the basic CalDAV and CarDAV services available, but they
are not our focus. We continue developing them but please do not
expect we will ever compete with dedicated calendar services.</p>
</blockquote>
<figcaption>From <cite><a href="https://www.migadu.com/procon/#basic-calendar">https://www.migadu.com/procon/#basic-calendar</a></cite></figcaption>
</figure>
<p>They are using <a href="https://sabre.io" title="Link to sabre/dav website">sabre/dav</a> and make it available at
<code>cdav.migadu.com</code>. Since for Migadu you always have to
bring your own domain, you can configure two <code>SRV</code>
entries in your DNS settings for your domain. This will allow
calendar and address book tooling to auto-discover your CalDav and
CardDav settings given your email address.</p>
<p><a href="https://www.rfc-editor.org/rfc/rfc6764" title="Link to RFC6764">RFC6765</a> defines how to do that. There are two
ways: a <code>/.well-known</code> location and the aforementioned
<code>SRV</code> entry. You already have to configure your DNS for
the email services anyways, so lets use that.</p>
<p>The following two <code>SRV</code> entries are required:</p>
<pre><code>_caldavs._tcp.example.com.  3000 IN TXT "path=/calendars"
_carddavs._tcp.example.com. 3000 IN TXT "path=/calendars"
_caldavs._tcp.example.com.  3000 IN SRV 0 1 443 cdav.migadu.com
_carddavs._tcp.example.com. 3000 IN SRV 0 1 443 cdav.migadu.com</code></pre>
<p>Replace the <code>example.com</code> domain with your own
domain. The two additional <code>TXT</code> entries are required,
because the CalDav / CardDav server is hosted at the
<code>/calendars</code> path.</p>
</div>
    </content>
  </entry>
  <entry>
    <title>Attention when deferring sync.WaitGroup
Wait()</title>
    <id>https://furrer.life/~timo/writings/2025/03/05/attention-when-deferring-waitgroup-wait</id>
    <link rel="alternate" type="text/html" href="https://furrer.life/~timo/writings/2025/03/05/attention-when-deferring-waitgroup-wait"/>
    <published>2025-03-05T09:50:31+01:00</published>
    <updated>2026-05-18T20:26:38+02:00</updated>
    <author>
      <name>Timo Furrer</name>
      <email>timo@furrer.life</email>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>In Go it's very common to see the pattern that after creating a
<code>sync.WaitGroup()</code> the <code>Wait()</code> call is
immediately <code>defer</code>-red, like this:</p>
<pre><code>func f() {
  var wg sync.WaitGroup
  defer wg.Wait()

  // rest of the logic
}</code></pre>
<p>We need to be careful with this pattern due to how and when Go
captures return values. For example, consider the following
function that does some concurrent processing and using a
<code>sync.WaitGroup</code> before returning the processed data to
the caller:</p>
<pre><code>func f(xs []int) map[int]string {
  var mu sync.Mutex
  var wg sync.WaitGroup
  defer wg.Wait()

  results := make(map[int]string, len(xs))

  for _, x := range xs {
    wg.Add(1)
    go func() {
      defer wg.Done()
      mu.Lock()
      defer mu.Unlock

      // some calculations
      results[x] = calc(x)
    }()
  }

  return results
}</code></pre>
<p>This works fine, but consider the same using a string slice as
return value, like this:</p>
<pre><code>func f(xs []int) []string {
  var mu sync.Mutex
  var wg sync.WaitGroup
  defer wg.Wait()

  results := make([]string, 0, len(xs))

  for _, x := range xs {
    wg.Add(1)
    go func() {
      defer wg.Done()
      mu.Lock()
      defer mu.Unlock

      // some calculations
      results = append(results, calc(x))
    }()
  }

  return results
}</code></pre>
<p>This will lead to a <strong>race condition</strong>. The problem
is that the function <code>f</code> will (more or less) return
immediately and capture the pointer to <code>results</code>, while
the goroutines make an assignment to <code>results</code>.</p>
<p>You can avoid this problem in multiple ways:</p>
<ol>
<li>do not <code>defer</code> the <code>Wait()</code> call, but
call it manually right before the <code>return</code>.</li>
<li>do not assign to the return variable in the goroutines. Instead
use the index assignment, like <code>results[i] =
calc(x)</code>.</li>
<li>use a named return value.</li>
</ol>
<h2>(1): manual calling Wait</h2>
<p>Manually deferring the <code>Wait()</code> is very easy, to code
example above would change to this:</p>
<pre><code>func f(xs []int) []string {
  var mu sync.Mutex
  var wg sync.WaitGroup

  results := make([]string, 0, len(xs))

  for _, x := range xs {
    wg.Add(1)
    go func() {
      defer wg.Done()
      mu.Lock()
      defer mu.Unlock

      // some calculations
      results = append(results, calc(x))
    }()
  }

  wg.Wait()

  return results
}</code></pre>
<h2>(2): do not assign to the return variable</h2>
<p>Since the underlying problem illustrated here is about the
assignment to the captured return variable, we may just not
re-assign it in goroutines:</p>
<pre><code>func f(xs []int) []string {
  var mu sync.Mutex
  var wg sync.WaitGroup
  defer wg.Wait()

  results := make([]string, 0, len(xs))

  for i, x := range xs {
    wg.Add(1)
    go func() {
      defer wg.Done()
      mu.Lock()
      defer mu.Unlock

      // some calculations
      results[i] = calc(x)
    }()
  }

  return results
}</code></pre>
<p>In this example, this may work because we know the length of the
returned slice ahead of time, but that may not always be
possible.</p>
<h2>(3): use a named return</h2>
<p>We can also use a named return which changes when Go captures
the return value. The following works just fine:</p>
<pre><code>func f(xs []int) (results []string) {
  var mu sync.Mutex
  var wg sync.WaitGroup
  defer wg.Wait()

  for _, x := range xs {
    wg.Add(1)
    go func() {
      defer wg.Done()
      mu.Lock()
      defer mu.Unlock

      // some calculations
      results = append(results, calc(x))
    }()
  }

  return results
}</code></pre>
<h2>Conclusion</h2>
<p>Pay attention when using <code>sync.WaitGroup</code> and
deferring <code>Wait()</code> calls. Make sure your return values
or whatever is awaited with the wait group doesn't depend on simple
variable assignments.</p>
</div>
    </content>
  </entry>
  <entry>
    <title>Review of my first month at Migadu</title>
    <id>https://furrer.life/~timo/writings/2025/03/06/review-of-first-month-at-migadu</id>
    <link rel="alternate" type="text/html" href="https://furrer.life/~timo/writings/2025/03/06/review-of-first-month-at-migadu"/>
    <published>2025-03-06T10:45:34+01:00</published>
    <updated>2026-05-18T20:26:42+02:00</updated>
    <author>
      <name>Timo Furrer</name>
      <email>timo@furrer.life</email>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>I've already hinted in <a href="writings/2025/02/18/migadu-caldav-carddav-auto-discovery" title="Migadu.com CalDav and CardDav auto-discovery">Migadu.com CalDav
and CardDav auto-discovery</a> that I've moved my personal email,
calendar and contacts away from Google to <a href="https://migadu.com" title="Migadu.com">Migadu</a>.</p>
<p>A little more than a month has passed now and I want to briefly
recap of what went well and what didn't.</p>
<h2>E-Mail</h2>
<p>Migadu's primary focus is email and so far I've been very
pleased with the on-boarding process, like the DNS setup for my
domains and the admin interface overall.</p>
<h3>Migrate emails</h3>
<p>The migration of my Gmail address was really straight-forward by
using <a href="https://imapsync.lamiral.info/" title="Imapsync website">Imapsync</a>. However, since that particular
address has been in used for 15 years it accumulated quite a bunch
of emails and Gmails <code>All Mail</code> folder that contains a
copy of each and every email in all folders (yes, their labels are
folders with copies of your emails) didn't particularly help with
the mailbox size. I took the better half of a week to fully sync my
mailbox from Gmail to Migadu. I'd also recommend to use
<code>imapsync --dry ...</code> to verify what imapsync will
perform.</p>
<h3 id="plus-addressing">Support for plus addressing</h3>
<p>Support for plus addressing was a must for me, but the default
configuration in Migadu is a little weird to me taste. They
basically <a href="https://www.migadu.com/guides/plus_addressing/">auto-create
folders for the detail part</a>. So emails sent to
<code>timo+test@furrer.life</code> would automatically end up in an
IMAP folder called <code>test</code>. That folder is also
auto-created, meaning that strangers are allowed to create IMAP
folders by simply sending an email. I'm not sure under what
circumstances someone would ever want that. Anyways, they document
how to disable it. Make sure though that you account for the
rewrites in your Sieve scripts, because the
<code>Delivered-To</code> header will then point to
<code>timo@furrer.life</code> instead of
<code>timo+test@furrer.life</code>. Luckily, we have the
<code>X-Envelope-To</code> header to use (that contains the SMTP
RCT header value) - although you cannot use the
<code>envelope</code> extension, because of their architecture and
how they delivery emails to your actual mailbox via proxies.</p>
<h3>Support for Sieve</h3>
<p>Sieve is a language to filter emails based on conditional logic
- basically a bunch of <code>if</code>s. In case you have ever used
Sieve, you wouldn't want to trade it with e.g. Gmail's filters.
Migadu hosts a managed sieve endpoint that you can enable access
for on an individual mailbox basis. You may use any sieve client to
connect to it with your mailbox credentials. I'd recommend <a href="https://github.com/philpennock/sieve-connect" title="sieve-connect GitHub project">sieve-connect</a> to upload the
Sieves scripts. It's probably worth pointing out that you should
test your scripts in a separate mailbox to not loose any incoming
emails by accident.</p>
<p>You can easily upload and activate a sieve script without using
the REPL, like this:</p>
<pre><code>sieve-connect -s imap.migadu.com -u timo@furrer.life --localsieve ./main.sieve --upload
sieve-connect -s imap.migadu.com -u timo@furrer.life --localsieve ./main.sieve --activate</code></pre>
<p>As mentioned in <a href="writings/2025/03/06/review-of-first-month-at-migadu/#plus-addressing" title="Support for plus addressing">Support for plus addressing</a>
above, you can't use the <a href="https://www.rfc-editor.org/rfc/rfc5233" title="Sieve Email Filtering: Subaddress Extension"><code>envelope</code>
Sieve extension</a>. According to the Migadu support that's because
<code>dovecot</code> and <code>postfix</code> are not on the same
servers. I'm not expert enough to actually tell if it wouldn't
somehow be possible to still use or support the
<code>envelope</code> extension, but yeah, it's a bummer.</p>
<p>For example, if <code>envelope</code> would be supported you
could easily refile to a folder like this:</p>
<pre><code>if envelope :detail "to" "test" {
  fileinto "test";
}</code></pre>
<p>Instead we have to resort to other headers, like
<code>X-Envelope-To</code>:</p>
<pre><code>if header :contains "X-Envelope-To" "timo+test@furrer.life" {
  fileinto "test";
}</code></pre>
<h2>Calendars and Contacts</h2>
<p>Migadu officially states that they only have very basic CalDav
and CardDav support:</p>
<figure>
<blockquote cite="https://www.migadu.com/procon/#basic-calendar">
<p>We make the basic CalDAV and CarDAV services available, but they
are not our focus. We continue developing them but please do not
expect we will ever compete with dedicated calendar services.</p>
</blockquote>
<figcaption>From <cite><a href="https://www.migadu.com/procon/#basic-calendar">https://www.migadu.com/procon/#basic-calendar</a></cite></figcaption>
</figure>
<p>And after using it for a while I can confirm exactly that. The
problem is calendar scheduling, that is, replying to events and
receiving replies from attendees on events that I'm organizing.
Migadu right now supports neither. They are specifically missing
support for <a href="https://datatracker.ietf.org/doc/html/rfc6047" title="iCalendar Message-Based Interoperability Protocol (iMIP)">iMIP</a>
and <a href="https://datatracker.ietf.org/doc/html/rfc5546" title="iCalendar Transport-Independent Interoperability Protocol (iTIP)">iTIP</a>.
So far, that's been the most limiting factor of the migration.
Right now, I'm manually updating attendee replies and sending out
replies for my own participation status.</p>
<h2>Conclusion</h2>
<p>I still have plenty of things to explore and address. Especially
the shortcomings in calendar scheduling support. I'm exploring
other hosting options for CalDav and CardDav, preferably
self-hosted. However, I'm also tempted to implement a little
bridging services for iMIP and iTIP.</p>
</div>
    </content>
  </entry>
  <entry>
    <title>No query strings here either</title>
    <id>https://furrer.life/~timo/writings/2026/05/19/no-query-strings</id>
    <link rel="alternate" type="text/html" href="https://furrer.life/~timo/writings/2026/05/19/no-query-strings"/>
    <published>2026-05-19T12:00:00+02:00</published>
    <updated>2026-05-19T09:50:30+02:00</updated>
    <author>
      <name>Timo Furrer</name>
      <email>timo@furrer.life</email>
    </author>
    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>A couple of weeks ago, Chris Morgan published <a class="u-in-reply-to" href="https://chrismorgan.info/no-query-strings" title="I've banned query strings - Chris Morgan"><cite>I've banned
query strings</cite></a>. I read it, liked it and then did roughly
the same thing on my own site - with two deliberate
differences.</p>
<p>Chris's opening sums up the motivation better than I could:</p>
<figure>
<blockquote cite="https://chrismorgan.info/no-query-strings">
<p>I don't like people adding tracking stuff to URLs. Still less do
I like people adding tracking stuff to <em>my</em> URLs.</p>
<p>[...] UTM parameters are for <em>me</em> to use, not
<em>you</em>. Leave my URLs alone.</p>
</blockquote>
<figcaption>From <cite><a href="https://chrismorgan.info/no-query-strings">chrismorgan.info/no-query-strings</a></cite></figcaption>
</figure>
<p>The premise is the same here. A <code>?utm_source=...</code> or
<code>?ref=...</code> tacked onto one of my URLs by some
intermediary is, at best, noise I never asked for and at worst a
tracker the referrer is using to nudge my visitor's behaviour into
a funnel. I'd rather refuse to serve those requests than pretend
they're a legitimate way to reach a page on my site. I'm also a fan
of having one true <em>canonical</em> URL to all my pages.</p>
<h2>Where I differ from Chris</h2>
<h3>1. cache-busters like <code>?v=&lt;digits&gt;</code> are
allowed</h3>
<p>Chris went for a true blanket ban - including breaking old
cache-busting URLs like <code>?t=...</code> and <code>?h=...</code>
that his site used to serve. That's likely the right call when none
of those URLs are still in circulation. In any case, those might
only be used for static assets anyways, where people don't have
bookmarks to.</p>
<p>My situation is slightly different: I actively use
<code>?v=&lt;n&gt;</code> as a cache buster on assets I serve
today. The very HTML you're reading links to
<code>main.css?v=1</code>. I use it so that I can set a very high
<code>Cache-Control: max-age: ...</code> on static assets. If I
matched Chris's strictness I'd have to either give up on
query-string cache busting (and switch to fingerprinted filenames
or <code>Cache-Control</code> juggling), or break my own page load
on every bump.</p>
<p>So my rule is a narrow allowlist: <strong>everything is blocked,
except <code>?v=&lt;digits&gt;</code></strong>. The matcher is
intentionally strict - the whole query string must be exactly
<code>v=</code> followed by digits, nothing else, no extra
parameters smuggled in alongside.</p>
<pre><code>(no_query_strings) {
    @bad_query `{http.request.orig_uri}.contains("?") &amp;&amp; !{http.request.uri.query}.matches("^v=[0-9]+$")`
    error @bad_query 403
}</code></pre>
<p>The first clause uses <code>orig_uri</code> so a bare trailing
<code>?</code> still trips the ban - Caddy's <code>{query}</code>
placeholder can't distinguish "absent" from "empty", and a lone
<code>?</code> deserves the same treatment as a parameter list. The
second clause uses the canonical <code>{uri.query}</code> because
Caddy doesn't expose <code>.query</code> as a sub-key on
<code>orig_uri</code> - the rewrite never touches the query so the
two are equivalent here.</p>
<h3>2. 403 Forbidden, not 414 URI Too Long</h3>
<p>Chris picked <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-414-uri-too-long">
414 URI Too Long</a>, and is upfront about it:</p>
<figure>
<blockquote cite="https://chrismorgan.info/no-query-strings?">
<p>You could argue that I'm abusing 414 URI Too Long. I respond
that it's funnier this way.</p>
</blockquote>
<figcaption>From Chris's <a href="https://chrismorgan.info/no-query-strings?">ban
page</a></figcaption>
</figure>
<p>It's indeed nice, but I wanted to pick a status code that I can
defend on RFC grounds rather than vibes. Here's how I read <a href="https://datatracker.ietf.org/doc/html/rfc9110">RFC 9110</a> and
<a href="https://datatracker.ietf.org/doc/html/rfc7725">RFC
7725</a> for this case:</p>
<dl>
<dt><a href="https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request">
400 Bad Request</a></dt>
<dd><q cite="https://datatracker.ietf.org/doc/html/rfc9110#name-400-bad-request">
The server cannot or will not process the request due to something
that is perceived to be a client error (e.g., malformed request
syntax)</q>. The request isn't malformed;
<code>?utm_source=x</code> is perfectly well-formed. Too
generic.</dd>
<dt><a href="https://datatracker.ietf.org/doc/html/rfc9110#name-403-forbidden">403
Forbidden</a></dt>
<dd><q cite="https://datatracker.ietf.org/doc/html/rfc9110#name-403-forbidden">The
server understood the request but refuses to authorize it</q>. That
is exactly what's happening: I understood the request, the URL
would otherwise resolve, and I am refusing on policy grounds. The
spec also explicitly notes that a server <q cite="https://datatracker.ietf.org/doc/html/rfc9110#name-403-forbidden">can
describe that reason in the response content</q>, which is what the
body of the 403 page does.</dd>
<dt><a href="https://datatracker.ietf.org/doc/html/rfc9110#name-404-not-found">404
Not Found</a></dt>
<dd>Misleading. The resource exists; I just won't serve it via this
URL. Also has unpleasant SEO and caching side effects.</dd>
<dt><a href="https://datatracker.ietf.org/doc/html/rfc9110#name-414-uri-too-long">
414 URI Too Long</a></dt>
<dd>A refusal to service the request because the request-target
<q cite="https://datatracker.ietf.org/doc/html/rfc9110#name-414-uri-too-long">
is longer than the server is willing to interpret</q>. The
objection is about <em>length</em>, not policy or content.
Although, I agree that one could argue that everything after the
canonical URL is <em>too long</em>.</dd>
<dt><a href="https://datatracker.ietf.org/doc/html/rfc7725">451
Unavailable For Legal Reasons</a></dt>
<dd>Not legal reasons. Just personal taste.</dd>
</dl>
<p>403 is the cleanest semantic match. I'm not 100% certain, but
I'd interpret the "authorize" from RFC9110 as not only HTTP
authentication or authorization, but rather the more general sense
of "permit".</p>
<p><em>(Okay, Chris is right that 414 is funnier, though. I'll
concede that one.)</em></p>
<h2>What it looks like</h2>
<p>When a request comes in with anything other than
<code>?v=&lt;digits&gt;</code>, Caddy short-circuits with a 403 and
serves a small explainer page. You can try it yourself: <a rel="nofollow" hx-boost="false" href="https://furrer.life/~timo/?utm_source=this-post">furrer.life/~timo/?utm_source=this-post</a>.
The page tells you what happened, why, and offers the same URL
without the query string.</p>
<p>If you want to follow Chris down this path, his post links the
relevant <code>Caddyfile</code> snippet on his site or use mine
above which is a small variant of that with the extra allowlist
clause shown above.</p>
</div>
    </content>
  </entry>
</feed>
