253 of my pages answered 200 to a browser and 404 to anything asking whether they existed
Nobody browsing a site sends a HEAD request, which is why this sat there for months with every check I had staying green.
HEAD is meant to be GET without the body, so one URL cannot honestly answer both ways. Mine did. 253 pages answered 200 to a normal request and 404 to a HEAD: the whole template matrix, every guide, four hub pages and the API reference. My framework matches a route against the method list that route declares, and those declared GET alone. An undeclared HEAD matched no route at all and fell through to the 404 handler.
The clients that send HEAD are the ones asking whether a page exists rather than asking for it. Link checkers. Uptime monitors. Shared caches revalidating a copy they already hold. That last one is what made this expensive rather than untidy. Those pages are served with five minutes of freshness and a day of stale-while-revalidate, so a cache is invited to keep them and check back. A 404 to that check is not a slow revalidation, it is a discarded copy.
So here is the check, and it is about two minutes. Take ten of your own URLs, ideally ones written at different times, send each a HEAD, and compare the status against a normal GET of the same address. Anything that disagrees is broken for a class of client you will never see in analytics, because none of them run JavaScript and most never appear in a session count at all.
Then the part I got wrong after fixing it. I wrote a test that reads every route in the build and fails when one declares GET without HEAD, which is the right shape for a rule nobody can see from the page. I scoped it to the group my public pages live in. There is a page outside that group, reached only from a link in an email, and it declares GET alone to this day. The guard reads a group. The defect lives in whatever is not in it.
Which makes the second check the one on your own instrument: whatever list it walks, go and find what is not on that list.
If you run the ten, I would like to know whether any of yours disagree, and if none do, whether your framework declares the pair for you or you have been writing both by hand.
Replies
Have not run the ten against my own pages yet so I will not claim a result, but the stale while revalidate detail is the one worth sitting with longest. A cache doing a background revalidation is not asking to see the page, it is asking whether the copy it already serves is still good, and a router that treats an unmatched HEAD as no route rather than GET without a body punishes exactly the client that was behaving best. The failure is also asymmetric in a way that makes it hard to catch by accident, a browser never sends the request that trips it, so nothing in normal traffic or session analytics will ever surface it, only the client you are least likely to be watching for. The rule worth keeping past this thread, any route matching that keys off an explicit method list needs HEAD added deliberately wherever GET is added, most frameworks do not infer the pair for you, yours clearly did not either until you wrote the guard by hand.