Splat params with an encoded `%2F` becomes indistinguishable from a path separator
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
A splat param ($ / _splat) spans multiple path segments, so the only way to consume it is to split it on /. But the router decodes the whole splat with decodeURIComponent before handing it over, which turns any %2F inside a segment into a literal /. By the time application code splits, the separator and the encoded slash are the same character and the original segmentation is unrecoverable.
Notably, the router is already careful about this elsewhere: location.pathname is decoded with decodeURI (via decodePath), which by design leaves reserved characters such as %2F encoded. So for the same URL, location.pathname preserves the distinction and params._splat destroys it.
Reproduction
StackBlitz:
import {
createRootRoute, createRoute, createRouter, createMemoryHistory,
} from "@tanstack/react-router";
const root = createRootRoute();
const named = createRoute({ getParentRoute: () => root, path: "/make/$make" });
const splat = createRoute({ getParentRoute: () => root, path: "/parts/$" });
const routeTree = root.addChildren([named, splat]);
async function inspect(url) {
const router = createRouter({
routeTree,
history: createMemoryHistory({ initialEntries: [url] }),
});
await router.load();
const match = router.state.matches.at(-1);
console.log(url);
console.log(" params :", JSON.stringify(match.params));
console.log(" location.pathname:", JSON.stringify(router.state.location.pathname));
if (match.params._splat !== undefined) {
console.log(" _splat.split('/'):", JSON.stringify(match.params._splat.split("/")));
}
}
// "Ford/New Holland" is one value containing a slash, encoded as %2F.
await inspect("/make/Ford%2FNew%20Holland");
await inspect("/parts/make/Ford%2FNew%20Holland");
// For comparison, a genuinely two-segment path.
await inspect("/parts/make/Ford/New%20Holland");
Output:
/make/Ford%2FNew%20Holland
params : {"make":"Ford/New Holland"}
location.pathname: "/make/Ford%2FNew Holland"
/parts/make/Ford%2FNew%20Holland
params : {"*":"make/Ford/New Holland","_splat":"make/Ford/New Holland"}
location.pathname: "/parts/make/Ford%2FNew Holland"
_splat.split('/'): ["make","Ford","New Holland"]
/parts/make/Ford/New%20Holland
params : {"*":"make/Ford/New Holland","_splat":"make/Ford/New Holland"}
location.pathname: "/parts/make/Ford/New Holland"
_splat.split('/'): ["make","Ford","New Holland"]
The last two URLs are different — one has an encoded slash inside a segment, the other has a real separator — and location.pathname still tells them apart. But they produce an identical _splat, so any consumer that splits on / cannot distinguish them.
Complete minimal reproducer
https://stackblitz.com/github/rshaul/tanstack-repro
Steps to Reproduce the Bug
- Go to https://stackblitz.com/github/rshaul/tanstack-repro
- Inspect console
Expected behavior
_splat should preserve the segmentation of the matched path, so that splitting it on / reproduces the segments the URL actually had
Screenshots or Videos
No response
Platform
@tanstack/react-router1.170.18@tanstack/router-core1.171.15- Node 24.15.0
Additional context
No response
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 with the complete minimal reproducer and compare the createRouter result for the two /parts URLs. Trace how location.pathname uses decodePath versus how the _splat parameter is decoded, then verify that the two URLs remain distinguishable when splitting _splat on '/'.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100