github h3js/h3 v2.0.1-rc.33

3 hours ago

compare changes

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

  1. Check every * in your routes. It now spans segments: /admin/* matches /admin/a/b. Use :name to match exactly one segment.
  2. Replace params._ with params[0], or name the catch-all (/**:path → params.path). On the base path (/foo for /foo/**), the key is now left out instead of being "".
  3. Rename params that contain - (:user-id → :userId).
  4. 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.
  5. 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) and routeRules({ "/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 /*.png becomes /([^\x2f]*).png.
  • (.*) and :name(.*) are catch-alls too, like *.
  • Segments after ** are matched. /**/_payload.json matches 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, /**/*.png now throws; write /**/:file.png instead.
  • Several captures in one segment split like URLPattern: :name takes 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\.bar matches /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: InferRouteParams reads param names the same way as the router. Optional params and a trailing * / ** are typed string | 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.route and the default cache rule 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 :param for the single segment.
  • An alternate path reading (for example /docs/x%2fy decoded to /docs/x/y) can bring back a permission that a broader false reset removed, but only from a pattern that is equal to or more specific than every pattern that reset it. Restricting custom handlers should keep setting restricting: true. (3f1c9e5)

See the updated Routing and Route Rules guides.

🚀 Enhancements

  • rules: Skip redirect when 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) with 404. Before, the path was collapsed to /admin, which reached the mounted app while skipping use("/api/admin/**") guards on the parent. This also applies to withBase() (1161eb7)
  • session: Keep loaded session data prototype-free (46bc1a2)

🩹 Fixes

  • validate: Preserve repeated query values in defineValidatedHandler. Query validators now receive string | string[], the same shape as getQuery (#1562)
  • cookie: Size cookie chunks by their encoded length, so non-ASCII or escaped values no longer exceed the chunk limit (#1565)
  • cookie: Include partitioned in 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 vary header when a single encoding is accepted (#1556)
  • static: Resolve the MIME type of a precompressed variant from the requested asset (#1564)
  • mime: Add .mjs and .cjs to the common MIME types (#1566)
  • rules: Allow headers: false in RouteRuleConfig (edcc329)

🌊 Types

  • auth: event.context.basicAuth.username and realm are always set after requireBasicAuth (#1550)
  • handler: EventHandlerRequest["query"] accepts string | string[] values (#1562)

📖 Documentation

  • Document raw query access via event.url.search (#1551)

❤️ Contributors

Don't miss a new h3 release

NewReleases is sending notifications on new releases.