Repository navigation
fix(router): stabilize rango-state cookie — review findings + test gaps - #566
Merged
ivogt merged 1 commit intoJun 12, 2026
Merged
Conversation
Three review rounds over the rango-state cookie branch (#560). Correctness: - Action fence leaked on the X-RSC-Reload (version-mismatch) terminal: like the simple-redirect and router-id-mismatch terminals it navigates away via a never-settling promise, so the finally never runs. finalizeAction() now runs in onReload, skipping invalidation (the mismatch short-circuits server-side, so nothing mutated and a broadcast would only risk hard-reloading a sibling). - _rotateStateCookie's monotonic guard read the prior value through the decoding getParsedCookies, double-decoding any version containing ':' or '%' (NaN timestamp -> prevTs 0 -> same-ms collision the divergence observer can't see). Reads raw via the shared getRawCookieValue now; an empty header/cookie counts as absent (no '' short-circuit of the ?? chain). - CF response cache replayed per-client signals on READ: a pre-fix or pinned-`version` KV envelope (or L1 entry) could replay one client's Set-Cookie to another. toClientResponse and kvGetResponse strip now; the KV hd filter is inside the corrupt-envelope try so a malformed hd evicts rather than throwing. - finalizeAction wraps invalidation in try/finally so a throw can't leak the fence; removed the dead NavigationStore.broadcastCacheInvalidation. Consolidation: - One PER_CLIENT_SIGNAL_HEADERS source for the strip/predicate/presence helpers. - One getRawCookieValue shared by both seats — their agreement IS the guard's contract, so the client mirror and server read the same jar entry. Tests (dev + production where e2e): - Latch exactly-once per terminal incl. simple-redirect (success + keepCache) and reload (no invalidation); equals-mirror observer no-op. - credentials-not-omit + no-store-under-fence on navigation and prefetch fetches. - percent-encoded + empty header/cookie server mint. - CF read-side strip: KV replay, malformed-hd evict, L1 serve. - cookie-cleared-mid-session mints fresh via popstate; Back-after-redirect revalidates the pre-mutation page. Docs: design-doc terminal enumeration (+reload), keep-row exemption + end-state delta, read-side strip rule; feature-file-map (+action-fence).
6 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #560 (base:
feat/rango-state-cookie). This is the stabilization delta for the rango-state cookie work — three adversarial review rounds over #560 surfaced one latent data bug, three fence/guard correctness defects, and a set of missing contracts. Review it as the diff on top of #560.Problem statement (consumer-observable)
Three issues a consumer could actually hit, none caught by #560's own tests:
beforeunload"Stay" on a dirty form), the action fence leaked permanently: prefetch consumption stayed suspended and every fetch wentno-store/SWRfor the rest of the session.:or%silently defeated cache invalidation. The server's monotonic guard double-decoded the prior state value, so a same-millisecond server rotation could mint a value equal to the client's — the divergence observer never fired and the client kept serving stale, Vary-keyed bytes.versioncould leak one user'sSet-Cookieto another. The CF response cache stripped per-client signals only on write. A KV envelope written by a pre-fix build, or persisted under a consumer-pinnedoptions.version, replayed its storedSet-Cookie(a session/consent cookie a loader set) to the next client for the wholettl+swrwindow.Before / after
(2) Server monotonic guard —
request-context.ts:(3) CF read-side strip —
cf-cache-store.ts:Terminal semantics (the fence-leak class)
Every action terminal must release the fence exactly once; the never-settling navigate-away terminals can't rely on the
finally.finally→finalizeAction()keepClientCache)X-RSC-Redirect)finalizeAction()finalizeAction()X-RSC-Reload)onReload→finalizeAction(true)The reload row is the one this PR adds — it was the only never-settling terminal without an explicit release. It skips invalidation deliberately: nothing executed server-side, and a broadcast would only risk hard-reloading a sibling tab mid-task.
Code-change summary
browser/server-action-bridge.ts—finalizeAction(skipInvalidation?)+try/finally;onReloadreleases the fence without invalidating; ungated repair-refetch comments point at the spec keep row.server/request-context.ts— raw monotonic-guard read via sharedgetRawCookieValue; empty header/cookie treated as absent; local parser removed.cache/cf/cf-cache-store.ts— read-side strip intoClientResponse+kvGetResponse(filter inside the corrupt-envelopetry).browser/cookie-name.ts— singlePER_CLIENT_SIGNAL_HEADERSsource;stripPerClientSignals; sharedgetRawCookieValue(also used by the clientreadCookie).browser/{navigation-store,types}.ts— removed deadbroadcastCacheInvalidation.feature-file-map(+action-fence).Test plan
pnpm run typecheckpnpm run test:unit— 3630 pass (+ new: latch per terminal incl. reloadtimes(0), equals-mirror observer, credentials/no-store on nav + prefetch, percent-encoded + empty header/cookie mint, CF KV-replay strip, malformed-hd evict, L1-serve strip)pnpm run lint/pnpm run formatpnpm check:e2e-bucketing(pre-commit) — every build-fixture describe correctly bucketedNotes
getRangoState()double-read (load-bearing — theawaitbetween the prefetch-key read and the header read lets an external rotation correctly miss the stale entry); abort-before-response rotating despite keep intent (fail-toward-freshness).buildPayloadMetadata()helper across the five RSC render sites; hoisting the e2ereadRangoStateparser into the shared helper.