Skip to content

fix(router-core): don't treat the current pathname as a route template when canonicalizing on load#7611

Open
Armin0001 wants to merge 1 commit into
TanStack:mainfrom
Armin0001:fix-concrete-pathname-roundtrip
Open

fix(router-core): don't treat the current pathname as a route template when canonicalizing on load#7611
Armin0001 wants to merge 1 commit into
TanStack:mainfrom
Armin0001:fix-concrete-pathname-roundtrip

Conversation

@Armin0001

@Armin0001 Armin0001 commented Jun 12, 2026

Copy link
Copy Markdown

Fixes #7598

The bug

With pathParamsAllowedCharacters: ['$'] the router itself generates URLs whose param values start with $, e.g. /files/$EXAMPLE_CODE%2Ffile.abap for the route /files/$filePath. Client-side navigation works, but a full page load destroys the URL: it is rewritten to /files/undefined via history.replaceState and useParams() returns filePath: "undefined".

Root cause

On mount, Transitioner rebuilds the current location through buildLocation({ to: router.latestLocation.pathname, ... }) to check whether the URL needs canonicalizing (router.beforeLoad does the same on the server). buildLocation treats to as a route template, so the concrete segment $EXAMPLE_CODE%2Ffile.abap is parsed as a param placeholder. No such param exists, and interpolatePath falls back to "undefined". The same ambiguity makes the routesByPath lookup miss, so destRoutes comes back empty and search middlewares are skipped for these URLs.

The fix

buildLocation gets an internal _concreteTo flag, set by the four current-location round-trip call sites (react/solid/vue Transitioner and router.beforeLoad). With the flag set, to is never interpolated (a concrete pathname has nothing to interpolate) and a routesByPath miss falls through to concrete route matching instead of bailing out to destRoutes = []. All other buildLocation/navigate callers are unchanged, in particular the template-miss behavior for a navigate({ to }) containing $.

Changed tests

The three load.test.ts cases added in #6882 asserted the old behavior (server startup at /$a, /$toString, /$__proto__ redirects to /undefined). They now assert that the path is preserved and matched as a param value. That keeps their original point - inherited object properties must not leak into the URL - and also covers what #6881 originally reported: /$toString no longer redirects at all.

New regression tests: the round-trip scenarios in build-location.test.ts (pathname preserved, search middlewares applied to a $-containing concrete pathname) and an integration test in react-router/tests/router.test.tsx reproducing the reload scenario from #7598. Both fail without the fix.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where path parameter values containing $ characters were being incorrectly rewritten to "undefined" during page reloads or server-side startup. The canonical URL handling has been improved to correctly preserve these parameter values in your routes across all supported routers.

…e when canonicalizing on load

On mount (Transitioner) and on server startup (beforeLoad) the current
location is rebuilt via buildLocation({ to: latestLocation.pathname }).
Interpolating that concrete pathname mangles segments that merely look
like params (possible with pathParamsAllowedCharacters: ['$']) into
"undefined" and commits the broken URL with replace: true.

buildLocation now takes an internal _concreteTo flag, set by the four
round-trip call sites: the pathname is never interpolated and a
routesByPath miss falls through to concrete route matching, so search
middlewares run for these URLs again.

Fixes TanStack#7598
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes path parameters starting with $ being incorrectly rewritten to "undefined" on full page load by introducing an internal _concreteTo flag that marks location destinations as concrete pathnames rather than route templates, preventing interpolation of $-prefixed segments as template parameters.

Changes

Concrete Pathname Fix

