[2.0.0-next.38 / next] Route params stay percent-encoded, so matchFilters miss non-ASCII values and paths don't round-trip
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
- Área
- backend-api-design
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. pathsdoesn'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/viewstill matches/:name/viewand givesname: "hello/world", which is what the routers above return. - The router now does encode:
paths.item("猫")runs the value throughencodeURIComponent, 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
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de solidjs/solid-router
-
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
solidjs/solid-router#654 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
<A> costs ~6us of server CPU per instance during SSR (20x a plain <a>), mostly mergeProps/splitPropsPosiblemente ocupada @birkskyum la tomó hace 63 días. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 55/100
solidjs/solid-router#583 ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
solidjs/solid-router#569 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 3/5 1-2 días Aptitud para principiantes 38/100
solidjs/solid-router#518 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 3/5 1-2 días Aptitud para principiantes 64/100
solidjs/solid-router#502 · 4 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de solidjs/solid-router
Issues similares
-
./collections should export setUmbracoVersion and setAllowFilePathUploads for in-process consumersAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
umbraco/Umbraco-CMS-MCP-Dev#563 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
afk-ok area:data-quality importer size:S
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
enorm-labs/event-junkie#3166 ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 1/5 Menos de una hora Aptitud para principiantes 80/100
alunduil/projects-v2-sync#21 ·