Skip to content

Splat params with an encoded %2F becomes indistinguishable from a path separator #7871

Description

@rshaul

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

  1. Go to https://stackblitz.com/github/rshaul/tanstack-repro
  2. 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-router 1.170.18
  • @tanstack/router-core 1.171.15
  • Node 24.15.0

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions