fix(router-core): canonicalize the search string on publicHref in parseLocation - #8450
Maher-Reven wants to merge 2 commits into
Conversation
…seLocation The fast path in `parseLocation` builds `publicHref` from the re-stringified search, so a landing url whose search is not in the router's canonical form still produces a canonical `publicHref`. The other branch — taken whenever a rewrite is configured, and also when the pathname needs encoding — returned the raw history href instead. With any rewrite set, even a no-op one, landing on `?q=a%2Ab`, `?q=two%20words` or `?a=1&&b=2` therefore left a non-canonical search on `publicHref` while `href` was already canonical. The transitioner compares that against what `buildLocation` produces, sees a difference, commits a location change, and every matched loader runs a second time right after hydration. Nothing is visible: the address bar does not change and no navigation happens, so the only symptom is a duplicate server-function call per landing. Only the query component is re-stringified. The path and hash are left exactly as history gave them, because an output rewrite is not necessarily the inverse of the input one — round-tripping the url through `executeRewriteOutput` here would both double a basepath and make a user's `output` rewrite run during parsing, which is observable. Fixes TanStack#8448
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe router now canonicalizes the search component of ChangesCanonical search handling
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Suggested reviewers: Merge Risk: ⚪ Minimal · up to The router now canonicalizes rewritten location searches consistently, with regression coverage for the reported hydration mismatch. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
🎯 Changes
Fixes #8448.
parseLocationhas two branches. The fast one buildspublicHreffrom the re-stringified search:The other one — taken whenever a rewrite is configured, and also when the pathname needs encoding — returns the raw history href instead:
So with any rewrite set, even a no-op one, landing on a url whose search is not in the router's canonical form (
?q=a%2Ab,?q=two%20words,?a=1&&b=2) leavespublicHrefnon-canonical whilehrefis already canonical. The transitioner compares that against whatbuildLocationproduces for the same location, sees a difference, commits a location change, and every matched loader runs a second time right after hydration.Nothing is visible — the address bar does not change and no navigation happens — so the only symptom is a duplicate server-function call on every landing. As the reporter puts it, ad networks encode
*and spaces differently fromstringifySearch, so in their app this fired on every paid-ad landing.The fix
Only the query component is re-stringified. The path and hash are left exactly as history gave them.
I first tried the other option the issue suggests — sending the canonical url back through
executeRewriteOutput, mirroring howbuildLocationderivespublicHref. That is wrong here, and the existing suite catches it:basepath: '/other'and a path that does not carry that basepath, the output rewrite prepends it again andpublicHrefbecomes/other/app/publicinstead of/app/public.outputrewrite run during parsing. Three existing tests assert the rewrite call log, and it went from['input:/public']to['input:/public', 'output:/internal']— an observable side effect in user code.Re-stringifying only the query avoids both: no assumption that the rewrite round-trips, and no user callback invoked during parse.
✅ Checklist
🚀 Release Impact
Tests
A regression test in
packages/router-core/tests/rewrite.test.tsasserts a parsed landing location matches the built one. It fails onmain:and passes with this change.
@tanstack/router-core: 126 files, 3246 passed, 4 expected-fail.@tanstack/react-router: 73 files, 1011 passed, 1 skipped (after buildingrouter-core; without that build these fail to resolve@tanstack/router-coreon a clean checkout too).tsc --noEmitonrouter-coreis clean, and both changed files are Prettier-formatted.Disclosure: developed with AI assistance (Claude Code); the diagnosis, the discarded first approach and the final diff were reviewed before submitting.
Summary by CodeRabbit