Skip to content

fix(web): align /internal/* result cache with the public routes (#143) - #144

Open
lukaso-bot wants to merge 22 commits into
mainfrom
fix/og-cold-cache-key-alignment
Open

fix(web): align /internal/* result cache with the public routes (#143)#144
lukaso-bot wants to merge 22 commits into
mainfrom
fix/og-cold-cache-key-alignment

Conversation

@lukaso-bot

@lukaso-bot lukaso-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #143. Regression of #53.

What was broken

Every OG card that wasn't already in the edge cache rendered the neutral
placeholder. Only the URLs the liveness probe re-requests each cycle came back
real, which is why this looked green for weeks. A crawler unfurls a link once,
so the placeholder is what got cached in Slack and X.

web-og fetches result JSON from web over the Service Binding at
https://web/internal/.... Two independent misalignments made that lookup unable
to use the result cache:

  1. Origin. makeWorkerCache(req) derives the Cache API key URL from the
    incoming request, so /internal/* keyed on https://web/__cache__/.... That
    is a different namespace from the public permalink routes'
    (https://released.blabberate.com/__cache__/...), and web is a
    non-routable hostname, which the Cache API silently declines to store — the
    same class of bug cache.ts's own header note records for cache.invalid.
    So the OG path could neither reuse a warm public entry nor persist its own.
    That is why Cold-cache OG unfurls serve the placeholder again (#53 regression) — probe is blind because it re-warms its own fixtures #143 saw three consecutive cold requests all return the
    placeholder: it never self-healed.
  2. Key parts. The public routes (result.tsx, issue.tsx, pr.tsx) key on
    five parts — ('res', host/path, id, 'cull', 'nopre') — and spell ids
    issue#<n> / pr#<n>. /internal used a three-part key with issue:<n> /
    pr:<n>. Even with the origin fixed, that can never land on a slot a public
    hit warms.

This is the "cache-key alignment" item the backlog had been carrying, and it was
the whole of the defect.

The fix

  • Key /internal/* on the canonical public origin: PUBLIC_BASE_URL, else the
    committed PROD_HOST var, else the request's own origin (wrangler dev and
    tests, where neither is set). No config change — PROD_HOST is already set in
    both envs.
  • Mirror the public routes' cache key exactly, and state strict: false, includePrereleases: false explicitly so the slot written is the one a default
    permalink hit reads back.
  • A cache read failure now degrades to a recompute instead of a 500. The key URL
    is deliberately no longer this request's origin, so a Cache API refusal must
    not become a placeholder.

The result cache holds a pure function of upstream state (which release first
contains X), so sharing slots between the public and OG paths — and across
prod/preview — is safe.

Why the existing tests missed it

Every pre-existing /internal/* test called the route with a public-looking
https://released.example/internal/.... Production calls it with
https://web/internal/.... The tests never exercised the URL shape that breaks.

The new packages/web/test/internal-cache-origin.test.ts calls it with the real
production shape throughout.

Mutation evidence

The guard was written first and run against the unfixed code. All 7 assertions
went red on #143 itself:

Test Files  1 failed (1)
     Tests  7 failed (7)

The decisive one — the cold-lookup write-back:

FAIL  /internal/* WRITES back to the slot the public routes read (#143)
      > warms the public cache key on a cold lookup, and writes nothing under `https://web`
AssertionError: expected [ Array(1) ] to include 'https://released.blabberate.com/__cac…'

Array(1) is the point: the cold lookup wrote exactly one entry, and it was
under https://web — the namespace the real Cache API drops. After the fix, 7/7
pass, and the same assertion pins that nothing lands under https://web/ at all.

Each assertion has a concrete failing input: swap issue# back to issue:, drop
either option suffix, or drop the origin override, and a named test goes red.

The five pre-existing /internal/* tests in integration.test.ts were seeding
the old three-part key; they are updated to the public scheme (that they had to
change is itself the bug).

Gate

pnpm -r test 592 passed, pnpm -r typecheck, pnpm -r build, pnpm lint all
clean locally.

⚠️ CI's osv dependency scan will likely fail this PR, for a reason unrelated
to this change.
pnpm-lock.yaml is untouched here (git diff origin/main -- pnpm-lock.yaml is empty), but newly published advisories now flag 3 High
vulnerabilities in the existing tree — js-yaml 4.3.0, nanoid 3.3.16, undici
7.28.0 — and osv-check.sh gates High/Critical on PRs. This affects every open
PR equally, not just this one. Filed separately.

Verifying after merge

Deploy is push-to-main. Once live, the check is a cold commit (one never
requested before):

  1. GET og.released.blabberate.com/r/<owner>/<repo>/c/<sha>.png for a fresh sha.
  2. Expect the real card and cache-control: max-age=86400. The placeholder's
    max-age=60 is the reliable tell that it regressed.

Note the liveness probe cannot see this: it re-requests a fixed set of OG URLs
each cycle, which keeps exactly those warm. A probe change for cold inputs is
tracked separately in #143's last section.


Round 3 — scope correction

Two threads on 4cdb212. One was a defect in this diff and is fixed in fff6404; one was pre-existing and is split out.

Fixed — shared back-off could hand the crawler a placeholder (fff6404). Aligning the key also aligned the <key>:neg back-off marker, which public page views write. A human's failed page view could therefore 503 the next OG render without ever calling findRelease, and the crawler caches that placeholder for good — #143 again, through the alignment meant to fix it. resolveLookup gains an opt-in bypassBackOffWhenCold, set only by /internal: with a prior, stale-serve still wins and the down host is untouched; only the cold case attempts, because there the alternative is permanent. Mutation-proven — against 4cdb212 the new test fails expected 503 to be 200, the exact placeholder path.

Corrected claim. My round-2 comment said the cold path means an unfurl is "cold at most once". That is only true for inputs where findRelease returns. It throws NotYetReleasedError for a commit that is merged but not yet in a release — the most likely thing to be freshly shared — and that still 503s to the placeholder, uncached, on every unfurl.

Not fixed here, filed as #150. That not_yet 503 is unchanged by this PR: main 503s on it too, via the old catch (err). It is a separate defect on a separate path (the webweb-og JSON contract) whose fix needs its own decisions — what shape the 200 carries and what TTL a pending "not yet" answer gets. It belongs in a PR that is about it, not a third topic under this title.

Note: the failing osv dependency scan on this PR is #145 (3 High advisories in main's lockfile blocking every PR), fixed by #146 — not a defect in this branch.


Round 4 — all three findings were in the SWR path, not the alignment

Three threads on df20483, all on the stale-while-revalidate machinery added in
4cdb212/fff6404. All three were real defects in this diff (on main,
/internal did a bare cache.get with a flat 30-minute put, so none of these
paths existed). Fixed in 11dc61c.

1. SWR had no upper staleness bound. prior && revalidate fired for any
entry past the 5-minute freshness window, but HARD_TTL_PENDING is 24h — so an
unfurl could be handed a 23h-old "not yet released" answer, which web-og then
pins for another 24h (renderImage is result ? longCache : shortCache). The
background refresh fixes the slot but cannot invalidate a PNG already rendered.
Now bounded by SWR_MAX_STALE = 30 * 60; past it we block. 30 minutes is what
/internal allowed as a flat TTL before it shared this slot, so the bound is by
construction never worse than the code it replaced.

2. The background refresh owned the singleFlight entry. The recursive call
reached singleFlight(key, …), whose module-level entry is cleared only in the
loader's finally. Under waitUntil, workerd can tear the IoContext down before
the subrequest settles: the promise never settles, the finally never runs, and
every later request in that isolate on that key — a human on the permalink,
badge.ts on the same cull/nopre key — joins a dead promise. The refresh now
passes coalesce: false and runs the loader directly.

3. The back-off bypass comment claimed a throttle that doesn't exist.
singleFlight collapses only concurrent calls within one isolate, so sequential
and cross-colo unfurls each run a full lookup against a down host. Comment
corrected to state the real cost; the gating alternative is declined on the
thread (it moves which unfurls get a permanent placeholder rather than
removing them).

Mutation evidence

Each guard was reverted individually against 11dc61c and reddens on its own
defect:

# revert the bound (prior && revalidate && … < SWR_MAX_STALE  →  prior && revalidate)
× blocks rather than hand back an answer stale past the bound
AssertionError: expected [ Promise{…} ] to have a length of +0 but got 1

# revert coalesce: false on the background refresh
× does not let a torn-down background refresh poison the key for the isolate
AssertionError: expected 'JOINED-A-DEAD-FLIGHT' not to be 'JOINED-A-DEAD-FLIGHT'

The second is the failure mode itself: with the mutation in place the follow-up
request hangs on the dead flight instead of computing.

Gate: pnpm test 611 passed, pnpm -r typecheck, pnpm lint clean.

Split out, not fixed here

The residual behind finding 1 is pre-existing and in another package: web-og
long-caches any non-null result for 24h, so even a perfectly fresh "not yet
released" card can't flip — the OG analogue of the badge invariant this repo
already states. Filed as #151 (with the one-line fix, the partial case, the
overlap with #141, and the guard it needs). Not pulled in here: it is a different
file, a different Worker, and #141 is already open against those same lines.

osv dependency scan is still red for #145's reason (3 High advisories in
main's lockfile, fixed by #146), not for anything in this branch.


Round 5 (2873d1c)

Two threads, both in this PR's own diff.

1. The SWR background refresh was uncoalesced and unconditional. Round 4's
coalesce: false kept a waitUntil task from owning the singleFlight entry, but it
also dropped the refresh out of coalescing entirely — and the branch fires on every
request in the stale window. One link unfurled by four platforms in the same second in
one colo ran four full findRelease traversals against the same repo on the shared
token.

Fixed with a second map (backgroundFlight) that foreground callers never join:
background refreshes collapse onto each other, and no live request can join a task the
runtime may tear down. The round-4 poisoning guard passes unchanged.

(Note: "join an existing flight without registering" — the shape the review suggested —
does not fix this case. All four requests are stale hits, so there is no foreground
flight to join and all four still run.)

2. The guard file exercised only 'a'.repeat(40). Production sends a 7-char sha
(ogImageUrlForCommitshortSha). Two tests added on that shape.

Mutation evidence:

# before the coalescing fix
× collapses concurrent background refreshes for one key onto a single lookup
    AssertionError: expected "vi.fn()" to be called 1 times, but got 4 times

# with cacheOrigin reverted to makeWorkerCache(req) — the #143 defect
× serves a cached result on the short-sha public key shape
    AssertionError: expected 503 to be 200
× writes a cold short-sha lookup back to the public origin, not `https://web`
    AssertionError: expected [ Array(1) ] to include 'https://released.blabberate.com/__cac…'

The remaining key-part misalignment (sha:<7> vs the full sha the public route keys on)
is #147, deliberately not folded in: its fix is in files this PR does not touch and
changes the public routes' key namespace.

Gate: 613 tests, typecheck, lint clean. The red osv dependency scan is inherited from
main's lockfile (#145) and clears when #146 merges.


Round 8 — 5896333

Four findings from the independent review of fd09b1b, all in code this PR introduces.
Each guard was mutation-proved against the defect in its own title, not a nearby one.

1. The computed-partial 503 was never recorded, so it wasn't throttled

Round 7 made /internal 503 a computed partial rather than pin a wrong "not yet
released" card. Correct — but resolveLookup writes that partial to the slot and then
no read path accepts it (fresh, SWR and back-off exits are all gated on !unpinnable),
so run()'s re-read fell straight through to load(). On a repo that reliably blows the
24s soft deadline that is a full traversal per unfurl on the shared token, where the
flat 30-minute TTL this route replaced made zero upstream calls.

Fixed in unpinnable() rather than with a new marker: a partial inside its own
HARD_TTL_PARTIAL is handed back and the route still 503s it, so the refusal costs a
cache read instead of a findRelease. Reusing the entry the resolver already wrote means
a real answer landing in the slot inside that window simply overwrites it and wins.

2. A gallop-only partial was treated as terminal and pinned for 30 days

The pin bound exempted any entry carrying a firstRelease. But find-release.ts:293-305
returns a partial whose firstRelease is the gallop hit — the bisect that would
confirm no earlier release contains the commit is exactly what the deadline cut short
("not necessarily the earliest", find-release.ts:724). The result card renders that
caveat; an OG card cannot, and web-og pins the bare tag for 24h. No partial is servable to
a pinning consumer now, either shape.

The underlying misclassification — hardTtlFor()/isFresh() both test firstRelease
before partial, so the shape is stored 30 days and reported fresh forever — is on main,
predates this PR, and affects the public routes too. Filed as #155, deliberately
not widened into this PR.

3. The request-origin fallback could silently reinstate #143

With both PUBLIC_BASE_URL and PROD_HOST unset, cacheOrigin returns the request
origin — for a real Service Binding, the non-routable https://web. Two guards:
originOf() now rejects a single-label host (localhost excepted, for wrangler dev),
and a new wrangler.toml suite enumerates [vars] + every [env.*.vars] and asserts each
sets one of the two, routably. The suite enumerates rather than hardcodes, so a new
named env is checked the day it is added.

4. backgroundFlight entries never expired

The map is module-level, so an entry registered under request A's IoContext is handed to
request B; when workerd cancels A's context the promise never settles and the loader's
finally — the only thing that clears the entry — never runs. Every later refresh for
that key joins the dead promise, so background revalidation is dead for the isolate's
lifetime and entries accumulate one per key. Entries now carry startedAt and expire at
30s (findRelease's hard deadline is 28s), and the finally only clears the entry if it
is still the live one — otherwise a late-settling abandoned promise would evict its own
replacement.

Mutation evidence

# deleting the `isRecentPartial(entry)` exemption
× fresh exit: hands back a 10s-old partial rather than re-run the lookup
× SWR exit: a fresh partial is never stale-served with a refresh behind it
× does not re-run the lookup for every unfurl of a deadline-blowing repo
      AssertionError: expected "findRelease" to be called 1 times, but got 2 times

# restoring `if (entry.value.firstRelease) return false;` + the old route bound
× a gallop-only partial is not treated as terminal, however old
× 503s rather than pin a partial whose tag the bisect never confirmed
× does not serve a 20-day-old partial the cache classified as terminal
      AssertionError: expected 'v4.9.0' to be 'v4.8.0'   # served the unverified gallop hit

# dropping PROD_HOST + PUBLIC_BASE_URL from [env.preview.vars]
× sets PUBLIC_BASE_URL in [env.preview.vars] to the preview Worker own origin
× [env.preview.vars] sets PROD_HOST or PUBLIC_BASE_URL

# with backgroundFlight's startedAt bound removed
× does not hand a later refresh a promise abandoned by a dead IoContext
      Error: Test timed out in 15000ms.        # joined the dead promise; 8ms with the fix

# with originOf's routability check removed
× ignores a single-label PROD_HOST rather than keying on a host it cannot cache
      AssertionError: expected 503 to be 200

Every new guard has a passing complement so it cannot hold by rejecting everything:
recomputes once the recorded partial ages out of its 60-second window,
serves a real answer that landed in the slot inside the partial window,
...but a TERMINAL answer of the same age is still served, never recomputed,
still joins a refresh that is merely SLOW, inside the deadline,
still accepts localhost, which wrangler dev really serves on, and
declares at least one env to check, so this suite cannot pass vacuously.

Two round-6/7 tests in resolve.test.ts asserted the old recompute-always behaviour and
were rewritten, not deleted — the property they protected (a partial is never pinned)
is unchanged; only where the refusal happens moved from the resolver to the route.

Gate on 5896333: typecheck clean across all 4 packages, lint clean, full pnpm test green.


Round 10 — 317ca2b — scope stated plainly, two costs written down

Three findings, all confirmed. None was a defect in this PR's diff, so the diff did not
grow: the change here is doc-only (+19 lines of comment, no behaviour change).

What this PR does NOT fix — say it before the title implies otherwise

The commit route — the product's most common permalink — is still misaligned, so
#143's user-visible symptom survives there.
og-meta.tsx:115 truncates the sha to
7 characters for the image URL, while index.ts puts the full 40 in the permalink
("Use the FULL SHA in the permalink"). So:

  • user opens /r/honojs/hono/c/<40-char sha>result.tsx warms
    cacheKey('res', 'github.com/honojs/hono', 'sha:<40>', 'cull', 'nopre')
  • the og:image is .../c/<7-char>.png → web-og calls /internal/result/honojs/hono/<7-char>
    → this PR keys on sha:<7>

Different digest, so the slot the permalink just warmed is invisible and the unfurl still
pays a full findRelease. Verified against production: the bot og:image for that permalink
is https://og.released.blabberate.com/r/honojs/hono/c/f82aba8.png?v=og.v1.

The issue and PR routes ARE fully aligned by this PR. The commit route gets the origin
fix and the key-part fix but not sha-length alignment — that is #147, whose fix changes
the public routes' key namespace in files this PR does not touch. The title's "align" should
be read as "align the origin, the key parts and the id spelling", not "close #143 on every
link shape".

Two costs now written into the code, not left implicit

Both are consequences of correctness decisions this PR makes deliberately. Neither is fixed
here; each has an issue with a concrete remedy sketch.

Gate on 317ca2b: full pnpm test green — 405 tests across 4 packages, lint and gitleaks
clean, osv clean of High/Critical.


Round 11 — three findings, all defects this PR introduced

Each is code this diff added, so each is fixed here rather than deferred. Each is bound to
a mutation that reproduces the specific failure claimed, run before and after.

1. /internal could join badge's 8-second flight and inherit its truncation.
Aligning the cache key also aligned the in-isolate single-flight key. singleFlight hands
every joiner the first registrant's promise and runs only the owner's loader, and
badge.ts:110 builds a byte-identical key for issue#N/pr#N — with a deliberately tighter
8s/9s deadline against this route's 24s/28s. A badge request landing first would hand
/internal a partial, which it 503s into a pinned neutral placeholder: #143's symptom, on
a link this route's own deadline answers.
On main it could not happen — /internal keyed
on a three-part key of its own — so this PR opened it.
resolveLookup now takes an optional flightKey; /internal passes its own. The cache slot
stays shared (the point of the PR); the truncation no longer travels with it. Concurrent OG
unfurls still collapse into one flight.
Mutation: delete the flightKey line → expected 503 to be 200.

2. hostname.includes(':') can never match a port. URL.hostname excludes the port, so
that clause matched only a bracketed IPv6 literal — a dead assertion reading as coverage. Its
cost is not over-strictness: a dev origin like http://app:8787 (Codespaces / Docker / WSL,
which the new README hunk invites) was rejected, and a rejected PUBLIC_BASE_URL is
indistinguishable from an unset one
, so the ?? chain fell to PROD_HOST and split the
namespace despite the var being set correctly. Now reads URL.port.
Mutation: reinstate the colon clause → expected 'v9.9.9' to be 'v4.2.0' (origin rejected,
fell to PROD_HOST, warm slot invisible, fresh lookup ran).

3. The wrangler.toml guard passed on the config this PR is fixing. It asserted
PUBLIC_BASE_URL ?? PROD_HOST is set and routable — and [env.preview] already declared the
production PROD_HOST, because every env pins it to gate analytics. That is precisely how the
preview keyed /internal on the prod origin while serving released-web-preview.*. Verified
rather than argued: the old assertion returns [] for the pre-fix [env.preview].
Replaced by a pure cacheOriginProblems(cfg) requiring each named env to have a routable
cache origin of its own, exercised against four known-bad configs — the pre-fix
[env.preview] among them, so the guard is bound to the bug in this PR's title — plus the
committed file. Not adopted: requiring the host to contain the env name, which would false-
positive on a custom-domain [env.staging]; the preview-specific test keeps that assertion
where it is genuinely true.

Two things deliberately left out, both pre-existing code this PR does not touch: badge and the
permalink pages have shared a flight key since before this PR (a much milder degrade — the
result card renders the partial caveat where the OG path pins a placeholder), and the TTL
misclassification of a gallop-only partial written by a public route is #155.

Gate on 5b1b72d: full pnpm test green — 405 tests across 4 packages, typecheck, lint,
shellcheck, actionlint, gitleaks, publint and both wrangler deploy --dry-run checks clean,
osv clear of High/Critical.

Scope — what this does NOT close (review round 16)

Fixes #143 stands, but the alignment is not uniform across the three routes and
the PR should not be read as claiming it is.

  • Issue and PR routes: fully aligned. Same origin, same five key parts, same
    issue#/pr# id spelling as the public permalink. A crawler unfurl lands on
    the slot a human page view warmed, and vice versa.
  • Commit route: origin fixed, key still misaligned. ui/og-meta.tsx builds
    the og:image URL from shortSha() (7 chars) while result.tsx keys the
    permalink on the full 40. So a commit unfurl does not reuse the permalink's
    warm slot.

The dominant half of #143 is closed for every route including commit: the origin
bug (https://web/ is non-routable, so the Cache API silently declined to store)
is why the OG path could never persist its own entry and never self-healed —
three consecutive cold requests all returned the placeholder. That is gone.

What survives on the commit route is narrower: the first unfurl pays one
findRelease instead of reading the human's warm slot, then persists normally.
Closing that needs a change to the public routes' key namespace, which is
tracked as #147 and deliberately not attempted here.

Adjacent findings raised in review and tracked separately rather than absorbed
into this diff: #159/#155 (a partial carrying a firstRelease is
classified terminal by hardTtlFor/isFresh, so it is cached 30 days and never
revalidated — this PR widens reachability of that pre-existing defect to
crawler traffic), #156 (a deadline-heavy repo gets no card while /internal
503s on partial), #157 (the negative back-off bypass is unconditional on
this path), #160 (ResultCard ignores result.partial).


Round 17 (5bc4e13) — 1 applied, 2 recorded as merge-decision facts

Three threads. One was a defect in this diff and is fixed in 70a1640; two are
explicitly "not asking to widen the diff" and are recorded below instead, because
what they ask for is that the merge decision know them.

Applied — the cache-origin fallback could relapse into #143 silently (70a1640).
cacheOrigin passes its two configured arms through originOf(), which rejects a
single-label host precisely because https://web is what the Cache API declines. The
request-origin fallback skipped that check — and for a real Service Binding that origin
is https://web. So if PROD_HOST is dropped from [vars], blanked in the dashboard,
or a new [env.*] ships that the committed wrangler.toml does not describe, every
/internal read misses and every write no-ops, and neverFatal renders it as "served,
just not cached" — #143 back with no error, no log, no metric. The cacheOriginProblems
suite guards the committed file; it cannot see a dashboard-set var. Now a console.warn
makes it visible in wrangler tail. Behaviour is unchanged.

Mutation evidence — each of the three assertions has a concrete input that reddens it:

# remove the warn entirely
× warns when it falls back to a request origin the Cache API will decline
AssertionError: expected '' to contain 'https://web'

# warn unconditionally (drop the routability gate)
× stays silent when the request-origin fallback is itself routable

# warn above the configured-origin early return
× stays silent when a routable origin IS configured
× stays silent when the request-origin fallback is itself routable

The first is the failure mode itself: silence is the bug.

Merge-decision facts (no code change, deliberately)

Both of these are real, both are already filed, and absorbing either would change a
policy this PR is not about — the diff-growth trap. Recording them here so the button
is pressed knowingly:

  1. Merge order is #144 → #158 → #156, and #144 alone regresses deadline-heavy
    repos
    (internal.ts:318, tracked as Deadline-heavy repos get a permanently blank OG card and one full traversal per minute after #144 #156). This branch refuses every partial
    with a 503 — correct, since an OG card cannot render the best-effort caveat — but
    HARD_TTL_PARTIAL and the placeholder's max-age are both 60s, so for a repo like
    gitlab.gnome.org/GNOME/gimp the card never converges and upstream load goes from
    ~0 to one full traversal per minute through a max_instances = 1 relay. On main
    that card at least rendered the gallop tag. fix(web-og): cache the OG card by terminality, not by result presence (#151) #158 (terminality-keyed OG cache) then
    Deadline-heavy repos get a permanently blank OG card and one full traversal per minute after #144 #156 is what closes it. Merging fix(web): align /internal/* result cache with the public routes (#143) #144 without fix(web-og): cache the OG card by terminality, not by result presence (#151) #158 behind it is the state that
    regresses.

  2. This PR adds crawler unfurls as a writer of unconfirmed 30-day entries
    (resolve.ts:386, tracked as partial-with-a-gallop-hit is cached as terminal: 30-day TTL, never revalidated #155/partial-with-a-firstRelease is cached as terminal on the web + badge surfaces (30d / 24h), so a truncated traversal pins a possibly-wrong tag #159). hardTtlFor() and isFresh()
    (resolve.ts:54,60) both test firstRelease before partial, so a gallop-hit
    partial is stored on the 30-day terminal TTL and reported fresh forever;
    badge.ts:141 reads that same key and serves the unverified tag at max-age=86400
    with nowhere to put a caveat. The shape is pre-existing — badge.ts and the
    permalink pages could already write it — but before this PR crawler traffic could
    not, and an unfurl needs no human in the loop. partial-with-a-gallop-hit is cached as terminal: 30-day TTL, never revalidated #155 owns the hardTtlFor/isFresh
    fix.

  3. Not a fix(web): align /internal/* result cache with the public routes (#143) #144 fact at all — raised here, filed as PAT-computed results are cached in a public, auth-agnostic key namespace #164. Round 18 flagged that
    /api/lookup honours a caller-supplied X-User-Github-Token yet keys the result
    with no auth component, so a private-repo answer lands in a slot the public routes
    read anonymously — and argued this PR "widens it to the full card". Checked against
    origin/main: issue.tsx:101, pr.tsx:86 and result.tsx:79 already build the
    byte-identical 5-part key and already render the full card, title included, on a
    public unauthenticated permalink. So the exposure is fully present on main with no
    OG path involved; this PR widens neither the data nor the reachability, and needs no
    change for it. Real confidentiality bug, wrong PR — PAT-computed results are cached in a public, auth-agnostic key namespace #164 owns it.

@github-actions

Copy link
Copy Markdown
Contributor

Preview deployed

Federated GitLab lookups (freedesktop / GNOME) degrade to the "use the CLI" card — the Anubis relay is off in preview. GitHub lookups, permalinks, and OG render work once INTERNAL_SECRET/GITHUB_TOKEN are set on the preview env.

Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/src/routes/internal.ts Outdated
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Status on this PR's red gate — none of it is the cache fix.

Three jobs failed on head 39a72d0-based run 32042060980. They split into two
unrelated causes:

1. Two were a GitHub infra outage, not this branch. a11y contrast and
deploy config (wrangler dry run) both died in Set up job, before any of our
code ran, unable to download the pnpm/action-setup action:

Failed to download action 'https://codeload.github.com/pnpm/action-setup/tar.gz/0977fd9...'
Error: Response status code does not indicate success: 429 (Too Many Requests)
... 503 (Service Unavailable)
Failed to download archive after 3 attempts.

GitHub is degraded right now more broadly — the GraphQL API is returning 503s to
gh as well. I have re-run the failed jobs; they should clear on their own.

2. osv dependency scan is real, but it is main's lockfile, not this diff.
New advisories were published against three transitive packages already on main
(nanoid 8.2, js-yaml 7.5, undici 7.4 CVSS), so that job is red on every open
PR. Filed as #145 and fixed in #146 (raises the pnpm.overrides floors,
verified 3 High → 0 High locally).

So this PR needs no code change for either. It goes green once #146 merges and
this branch picks up the new lockfile. Rebase state is fine: 0 commits behind
origin/main, no conflicts — the NOT REBASEABLE banner is a false reading from
the bot token's permissions, not a real merge block.

lukaso pushed a commit that referenced this pull request Aug 17, 2026
Addresses the three review findings on #144. All three are defects in what
this PR introduced — it made /internal share the public routes' cache slot,
so it has to share the policy and the failure modes that govern it.

- Cache faults are never fatal. Only the FIRST get was guarded; the inner
  get and the put sat inside the try whose catch returns 503, which web-og
  renders as the neutral placeholder — the #143 symptom, from a successful
  lookup. A neverFatal() wrapper degrades any Cache API refusal to "served,
  just not cached".
- /internal now resolves through resolveLookup, the same resolver the public
  routes use: per-state hard TTLs (30d terminal / 24h pending / 60s partial)
  instead of a flat 30 minutes, getEntry()+isFresh() instead of a bare read,
  and the negative back-off for free. A cold OG render no longer downgrades a
  30-day terminal slot to 30 minutes, and no longer serves a 60-second partial
  for half an hour after the public page has moved on.
- Preview sets its own PUBLIC_BASE_URL. PROD_HOST is committed in
  [env.preview.vars] too, so preview keyed /internal on a host it does not
  serve while its public routes keyed on the preview origin — #143, unfixed
  in the one environment OG changes get reviewed in.

Guards mutation-proved: all 7 new tests were RED on the pre-fix code
(503 on a refused write, 503 on a throwing read, max-age=1800 where the
policy says 2592000 and 60, a 10-minute-old pending answer served without
revalidation, no negative marker, no preview PUBLIC_BASE_URL), 14/14 green
after. Full gate green: 599 tests, typecheck, lint, build, deploy-config.
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Review round addressed in 4d2a239 — all three threads applied and resolved.

  • Cache faults are never fatal. neverFatal() wraps the cache, so a Cache API refusal on any read or write degrades to "served, just not cached" instead of a 503 that web-og renders as the placeholder.
  • /internal now resolves through resolveLookup, the same resolver the public routes use — per-state hard TTLs (30d / 24h / 60s), getEntry() + isFresh(), and the negative back-off, rather than a flat 30-minute TTL and a bare read.
  • Preview keys on its own origin. [env.preview.vars] sets PUBLIC_BASE_URL = https://released-web-preview.lukaso.workers.dev (verified live, /healthz → 200), so preview no longer keys /internal on a host it does not serve.

7 new tests, every one RED on the pre-fix code and green after; full local gate green (599 tests, typecheck, lint, build, deploy-config dry run).

The osv dependency scan job will stay red here until #146 merges — it fails on main's untouched lockfile (#145), not on anything in this PR.

Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/wrangler.toml
lukaso pushed a commit that referenced this pull request Aug 19, 2026
…cache origin

Two findings from the review round on #144, both #143 reintroductions through
the new /internal cache path.

1. Sharing the public routes' cache policy also shared their WAIT. resolveLookup
   revalidates a pending answer after 5 minutes and a partial after 60 seconds,
   and web-og awaits /internal with no timeout — so a merely-stale entry put
   findRelease's 24s soft deadline back on the crawler's critical path, and a
   blown deadline hands the crawler the neutral placeholder at max-age=60. That
   is the #143 outcome reached from a stale slot instead of a cold one.
   resolveLookup gains an opt-in `revalidate` callback: a stale answer is served
   immediately and the refresh runs via executionCtx.waitUntil. The public HTML
   routes omit it and keep the blocking behaviour, so their semantics are
   unchanged. A genuinely cold slot still blocks — there is nothing to serve —
   but it write-backs, so it is cold at most once.

2. cacheOrigin concatenated a scheme onto PROD_HOST unconditionally. PROD_HOST is
   shared with isProdRequest(), which documents itself as tolerant of a value
   written WITH a scheme; `new URL('https://https://host')` does not throw, it
   yields origin `https://https`, so every entry would key on a non-routable host
   the Cache API drops — silently, with neverFatal swallowing it. The reverse slip
   was worse: a scheme-less PUBLIC_BASE_URL made `new Request()` throw OUTSIDE
   neverFatal, turning a computed answer into a 503 → placeholder. Both spellings
   now normalise through URL().origin.

Mutation-tested — each guard was watched failing on the defect it names:
  - drop `revalidate:` → both stale-while-revalidate tests time out (the render
    waits on an upstream lookup that never resolves).
  - restore the naive concatenation → the PROD_HOST-with-scheme test reads
    'MISSED-THE-PUBLIC-SLOT' instead of the seeded 'v4.16.0' (proving the entry
    landed in a different namespace), and the scheme-less PUBLIC_BASE_URL test
    gets 500 instead of 200.
Tests use a distinct SHA each so a hanging test cannot poison the next through
singleFlight's module-level in-flight map.

Full gate green: 296 passed | 6 skipped (web), typecheck clean.
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Review round 2 addressed — 4cdb212

All four threads triaged and resolved. Two were defects in this PR's own diff and are fixed;
two are real but belong elsewhere, and are filed rather than folded in (this PR is already
+478/−41 and on its second round — growing it further makes it harder to merge, not safer).

Applied

1. The revalidation no longer blocks the render. Sharing the public routes' cache policy had
also shared their wait: isFresh gives a pending answer 5 minutes and a partial 60 seconds, and
web-og awaits /internal with no timeout — so any merely-stale entry put findRelease's 24 s soft
deadline back on the crawler's critical path, and a blown deadline hands the crawler the neutral
placeholder at max-age=60. That is the #143 outcome reached from a stale slot instead of a cold
one. resolveLookup now takes an opt-in revalidate callback: stale is served immediately, the
refresh runs via executionCtx.waitUntil. The public HTML routes omit it and keep blocking, so
their semantics are unchanged. A genuinely cold slot still blocks — there is nothing to serve —
but it write-backs, so it is cold at most once.

2. cacheOrigin normalises both spellings. PROD_HOST is shared with isProdRequest(), which
documents itself as tolerant of a value written with a scheme. Blind concatenation does not throw
on that: new URL('https://https://released.blabberate.com') yields origin https://https
(verified in node), a non-routable host the Cache API drops — #143 again, silently, with
neverFatal swallowing the write. The reverse slip was worse: a scheme-less PUBLIC_BASE_URL made
new Request() throw outside neverFatal, turning a computed answer into a 503 → placeholder.
Both now go through one originOf()URL.origin.

Mutation evidence

Each guard was watched failing on the defect it names, not merely watched passing:

mutation result
remove revalidate: from /internal serves a stale pending answer without waiting and serves a stale PARTIAL without waiting both time out — the render waits on an upstream that never resolves
restore the naive scheme concatenation PROD_HOST written WITH a scheme reads MISSED-THE-PUBLIC-SLOT instead of the seeded v4.16.0 (proving the entry landed in a different namespace, not just that a string differed); PUBLIC_BASE_URL written WITHOUT one gets 500 instead of 200

Each test uses a distinct SHA, because singleFlight keys a module-level map that outlives a test —
a hanging test would otherwise poison the next one through it (that artifact cost me a confusing
first mutation run showing 4 failures where only 2 were real).

Gate: 296 passed | 6 skipped (web), full pnpm -r test green, typecheck clean. The red osv dependency scan is the main-wide #145 (3 High in the lockfile), which #146 clears — unrelated to
this diff.

Split out, not dropped

  • OG cards key on a 7-char SHA while permalinks key on the full SHA — the first unfurl never reuses the warm slot #147 — commit OG cards key on a 7-char SHA while permalinks key on the full SHA, so the
    primary search → view → share flow doesn't reuse the warm public slot on the first unfurl. The fix
    touches og-meta.tsx/result.tsx (untouched here) and changes the public key namespace — a
    cache-invalidation event that deserves its own review.
  • Preview URL drift (the wrangler.toml thread) — the committed subdomain is verified
    (/healthz 200, and it matches the URL preview.yml posts here), so I left the value alone. The
    drift guard belongs in preview.yml, which should write its discovered URL into the var;
    backlogged. I deliberately did not add a test asserting the literal host — it can't fail for
    the reason that matters, so it would read as coverage while proving nothing.

⚠️ The preview could not actually exercise this change — see #148

I exercised the preview deploy as required and found it renders the neutral card for every
input: a real commit gives 14348 B and a nonsense SHA gives 14244 B, both under a second, versus
28899 B in production. og-preview isn't reaching web-preview's /internal at all (likely
INTERNAL_SECRET unset on the preview Workers — preview.yml documents it as a manual per-env
step, and isServiceBinding correctly fails closed). Filed as #148 with the exact commands;
it needs a human, since the loop doesn't touch secrets.

So: this PR is verified by the gate and by mutation testing, not by the preview. Worth knowing
before merge, given the change is specifically about the OG path.

Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Preview exercised on fff6404 (Preview build succeeded 2026-08-20T11:56Z), against
https://released-web-preview.lukaso.workers.dev:

200  /healthz
200  /  (homepage, title marker present)
200  /how-it-works
200  /r/honojs/hono/c/f82aba8e…  → first release v4.12.11 ✅
200  …/badge.svg                 → v4.12.11, cache-control: public, max-age=86400, s-maxage=86400 ✅
200  POST /api/lookup            → real result, canonical SHA resolved ✅

That covers the actual risk this PR carries. The change is in resolveLookup, which the public
permalink/badge/API routes share with /internal, so a regression in the cache key or the
bypassBackOffWhenCold opt-in would surface as a wrong or missing answer on those routes. It
doesn't: the permalink resolves, and the badge still long-caches a released result (the CUJ #3
semantics that would break first if the key alignment were wrong).

What the preview cannot cover: /internal itself, because og-preview can't reach
web-preview's /internal (INTERNAL_SECRET unset on the preview env — filed as #148). That path
is covered by the mutation-proven unit test instead: against 4cdb212 it fails
expected 503 to be 200.

Gate status unchanged: the single red check is osv dependency scan, which is #145 (three High
advisories in main's lockfile) and fails on every open PR regardless of its diff. #146 is the
fix; once it merges this goes green with no change here.

Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/wrangler.toml
Comment thread packages/web/src/routes/internal.ts Outdated
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

The one remaining red is not this PR's

osv dependency scan is the only failing check on fff6404. It fails on main's
lockfile, not on anything this branch changed.

Evidence — this branch's dependency files are byte-identical to main's:

$ git diff --stat origin/main origin/fix/og-cold-cache-key-alignment -- pnpm-lock.yaml package.json
(no output)

The three High advisories the gate trips on are the ones tracked in #145:

| GHSA-2v37-7h3g-55p8 | 8.2 | nanoid  | 3.3.16 | 3.3.18 |
| GHSA-5p4m-2wfm-xmqj | 7.5 | js-yaml | 4.3.0  | 4.3.1  |
| GHSA-4cwx-7wf7-3272 | 7.4 | undici  | 7.28.0 | 7.29.0 |
✗ osv: 0 Critical + 3 High vulnerability(ies) — must be resolved before merge.

#146 raises the override floors that clear all three. I re-scanned #146's
lockfile today against current OSV data (its CI green is from 17 Aug, and the
advisory database moves), and it still clears the gate:

Total 1 package affected by 4 known vulnerabilities (0 Critical, 0 High, 3 Medium, 1 Low)

Only four hono Mediums remain, which scripts/osv-check.sh does not gate on.

Merge order

  1. fix(deps): raise override floors to clear 3 High advisories (nanoid, js-yaml, undici) #146 first.
  2. Then re-run the checks on this PR. No rebase needed: ci.yml's osv job
    uses a bare actions/checkout on a pull_request event, so it scans
    refs/pull/144/merge — this branch merged into main's tip. Once fix(deps): raise override floors to clear 3 High advisories (nanoid, js-yaml, undici) #146 is on
    main, that merge ref already carries the fixed lockfile.

The catch is that GitHub does not re-run PR checks by itself when the base branch
moves, so after merging #146 this PR will still show the stale red until someone
hits re-run. I will re-run it on the next cycle if you have not.

Everything else here is green, and the preview was exercised on fff6404 above.

@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Review round 2 addressed — head is now df20483

Three new threads landed from the automated review of fff6404. All three are disposed and resolved: two applied, one declined as deliberate and documented at the call site.

# Finding Disposition
internal.ts:75 originOf can return the literal string "null" Applied + regression test
wrangler.toml:130 The alignment can't be exercised in preview Applied (comment corrected)
internal.ts:180 Crawler-written :neg marker degrades the human permalink Declined — deliberate, now documented

Guard proof (the opaque-origin fix)

Written test-first and watched fail on the defect in its own title, not an adjacent one:

  • Mutation: PUBLIC_BASE_URL=file:///srv/web. It contains //, so it skips the scheme-prefix branch, new URL() parses it, and .origin is the string "null" — non-null, so it satisfies ?? and reaches new Request(cacheOrigin(env, req)), which is outside neverFatal.
  • Assertion that fired: expect(res.status).toBe(200)AssertionError: expected 500 to be 200. That 500 is app.onError turning a computable OG lookup into web-og's neutral placeholder — the same class as the scheme-less PUBLIC_BASE_URL slip already fixed here.
  • After the guard: 200, falls through to PROD_HOST's public slot, findRelease never called.

On the preview claim

I checked Cloudflare's current Cache API docs rather than trusting my priors. Cache operations are functional only for Workers on custom domains. Our preview is *.workers.dev, so it can never warm a slot and therefore can never validate this alignment. The comment claiming otherwise is gone; the PUBLIC_BASE_URL var stays (it keeps preview off the prod key namespace and drives canonical URLs). The alignment is proven by the unit tests and takes effect on the prod custom domain.

Verification on df20483

  • Full gate local: lint (exit 0), typecheck, pnpm -r test601 tests pass.
  • CI on this head: all test legs, a11y, deploy-config dry run, gitleaks, shell+workflow lint pass.
  • Preview exercised (released-web-preview.lukaso.workers.dev): /healthz, /, /how-it-works 200; permalink honojs/hono@f82aba8v4.12.11; badge renders. Note per the above that preview cannot exercise the cache behaviour itself — this proves the code runs, not that slots warm.

The one remaining red is still not this PR's

osv dependency scan fails on main's lockfile, not on anything here — this branch's package.json and pnpm-lock.yaml are byte-identical to main's. That's #145, and #146 is the fix.

Merge order: #146 first. Then gh run rerun this PR rather than rebasing — ci.yml uses a bare actions/checkout on pull_request, so the checks already scan refs/pull/144/merge (this branch merged into main's tip); #146 landing clears the scan with no commit here, but GitHub won't re-run on its own when the base moves.

Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/src/resolve.ts Outdated
Comment thread packages/web/src/resolve.ts Outdated
Comment thread packages/web/src/resolve.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/resolve.ts
Comment thread packages/web/src/resolve.ts Outdated
Comment thread packages/web/src/resolve.ts Outdated
… rationales

Round 15 review of #144. Three findings, all in this diff's own code or its
own docs.

1. `writtenNoEarlierThan`'s missing-stamp fallback went the OPPOSITE way from
   the direction its header claimed. `cache.getEntry` reports `ageSeconds: 0`
   for an entry with no `x-cached-at`, so an unstamped pair evaluated `0 <= 0`
   -> true and the marker vouched for the entry unconditionally. The fallback
   is removed: an unprovable ordering now recomputes, as documented.

2. The `bypassBackOffWhenUnservable` doc still said "the marker is still
   WRITTEN on failure". `ea541fe` added `if (!backedOff)`, which makes that
   false on exactly the path the flag creates.

3. The shared-TTL note explained the gallop-partial misclassification but not
   what this PR changes about it: sharing the key makes an UNFURL a writer of
   the slot, so crawler traffic can now pin badge/permalink to an unconfirmed
   gallop tag. Stated plainly, with the fix still scoped to #155/#159.
@lukaso-bot

Copy link
Copy Markdown
Collaborator Author

Round 15 — all five threads drained, and a decision this PR now needs from you

Three findings applied (ef2bde6), one declined with reasoning, one split out as #162. Zero unresolved threads. Gate green: 615 tests, typecheck, lint, gitleaks, osv all clean.

But draining threads is not the same as this PR being ready, and I want to put the real state in front of you rather than leave it in a thread.

Where this PR actually is

It opened as "align /internal/*'s cache key with the public routes" — a two-part fix. It is now 19 commits, +2280/−69 across 9 files, over 15 review rounds. Every round has been legitimate: sharing a cache slot with the public routes genuinely created new interactions (back-off marker, SWR bounds, partial pinning, background flight), each round found real defects in that new machinery, and fixing them added more machinery to review. That is the loop, and it is not converging on its own.

The thing that should decide it

The finding on internal.ts:191 (now #162) challenges the premise, not the code: caches.default may be scoped to the invocation rather than to the URL passed to match()/put(). If it is, then rewriting the key URL to the public origin changes nothing, every put still no-ops, and most of the machinery in this PR is sitting on a permanently cold path.

I could not settle it. Cloudflare's Cache API docs list where the Cache API has no effect — dashboard, Playground, Workers behind Access — and say custom-domain Workers have functional caching. Service bindings are not mentioned either way. The unit tests use an in-memory store and pass under both causes; *.workers.dev preview has no custom domain, so it cannot discriminate either. So I am not able to tell you that the diagnosis is confirmed, and I would rather say that than let 2280 lines imply it.

Three ways to go, and what I'd pick

A — Merge as-is, then run the one check that settles it. (My recommendation.) #143 is a real defect live in production right now: every cold OG unfurl serves the neutral placeholder, and that placeholder is what Slack and X have cached. This PR is green and every guard in it is mutation-proved. Merge it, then request a cold OG URL twice:

GET https://og.released.blabberate.com/r/<owner>/<repo>/c/<sha-never-requested-before>.png

Second response real card + max-age=86400 → the key URL was the cause and this PR is load-bearing. Still the placeholder + max-age=60 → the cache is invocation-scoped, and #162's change is what actually fixes it. Either answer is worth more than another review round, and it takes one command.

B — Land less. If 2280 lines is more than you want to merge on an unconfirmed diagnosis, I can split: the key alignment alone (the original fix) in one PR, the SWR/partial/back-off machinery in a second. Honest cost: a few more rounds, and #143 stays broken in production while we do it.

C — Do #162 first. Have web-og call the binding on PUBLIC_BASE_URL instead of https://web. That is correct under both hypotheses and deletes cacheOrigin's ?? chain, originOf()'s single-label guard, and the wrangler.toml suite — a net simplification. Cleanest end state, but it rewrites part of this PR, so it means starting the review over.

I'd take A: ship the fix for the live bug, then spend one command learning how much of it we get to delete. I'm not merging anything — this is your call.

Round 15 changes in detail

Applied — writtenNoEarlierThan's missing-stamp fallback did the opposite of its documented job. cache.getEntry reports ageSeconds: 0 for an unstamped entry, so an unstamped pair evaluated 0 <= 0 → true and the marker vouched for the entry unconditionally, where the header claimed "an unprovable ordering recomputes". The fallback is removed rather than guarded — no reachable case left that it decided correctly. Two guards, both mutation-proved against this defect (fallback restored):

× recomputes when NEITHER the marker nor the entry carries a stamp
    AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times
× recomputes when only ONE side of the pair carries a stamp
    AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times

The second is seeded so the age comparison gives the wrong answer by a different route (entry unstamped at 5s, marker stamped at 0s → 0 <= 5 → vouch); without that it would have passed for the wrong reason.

Applied — two rationales that no longer matched the code. The bypassBackOffWhenUnservable doc still said "the marker is still WRITTEN on failure", which ea541fe's if (!backedOff) made false on the one path the flag creates. And the shared-TTL note explained the gallop-partial misclassification without saying what this PR changes about it: sharing the key makes an unfurl a writer of that slot, so crawler traffic can now pin badge.svg to an unconfirmed gallop tag for 30 days. Said plainly now; the fix stays in #155/#159, where it can be reviewed as the change to the public routes that it is.

Declined — the partial throttle's re-read. ownRecentPartial is frozen from the first read, so a concurrent caller's correctly-marked partial gets retraversed. The analysis is right, but the window is a same-colo, different-isolate write landing inside one Cache API round trip, and the fix adds a fourth conditional cache read to the function that is already why this PR is on round 15. Backlogged with file/line.

Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/resolve.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/test/cache.test.ts Outdated
… test rationale

Two comment-only corrections from review round 16. No behaviour change.

- `routes/internal.ts`: the doc block claimed the `/internal` key "MUST match
  the public permalink routes' exactly" without noting that this holds for the
  issue and PR routes only. On the commit route `og-meta.tsx` builds the
  `og:image` URL from `shortSha()` (7 chars) while `result.tsx` keys the
  permalink on the full 40, so a commit unfurl still misses the slot the
  permalink warmed. Record the caveat and point at #147, so the function is not
  read as having closed #143 for commit links.

- `test/cache.test.ts`: the rationale said an unstamped pair makes an ordering
  test "fall back to ages". `writtenNoEarlierThan` does the opposite — it
  returns false when either stamp is null, so the pair is always recomputed,
  never served. State what ships.
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts Outdated
Comment thread packages/web/src/resolve.ts
Round 17 review, packages/web/src/routes/internal.ts:58. `cacheOrigin`
passes its two CONFIGURED arms through `originOf`, which rejects a
single-label host precisely because `https://web` is what the Cache API
silently declines — but the request-origin fallback skips that check, and
for a real Service Binding that origin IS `https://web`.

So if PROD_HOST is dropped from [vars], blanked in the dashboard, or a new
[env.*] ships that the committed wrangler.toml does not describe, every
/internal read misses and every write no-ops. `neverFatal` then renders
that as "served, just not cached": #143 is back with no error, no log and
no metric — the silence that made it look green for weeks.

Behaviour is unchanged; the relapse just stops being invisible. The
wrangler.toml guard in internal-cache-origin.test.ts covers the committed
file, and this covers the config it cannot see.

Mutation evidence (all three assertions fail on a concrete input):
- remove the warn        -> "warns when it falls back to a request origin
                            the Cache API will decline" fails,
                            expected '' to contain 'https://web'
- warn unconditionally   -> "stays silent when the request-origin fallback
                            is itself routable" fails
- warn above the
  configured early return -> both "stays silent" tests fail

pnpm -r test 429 passed, pnpm -r typecheck, pnpm lint clean.
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/resolve.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
Round 18 review, packages/web/src/routes/internal.ts:73. The warning added
in 70a1640 only fires when NEITHER var is set. A var that IS set but
rejected by `originOf` is indistinguishable from unset: the `??` chain
falls straight through to the next arm, `configured` comes back truthy,
and the fallback branch is never reached.

The reviewer's own scenario does not hold — a fall-through to a valid
PROD_HOST lands on `https://released.blabberate.com`, which this Worker
does serve, so the cache works. But the finding survives that correction,
because the real hazard is PREVIEW, not prod: PUBLIC_BASE_URL exists
specifically so preview does not key on production's origin, and PROD_HOST
is committed in [env.preview.vars] too (it gates analytics). Mistype
PUBLIC_BASE_URL in the dashboard — where the wrangler.toml guard cannot see
it — and `originOf` discards it, PROD_HOST answers instead, and the preview
Worker writes every /internal entry onto the PRODUCTION origin. Silently,
with a perfectly routable origin hiding the fault.

Behaviour is unchanged; the discarded override just stops being invisible.

Mutation evidence (every assertion fails on a concrete input):
- drop the `raw &&` present-check  -> "stays silent when a routable origin
                                      IS configured" fails (expected 1 to
                                      be +0) AND "stays silent when the
                                      request-origin fallback is itself
                                      routable" fails (expected 2 to be +0)
- check only PUBLIC_BASE_URL       -> "warns when PROD_HOST is set but
                                      rejected" fails, expected '' to
                                      contain 'PROD_HOST'
- before the fix, both new tests were red on the real defect:
  expected '' to contain 'PUBLIC_BASE_URL' / 'PROD_HOST'

pnpm test 431 passed, pnpm -r typecheck, pnpm lint clean.
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
Comment thread packages/web/src/routes/internal.ts
@lukaso-bot lukaso-bot mentioned this pull request Sep 1, 2026
@lukaso-bot lukaso-bot added the user action needed The liveapp engine is blocked on the human — the issue body carries the exact action needed label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

user action needed The liveapp engine is blocked on the human — the issue body carries the exact action needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cold-cache OG unfurls serve the placeholder again (#53 regression) — probe is blind because it re-warms its own fixtures

1 participant