[2.0.0-next.38 / next] Route params stay percent-encoded, so matchFilters miss non-ASCII values and paths don't round-trip
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 42/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- backend-api-design
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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)
- Ngôn ngữ chính
- TypeScript
- Star
- 1.3k
- Fork
- 182
- Merge trung bình
- 16 giờ 9 phút
- Pull request đã merge (30 ngày)
- 42
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của solidjs/solid-router
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 52/100
solidjs/solid-router#654 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
<A> costs ~6us of server CPU per instance during SSR (20x a plain <a>), mostly mergeProps/splitPropsCó thể đã có người làm @birkskyum đã nhận 63 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 55/100
solidjs/solid-router#583 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
solidjs/solid-router#569 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 38/100
solidjs/solid-router#518 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 64/100
solidjs/solid-router#502 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của solidjs/solid-router
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Agenta-AI/agenta#7542 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
code quality security
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement good first issue
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 90/100
-
feat(icons): add [solidping]Đang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 82/100
homarr-labs/dashboard-icons#2939 ·
Maintainer thường phản hồi trong vòng 1 ngày