Skip to content

fix(routing): decode the flash cookie once per request, not per router - #647

Merged
ryansolid merged 2 commits into
nextfrom
fix/flash-decode-per-request
Oct 1, 2026
Merged

ryansolid merged 2 commits into
nextfrom
fix/flash-decode-per-request

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Rendering a page after a failed no-JavaScript form post hangs the server: the stream retries forever, allocating until the process runs out of memory. Found in examples/todos-server in solidjs/solid (#3717) once the flash cookie had a secret to be written with.

Cause

A server render can re-create the router while it retries a suspension. The trigger is ordinary app code:

<Document>                      // {props.children} in <body>
  <Router>
    {props => (
      <Errored fallback={...}>
        <Header />              // const adds = useSubmissions(addTodo)
        <Loading>{props.children}</Loading>
      </Errored>
    )}
  </Router>
</Document>

useSubmissions in Header's body reads the flash seeding memo, which suspends on the async (encrypted) decode. The server <Errored> escalates a suspension in its children to the nearest retryable hole, and under the hydratable server output that hole is the document's {props.children}, above the router — so each retry renders a new router. The decode was cached on the router instance, so each new router started it again: the read never settled, each pass added a fresh blocking promise, and the stream retried without end. Each new router also appended another one-shot Set-Cookie clear.

The cookie header and its decode now live in a WeakMap keyed by the request event, shared by every router created for that request.

Re-creating components above a suspension on retry is how the server renderer already behaves (core's lazy() asset lookup was made per-request for the same reason); whether <Errored> should widen the retry that far is a separate question I'm raising in solidjs/solid.

Public API changes

None. Behavior: the flash cookie is detected, cleared and decoded once per request instead of once per router instance — a render that re-creates the router now settles, and appends a single Set-Cookie clear.

Tests

  • test/server/flash-decode-per-request.spec.tsx: the shape above, streamed with a flash cookie. Asserts the outcome renders, the router was in fact rendered more than once, the decode ran once and the cookie was cleared once. Times out before the fix.
  • The server test config now compiles hydratable, as apps do — the hang only reproduces under the hydratable server output. The other server specs pass unchanged under it.

Server suite 77 passed, unit suite 469 passed, tsc clean. Independent of #646.

A server render can re-create the router while it retries a suspension.
A `useSubmissions` read in a component body under `<Errored>` suspends on
the async flash decode; the server boundary escalates that to the nearest
retryable hole, and in a document shell that hole is `{props.children}`
above the router, so each retry renders a new router. The decode was cached
on the router instance, so every new router started it again: the seeding
read never settled, each pass added a fresh blocking promise, and the
stream retried until the process ran out of memory. Each new router also
appended another one-shot `Set-Cookie` clear.

The cookie header and its decode now live in a WeakMap keyed by the request
event, shared by every router created for that request.

The server test config now compiles hydratable, as apps do. The hang only
reproduces under the hydratable server output, which is what places the
retry above the router; the other server specs pass unchanged under it.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bd4d58c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit a6a2cee into next Oct 1, 2026
4 checks passed
@ryansolid
ryansolid deleted the fix/flash-decode-per-request branch October 1, 2026 07:49
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.

1 participant