Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đang mở
#670 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

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.
  • 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)

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

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của solidjs/solid-router

Tất cả issue của solidjs/solid-router

Issue tương tự

Thêm issue về TypeScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.