fix(with-tanstack-router): use per-request router for SSR - #271
Merged
birkskyum merged 1 commit intoAug 9, 2026
Merged
Conversation
The v2 template shared a single module-level router instance across all SSR requests. Under concurrency this leaks match state and loader data between requests — deterministically once any route has an async loader (the shipped demo hides it only because its routes have no loaders). This is the structural issue reported in solidjs#268. Create a fresh router per request inside routerLoad, stash it on event.locals.router, and read it back in app.tsx via getRequestEvent(). The module-level singleton export is kept as the CSR fallback, since the browser shares no state across requests. Closes solidjs#268
nerdchanii
added a commit
to nerdchanii/solid-tanstack-router-ssr-race
that referenced
this pull request
Aug 8, 2026
Before/after measurement on @solidjs/start 2.0.0 stable (isolated ports): - singleton (baseline): 15/15 cold-start concurrent leaks (exit 2) - per-request PR #271 (event.locals.router, 3-arg createHandler): 0/15 (exit 0) - per-request control (FACTORY_PATCH context.__router, 2-arg): 0/5 (exit 0) Both per-request approaches are equivalent and both pass; the singleton baseline leaks 100%. Documents the isolated-port harness trap that initially made the per-request configs appear to leak 15/15 (a stale server was still listening on port 3000). Refs solidjs/templates#268, solidjs/templates#271
Contributor
Author
|
@birkskyum Please confirm the Pull Request. |
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.
Summary
Closes #268.
The
solid-start-v2/with-tanstack-routertemplate shared a single module-level router instance across all SSR requests. Under concurrency this leaks match state and loader data between requests — deterministically, once any route has an async loader. The shipped demo hides the leak only because its routes are static with no loaders.This PR keeps SSR but makes the server-side router per-request.
Changes
entry-server.tsx—routerLoadnow callscreateRouter()(the factory) once per request and stashes the instance onevent.locals.router, instead of mutating the shared singleton.app.tsx— reads the per-request router back viagetRequestEvent()?.locals.router, falling back to the singleton for CSR.router.tsx— unchanged. It already exports thecreateRouter()factory. The module-level singleton export (router) is retained as the CSR fallback, since the browser shares no state across requests.Why a per-request router?
A router instance holds its match cache and loader results. On
@solidjs/startv2 (Nitro/h3) concurrent requests run on one event loop, so two requests sharing one router can commit one another's data — including user-scoped loader data. This reproduces 100% of the time once a route has an async loader (reproducer in #268).CSR is unaffected: each browser tab is an isolated runtime that never shares a router across requests, so the singleton is safe there and is kept as the fallback.
Why the server router never leaks to the client
getRequestEventresolves to a no-op returningundefinedin the client bundle (solid-js/webshipsvoidFn as getRequestEvent), sogetRequestEvent()?.locals.routershort-circuits to the singleton in the browser. The server's router object never crosses the boundary; only the current request's dehydrated state is serialized into the HTML via$_TSR, as before.Verification
@solidjs/start@2.0.0(stable):GET /andGET /aboutreturn 200 in bothdevandbuild && start.@solidjs/startv2, and a possible router-singlelet concern #268. I'll re-run the reproducer from [Bug] with-tanstack-router template: SSR returns 500 on@solidjs/startv2, and a possible router-singlelet concern #268 against this patch and post the before/after numbers.Notes
@solidjs/startv2, and a possible router-singlelet concern #268 (the 500 was already fixed upstream bycreateHandlerre-accepting therouterLoadarg on stable).createRoutervsgetRouter) and to splitting/combining commits as desired.