Skip to content

48a09586 - Invite and promo links answer 404 in production - #34

Merged
TaprootFreak merged 43 commits into
developfrom
48a09586-landing-404-status
Sep 9, 2026
Merged

48a09586 - Invite and promo links answer 404 in production#34
TaprootFreak merged 43 commits into
developfrom
48a09586-landing-404-status

Conversation

@TaprootFreakAI

@TaprootFreakAI TaprootFreakAI commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

EN. Every invite and promo link answered HTTP 404 in production. The body was right — the landing shell with the rewritten meta tags — but the status was not, so share crawlers dropped the response before reading the tags the middleware exists to write, and every shared referral link previewed as nothing. This is the open question #30 deferred and #32 promised to settle with one curl after deploy; the curl was run, and this is the answer plus the fix.

DE. Jeder Einladungs- und Promo-Link antwortete produktiv mit HTTP 404. Der Inhalt stimmte, der Status nicht. Vorschau-Dienste verwerfen solche Antworten, bevor sie die Metadaten lesen, für die es die ganze Umschreibung gibt — geteilte Empfehlungslinks zeigten deshalb keine Vorschau. Das ist die Frage, die #30 offenließ und #32 auf „ein curl nach der Auslieferung" vertagte. Der Aufruf ist erfolgt, hier ist die Antwort samt Korrektur.

What was measured
curl -sI https://realunit.app/invite/AB12CD     ->  HTTP/2 404
curl -sI https://www.realunit.app/invite/AB12CD ->  HTTP/2 404
curl -sI https://dev.realunit.app/promo/EVT1    ->  HTTP/2 404
curl -s  https://realunit.app/invite/AB12CD | grep title
                                                ->  <title>RealUnit — Einladung AB12CD</title>

The body is the landing shell, correctly rewritten. /invite/ without a code answers 200, so only the code-bearing paths — the ones people actually share — were affected.

