Repository navigation
Method-less tree with route-level method matching - #71
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #71 +/- ##
==========================================
+ Coverage 88.89% 89.69% +0.79%
==========================================
Files 25 26 +1
Lines 4135 3977 -158
==========================================
- Hits 3676 3567 -109
+ Misses 359 320 -39
+ Partials 100 90 -10 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
… detection matcher-aware
* feat!: change tsr priority to be equivalent to exact match * fix: exclude wildcard nodes from trailing slash matching * fix: exclude wildcard nodes from trailing slash matching * fix: skip tsr matching for routes with strict slash handling * fix: prioritize exact suffix catch-all over infix wildcard tsr match * fix: use offset instead of charsMatched for wildcard tsr path matching * refactor: remove dead code for trailing slash conflict detection * refactor: simplify route conflict error by replacing kind enum with boolean flag
…and sub-router example
tigerwill90
marked this pull request as ready for review
January 4, 2026 00:52
…call guard to HandleNoRoute
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR reworks how the router tree handles HTTP methods. Previously, each method had its own branch in the tree, and
MethodAnyroutes were evaluated as a fallback after all specific methods, which required a second tree lookup. This caused a few annoying issues.The main problem was that a wildcard route like
GET /*{any}would shadow any more specificMethodAnyroute likeANY /api/users, because the tree would find the GET match first and never fall back to the ANY route. This was particularly problematic for sub-routers, which typically useMethodAnyto capture all methods for a prefix before delegating to the mounted router.Now the tree is indexed by pattern and methods are evaluated after the hostname/path matches but before route matchers, so they're still a higher priority discriminant than matchers. For a given pattern, specific methods always take precedence over method-less routes:
Here
GET /resourcehitsGetHandler,POST /resourcehitsFallbackHandler, andCatchAllHandleronly kicks in if nothing else matches/resourcefirst.This makes sub-routers work correctly alongside catch-all routes:
Requests to
/api/usersgo to the sub-router because/api+{mount}is more specific than/+{filepath}, regardless of which methods are registered.Trailing slash handling
The trailing slash handling also needed a rethink. When the router found a potential redirect (e.g.
/users/could redirect to/users), it would store that match and keep evaluating routes looking for a direct match. The problem is that catch-all routes match everything, so a file server route likeGET /+{filepath}would capture/users/instead of letting the redirect happen:Now trailing slash matches are treated as direct matches and returned immediately. This is both more correct and about 40% faster.
Sub-router rework
The sub-router API has been simplified. Previously you had to create for a sub-router, then pass it to
NewSubRouterand finally register the route createdNow
Router.Mount()return anfox.HandlerFuncthat can be uses directly. This is also harder to misuse.Sub-router is also now "zero-alloc" on the hot-path.
Multi-method registration
Routes can now be registered with multiple methods at once. This is useful for handlers that respond identically to different methods, like HEAD and GET:
The router still detects conflicts: if you try to register a route with a pattern and matchers that already exist, and the methods overlap, it returns an
ErrRouteConflict. This prevents accidentally shadowing routes that would be impossible to match.Reworked priority rules
For reference, here's the full evaluation order:
/bucket/*{path}/metabefore/bucket/*{path})If a match candidate fails (e.g. matcher doesn't pass), the router backtracks and tries the next alternative following the same priority order.
Differential benchmark
Beyond the architectural changes, I found a few optimizations along the way. The trailing slash handling got the biggest win at -40%, but most benchmarks show solid improvements across the board.
Note: parallel benchmark gain are due to the fix of a race condition in the benchmark.