Skip to content

fix(with-tanstack-router): use per-request router for SSR - #271

Merged
birkskyum merged 1 commit into
solidjs:mainfrom
nerdchanii:fix/tanstack-router-per-request-ssr
Aug 9, 2026
Merged

fix(with-tanstack-router): use per-request router for SSR#271
birkskyum merged 1 commit into
solidjs:mainfrom
nerdchanii:fix/tanstack-router-per-request-ssr

Conversation

@nerdchanii

Copy link
Copy Markdown
Contributor

Summary

Closes #268.

The solid-start-v2/with-tanstack-router 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 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.tsxrouterLoad now calls createRouter() (the factory) once per request and stashes the instance on event.locals.router, instead of mutating the shared singleton.
  • app.tsx — reads the per-request router back via getRequestEvent()?.locals.router, falling back to the singleton for CSR.
  • router.tsx — unchanged. It already exports the createRouter() 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/start v2 (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

getRequestEvent resolves to a no-op returning undefined in the client bundle (solid-js/web ships voidFn as getRequestEvent), so getRequestEvent()?.locals.router short-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

Notes

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
@nerdchanii

Copy link
Copy Markdown
Contributor Author

@birkskyum Please confirm the Pull Request.

@birkskyum
birkskyum merged commit 421a6b4 into solidjs:main Aug 9, 2026
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.

[Bug] with-tanstack-router template: SSR returns 500 on @solidjs/start v2, and a possible router-singlelet concern

2 participants