Layer / File(s) Summary
Type contracts and buildLocation option shape
packages/router-core/src/RouterProvider.ts, packages/router-core/src/router.ts
Add _concreteTo?: boolean optional flag to BuildLocationFn options and BuildNextOptions to signal that destination locations should be treated as concrete pathnames rather than route templates.
Core buildLocation logic for concrete pathnames
packages/router-core/src/router.ts
Update destination route matching and pathname computation: skip $-template mismatch fallback when _concreteTo is set, and preserve nextTo uninterpolated for concrete pathnames to prevent treating $-prefixed segments as interpolation placeholders.
Apply _concreteTo flag in initialization and navigation
packages/router-core/src/router.ts, packages/react-router/src/Transitioner.tsx, packages/solid-router/src/Transitioner.tsx, packages/vue-router/src/Transitioner.tsx
Pass _concreteTo: true during server-side initial load in beforeLoad and in framework Transitioner components when building next location for subscribed navigation, ensuring current pathnames are treated as concrete.
Test coverage for _concreteTo behavior and regression fixes
packages/router-core/tests/build-location.test.ts, packages/router-core/tests/load.test.ts, packages/react-router/tests/router.test.tsx
Add buildLocation unit tests with helpers verifying _concreteTo preserves pathnames containing $, executes search middlewares, and contrasts with template interpolation; add integration tests asserting $-prefixed params survive reload and initial server load.
Release metadata and changelog
.changeset/heavy-donkeys-wave.md
Add changeset entry documenting patch version bumps for all affected @tanstack/*router* packages and describing the canonical-URL fix on mount and server startup.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

package: router-core, package: react-router

Poem

🐰 A dollar sign path once lost its way,
Rewritten to undefined—what dismay!
But now with concrete flags held tight,
These wayward params stay in sight,
On reload and startup, they're staying today! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.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 clearly summarizes the main fix: preventing the current pathname from being treated as a route template during canonical URL resolution on load.
Linked Issues check ✅ Passed The PR fully addresses issue #7598: it introduces the _concreteTo flag to prevent pathname interpolation, adds tests verifying $-prefixed params are preserved on reload, and fixes both the URL rewriting and search middleware skipping symptoms.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the pathname template interpretation issue: internal flag additions, test regressions, and Transitioner updates across all router variants are in scope.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
.changeset/heavy-donkeys-wave.md (1)

8-8: 💤 Low value

Use hyphenated compound adjective: "full-page load".

In release notes, the compound adjective form "full-page load" is preferred over "full page load" for consistency and clarity.

-Fix path param values starting with `$` being rewritten to "undefined" on full page load. The canonical-URL check on mount and on server startup no longer treats the current concrete pathname as a route template.
+Fix path param values starting with `$` being rewritten to "undefined" on full-page load. The canonical-URL check on mount and on server startup no longer treats the current concrete pathname as a route template.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/heavy-donkeys-wave.md at line 8, Update the release note text to
use the hyphenated compound adjective "full-page load" instead of "full page
load"; locate the sentence containing "Fix path param values starting with `$`
being rewritten to \"undefined\" on full page load." and change it to read "Fix
path param values starting with `$` being rewritten to \"undefined\" on
full-page load." to maintain consistent style.
packages/router-core/tests/build-location.test.ts (1)

2325-2334: ⚡ Quick win

Avoid as any in the new helper and keep strict typing.

Please type the buildLocation options instead of casting to any so this regression helper still benefits from strict-mode checks.

Suggested refactor
 function buildCurrentLocation(router: ReturnType<typeof setup>) {
-  return router.buildLocation({
+  const opts: Parameters<typeof router.buildLocation>[0] = {
     to: router.latestLocation.pathname,
     search: true,
     params: true,
     hash: true,
     state: true,
     _includeValidateSearch: true,
     _concreteTo: true,
-  } as any)
+  }
+  return router.buildLocation(opts)
 }

As per coding guidelines, "**/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/tests/build-location.test.ts` around lines 2325 - 2334,
The helper is using an unsafe cast "as any"; replace it with the actual options
type for buildLocation by typing the object with Parameters<ReturnType<typeof
setup>['buildLocation']>[0] (or a similarly derived type) and pass that typed
object to router.buildLocation; update buildCurrentLocation to create a
strongly-typed opts variable (e.g. const opts: Parameters<ReturnType<typeof
setup>['buildLocation']>[0] = { ... }) and then return
router.buildLocation(opts).

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.changeset/heavy-donkeys-wave.md:
- Line 8: Update the release note text to use the hyphenated compound adjective
"full-page load" instead of "full page load"; locate the sentence containing
"Fix path param values starting with `$` being rewritten to \"undefined\" on
full page load." and change it to read "Fix path param values starting with `$`
being rewritten to \"undefined\" on full-page load." to maintain consistent
style.

In `@packages/router-core/tests/build-location.test.ts`:
- Around line 2325-2334: The helper is using an unsafe cast "as any"; replace it
with the actual options type for buildLocation by typing the object with
Parameters<ReturnType<typeof setup>['buildLocation']>[0] (or a similarly derived
type) and pass that typed object to router.buildLocation; update
buildCurrentLocation to create a strongly-typed opts variable (e.g. const opts:
Parameters<ReturnType<typeof setup>['buildLocation']>[0] = { ... }) and then
return router.buildLocation(opts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34da1c68-2e96-4058-8ca8-3f0d5564297d

📥 Commits

Reviewing files that changed from the base of the PR and between 96eca43 and 45b1a6b.

📒 Files selected for processing (9)
  • .changeset/heavy-donkeys-wave.md
  • packages/react-router/src/Transitioner.tsx
  • packages/react-router/tests/router.test.tsx
  • packages/router-core/src/RouterProvider.ts
  • packages/router-core/src/router.ts
  • packages/router-core/tests/build-location.test.ts
  • packages/router-core/tests/load.test.ts
  • packages/solid-router/src/Transitioner.tsx
  • packages/vue-router/src/Transitioner.tsx

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.

Path params starting with $ are rewritten to "undefined" on page load

1 participant