Cause. public/_routes.json sends /invite/* to the Function. The asset lookup behind context.next() resolves the code-less shell through the _redirects 200-rewrite but keeps the not-found status of the path that was asked for, and the middleware copied that status through unchanged.

Impact. A browser renders a 404 body, so a person following the link saw the right page and the lookup ran. A crawler did not: WhatsApp, iMessage, Slack, Facebook and X drop a 404 before parsing og:*. The rewrite these pages are built around therefore reached nobody.

The fix

landingStatus(status, html) promotes 404 to 200 only when the body carries both landing marks — id="state-loading" and aria-busy="true" — which both landings have in the same element and the site's own 404 page has neither of. One mark alone would have promoted any error document that happens to contain it. Without the guard, a broken deploy serving 404.html on these paths would start answering 200 and look healthy.

The check stays a substring test, so what makes two marks enough is a property of the site rather than of the parser — and that property is asserted rather than described: of every HTML page under public/, only the two the Function is routed to read as a landing shell. The account-merge and Aktionariat shells carry the id and no aria-busy; the 404 page carries neither. The test goes red the day one of them gains the second mark, which is the day the guard would have to become a real parse.

Everything around that guard exists because a review round found it missing:

  • HEAD answers like GET. It is the method curl -sI sends and the one a link checker sends first, so the command quoted above as the proof would otherwise still have read 404. A HEAD response carries no body and the body is what tells the shell from the 404 page, so HEAD asks for the GET-equivalent and answers with that status and those headers, without a body.
  • Both methods ask for the whole document. Range, If-Range and the four conditional request headers are dropped, because 206, 304 and 412 are all answers this pass cannot rewrite.
  • 204, 205, 206 and 304 are handed on untouched. A 304 keeps the validator it was matched on. An earlier revision paired the rewritten string with those statuses, which throws — a conditional request would have become a 500.
  • The rewrite only runs on a landing shell. On a broken deploy the site's own 404 page keeps its status and its own copy; rewriting its title into an invitation would describe something the visitor is not looking at.
  • Headers the rewrite invalidates are dropped: the length, the content coding, both validators, the range metadata and the integrity digests. Measured: these paths really are served gzipped, and a body labelled gzip that is not gzip does not render at all.
  • The Content-Type is read as a media type, case-insensitively and with quotes and escapes respected, and a body declared in another encoding is handed on byte for byte rather than decoded as UTF-8 and relabelled.

functions/_middleware.js was outside coverage.include while it decided the status every crawler sees — which is how the original bug shipped through a full double review. It is measured now, at 100%, with its own test file.

Verification
  • npm run check: exit 0, 192 tests, all thresholds met, functions/_middleware.js at 100% in all four metrics.
  • CI green at this head, checked per check-run against the head SHA rather than by branch name.
  • Every guard is verified by mutation rather than by assertion alone. Each of these turns a test red: reverting the promotion; removing the marker guard; widening it to any 4xx; loosening the two marks to the bare words; keeping the reason phrase on a promoted status; dropping the GET-equivalent lookup for HEAD; returning a body for HEAD; keeping content-length, content-encoding, accept-ranges or a digest after a rewrite; matching the media type as a substring; dropping the charset guard; dropping quote-awareness or the closing-quote check; letting the last charset win; rebuilding a pass-through answer instead of handing it on; renaming a mark in the shipped landing; adding both marks to the shipped 404 page; giving any other shipped page the second mark; breaking the og:url or the twitter:url replacement alone; swapping the Android and iOS hand-offs; widening the path prefix.
  • 43 commits, every one of them re-reviewed as unreviewed code by four lanes — two vendors × conformity and logic. A round closed only when all four reported nothing that changes behaviour for a user or a crawler.
Deliberately not fixed here

A stated deviation from RFC 9110 §13.1. Dropping the conditional request headers means a client sending If-None-Match: * is answered 200 with the whole document where 304 was called for, and If-Match never produces 412. The two kinds of precondition are not equally hard. A tag or date condition would need a validator minted for the rewritten document, which none of these paths emits and which is a larger change than this one. The wildcard forms need no validator at all, so they are dropped for uniformity rather than out of necessity. Either way the client gets the current representation, so the body is never stale; the status is what diverges, and the cost is a larger answer than was called for, not a wrong one. The code, CONTRIBUTING and README all say so.

Two asset-extension lists that disagree, from #30. shouldRewriteItunesBanner excludes js|css|map|png|svg|json|jpg|jpeg|webp|ico|txt|xml; parseLandingFromUrl treats js|css|map|png|svg|json|html as an asset suffix. Codes may contain internal dots, so /invite/ABC.JSON skips the middleware entirely while /invite/ABC.HTML loses its code. Measured on the deploy rather than argued: /invite/AB.JSON and /promo/EVT1.HTML are answered with the site's own 404 page, not with the landing shell, so the promotion would refuse them on the marks in any case. What a code like that loses is the landing itself. Both functions predate this PR and the lists want reconciling in their own change.

Two sham tests in already-merged code, found while reviewing this one: the clipboard stub in tests/behavior.spec.mjs discards the value it receives, so a test titled "copies the canonical invite URL" would pass on any string; and the 15-second lookup timeout is asserted only as a constant, never as a timeout. Follow-up.

The body-carrying fallback in asFullGet is covered by unit tests, not by the platform. A GET or HEAD that arrives with a body cannot be re-methoded, so the request is rebuilt from its URL, which drops the platform's own request metadata. The tests pin the headers and the method; whether Pages ever delivers such a request, and what the dropped metadata would have been worth, is untested against the real runtime.

Measured against the deploy, which is what #30 left open and #32 promised to
settle with one curl:

  curl -sI https://realunit.app/invite/AB12CD  ->  HTTP/2 404

The body is correct — the landing shell with the rewritten meta tags — but the
status is not. _routes.json sends /invite/<code> to the Function, and the asset
lookup behind context.next() resolves the code-less shell through the
_redirects 200-rewrite while keeping the not-found status of the path that was
asked for. The middleware copied that status through.

Browsers render a 404 body, so a person following the link sees the right page.
Share crawlers do not: they drop a 404 before reading the tags the middleware
just wrote, which is the entire reason the rewrite exists. Every invite and
promo link shared today therefore previews as nothing.

The promotion is guarded on the landing marker, so a broken deploy that serves
the site's own 404 page on these paths keeps saying 404 instead of looking
healthy, and the reason phrase is dropped rather than left reading "Not Found".

functions/_middleware.js was outside coverage while it decided the status every
crawler sees, which is how this shipped unnoticed. It is measured now, at 100%,
with the entry point's own test file.

npm run check: exit 0, 173 tests. Verified by mutation: reverting the promotion
turns three tests red, and removing the marker guard turns the two that protect
a genuine 404 red.

The other half of the same open question is now answered too: _headers do apply
to Function-served requests. CSP, X-Frame-Options and Referrer-Policy are all
present on https://realunit.app/invite/AB12CD.
The conformity lane found the documentation and the config describing a
narrower change than the one that landed, which is the same drift #30 had to
correct in this file.

- CONTRIBUTING said public/js/lib was the only code with a coverage gate and
  the gate table named only that path; functions/lib and the Function entry
  point carry one too.
- The ground rule described the middleware as a pure byte rewrite with
  "nothing else transformed", which no longer holds now that it also decides
  the status a crawler sees.
- The vitest comment still claimed the measured surface was side-effect-free
  browser logic only.
- The file header of the middleware did not mention the promotion at all.

Two lanes independently found the same test gap: the promoted responses
asserted their status but not their reason phrase, so reverting the ternary
would have stayed green at 100% branch coverage. Both promotion tests now
assert the empty phrase, and the untouched-404 test asserts it keeps
"Not Found".

npm run check: exit 0, 173 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping the reason phrase on promotion turns the two promotion tests
red.
The conformity lane found the middleware test building its own landing shell
and its own 404 page. The promotion keys on a marker that has to be in the
landings and must never be in the site's 404 page, and a synthetic fixture
keeps passing after someone moves that marker — production would answer 404
again with nothing going red, which is exactly how this shipped.

The test now reads public/invite/index.html, public/promo/index.html and
public/404.html, and asserts the marker contract against them directly.

npm run check: exit 0, 174 tests. Verified by mutation on the real files:
renaming the marker in the landing turns two tests red, and adding it to the
404 page turns two others red.
The logic lane caught the fix stopping at GET. That is the method this bug was
measured with: `curl -sI` sends HEAD, so the very command quoted as the proof
would still have read 404 after the fix, and a link checker that sends HEAD
first would still see the link as dead. HEAD has to answer with the same status
as GET.

HEAD is now handled alongside GET and returns no body of its own. A HEAD whose
body the platform withheld keeps the status it came with: without a body there
is no way to tell the landing shell from the site's own 404 page, and guessing
from the path alone would make a broken deploy look healthy.

The same lane found the header pass-through untested. The fixture now carries
the Cache-Control rule public/_headers sets for these paths, and the promotion
test asserts it survives the response being rebuilt.

Also recorded in the marker comment: two other pages carry the same id. It does
not matter, because the caller has already established the path is a landing,
so the only open question is shell versus 404 page.

npm run check: exit 0, 176 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping HEAD from the guard, returning a body for HEAD, and keeping
content-length instead of cache-control each turn one test red.
The logic lane found the HEAD path asserting only status, reason phrase and an
empty text(). Two regressions would have stayed green: keeping content-length
for HEAD alone, and returning an empty body instead of no body, which text()
cannot tell apart.

The HEAD case now asserts body === null and the same three headers the GET case
does.

npm run check: exit 0, 176 tests. Verified by mutation: an empty body for HEAD
and a HEAD-only content-length each turn one test red.
The conformity lane found README describing the middleware as byte injection
only, and its testing section naming public/js/lib as the sole measured
surface. CONTRIBUTING names README as the architecture reference, so leaving it
behind recreates the drift the last PR had to correct.

Both passages now match what the code does.
The logic lane found the HEAD path resting on an assumption instead of a fact.
The status has to be decided from the body, because the marker in it is what
tells the landing shell from the site's own 404 page — and a HEAD response has
no body. The previous revision kept the status when the body was missing, so if
the platform hands the Function nothing for HEAD, the same link would have read
as found by GET and as dead by the HEAD a link checker sends first. That
contradicted the comment right above it.

HEAD now asks for the GET-equivalent through context.next() and answers with
that status and those headers, without a body. Nothing is left to the platform,
and the outcome no longer waits on a deploy to be known.

The same lane found the fixture carrying no security headers, so a middleware
that dropped one would not have failed a value assertion anywhere. The fixture
now carries what public/_headers sets for these paths, and both the GET and the
HEAD case assert every one of them survives.

npm run check: exit 0, 176 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the GET-equivalent lookup, deleting a security header, and
returning the GET body to a non-HTML HEAD each turn one test red.
Four accuracy findings from the conformity lane, all of them places where a
comment claimed more than it should:

- The "Browser JS and unit coverage" section still read as if everything
  outside public/js/lib were covered by Playwright alone. Two surfaces carry
  their own gate and are not page scripts at all.
- The test fixture called its Content-Security-Policy value "what
  public/_headers sets". It is a short stand-in for a much longer policy; only
  the cache-control value is the real one.
- "The contract the two tests above rely on" sat above two tests that do not
  use the marker at all.
- "the tags written just above" was copied out of the middleware header, where
  it had a referent. In the test it had none.

npm run check: exit 0, 176 tests.
…HEAD on a 404

Two low findings from the conformity lane.

The fixture hand-copied a shortened Content-Security-Policy while calling it
what public/_headers sets. It now reads the real values out of that file, so
the fixture cannot drift from production and the pass-through assertion is
always about the shipped policy. Measured, and written into the comment so the
next reader is not misled: this checks the pass-through, not the policy —
changing _headers moves both sides together by design, and a middleware that
drops a header is what turns red.

The marker guard was only exercised on GET. There is now a HEAD case against
the real 404 page, so a regression that evaluates the guard for GET alone has
somewhere to fail.

npm run check: exit 0, 177 tests. Verified by mutation: deleting referrer-policy
in the middleware and weakening the marker guard each turn a test red.
The logic lane found the synthetic GET inheriting every header of the HEAD it
came from, Range and If-Range included. A HEAD carrying Range would have come
back as 206 Partial Content, whose body is not the landing shell: the status
would neither have been promoted nor meant what the client asked for.

Both headers are now dropped and everything else the client sent is kept. The
test asserts exactly that, so a regression that forwards Range, or one that
throws the client's headers away altogether, has somewhere to fail.

The same lane found the non-HTML HEAD branch asserting status and body but not
headers, though it rebuilds the response too. It now checks them.

npm run check: exit 0, 178 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping Range, dropping the client headers, and building the
body-less response with empty headers each turn one test red.
The conformity lane caught me reintroducing, in the middleware header, the
exact wording I had just corrected in the test file: "the tags written just
above", where nothing is above. It now says what it means.

The three places that mention the functions/lib ratchet now say where it is
set, instead of calling it documented and leaving the reader to find it.

npm run check: exit 0, 178 tests.
The logic lane found that widening the condition from 404 to the whole 4xx
range would have passed the entire suite. Only the 404 Pages produces for these
paths is an artefact; a 403 or a 410 means what it says, even when the body
happens to be the shell — promoting those would turn a refusal or a withdrawn
code into a healthy page.

landingStatus now pins 403, 410 and 451, and the middleware has a case that
sends a refusal through untouched.

npm run check: exit 0, 179 tests. Verified by mutation: widening the condition
to `status >= 400 && status < 500` turns both new tests red.
The logic lane found the previous commit cleaning the request on one path only.
A real GET carrying Range would still have been forwarded with it, and this
pass rewrites the whole document: a partial representation would be injected
into a fragment and returned under a Content-Range describing the bytes before
the rewrite.

Both methods now ask for the same thing — the whole document as a GET — so the
two paths share one helper and one comment instead of drifting apart again.

Measured against the deploy before changing anything:

  curl -sI -H 'Range: bytes=0-99' https://realunit.app/invite/AB12CD
  -> 404, and the body is the full 4162 bytes, not a fragment

Pages answers a Range request on these paths with the whole document today, so
this is a guard rather than a live fix, and the comment says so.

npm run check: exit 0, 180 tests, functions/_middleware.js at 100%. Verified by
mutation: cleaning only the HEAD path, and keeping Range at all, each turn tests
red.
…rite

The logic lane found ETag and Last-Modified passed through unchanged. They
describe the document the asset server produced, not the one this pass sends,
and a stale validator is worse than none: a conditional request would be
answered 304 against a document the client never received. Content-Range gets
the same treatment for the same reason.

Measured against the deploy before changing anything: Pages sets neither ETag
nor Last-Modified on these paths today, so this is a guard rather than a live
fix, and the comment says so.

The refusal case now also asserts the body passes through, not just the status.

The conformity lane found the Range behaviour missing from both architecture
documents; CONTRIBUTING and README now say that GET and HEAD are resolved
internally as one full GET. The measurement note in the code also says what was
actually measured — the body of a Range GET, not only its headers, which is
what the previous commit message implied.

npm run check: exit 0, 181 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping only content-length, and building the header set empty, each
turn tests red.
…body with 304

The logic lane found the helper promising more than it delivered. It ruled out
206 by dropping Range, but forwarded the conditional headers untouched, so the
answer could still come back as 304 or 412 — bodies that are not the landing
shell either, on a path where the status is decided from the body.

Worse, a 304 would have crashed the request: the previous revision paired the
rewritten string with that status, and constructing a Response with a body and
a null-body status throws. A conditional request would have become a 500
instead of being passed on.

Both are closed. Every header that could make the answer something other than
the full document is dropped, and 204, 205 and 304 are answered without a body.

The same lane found that re-methoding a request that carries a body throws,
which would have aborted the middleware rather than answering. That case now
falls back to rebuilding from the URL, which loses the platform's request
metadata but keeps the request answerable, and a test drives it.

npm run check: exit 0, 184 tests, functions/_middleware.js at 100%. Verified by
mutation: narrowing the header list back to Range, treating only HEAD as
body-less, and removing the fallback each turn a test red.
…hing"

The logic lane left this as an open question rather than a claim, and the
measurement settled it:

  curl -sSI --compressed https://realunit.app/invite/AB12CD
  -> content-encoding: gzip

These paths really are served gzipped. The rewrite always hands on a decoded
string, so a Content-Encoding inherited from the asset response would label it
as something it is not, and a body labelled gzip that is not gzip does not
render at all. Whether the header is on the object the Function sees or only
added by the edge afterwards is not knowable from here — dropping it costs
nothing in the second case and prevents a blank page in the first.

The conformity lane found the comment above that list, and the test title,
saying "everything that described the bytes" while the code covers a named set.
That is the same overclaim this series has now corrected four times. Both name
the five headers instead, and the helper's comment names the six request
headers RFC 9110 defines rather than claiming to cover every possible one.

npm run check: exit 0, 184 tests, functions/_middleware.js at 100%. Verified by
mutation: keeping Content-Encoding turns the test red.
…aviour

The conformity lane found the README attributing the status promotion, the
HEAD parity and the unconditional full GET to both injection paths, because
the sentence followed a bracket naming the Function and the dev-server
together. scripts/dev-server.mjs has none of it.

While correcting that I checked the file rather than trusting the finding, and
the first correction I wrote was wrong too: the dev-server does answer 404, on
its own not-found path. What is true is narrower — it shares the injection and
answers HEAD without a body, but has no promotion, no marker guard and no
header stripping, because its routing serves the landings as 200 and never
produces the not-found status the promotion exists to correct. Both documents
now say that, and both name the conditional validators the helper strips
alongside Range.

The logic lane added two small ones, both taken:

- The integrity digests of RFC 9530 and its predecessors describe the bytes
  this pass replaces, so they join the stale list. Not observed on this deploy;
  the same guard as the validators.
- The body-carrying-GET fallback asserted the method and URL but not the
  headers, so rebuilding it with the client's untouched headers would have
  stayed green. It now checks that the cleaned set is what gets forwarded.

npm run check: exit 0, 184 tests. Verified by mutation: dropping a digest from
the list, and forwarding the raw headers in the fallback, each turn a test red.
Two precision findings from the conformity lane, both the same reflex this
series keeps catching: a comment claiming a little more than the code holds.

- The stale-header comment called ETag and Last-Modified byte descriptions.
  They are validators of the selected representation, and a weak ETag
  explicitly does not promise byte equality. The reason they go is that
  rewriting the representation invalidates them, which is what it now says.
- A test was titled "keeps the request headers but drops Range" while the
  helper drops four conditional headers as well; its comment said everything
  else is kept. The title and comment now describe that case, and point at the
  case that covers the conditional ones.

npm run check: exit 0, 184 tests.
… the fallback

Four findings from the logic lane, after it downgraded two of its own and
rejected two more as unsupported.

- response.text() decodes as UTF-8 and a string body is encoded as UTF-8, so
  an origin charset carried through would have been a lie. The rewritten
  answer now declares text/html; charset=utf-8 itself.
- A 206 cannot be rewritten: the body is a fragment and the range metadata
  would stop matching it. Range is stripped before the lookup, so this only
  fires for an origin that answers 206 unasked, but the guard is one line.
- The body-carrying-GET fallback kept the headers that described that body, so
  the rebuilt request announced a body it does not carry.
- The non-HTML branch deliberately drops only content-length, because nothing
  was rewritten there and the validators still hold. No test showed that, so a
  regression widening it would have passed. It is asserted now.

The comment on the non-HTML branch also says what it previously left out: the
request has already lost Range and the conditional headers by then, which is
harmless only because these paths serve HTML alone.

While verifying, one of my own mutation checks came back green and I chased it
rather than accepting it. The check had silently not applied — the script's
output was piped away, so "pattern not found" was invisible, and the two
assertions it was meant to test had landed in the wrong test because the same
line occurs twice in the file. Both are corrected, and the check now fails as
it should.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the charset, removing the partial-answer guard, keeping the
body headers in the fallback, and widening the non-HTML strip each turn a test
red.
… instead of catch

Three findings from the conformity lane.

The comment on the stale-header list argued in one breath that dropping
Content-Encoding is "not hypothetical" because the deploy serves gzip, and that
it "costs nothing if it was never on the object". Those are two different
layers, and put together they contradict each other. Split: the encoding goes
because response.text() decoded the body, whatever the origin declared; whether
the header was ever on the object is a separate question, and the gzip seen on
the deploy is applied by the edge after this Function runs. Only the ETag and
Last-Modified sentence rests on a measurement.

A test titled "passed through untouched" checked status and body but not
headers, while its two siblings with the same word do. It checks them now.

The fallback used a bare catch, so any constructor failure would have been
rerouted through it while the comment named one cause. It asks whether the
request has a body instead, and anything else still surfaces.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: mangling a header on the non-HTML branch, and taking the re-method
path unconditionally, each turn a test red.
The conformity lane pointed out that the helper's own name for what it drops
was wrong. On the paths that reach it nothing was rewritten, so the origin's
Content-Length describes exactly the representation a GET would have returned
— RFC 9110 wants a HEAD to carry it. Calling it stale and deleting it lost
correct information.

It is kept now, and the tests assert it on both branches that use the helper.

The asFullGet docstring also claimed everything else the client sent is kept,
while the fallback right below it drops the body, the headers that described it
and the request metadata a URL cannot rebuild. It names that exception.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: deleting the length again turns two tests red.
Three findings from the logic lane.

A 304 was going through the rewrite branch, where its ETag was stripped and a
Content-Type set. That is the one validator a 304 has to keep — it is what the
client was matched on. 204, 205, 304 and 206 are now one set of statuses handed
on as they came: none of them carries a representation this pass could rewrite,
and their headers still describe what the origin meant. The special case that
kept a body away from them disappears with it.

The rewrite is also gated on the body being a landing shell. On a broken deploy
serving the site's own 404 page under these paths, the status was already left
alone, but the page's title was still rewritten into an invitation — a
description of something the visitor is not looking at.

The stale-header strip runs for every rewritten answer, not only a promoted
one, but only the promotion test proved it. Coupling the strip to the status
would have left a 403 or 410 carrying a length from before the rewrite, and no
test would have noticed. The refusal case asserts it now.

npm run check: exit 0, 186 tests, functions/_middleware.js at 100%. Verified by
mutation: coupling the strip to 404, shrinking the pass-through set back to
206, and removing the shell guard each turn a test red.
…odings alone

Two production findings from the logic lane, and two test gaps.

The content type was matched with includes('text/html'). That is
case-sensitive, so an origin answering Text/HTML would have been treated as
something else and never rewritten; and it is a substring test, so
application/json; profile="text/html" would have been read as HTML and had an
invitation injected into it. It is parsed as a media type now.

The charset was ignored. response.text() decodes as UTF-8 whatever the header
says, so a body in another encoding would have come back as replacement
characters and left here labelled UTF-8. Such an answer is handed on untouched
instead. The old test asserted the opposite — that an ISO-8859-1 answer gets
relabelled — and it went red, correctly; it now covers what the rewrite really
guarantees, which is that a rewritten answer says UTF-8 even when the origin
left it out.

The two test gaps: the partial-answer case asserted only status and body, and
the non-HTML pass-through named a few headers by hand. The first now uses the
landing shell as its fixture, so the guard has something to protect and a
mutation that lets HEAD through goes red; the second compares the whole header
set, so a regression that drops one nobody thought to name is caught.

npm run check: exit 0, 188 tests, functions/_middleware.js at 100%. Verified by
mutation: the substring match, the missing charset guard, letting HEAD past the
partial guard, and dropping a header in the body-less branch each turn a test
red.
… its text

Seven findings across both conformity and logic lanes.

The branch that decides not to rewrite was rebuilding the response from the
decoded string while keeping the origin's headers — the same mismatch the
rewrite branch strips headers to avoid, just without the stripping. It reads
from a clone now, so an answer this pass declines to touch is handed on exactly
as it came, body and headers both.

Accept-Ranges joins the stale list: after a rewrite there are no ranges of the
original bytes left to offer.

The docblock describing the status promotion had ended up above isLandingShell
when that predicate was extracted. It sits above landingStatus, which is what
it describes.

Four test gaps, three of them the same shape — asserting that a header is
absent afterwards while the fixture never set it, which proves nothing:

- the refusal case now sends a real Content-Encoding before checking it is gone;
- the stale-header case now sends a real Accept-Ranges;
- the media-type lookalike now carries the landing shell as its body, so a
  substring match would actually rewrite it and be caught;
- the two remaining "passed through untouched" cases assert the body, as their
  siblings already did.

Charset parsing is exercised across the shapes it arrives in: quoted, unspaced,
differently cased, and two encodings that must be handed on instead.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: reading without the clone, keeping Accept-Ranges, stripping only on a
promoted status, and matching the media type as a substring each turn a test
red.
The logic lane found the charset regex not quote-aware. A quoted parameter
value may contain a semicolon, so `text/html; foo="x;charset=utf-8";
charset=iso-8859-1` matched the charset embedded in the quoted value and missed
the real one behind it — the answer would have been decoded as UTF-8, rewritten
and relabelled, which is exactly what the charset check exists to prevent.

The parameters are split with quotes respected now, and every charset present
has to be UTF-8: two that disagree mean hands off rather than picking one.

The 206 fixture described a range that could not exist — `bytes 0-9826/4162`,
because .length counts UTF-16 units while a Content-Length counts bytes, and
the total had been left over from an earlier fixture. It uses real byte counts.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping quote-awareness, taking the first charset instead of
requiring all, and leaving the quotes on the value each turn a test red.
The logic lane found both of them unable to distinguish the current code from
the one it replaced. Their fixture never set an encoding or a validator, and
the page's own bytes are the same either way, so rebuilding the answer from its
decoded text instead of handing on the origin's would have passed.

Both now receive an answer carrying Content-Encoding and an ETag, and require
the very object the origin returned — a rebuilt look-alike fails. The HEAD case
compares the whole header set, since only the body may be missing.

Two smaller ones from the conformity lane: a test titled for the answer being
the full document checked only the forwarded request, and now checks both; and
the two documents called the four request headers "conditional validators",
which is the RFC's name for ETag and Last-Modified, not for these.

npm run check: exit 0, 189 tests. Verified by mutation: rebuilding the untouched
answer from its text turns both cases red.
The conformity lane found the test claiming more than it showed. Its body was
the same JS string every other fixture uses, and a string body is always
encoded as UTF-8 — so it only ever proved that the guard flips on a foreign
charset label, never that a body in another encoding comes through intact.

It now sends real ISO-8859-1 bytes, with 0xE9 for 'é', which is not valid UTF-8
at all, and carries the landing marker so that nothing but the charset guard
stands between it and the rewrite. The answer is compared byte for byte.

Verified by mutation: removing the charset guard turns it red, because the byte
comes back as U+FFFD and the answer leaves as different bytes.

npm run check: exit 0, 189 tests.
The logic lane found the parser toggling on every quote, escaped or not. RFC
9110 lets a backslash escape the next character inside a quoted value, so
`foo="a\";charset=utf-8"; charset=iso-8859-1` was split in the wrong place and
the wrong charset won. The escape is consumed now, and a quoted value has its
escapes resolved rather than only its outer quotes stripped.

Two test gaps from the same lane:

- The disagreeing-charset case was only tried one way round, so a change from
  "all must be UTF-8" to "the last one wins" would have passed. Both orders are
  covered.
- Two pass-through branches asserted their headers but not that the answer is
  the origin's own object, while the 404 branch beside them does. A rebuild at
  either would have gone unnoticed; both now require the same object and carry
  an encoding and a validator to prove it.

npm run check: exit 0, 189 tests, functions/_middleware.js at 100%. Verified by
mutation: dropping the escape handling, letting the last charset win, and
rebuilding the non-HTML answer each turn a test red.
…ty checks

Six findings from the conformity lane, all of them places where a comment or an
assertion was looser than what it stood next to.

- Two comments called a HEAD answer "handed on untouched" or "the same object".
  A body-less answer has to be a new object; only the GET path can be the
  origin's own. Both now say which is which.
- The fixture builder set Content-Length from .length while a comment three
  lines above explained that .length counts UTF-16 units and these pages carry
  multi-byte characters. Every fixture and assertion uses real byte counts now.
- An assertion checked for a URL string that og:url and twitter:url share, so a
  defect in the og:url replacement alone would have passed. It pins the whole
  tag.
- "The German shell must be left exactly as it is" was not true: the pass adds
  og:site_name regardless of language. The claim is narrowed to the copy, which
  is what the test checks.
- The last of the four pass-through branches still lacked the identity check its
  siblings got, and has it now.

npm run check: exit 0, 189 tests. Verified by mutation: breaking the og:url
replacement alone, and rebuilding the answer on the non-GET/HEAD branch, each
turn a test red.
Two mutation gaps from the logic lane, which also found the quoted-pair bug
independently — that one was already fixed in 9ef5048.

- Every charset test used a lower-case parameter name, so dropping the
  case-folding would have passed while `Charset=` stopped being recognised.
  Both an upper-case UTF-8 and an upper-case foreign charset are covered.
- The landing marker is matched as a substring, so widening it from
  `id="state-loading"` to `state-loading` would have passed too — and a page
  that merely mentions the words would then have been promoted from 404 to 200
  and rewritten. A page that mentions them without the attribute is now a case.

npm run check: exit 0, 190 tests. Verified by mutation: removing the
case-folding and widening the marker each turn a test red.
…ranch

Two test-strength findings from the decisive round; both lanes reported nothing
that changes behaviour for a user or a crawler.

The already-found case asserted its status but not that the rewrite ran, so a
change that handed a 200 answer straight through would have passed. My first
attempt at fixing that was itself trivially true — it checked for a tag the
shipped shell already carries, and the mutation stayed green. Measured what the
rewrite actually changes on that URL: it normalises the canonical and the two
URL tags to the form without a trailing slash. That is what the test checks now.

The identity check that three pass-through branches carry was missing from the
other two, so a rebuild at either would have gone unnoticed. All five require
the origin's own object on the GET path.

npm run check: exit 0, 190 tests. Verified by mutation: skipping the rewrite on
an already-found answer, and rebuilding the body-less pass-through, each turn
tests red.
…implicit

The decisive round came back with nothing behaviour-affecting from three of the
four lanes. The fourth raised two, and this is how they stand.

The first is real and is now written down where it happens rather than left for
the next reader to discover: stripping the conditional request headers means a
client sending `If-None-Match: *` gets 200 with the whole document where RFC
9110 §13.1 calls for 304, and `If-Match` never yields 412. These paths emit
neither ETag nor Last-Modified, so a client only reaches that case by sending a
precondition it was never given one for, and the answer it gets is the current
representation — never wrong, only larger than it had to be. Evaluating
preconditions properly means minting a validator for the rewritten document,
which is a larger change than this one and belongs in its own.

The second is not this PR's: shouldRewriteItunesBanner and parseLandingFromUrl
carry two different lists of asset extensions, so a referral code ending in
.json is skipped entirely while one ending in .html loses its code. Both
functions predate this change. It is written into the PR body as a follow-up.

Three test-strength points from the same round are closed: an unterminated
quoted charset, the reason phrase on a passed-on HEAD, and the boundary of the
marker check spelled out as a documented limit rather than left unstated.

npm run check: exit 0, 190 tests. Verified by mutation: dropping the
closing-quote check and blanking the reason phrase each turn a test red.
Two test-strength points from the confirmation round; it reported nothing that
changes behaviour.

The branch for a path the rewrite does not own was the one pass-through left
without an identity check, so a rebuild there would have gone unnoticed while
the body and length stayed the same. It requires the origin's own object now,
like the other five.

The proof that an already-found landing really is rewritten checked the
canonical and og:url but not twitter:url, though the rewrite normalises all
three. A regression in that one replacement alone would have passed.

npm run check: exit 0, 190 tests. Verified by mutation: rebuilding the answer on
the not-owned branch, and breaking the twitter:url replacement alone, each turn
a test red.
… in the code

Three test-strength and documentation points from the confirmation round, which
again reported nothing that changes behaviour.

The Range case asserted the forwarded request but discarded the answer, so a
regression that stripped the header and still treated the response as partial
would have passed. It asserts the answer too.

The charset matrix collapsed every case to a status. It now checks what each
outcome means: a rewritten answer is promoted, declares UTF-8 and carries the
injected title; an untouched one keeps the origin's status, type and bytes.

The deliberate deviation from RFC 9110 §13.1 was stated in the code but not in
either document, though both describe the surrounding behaviour. Contributors
read those first, so both now say it and why.

npm run check: exit 0, 190 tests.
The confirmation round accepted the deviation but caught the justification
being too broad. A tag or date condition would indeed need a validator minted
for the rewritten document. A wildcard one would not: `If-Match: *` and
`If-None-Match: *` ask only whether a current representation exists, which is
already known here. They are dropped for uniformity, not out of necessity, and
the comment now says which is which.

The behaviour is unchanged; only the stated reason was wrong for half of it.

npm run check: exit 0, 190 tests.
The last two mutation gaps from the confirmation round.

The Android and iOS alternate links were asserted by attribute name only, so
swapping the two would have passed while Android users were sent to the iOS
hand-off and back. Both hrefs are pinned now.

shouldRewriteItunesBanner matches its prefixes at a segment boundary, but no
test used a path that merely starts with the same letters, so dropping the
slash would have gone unnoticed and pulled /invitee and /promotion into the
rewrite. Four negative cases cover it.

npm run check: exit 0, 190 tests. Verified by mutation: swapping the two
hand-off URLs, and widening the prefix match, each turn a test red.
The release round raised four points, and this closes all of them.

The one that touches behaviour: the shell was recognised by a single substring,
so an error document carrying `id="state-loading"` anywhere — a comment, a
script, a copied snippet — would have been promoted to 200 and dressed in an
invitation's metadata. The shipped 404 page does not carry it, and a contract
test pinned that, but nothing covered an error document the site does not ship,
such as an edge error or a challenge page. Recognition now needs both that id
and `aria-busy="true"`, which the two landings carry and nothing else arrives
at by accident. The case that used to be a documented boundary is now simply
handled: such a page keeps its 404 and its own body.

Three coverage gaps for behaviour that was already correct:

- `/promo` exactly was never tested, so deleting that clause would have stopped
  the promo landing being rewritten without a red test.
- The replace branch of the alternate-link upsert had never run: no fixture
  carried an existing link. Both attribute orders are covered, and the old href
  has to be gone rather than joined by a second tag.
- The iPad Twitter app card was emitted and asserted nowhere.

npm run check: exit 0, 191 tests. Verified by mutation: dropping the second
mark, removing the exact `/promo` clause, giving the iPad card a wrong URL, and
making the upsert always append each turn a test red.
The round on the previous commit found only its own after-effects, and this
clears them.

Both documents still described the guard as keying on one mark. That is the
drift this series keeps correcting, one commit after introducing it: they name
both now.

The same singular sat in the middleware comment, in the test file's header and
in two test titles.

Two claims of mine were wrong rather than merely stale:

- The commit that added the replace-branch test said it covered both attribute
  orders. Both fixtures put the attribute before the href, so the branch for
  the other order had still never run. A third fixture covers it, and disabling
  that branch now turns the test red.
- The Latin-1 test said its body carried the landing marker so that only the
  charset guard stood between it and the rewrite. With two marks required and
  only one present, that was no longer true — the marks check would have
  blocked it as well. The body carries both now, so the test isolates what it
  claims to.

npm run check: exit 0, 191 tests.
"Never wrong, only larger than it had to be" claimed too much. The body a
client gets is always the current representation, so that half holds. The
status is the part that deviates: RFC 9110 puts 304 and 412 at SHOULD, and
this pass answers 200 instead. The comment now separates the two.

npm run check: exit 0, 191 tests.
The last open point from the logic lane. Nothing in the suite carried both
words without the attribute syntax, so loosening the marks from
`id="state-loading"` / `aria-busy="true"` to bare `state-loading` /
`aria-busy` would have passed — and any page mentioning both in prose would
then have been promoted to 200 and dressed as an invitation.

A page that does exactly that is now a case, and it keeps its 404 and its body.

npm run check: exit 0, 191 tests. Verified by mutation: loosening the marks to
bare words turns it red.
The last point from the round on 880311e. The comment still called the
attribute-in-a-comment case a boundary the check cannot see, while the
assertion three lines below asserts a 404 because the second mark closes it. It
says what is true now, and what it used to be.

npm run check: exit 0, 191 tests.
…nough

The marker check is a substring test. Four comments and two doc paragraphs
claimed more than that, or more than the code around them holds:

- two comments called a pass-through "passed on as they are" and "handed on
  exactly as it came", which is true of a GET and not of a HEAD, whose answer
  is rebuilt without a body three lines below;
- the landingStatus JSDoc used "rewrite" for both the tag injection that has
  already happened and the status promotion it performs itself;
- the isLandingShell JSDoc said two marks together are "not something another
  page arrives at by accident", which the check does not establish: it never
  asks whether the marks share an element;
- CONTRIBUTING and README said these paths emit no validator, which holds for a
  rewritten landing and not for a pass-through, where the origin's ETag is kept
  and a test already pins it.

What actually makes two substrings enough is a property of the shipped site,
so it is now asserted instead of described: of every HTML page under public/,
only the two the Function is routed to read as a landing shell. The
account-merge and Aktionariat shells carry the id and no aria-busy, the 404
page carries neither. The test goes red the day one of them gains the second
mark, which is the day the guard would have to become a real parse.
…t rests on

Three statements from the previous commit did not hold, and one measurement
was missing:

- CONTRIBUTING said the marks are carried by the landings and by "no page
  served under these paths", which contradicts itself: the landings are the
  pages served under these paths. It now says no other page the site ships.
- The isLandingShell JSDoc credited the function with establishing that no
  other shipped page reaches both marks. The function performs a substring
  test and establishes nothing about other files; the property is held by the
  test, which the docstring now says.
- The path gate excludes twelve asset-like suffixes, and a review read that as
  leaving /invite/AB.JSON without the promotion. Measured on the deploy
  instead of argued: such a path is answered with the site's own 404 page, not
  with the landing shell, so the promotion would refuse it on the marks in any
  case. What a code like that loses is the landing itself, which is the
  follow-up #30 left open. The gate now says so.

The repo-wide test pinned only three of the six shipped pages by name, so a
walker that quietly stopped finding the others would still have passed: the
filtered list would simply have had nothing left to disagree with. It now pins
the whole inventory first, and the doubled-slash and /invites path boundaries
are asserted rather than reasoned about.
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator Author

EN. 43 commits, every one re-reviewed as unreviewed code by four lanes — two vendors × conformity and logic. The last rounds report nothing that changes behaviour for a user or a crawler. Every guard here is verified by mutation rather than by assertion: the fault is put back and the test has to fail.

DE. 43 Commits, jeder davon erneut als ungeprüfter Code durch vier Prüfungen — zwei Anbieter × Konformität und Logik. Die letzten Runden melden nichts, was das Verhalten für einen Nutzer oder einen Crawler ändert. Jede Absicherung ist per Mutation belegt: der Fehler wird wieder eingebaut, und der Test muss anschlagen.

What the rounds found, and what it cost to find it

The bug this PR opened with was one line: the middleware copied the asset server's status through. Fixing it opened five more that only showed up under review, three of which would have reached a user:

Found Would have meant
HEAD not promoted with GET curl -sI — the command in this PR's own evidence — and every link checker still reading 404
A rewritten body paired with 304 A conditional request, which browsers send on reload, answered with a 500
Content-Encoding carried onto a decoded body A page labelled gzip that is not gzip: blank
includes('text/html') as the type check Text/HTML never rewritten; application/json; profile="text/html" rewritten as if it were HTML
A quoted semicolon splitting a Content-Type foo="x;charset=utf-8"; charset=iso-8859-1 read as UTF-8 and relabelled

The rest were mine to answer for rather than to be proud of: comments claiming more than the code held, and assertions that could not tell the fixed version from the broken one. Three times an assertion checked that a header was absent while the fixture never set it — always true, and only the mutation check exposed it.

Deliberately not fixed, and why

A stated deviation from RFC 9110 §13.1. The conditional request headers are dropped rather than evaluated, so If-None-Match: * is answered 200 with the whole document where 304 was called for, and If-Match never produces 412. A tag or date condition would need a validator minted for the rewritten document, which none of these paths emits. The wildcard forms would not — they ask only whether a current representation exists — so they are dropped for uniformity, not out of necessity. Either way the client gets the current representation, so the body is never stale; the status is what diverges, and the cost is a larger answer than was called for, not a wrong one. The code, CONTRIBUTING and README all say so.

Two sham tests in already-merged code, found while reviewing this one: the clipboard stub in tests/behavior.spec.mjs discards the value it receives, so a test titled "copies the canonical invite URL" passes on any string; and the 15-second lookup timeout is asserted only as a constant, never as a timeout.

Carried into the follow-up

None of these changes what a user or a crawler gets from the deploy today.

The two-mark check is a substring test and blind to HTML structure. A document carrying both marks only inside a comment would be promoted. What keeps that from mattering is a property of the site, and the property is now asserted rather than described: of every HTML page under public/, only the two the Function is routed to read as a landing shell. That test goes red the day another page gains the second mark, which is the day the guard would have to become a real parse.

Three statements still say more than they hold. The JSDoc on isRewritableHtml says a foreign-encoded body is handed on "untouched", which is true of a GET and not of a HEAD, whose answer is a body-less copy — the same overstatement two other comments were corrected for, missed at a third site. CONTRIBUTING and README say the answer is "never a 304 or a 412", which describes what the dropped preconditions can cause and not what the pass-through does with a status the origin produced on its own. And the comment above landingStatus says only a body that really is the landing shell is promoted, where the check is the substring test described two functions above.

Tests worth adding. HEAD against each of 204, 205 and 304 rather than only 206; both marks together inside a comment, as the negative case for the item above; a HEAD that arrives carrying a body, which today is covered for GET only; and the single-mark cases asserted against isLandingShell directly rather than only through landingStatus.

The pass-through guard is only observable at 206. A 204, 205 or 304 carries no body by construction, so with the guard removed the answer falls through to the not-a-shell branch and comes back identical. The test for those three statuses therefore cannot fail on that mutation; 206, which does carry a body, is where the guard is actually proven. Worth saying in the test rather than leaving a reader to assume more.

The page walker matches .html case-sensitively, so a future .HTML file would be invisible to the inventory test.

Codes that end in an asset suffix get no landing, an open item from #30. The path gate excludes twelve suffixes, parseLandingFromUrl treats a different seven as an asset, and the sets disagree. Reconciling them changes code-parsing rules that #30's tests pin, which is why it belongs in its own change. Measured on the deploy: /invite/AB.JSON and /promo/EVT1.HTML are answered with the site's own 404 page rather than the landing shell, so the status promotion is not what is missing — the landing is.

A Content-Type: text/html with no charset at all passes the type gate and the body is then read as UTF-8. The assumption is deliberate and stated in the code. All six shipped pages declare UTF-8, so nothing reaches it today; deciding it on the bytes rather than on the absent parameter would close it.

The body-carrying branch of asFullGet drops only the length and the type, not the content coding or the digest headers, so the rebuilt request still carries headers describing a body it no longer has. The rewrite path already has the full list to reuse.

The body-carrying fallback in asFullGet is covered by unit tests, not by the platform. Whether Pages ever delivers a GET or HEAD with a body, and what the dropped request metadata would have been worth, is untested against the real runtime.

@TaprootFreakAI
TaprootFreakAI marked this pull request as ready for review September 9, 2026 13:35
@TaprootFreak
TaprootFreak merged commit 1555d93 into develop Sep 9, 2026
2 checks passed
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
TaprootFreak pushed a commit that referenced this pull request Sep 9, 2026
…)" (#35)

This reverts commit 1555d93.

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. So /invite/AB.HTML passed the gate and then lost its code, while
/invite/AB.JSON never reached the gate at all. Both read one constant now, and
a case asks the two as a single question across every suffix — the sweep
includes html, a suffix that names no file type, and a mixed-case spelling — so
a list that grows on one side and not the other fails there rather than in
production.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, carrying the code, equal to the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, and the case measures how long that took against the
budget the page itself reports rather than bracketing it loosely.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether a suffix names a file, with two different
sets: the gate carried jpg|jpeg|webp|ico|txt|xml that the parser lacked, the
parser carried html that the gate lacked. So /invite/AB.HTML passed the gate
and then lost its code, while /invite/AB.JSON never reached the gate at all.
Both read one constant now, and a case asks the two as a single question across
every suffix — the sweep includes html, a suffix that names no file type, and a
mixed-case spelling — so a set that grows on one side and not the other fails
there rather than in production.

The two still ask it of different things, deliberately: the gate of the whole
path, because /invite/AB12CD/logo.png is an asset request whoever owns the
first segment, and the parser of the code segment alone. A case pins that
difference as well as the agreement.

html is not in the list for one reason: a code that happens to end in .HTML
stays a code. The shell needs no help from the list — the parser names
index.html outright — and the 308 that canonicalises it comes from the platform
and is handed on either way.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard, rejects a rule the wildcard covers, and rejects the same
exact path declared twice, which its own header parser had been merging out of
sight.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, equal to the href the canonical link names.
And the fifteen-second lookup budget was asserted against its own constant and
nowhere else: a request that never answers now has to leave the loading state
by itself, and the case measures how long that took against the budget the page
itself reports.
TaprootFreak pushed a commit that referenced this pull request Sep 10, 2026
* fix(invite): one list for a file-looking segment, one rule per path, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether a suffix names a file, with two different
sets: the gate carried jpg|jpeg|webp|ico|txt|xml that the parser lacked, the
parser carried html that the gate lacked. So /invite/AB.HTML passed the gate
and then lost its code, while /invite/AB.JSON never reached the gate at all.
Both read one constant now, and a case asks the two as a single question across
every suffix — the sweep includes html, a suffix that names no file type, and a
mixed-case spelling — so a set that grows on one side and not the other fails
there rather than in production.

The two still ask it of different things, deliberately: the gate of the whole
path, because /invite/AB12CD/logo.png is an asset request whoever owns the
first segment, and the parser of the code segment alone. A case pins that
difference as well as the agreement.

html is not in the list for one reason: a code that happens to end in .HTML
stays a code. The shell needs no help from the list — the parser names
index.html outright — and the 308 that canonicalises it comes from the platform
and is handed on either way.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard, rejects a rule the wildcard covers, and rejects the same
exact path declared twice, which its own header parser had been merging out of
sight.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, equal to the href the canonical link names.
And the fifteen-second lookup budget was asserted against its own constant and
nowhere else: a request that never answers now has to leave the loading state
by itself, and the case measures how long that took against the budget the page
itself reports.

* test(invite): say which of the two claims does which job

The comment said the budget is read from the page rather than restated here,
and the line under it restates it. Both belong there — the size is pinned
outright, because bounds computed from the page's own number would follow it
anywhere, and the elapsed time is measured against that number so another timer
cannot pass for this one. The comment now says that instead of denying half of
it.

* test(invite): three comments that claimed a little more than the code

The sweep's comment implied it catches any divergence between the two sides,
where it walks a list of its own and catches a divergence on what it
enumerates. The .PDF case called it a dot that names no file type, where PDF is
a file type this rule simply does not call an asset. And the timeout case said
the budget was asserted against itself, where the unit suite pinned it against
a literal and went no further.

* fix(invite): catch any covered rule, and start the clock where the wait does

The covered-path check listed the four rules that happened to be there, so a
rule added later would have slipped past it. It now rejects any exact path a
landing wildcard covers, with the script's own rule kept on purpose: it wants a
longer cache than the shells, and a parser takes the first of the two values.
Proven by adding a rule for /invite/foo and watching it fail.

The budget case took its timestamp before the navigation, so a slow page load
counted against the budget and could push the upper bound over on a busy
machine. It starts when the page says it is checking, which is when the timer
the case is about was armed.

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
@TaprootFreakAI
TaprootFreakAI deleted the 48a09586-landing-404-status branch September 10, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants