Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[2.0.0-next.38 / next] Route params stay percent-encoded, so matchFilters miss non-ASCII values and paths don't round-trip

Abierto
#670 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
42/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
typescript

Línea de trabajo

Read createMatcher in src/utils.ts and the parameter encoding in src/paths.ts, then inspect the router's matcher and route parameter tests. Verify how decoded values affect segment matching, matchFilters, splat parameters, and malformed escapes; add coverage for the reproduction cases. Done means params and filters have a consistent documented behavior, with tests covering encoded values and round-tripping through paths.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Summary

useParams() and props.params return dynamic segments still percent-encoded: on /item/猫 the param is "%E7%8C%AB", on /item/a%20b it is "a%20b". Route matchFilters are checked against the same encoded text:

  • A filter written with the plain value silently never matches. With matchFilters: { k: ["猫"] }, /kanji/猫 doesn't match the route, with no error or warning, because the filter compares "猫" with "%E7%8C%AB". It only works when written pre-encoded (["%E7%8C%AB"]), and regex and predicate filters get the same encoded string (see Cause). The README's filter examples are all ASCII (["mom", "dad"], /^\d+$/), so they never show this.
  • paths doesn't round-trip. paths.item("猫") builds /item/%E7%8C%AB, and the page reads back "%E7%8C%AB", not "猫".
  • None of this is documented. The README and the docs only say params are strings.

Every other widely used router tested below hands back decoded params. Params, and the value filters see, should be decoded per segment after matching. If raw values are intended, that should be documented on useParams, RouteProps.params and matchFilters.

Reproduction

@solidjs/[email protected], solid-js and @solidjs/web 2.0.0-rc.14, Node 24.21. A server render through the url prop; Router.match(url) returns the same params.

import { createComponent, renderToString } from "@solidjs/web";
import { createRouter, useParams } from "@solidjs/router";

let params;
const Page = () => {
  params = { ...useParams() };
  return null;
};

const Router = createRouter({
  routes: [
    { path: "/item/:id", component: Page },
    { path: "/:name/view", component: Page },
    { path: "/kanji/:k", component: Page, matchFilters: { k: ["猫"] } },
  ],
});

const built = String(Router.paths.item("猫"));
console.log(`paths.item("猫") = ${built}`);

for (const url of [
  "/item/%E7%8C%AB",
  "/item/猫",
  built,
  "/item/a%20b",
  "/item/%25",
  "/item/%E0%A4%A",
  "/hello%2Fworld/view",
  "/kanji/%E7%8C%AB",
]) {
  params = undefined;
  renderToString(() => createComponent(Router, { url }));
  console.log(url, "→", JSON.stringify(params));
}
URL Params
/item/%E7%8C%AB, also what paths.item("猫") builds { id: "%E7%8C%AB" }
/item/猫 { id: "%E7%8C%AB" }
/item/a%20b { id: "a%20b" }
/item/%25 { id: "%25" }
/hello%2Fworld/view { name: "hello%2Fworld" }
/kanji/%E7%8C%AB with matchFilters: { k: ["猫"] } no match

The value doesn't depend on how the URL was written: a raw /item/猫 is normalized to the encoded form first, so a param is always the encoded segment.

Other routers

Each router was installed at the version shown and given /item/%E7%8C%AB%20a%2Fb (猫, a space and an encoded slash) for an /item/:id-style route:

Router Version Checked through Param
React Router 8.4.0 matchRoutes, createMemoryRouter 猫 a/b
TanStack Router router-core 1.171.35 router-core matcher (findSingleMatch) 猫 a/b
Vue Router (Nuxt pages) 5.4.0 router.resolve 猫 a/b
SvelteKit 3.0.1 exec on decode_pathname(pathname), as its client and server do 猫 a/b
Angular Router 22.2.2 URL segments from DefaultUrlSerializer.parse 猫 a/b
preact-iso 2.12.2 exec 猫 a/b
Express 5.3.0 req.params 猫 a/b
Hono 4.13.13 c.req.param() 猫 a/b
Fastify 5.12.5 req.params 猫 a/b
h3 v1 (Nitro 2, Nuxt server routes) 1.15.11 getRouterParam 猫 a%2Fb
h3 v2 2.0.2 getRouterParam %E7%8C%AB%20a%2Fb; with { decode: true }: 猫 a%2Fb
Solid Router 2.0.0-next.38 useParams, Router.match %E7%8C%AB%20a%2Fb

h3 is the only other one that doesn't fully decode, and it isn't a consistent model. Within each version it treats the slash differently from every other character: v1 decodes 猫 and the space by default but leaves %2F encoded, and v2's opt-in decode does the same. So one param can come back partly decoded (猫 a%2Fb), and code reading it still has to know which escapes survived. It also differs between majors: v1 decodes by default and v2 doesn't. Every other router decodes a single-segment param completely, %2F included, without treating it as a separator.

Because this is the common convention, code written for a router (by developers and by coding agents alike) reads params.id as decoded text. With Solid Router that works for ASCII slugs and silently breaks for non-ASCII text, spaces and reserved characters, and nothing in the types or docs points at it.

Why #220 doesn't settle it

Decoding was removed in #222 for #220. There, decoding the whole pathname before matching turned %2F into a separator, so /hello%2Fworld/view stopped matching /:name/view; the reasoning was also that the router shouldn't decode what it didn't encode. Neither holds for 2.0:

  • Decoding each matched segment after matching can't create a separator. With the change below, /hello%2Fworld/view still matches /:name/view and gives name: "hello/world", which is what the routers above return.
  • The router now does encode: paths.item("猫") runs the value through encodeURIComponent, so the param read back is one the router itself encoded.
Cause

createMatcher in src/utils.ts (unchanged on next) stores the raw segment, and filters see it raw too:

if (dynamic && matchSegment(locSegment, filters?.[key])) {
  match.params[key] = locSegment;

while src/paths.ts encodes what it is given:

const encodeParam = (value: unknown) => String(value).split("/").map(encodeURIComponent).join("/");
Expected

Params decoded once per segment after matching, as in the routers above, with matchFilters receiving the decoded value. If raw values are intended instead, document it on useParams, RouteProps.params and matchFilters, including how to decode safely: a malformed escape such as %E0%A4%A makes decodeURIComponent throw.

Related: #220, #222.

Possible fix

Tested only against the reproduction above, by editing the published 2.0.0-next.38 dist/index.js; the router's own test suite was not run.

const decodeParam = (s) => {
  try {
    return decodeURIComponent(s);
  } catch {
    return s;
  }
};

// in createMatcher's segment loop:
const value = dynamic ? decodeParam(locSegment) : locSegment;
if (dynamic && matchSegment(value, filters?.[key])) {
  match.params[key] = value;
URL Before After
/item/%E7%8C%AB (= paths.item("猫")) { id: "%E7%8C%AB" } { id: "猫" }
/item/猫 { id: "%E7%8C%AB" } { id: "猫" }
/item/a%20b { id: "a%20b" } { id: "a b" }
/item/%25 { id: "%25" } { id: "%" }
/item/%E0%A4%A (malformed) { id: "%E0%A4%A" } { id: "%E0%A4%A" }
/hello%2Fworld/view { name: "hello%2Fworld" } { name: "hello/world" }
/kanji/%E7%8C%AB, filter ["猫"] no match { k: "猫" }

The edit leaves match.path, which is built from the raw segment, untouched. Code that already decodes params itself would decode twice (for /item/%25, decodeURIComponent("%") throws), so this is a breaking change that fits a pre-release major. If a breaking change isn't acceptable, an opt-in would still help: for example a createRouter option that turns this decoding on, for both params and the values matchFilters see.

Catch-all params

The edit above doesn't touch splat params, which stay fully encoded. Other routers decode those too. Given /files/a%2Fb/c%20d/%E7%8C%AB (an encoded slash, a space and 猫) on a catch-all route:

Router Catch-all param
React Router 8.4.0 (*) "a/b/c d/猫"
TanStack Router 1.171 ($) "a/b/c d/猫"
SvelteKit 3.0.1 ([...rest]) "a/b/c d/猫"
preact-iso 2.12 (:rest*) "a/b/c d/猫"
Hono 4.13 (:rest{.+}) "a/b/c d/猫"
Fastify 5.12.5 (*) "a/b/c d/猫"
Vue Router 5.4.0 (:rest(.*)) "a/b/c d/猫"
Vue Router 5.4.0 (:rest(.*)*) ["a/b", "c d", "猫"]
Express 5.3 (*rest) ["a/b", "c d", "猫"]
Angular 22.2 (** has no param; URL segments) ["files", "a/b", "c d", "猫"]
h3 v1 1.15.11 (**:rest) "a%2Fb/c d/猫"; with decode: true: "a/b/c d/猫"
h3 v2 2.0.2 (**:rest) raw; with decode: true: "a%2Fb/c d/猫"
Solid Router 2.0.0-next.38 (*rest) "a%2Fb/c%20d/%E7%8C%AB"

Most decode the splat into one string, accepting that a%2Fb/c and a/b/c both read as "a/b/c". Vue's repeatable params, Express 5 and Angular return the decoded segments as an array instead, which keeps an encoded slash distinguishable from a real one. Either would fit Solid Router: a decoded string like most routers, or an array of decoded segments if telling %2F apart from / matters.

(submitted by Claude Opus 5.5 on behalf of rvlzzr)

Lenguaje dominante
TypeScript
Estrellas
1.3k
Forks
182
Merge medio
16 h 9 min
PR fusionados (30 d)
42

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de solidjs/solid-router

Todos los issues de solidjs/solid-router

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.