Important
This release upgrades the router to rou3 v1 (from v0.9). Route patterns now follow URLPattern syntax more closely, and some patterns match differently. Please read the routing changes section below before you upgrade. The full guide is the rou3 migration guide.
⚠️ Routing changes (rou3 v1)
These changes apply to app.get() / app.on() / ... routes, app.use(route, ...) middleware scopes, mount(), and routeRules keys alike.
Upgrade checklist
- Check every
*in your routes. It now spans segments:/admin/*matches/admin/a/b. Use:nameto match exactly one segment. - Replace
params._withparams[0], or name the catch-all (/**:path→params.path). On the base path (/foofor/foo/**), the key is now left out instead of being"". - Rename params that contain
-(:user-id→:userId). - Start your app once. Patterns that used to be accepted with a surprising meaning now throw a
rou3: ...error when you register them. The error quotes the route. - Run on Node.js 20.19 or later (rou3 v1 requires
>=20.19.0).
Behavior changes
| Route | Request | rc.32 | rc.33 |
|---|---|---|---|
/users/*
| /users/a/b
| 404 | { 0: "a/b" }
|
/foo/**
| /foo
| { _: "" }
| {}
|
/foo/**
| /foo/a/b
| { _: "a/b" }
| { 0: "a/b", _: "a/b" } (_ is deprecated)
|
/about
| /about//
| match | 404 (only one trailing slash is ignored) |
/u/:id/x
| /u//x
| { id: "" }
| 404 (:name never matches an empty segment)
|
/api/:user-id
| /api/abc-id
| { "user-id": "abc-id" }
| { user: "abc" } (- ends a param name)
|
/f/:name.:ext
| /f/a.tar.gz
| { name: "a.tar", ext: "gz" }
| { name: "a", ext: "tar.gz" }
|
/d/:a-:b
| /d/x-y-z
| { "a-": "x-y-", b: "z" }
| { a: "x", b: "y-z" }
|
*is a greedy catch-all. This also widens middleware and route rules:app.use("/admin/*", guard)androuteRules({ "/admin/*": ... })now apply at every depth below/admin, not just one level. To match exactly one segment, use/admin/:id(or/admin/:id?to also match/admin). Inside a segment, replace*with([^\x2f]*), so/*.pngbecomes/([^\x2f]*).png.(.*)and:name(.*)are catch-alls too, like*.- Segments after
**are matched./**/_payload.jsonmatches only paths that end in/_payload.json. Before, those trailing segments were ignored. - One catch-all per route. A route can have only one of
*,**,:x+,:x*,(.*)or:x(.*). For example,/**/*.pngnow throws; write/**/:file.pnginstead. - Several captures in one segment split like URLPattern:
:nametakes as little as it can. To keep the old split, add a constraint, e.g./:name.:ext(\w+). prefix-:param?makes only the param optional, not the whole segment. Write/a/{pre-:x}?to make the whole segment optional.- Backslash escapes are literal characters:
/foo\.barmatches/foo.bar. - Method-agnostic routes are no longer hidden by method routes on the same path shape. When both match, the more specific one wins. On a tie, the method route still wins.
- Patterns that now throw, among others:
+/*after a constrained or prefixed param (/a/:x(\d+)+), invalid param names (/:0,/:id$,/:café), duplicate param names, unbalanced braces or parens, look-arounds / anchors / capturing groups inside a regex constraint, and a dot segment next to a param. See Patterns that now throw. - Types:
InferRouteParamsreads param names the same way as the router. Optional params and a trailing*/**are typedstring | undefined.
Route rules (h3/rules)
- Rule keys are normalized exactly like route patterns, so a rule always matches the requests that the route registered with the same string serves. Non-ASCII and space characters in rule keys are now stored percent-encoded:
MatchedRouteRule.routeand the defaultcacherule name read/caf%C3%A9/**for a"/café/**"key. (7cbf21c) - Because
*is now a catch-all, a redirect or proxy key such as/x/*/old/**has two catch-alls and is rejected when the rules are created. Use:paramfor the single segment. - An alternate path reading (for example
/docs/x%2fydecoded to/docs/x/y) can bring back a permission that a broaderfalsereset removed, but only from a pattern that is equal to or more specific than every pattern that reset it. Restricting custom handlers should keep settingrestricting: true. (3f1c9e5)
See the updated Routing and Route Rules guides.
🚀 Enhancements
- rules: Skip
redirectwhen the request is already at the target, so"/docs/**": { redirect: "/docs/v2/**" }no longer loops (#1559)
🔒 Security hardening
- mount: Reject an empty segment directly after a mount base (
/api//admin) with404. Before, the path was collapsed to/admin, which reached the mounted app while skippinguse("/api/admin/**")guards on the parent. This also applies towithBase()(1161eb7) - session: Keep loaded session data prototype-free (46bc1a2)
🩹 Fixes
- validate: Preserve repeated query values in
defineValidatedHandler. Query validators now receivestring | string[], the same shape asgetQuery(#1562) - cookie: Size cookie chunks by their encoded length, so non-ASCII or escaped values no longer exceed the chunk limit (#1565)
- cookie: Include
partitionedin the distinct-cookie key (#1553) - sse: Preserve buffered events during overlapping flushes (#1555)
- static: Honor q-values and case in
accept-encoding(#1560) - static: Set the
varyheader when a single encoding is accepted (#1556) - static: Resolve the MIME type of a precompressed variant from the requested asset (#1564)
- mime: Add
.mjsand.cjsto the common MIME types (#1566) - rules: Allow
headers: falseinRouteRuleConfig(edcc329)
🌊 Types
- auth:
event.context.basicAuth.usernameandrealmare always set afterrequireBasicAuth(#1550) - handler:
EventHandlerRequest["query"]acceptsstring | string[]values (#1562)
📖 Documentation
- Document raw query access via
event.url.search(#1551)
❤️ Contributors
- Pooya Parsa (@pi0)
- @pi0x
- @00200200
- Alexandre Kohler (@kwy404)
- Kauê Leivingson (@Kaue-TecsaGroup)
- Maxim Gagiev (@maximilliangrand)
- Oskar Lebuda (@OskarLebuda)
- Breken (@breken-ai)
- Geonseok (@hxperl)
- Okxint (@okxint)