lazyRouteComponent reload guard key collides on Safari, capping stale-deploy recovery at one per tab
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Router
Describe the bug
lazyRouteComponent's stale-deploy recovery guards its one-time reload with a sessionStorage key derived solely from the error message:
https://github.com/TanStack/router/blob/main/packages/react-router/src/lazyRouteComponent.tsx
const storageKey = `tanstack_router_reload:${error.message}`
if (!sessionStorage.getItem(storageKey)) {
sessionStorage.setItem(storageKey, '1')
window.location.reload()
throw new Promise(() => {})
}
That works when the message identifies the chunk, which it does on Chrome and Firefox — both append the module URL. It does not on Safari, whose message is the bare string Importing a module script failed. with no URL. isModuleNotFoundError's own comments record this difference:
// chrome: "Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
// firefox: "error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
// safari: "Importing a module script failed."
So on Safari the key collapses to a single value — tanstack_router_reload:Importing a module script failed. — shared by every lazy route in the application, for the lifetime of the tab.
The consequence is that a Safari tab gets one recovery, ever, no matter how many separate chunks fail or how many deploys it lives through. The first stale chunk reloads and recovers; every subsequent one finds the key already set, skips the reload, and rethrows — leaving the user on the error component with no automatic recovery, which is the exact situation the guard exists to prevent.
Chrome and Firefox are unaffected: each chunk URL yields its own key, so the guard does what it is meant to do (suppress a reload loop for one chunk that stays missing, while still recovering other chunks).
Note this is distinct from #7633. That issue is about isModuleNotFoundError failing to classify certain failures; this is about the guard key being non-unique once a failure has been correctly classified. Widening the predicate does not address it, and #7653 (now closed) would not have.
Complete minimal reproducer
No hosted reproducer — this is reasoned from the source above rather than from a running app, because reproducing it needs two successive deploys against one long-lived Safari tab. The conditions are exact and the code path is short, so I hope the inspection is enough to act on; happy to build a reproducer if that is the blocker.
The observable prerequisite (Safari's message carrying no URL) is already asserted by the router's own source comments, quoted above.
Steps to Reproduce the Bug
- Build and deploy an app with at least two code-split routes,
/aand/b, whose chunks are content-hashed. - Open the app in Safari and stay on the home route without refreshing.
- Deploy a change that rehashes the chunks for both
/aand/b. The old chunks are no longer served. - Navigate to
/a. The import fails,isModuleNotFoundErrormatches, the guard key is set, and the tab reloads — recovered, as designed. - With that tab still open, deploy a second change that rehashes the chunks again.
- Navigate to
/b(or back to/a).
Expected behavior
Step 6 performs one reload and recovers, the same as step 4 did — the guard should suppress a repeated reload for the same failing chunk, not for every chunk in the app.
Actual behavior
Step 6 performs no reload. sessionStorage already holds tanstack_router_reload:Importing a module script failed. from step 4, so the guard declines and the error is rethrown to the error component. The user is stuck until they refresh by hand.
Platform
- Browser: Safari (any version; the behavior follows from Safari's error message wording)
- Affected packages:
@tanstack/react-router(guard),@tanstack/router-core(message classification) - Versions: reproduces on current
latest—@tanstack/react-router@1.170.34,@tanstack/router-core@1.171.29
Additional context
A fix would be to key the guard on something that identifies the chunk rather than on the message alone — the route id available at the lazyRouteComponent call site is the obvious candidate, since it is stable across deploys and unique per split route:
const storageKey = `tanstack_router_reload:${routeId}:${error.message}`
That preserves the existing loop protection (the same route failing twice in one tab still only reloads once) while letting a different route recover on its own.
Found while tracking down why a stale tab was not recovering after a deploy on a TanStack Start app. The server-side half of that turned out to be ours to fix, but this guard caps how well the built-in recovery can work on Safari regardless.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/react-router/src/lazyRouteComponent.tsx, where the sessionStorage guard is assembled, and trace the lazyRouteComponent call site to confirm the available route id. Preserve one-time reload protection for the same route while allowing separate Safari lazy-route failures to recover independently; verify the behavior across two lazy routes and successive stale-deploy failures.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100