Skip to content

fix(router-core): fall back to CSR when SSR bootstrap data is missing during hydration#7674

Open
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:ynizipli/fix-hydrate-missing-bootstrap
Open

fix(router-core): fall back to CSR when SSR bootstrap data is missing during hydration#7674
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:ynizipli/fix-hydrate-missing-bootstrap

Conversation

@anonrig

@anonrig anonrig commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

hydrate() throws Invariant failed and crashes the entire app (the error propagates through Await / useAwaited to the nearest error boundary) when the SSR bootstrap global is missing:

// packages/router-core/src/ssr/ssr-client.ts
if (!window.$_TSR) {
  // ...dev throw...
  invariant() // prod: throw new Error('Invariant failed')
}
// ...
if (!window.$_TSR.router) {
  invariant()
}

window.$_TSR is populated by an inline <script> that the server streams with the HTML. It can legitimately be absent at hydration time when:

  • the streamed HTML is truncated (aborted navigation, flaky mobile, the client disconnecting mid-stream),
  • the inline bootstrap script never executes — link-preview crawlers / social bots, in-app webviews (TikTok, Instagram, etc.), CSP, or extensions that strip inline scripts.

In all of these cases the right behavior is to render on the client, not to white-screen. Today the hard invariant() turns a recoverable condition into a full app crash.

This is the same class of problem reported in #7524 ("the $_TSR is deleted too early, and ends up undefined"). That PR tried to delay teardown and was reverted in #7533 in favor of immediate teardown — so the absence of $_TSR still surfaces as a fatal invariant rather than being handled.

Fix

When the bootstrap data is missing, bail out of hydration instead of throwing. Leaving router.ssr unset means the Transitioner runs router.load() on mount — exactly the path a pure client-side app uses:

// packages/react-router/src/Transitioner.tsx
useLayoutEffect(() => {
  if (
    // if we are hydrating from SSR, loading is triggered in ssr-client
    (typeof window !== 'undefined' && router.ssr) || /* ... */
  ) {
    return
  }
  // ...
  await router.load()
}, [router])

So the page recovers with a normal client render instead of crashing. A dev-only console.warn is logged so genuine SSR misconfiguration is still easy to spot (the message is stripped from prod bundles by the existing process.env.NODE_ENV guard).

This lives in core hydrate(), so the React / Solid / Vue start clients all benefit.

Behavior change

  • Before: missing window.$_TSR / window.$_TSR.routerInvariant failed thrown → app crashes to the error boundary.
  • After: missing data → hydration is skipped → client renders via router.load(); dev-only warning logged.

Tests

  • Updated hydrate.test.ts: the two cases that asserted a throw now assert graceful fallback (hydrate resolves, matchRoutes is not called, router.ssr stays unset, a dev warning is emitted).
  • Full @tanstack/router-core unit suite passes (1178 passed, 3 expected-fail), no type errors, eslint clean.

Summary by CodeRabbit

  • Bug Fixes
    • Hydration no longer fails when server-side rendering bootstrap data is unavailable. The application now gracefully falls back to client-side rendering instead of throwing an error, improving robustness for edge cases like truncated HTML streams.

… during hydration

`hydrate()` threw `Invariant failed` and crashed the whole app through the
error boundary when `window.$_TSR` (or `window.$_TSR.router`) was absent. That
data can legitimately be missing when the streamed HTML is truncated or the
inline bootstrap script never runs: aborted navigations, crawlers/link-preview
bots, in-app webviews, and CSP/extension-blocked inline scripts.

Instead of throwing, hydration now bails out and lets the client render from
scratch. Leaving `router.ssr` unset makes the Transitioner run `router.load()`
on mount (the same path a pure client-side app uses), so the page recovers
instead of white-screening. A dev-only warning is logged to surface genuine
SSR misconfiguration.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de0b218e-cd50-4f14-9f57-3e51ffb7eca1

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and 470b354.

📒 Files selected for processing (3)
  • .changeset/hydrate-missing-bootstrap-fallback.md
  • packages/router-core/src/ssr/ssr-client.ts
  • packages/router-core/tests/hydrate.test.ts

📝 Walkthrough

Walkthrough

hydrate() in router-core is updated so that missing window.$_TSR or window.$_TSR.router globals no longer throw a fatal error. Both cases now emit a development-only console.warn and return early, leaving router.ssr unset so the app falls back to client-side rendering. Tests and a changeset entry reflect this behavior change.

hydrate() SSR bootstrap fallback

Layer / File(s) Summary
hydrate() non-fatal bootstrap fallback implementation
packages/router-core/src/ssr/ssr-client.ts
Both window.$_TSR absent and window.$_TSR.router absent cases are changed from fatal invariant/throw to a dev-only console.warn followed by an early return, leaving router.ssr unset for client-side rendering.
Tests and changeset
packages/router-core/tests/hydrate.test.ts, .changeset/hydrate-missing-bootstrap-fallback.md
Tests replaced from asserting rejection to asserting resolution, verifying matchRoutes is not called, router.ssr is undefined, and console.warn is triggered. Changeset documents the @tanstack/router-core patch bump.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • TanStack/router#7416: Modifies the same hydrate() control flow in ssr-client.ts and updates the same hydrate.test.ts file around the pre-matchRoutes hydration phase.

Suggested labels

package: router-core

Poem

🐇 No more crashes when the stream gets cut!
The bootstrap globals shut their door, but
hydrate just winks, warns softly instead,
and lets the client render ahead.
A graceful hop past SSR's missing bread~ 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing hydration to fall back to client-side rendering when SSR bootstrap data is missing, which is the core objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@schiller-manuel

Copy link
Copy Markdown
Collaborator

"t can legitimately be absent at hydration time when:"

really? we ensure that $_TSR is emitted before the client entry script, so if the HTML is truncated, the client entry should not execute at all and thus no hydration happens.

is it really athing that some clients dont execute inline scripts, but execute external scripts?

@anonrig

anonrig commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

We see this error as "invariant failed" and nothing else. So hard to explain and reproduce.

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.

2 participants