Skip to content

Start SSR: singleton getRouter() silently leaks request-scoped router state across requests - please add dev warning #6924

@jmalmo

Description

@jmalmo

Which project does this relate to?

Start

Describe the bug

The routing docs correctly state that getRouter() must return a new router instance each time. However, violating this contract fails catastrophically and silently under SSR - no error, no warning, and the app works perfectly for hours before breaking.

When getRouter() returns a singleton, request-scoped router state (including redirect, matches, location, resolvedLocation, statusCode) leaks across server requests. In my case, a single bot request that triggered URL canonicalization set router.state.redirect on the shared instance, and every subsequent HTML GET request returned a 307 redirect until the process was restarted. POST requests (server functions) were unaffected because they take the early server-action branch before router.load().

This is easy to introduce because the natural pattern when you need a stable reference for type registration looks like:

export const router = createRouter({ routeTree })

export function getRouter() {
  return router // singleton - catastrophic under SSR, no warning
}

Steps to reproduce

  1. Create a Start app with SSR enabled
  2. Make getRouter() return a singleton:
    const router = createRouter({ routeTree })
    export function getRouter() { return router }
  3. Start the server
  4. Send a request to a URL that triggers a redirect (e.g., a route with beforeLoad that throws redirect(), or a URL that triggers URL canonicalization like //double-slash-path)
  5. Send a normal GET request to / - it returns the stale 307 redirect from step 4
  6. All subsequent GET requests continue returning the stale redirect until the process is restarted

Expected behavior

In development, createStartHandler should detect when getRouter() returns the same router instance across multiple requests and emit a warning:

[TanStack Start] getRouter() returned the same router instance across multiple
server requests. This will cause request-scoped state to leak between requests.
getRouter() must return a new createRouter() instance each call.
See: https://tanstack.com/start/latest/docs/framework/react/guide/routing

A simple referential equality check against the previous call's return value would catch this.

Request

  1. Dev-time warning in createStartHandler when the same router instance is observed across requests (low-risk, high-value)
  2. Docs update - add an explicit "don't do this" example showing the singleton anti-pattern alongside the correct pattern in the routing guide

Workaround

Return a fresh createRouter() per call:

const routerOptions = { routeTree, scrollRestoration: true }

export function getRouter() {
  return createRouter(routerOptions)
}

Additional context

  • The failure mode is severe: complete site outage for all SSR GET requests, with no errors in logs
  • It is time-delayed: the app works after restart until a request triggers any redirect, which can take hours depending on traffic patterns
  • POST requests (server functions) always work, making diagnosis harder
  • I observed this on @tanstack/react-start ~1.166.x with Nitro node-server preset

I am happy to submit a PR for the dev warning and/or docs update if you would like - just let me know the preferred approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions