Skip to content

feat!: Use a radix trie for route matching - #628

Open
ElijahAhianyo wants to merge 20 commits into
masterfrom
elijah/router-trie
Open

feat!: Use a radix trie for route matching#628
ElijahAhianyo wants to merge 20 commits into
masterfrom
elijah/router-trie

Conversation

@ElijahAhianyo

@ElijahAhianyo ElijahAhianyo commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

The current router implementation uses a Vec to store routers, which has some real limitations. For example, using a vec meant route conflict detection was cumbersome and hacky to get right. Using a Radix trie is the right data structure for this problem. This PR delegates the core Trie logic to the matchit crate. We still keep our business logic in a light wrapper over the Matchit Router.

Breaking Changes

  1. Registering a route with a duplicate parameter at the same segment will now fail
Router::with_urls([
    Route::with_handler_and_name("/foo/{bar}", index, "index"),
        Route::with_handler_and_name("/foo/{baz}", add_example_form, "add"),
])
  1. Registering 2 views with the same url_prefix will fail
struct TestProject;
impl Project for TestProject {
   ...
    fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
        apps.register_with_views(App1, "");
        apps.register_with_views(App2, "");
    }
}
  1. When a handler and router are registered for the same route, the handler will take precedence at lookup time
let nested_router = Router::with_urls([Route::with_handler_and_name(
        "/inner/{id}",
        nested,
       "nested",
)]);

Router::with_urls([
    Route::with_handler_and_name("/foo", index, "index"),
    Route::with_router("/foo", nested_router),
])
  1. A route is treated as different from its trailing-slash counterpart:
Router::with_urls([
    Route::with_handler_and_name("/foo", index, "index"),
    Route::with_handler_and_name("/foo/", another_handler, "another" ),
])
  1. This does not apply to routers. The example below is invalid and will fail:
Router::with_urls([
    Route::with_router("/foo", nested_router_a),
    Route::with_router("/foo/", nested_router_b ),
])

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor / cleanup
  • Performance improvement
  • Other (describe above)

@github-actions github-actions Bot added the C-lib Crate: cot (main library crate) label Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Projectcot
Branchelijah/router-trie
Testbedgithub-ubuntu-latest
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
empty_router/empty_router📈 view plot
🚷 view threshold
14.14 ms
(+62.95%)Baseline: 8.68 ms
16.57 ms
(85.32%)
json_api/json_api📈 view plot
🚷 view threshold
1.14 ms
(+8.23%)Baseline: 1.05 ms
1.37 ms
(82.96%)
nested_routers/nested_routers📈 view plot
🚷 view threshold
1.09 ms
(+11.01%)Baseline: 0.98 ms
1.26 ms
(86.86%)
single_root_route/single_root_route📈 view plot
🚷 view threshold
1.03 ms
(+8.99%)Baseline: 0.95 ms
1.22 ms
(84.64%)
single_root_route_burst/single_root_route_burst📈 view plot
🚷 view threshold
18.29 ms
(+7.00%)Baseline: 17.09 ms
21.56 ms
(84.83%)
🐰 View full continuous benchmarking report in Bencher

@github-actions github-actions Bot added the A-deps Area: Dependencies label Aug 17, 2026
@ElijahAhianyo

Copy link
Copy Markdown
Contributor Author

Should be rebased on and merged after #586

@ElijahAhianyo ElijahAhianyo changed the title Use a radix trie for route matching feat!: Use a radix trie for route matching Aug 18, 2026
@ElijahAhianyo
ElijahAhianyo marked this pull request as ready for review August 25, 2026 04:22
@ElijahAhianyo
ElijahAhianyo requested a review from a team August 25, 2026 20:30
Comment thread cot/tests/project.rs
fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
apps.register_with_views(App1, "");
apps.register_with_views(App2, "");
apps.register_with_views(App2, "/foo");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any way of registering multiple routers at the same URL? This is an important feature - people might want to merge different routers at the same base URL.

If matchit doesn't let us do that because it detects a conflict, we should merge the routers. I'm not sure if we should have separate methods to do that explicitly, or keep the implicit behavior - I'll let you decide.

Comment thread cot/src/router/path.rs
use thiserror::Error;
use tracing::debug;

const PATH_MATCHER_ERROR_PREFIX: &str = "route conflict error:";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really "conflict" in most cases, is it? For instance, a numeral-only parameter name doesn't really conflict with anything. I think this error message is misleading.

Comment thread cot/src/router.rs
/// Panics when a url string could not be parsed into a [`Route`]
#[must_use]
pub fn with_urls<T: Into<Vec<Route>>>(urls: T) -> Self {
match Self::try_with_urls(urls) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use expect here?

Comment thread cot/tests/admin.rs
password: &str,
) -> Result<(), Box<dyn Error>> {
driver.goto(&format!("{}/admin/", server.url())).await?;
driver.goto(&format!("{}/admin", server.url())).await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the change coming from? If there's a change in trailing slash handling coming from the migration, I think we should consistently use trailing slashes for URLs.

Comment thread cot/src/openapi.rs
//! # let mut client = cot::test::Client::new(ApiProject).await;
//! #
//! # let response = client.get("/swagger/").await?;
//! # let response = client.get("/swagger").await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we should probably modify the URL routes to include the trailing slashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-deps Area: Dependencies C-lib Crate: cot (main library crate)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants