fix(openapi): improve OpenAPI path matching with rou3 v1 - #2173
Conversation
rou3 1.0 aligns its pattern syntax with URLPattern, which changes how the
patterns OpenAPIMatcher built were read. Adapt the matcher so routing keeps
working:
- Give params positional rou3 keys and map them back to the OpenAPI names:
rou3 now ends a name at `-` (`{user-id}` stopped matching) and throws on
names like `{0}` or a repeated name.
- Escape literal path text, so characters rou3 reads as syntax (`:`, `*`,
`?`, `(`, `{`, dot segments, ...) match literally instead of throwing or
creating hidden params.
- Map `{+name}` to `:name(.*)` so values with empty segments (an encoded
URL's `//`, absolute paths) still match, and let a match that leaves the
catch-all empty give way to the next most specific route, keeping the
old "needs a value" behavior.
- Cut lazy-router prefix matchers at a catch-all (rou3 allows one per
route) and drop a trailing prefix slash, which rou3 now reads as an
empty segment.
- Retry with a normalized path whenever normalization could change it, so
raw characters rou3 now stores percent-encoded (`café`, `^`) still match.
- Convert every route before changing the tree, so an invalid path never
leaves a lazy router half indexed; a path with two `{+name}` params now
throws a clear error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU
- Store each route's `[rou3 key, OpenAPI name]` pairs on the tree entry, so
matching no longer rebuilds `p${i}` keys and tuple arrays per request.
- Decode params with a plain loop, keeping a `__proto__` name an own
property.
- Drop the `Rou3Route` interface that duplicated `TreeEntry` fields, and
simplify the empty catch-all fallback to `reverse().find()`.
- Merge duplicated catch-all test setups.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU
- Go back to retrying a missed lookup only when the path holds a `%`. The wider check made every miss with characters like `:` or `@` normalize and look up twice (~300 ns -> ~2000 ns). Raw characters that rou3 stores percent-encoded (e.g. `/café`) are now a documented limitation: HTTP clients send them encoded. - Escape `.` only in `.` / `..` segments, so paths such as `/v1.2/report.json` register without the slower escape handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU
Remove comments in OpenAPIMatcher whose behavior the tests already pin
down, and add tests for the two behaviors that were only documented in
comments:
- the empty catch-all fallback picks the most specific other route
(`/{name}` over `/{+path}`), also on a repeated match;
- a single `.` segment matches literally, like `..`.
Keep two short comments that explain choices tests can't show: why raw
characters are not retried, and why only dot segments are escaped. Label
the param tuples `rou3Key` so their fields need no doc comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU
Use the shared `setOwn` helper instead of an inline `__proto__` branch; it keeps a `__proto__` param an own property the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | ef3096b | Commit Preview URL Branch Preview URL |
Oct 04 2026, 04:09 AM |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ No critical issues — one informational note on the dependency bump.
Reviewed changes
rou30.9.1 → 1.0.0 —packages/openapi/package.json, the lockfile, and thepnpm-workspace.yamlrelease-age allowlist.- Route pattern generation (
toRou3Route) — escapes rou3 syntax in literal segments, remaps non-identifier and repeated param names to internalp0…keys, rejects more than one catch-all, and records the catch-all key on the entry. - Match resolution (
findMatch) — when the best match's catch-all captured an empty value, falls back to the most specific valid route viafindAllRoutes; empty catch-alls are no longer valid matches. - Param decoding (
decodeParams) — maps internal keys back to the original names and decodes values, usingsetOwnso a__proto__param stays an own property. - Prefix and lazy indexing (
toRou3PrefixMatcher,index) — prefix matchers now handle trailing slashes and catch-all prefixes, and routes are collected before being committed so a single invalid path in a lazy router indexes nothing. - Tests and docs — extensive edge-case coverage (non-identifier names, repeated names, special characters, non-ASCII, catch-all suffix and empty fallback, lazy resolution) plus a docs sentence about
{+name}.
Behavior looks correct and thoroughly covered. I ran the full @orpc/openapi suite (481 passed), the package typecheck, and eslint on both changed files — all clean. I also confirmed against rou3 v1 that findAllRoutes orders least→most specific, so .reverse().find(...) genuinely yields the most specific valid route, and probed __proto__/special-character params.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Upgrade to rou3 v1.0.0 and improve OpenAPI path matching to handle edge cases with catch-all parameters, non-identifier parameter names, special characters, and percent-encoding.
Summary
This PR upgrades the rou3 dependency to v1.0.0 and refactors the OpenAPI path matching logic to properly handle:
{+path}) that may be empty or followed by additional segments{user-id},{0}):,*,?,+,(),{},., etc.)Key Changes
Route pattern generation: Replaced simple parameter substitution with a more robust
toRou3Route()function that:p0,p1, etc.) to support non-identifier namesMatch resolution: Added
findMatch()method that:findAllRoutes()to find the most specific valid matchParameter decoding: Updated
decodeParams()to map from internal rou3 keys back to original parameter names and decode URI componentsPrefix matching: Enhanced
toRou3PrefixMatcher()to handle prefixes with catch-all parametersLazy router handling: Improved to resolve lazy routers with prefixes ending in
/or containing catch-all parameters, and to properly handle errors during lazy loadingTest coverage: Added comprehensive tests for edge cases including special characters, non-ASCII paths, catch-all parameter validation, and lazy router resolution
Implementation Details
[rou3Key, originalName]tuples in tree entries to support non-identifier names.and..segments%to keep misses efficienthttps://claude.ai/code/session_01YRJj66odKexHiFFZ1qyVcU