Skip to content

fix(router-core): encode URL-unsafe ASCII chars in encodePathLikeUrl to break infinite redirect on <test-style paths#7594

Open
spokodev wants to merge 2 commits into
TanStack:mainfrom
spokodev:fix/encoded-unsafe-chars-pathname
Open

fix(router-core): encode URL-unsafe ASCII chars in encodePathLikeUrl to break infinite redirect on <test-style paths#7594
spokodev wants to merge 2 commits into
TanStack:mainfrom
spokodev:fix/encoded-unsafe-chars-pathname

Conversation

@spokodev

@spokodev spokodev commented Jun 10, 2026

Copy link
Copy Markdown

What

Requests whose pathname contains encoded URL-unsafe ASCII characters (e.g. <, >, ", `, {, }) crashed with ERR_TOO_MANY_REDIRECTS because the router kept rewriting them between encoded and decoded forms. Visiting http://localhost:3000/<test on any fresh TanStack Start project would loop until the browser gave up. This PR makes encodePathLikeUrl percent-encode the WHATWG URL "path percent-encode set" so its output is a fixed point of decodePath, and the SSR redirect comparator no longer fires on these inputs.

Closes #7587.

Root cause

The SSR pipeline does three things to incoming URLs:

  1. decodePath (in packages/router-core/src/utils.ts) strips control characters and decodes percent sequences via decodeURI. For /%3Ctest this returns "/<test".
  2. parseLocation (router fast path) keeps publicHref as the raw pathname ("/%3Ctest") and stores the decoded form in pathname ("/<test").
  3. buildLocation rebuilds the URL from the decoded pathname and runs the assembled path through encodePathLikeUrl, which previously only re-encoded whitespace and non-ASCII characters. For < (an ASCII char in the WHATWG path percent-encode set) it returned the input verbatim, so the new publicHref was "/<test".

The redirect detector in router.ts then compared:

if (this.latestLocation.publicHref !== nextLocation.publicHref) {
  throw redirect({ href, ... })
}

"/%3Ctest" !== "/<test" so the router threw a redirect to /<test. The browser percent-encoded the response Location header back to /%3Ctest and re-requested, which produced the same diff. The loop terminated only when Chrome stopped at ERR_TOO_MANY_REDIRECTS.

The clean fix is to make encodePathLikeUrl invertible against decodePath for the characters decodePath decodes. The WHATWG URL spec defines the path percent-encode set as: C0 control + space + " + < + > + ` + { + }. Whitespace and the control range were already covered; this PR adds <, >, ", `, {, }. ? and # are deliberately not added because the function is called on the combined pathname + search + hash string where those characters are the component separators.

Tests added

A new describe('URL-unsafe ASCII chars (WHATWG path percent-encode set)') block in packages/router-core/tests/utils.test.ts. Each it targets a specific contract:

  • **encodes < > " { } in path segments** — happy path for every character in the added encode set. Without the fix, all five assertions fail with expected "/<test" to be "/%3Ctest"` etc.
  • round-trips with decodePath for unsafe ASCII chars — guards the actual invariant used by the SSR comparator (encodePathLikeUrl(decodePath(raw).path) === raw) across three inputs.
  • does not double-encode already-percent-encoded sequences — negative regression so the new regex does not match % literals.
  • preserves URL component separators (?, #, /) — confirms ?, #, and / still pass through, so search/hash survive when encodePathLikeUrl is called on a combined path string.

The existing encodePathLikeUrl happy-path tests (unicode, spaces, emoji, [1] brackets) still pass unchanged, so the fix does not widen the encode set beyond what the WHATWG spec mandates.

Verified the failing-test -> fix -> passing-test loop locally:

  • Before the fix: 3 of the new tests fail with the exact mismatch above.
  • After the fix: all 116 tests in packages/router-core/tests/utils.test.ts pass (113 passed | 3 expected fail, no regressions in any existing case).

Repro

Before the fix, on any TanStack Start project (the issue's reproducer):

  1. npx @tanstack/cli@latest new start-basic && cd start-basic
  2. pnpm dev
  3. Visit http://localhost:3000/<test
  4. Browser crashes with ERR_TOO_MANY_REDIRECTS within seconds.

Direct repro at the utility layer (no Start app required):

import { decodePath, encodePathLikeUrl } from '@tanstack/router-core'

const raw = '/%3Ctest'
const decoded = decodePath(raw).path           // "/<test"
const reencoded = encodePathLikeUrl(decoded)   // before: "/<test"   after: "/%3Ctest"
console.log(reencoded === raw)                 // before: false      after: true

The reencoded === raw invariant is exactly what the SSR redirect comparator relies on - the boolean flip is what stops the redirect loop.

Validation

pnpm vitest run tests/utils.test.ts                           # 113 passed | 3 expected fail, 0 failures
pnpm vitest run tests/utils.test.ts -t "encodePathLikeUrl"    # 10 passed (6 existing + 4 new)

Pre-existing import resolution errors in unrelated test files (build-location.test.ts, callbacks.test.ts etc. fail to resolve @tanstack/history regardless of this branch — confirmed by running them on main with the change stashed).

Changeset added as a patch bump on @tanstack/router-core.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an infinite redirect loop that occurred when request URLs contained special ASCII characters such as <, >, ", `, {, and }.

…to break infinite redirect on `<test`-style paths

`decodePath` decodes `%3C` -> `<` (and the rest of the WHATWG URL "path
percent-encode set": `<`, `>`, `"`, `` ` ``, `{`, `}`), but
`encodePathLikeUrl` only encoded whitespace and non-ASCII characters.
The pair therefore failed to round-trip for those characters.

The SSR redirect comparator in router.ts uses
`latestLocation.publicHref !== nextLocation.publicHref` as the signal
that the URL was rewritten and needs a redirect. For requests like
`/<test` the browser sends `/%3Ctest`, the fast path in `parseLocation`
keeps `publicHref = "/%3Ctest"`, but `buildLocation` rebuilds the URL
from the decoded `pathname = "/<test"` and runs it through
`encodePathLikeUrl`, which left `<` un-encoded - so the new
`publicHref = "/<test"` differs from the latest `"/%3Ctest"`, the
router throws a redirect to `/<test`, the browser re-encodes to
`/%3Ctest`, and the loop continues until the browser gives up with
`ERR_TOO_MANY_REDIRECTS`.

Extending the encode regex to include the WHATWG "path percent-encode
set" makes encode/decode invertible for those characters. `?` and `#`
remain unencoded because the function is called on the combined
`pathname + search + hash` string where they are component separators.

Closes TanStack#7587
@coderabbitai

coderabbitai Bot commented Jun 10, 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: 53941c69-e93a-43f5-a2b4-760e1d70ba44

📥 Commits

Reviewing files that changed from the base of the PR and between 74b0424 and 91fa83b.

📒 Files selected for processing (1)
  • packages/router-core/src/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/src/utils.ts

📝 Walkthrough

Walkthrough

This PR fixes an infinite redirect loop that occurs when request pathnames contain URL-unsafe ASCII characters. The encodePathLikeUrl utility is updated to percent-encode additional unsafe characters per the WHATWG URL standard, ensuring proper round-tripping with decodePath and preventing spurious URL change detections in SSR redirect comparisons.

Changes

URL-unsafe character encoding

Layer / File(s) Summary
encodePathLikeUrl fix with validation
packages/router-core/src/utils.ts, packages/router-core/tests/utils.test.ts, .changeset/encode-url-unsafe-ascii-chars.md
encodePathLikeUrl now encodes <, >, ", `, {, } in addition to whitespace and non-ASCII characters, fixing the redirect loop. Tests verify encoding of unsafe ASCII, round-trip behavior with decodePath, prevention of double-encoding, and preservation of URL separators.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

package: router-core

Poem

A rabbit hops through encoded URLs with care,
Fixing redirects that led nowhere, nowhere!
With < and > now safely wrapped in percent,
No more infinite loops—the fix is heaven-sent. 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: encoding URL-unsafe ASCII characters in encodePathLikeUrl to prevent infinite redirects on paths like <test.
Linked Issues check ✅ Passed The PR implementation fully addresses #7587 by extending encodePathLikeUrl to percent-encode WHATWG path percent-encode set characters, preventing the infinite redirect loop when URLs contain encoded unsafe characters.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the infinite redirect bug: implementation in utils.ts, comprehensive test coverage, and changeset documentation. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

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

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@packages/router-core/src/utils.ts`:
- Line 692: The single-line if using the regex /[\s<>"`{}]|[^\u0000-\u007F]/ on
variable path must use curly braces; replace the single-line form "if
(!/.../.test(path)) return path" with a block-style if that contains the return
statement so it conforms to the repo's control-statement brace rule (locate the
usage in utils.ts where path is tested against that regex).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fe1e0687-9d1f-417c-9110-c54e257d9c3b

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1daf5 and 74b0424.

📒 Files selected for processing (3)
  • .changeset/encode-url-unsafe-ascii-chars.md
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/utils.test.ts

Comment thread packages/router-core/src/utils.ts Outdated
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.

Infinite redirect loop ("ERR_TOO_MANY_REDIRECTS") caused by encoded unsafe characters in URL pathname

1 participant