-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor route matching to occur after middleware #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Moved route matching logic into the middleware chain so that middleware (such as CORS) can intercept requests before route matching. Added a set_path_params method to Request for updating path parameters after route matching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request refactors the request handling flow to defer route matching until after middleware execution, enabling middleware (such as CORS) to intercept requests before routes are resolved. Previously, route matching occurred before middleware, preventing certain middleware from functioning correctly on unmatched routes.
Changes:
- Route matching moved inside the middleware chain via a routing handler closure
- Path parameters are now set on the Request object after route matching instead of at construction
- Error responses for NotFound and MethodNotAllowed are generated within the routing handler
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| crates/rustapi-core/src/server.rs | Restructured handle_request to create a routing handler that performs route matching inside the middleware chain; path parameters are set after a successful match |
| crates/rustapi-core/src/request.rs | Added set_path_params method to allow setting path parameters after route matching |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let path_clone = path.clone(); | ||
| let method_clone = method.clone(); | ||
| let routing_handler: BoxedNext = Arc::new(move |mut req: Request| { | ||
| let router = router_clone.clone(); | ||
| let path = path_clone.clone(); | ||
| let method = method_clone.clone(); | ||
| Box::pin(async move { | ||
| match router.match_route(&path, &method) { | ||
| RouteMatch::Found { handler, params } => { | ||
| // Set path params on the request | ||
| req.set_path_params(params); | ||
| handler(req).await | ||
| } | ||
| RouteMatch::NotFound => { | ||
| ApiError::not_found(format!("No route found for {} {}", method, path)) | ||
| .into_response() | ||
| } | ||
| RouteMatch::MethodNotAllowed { allowed } => { | ||
| let allowed_str: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect(); | ||
| let mut response = ApiError::new( | ||
| StatusCode::METHOD_NOT_ALLOWED, | ||
| "method_not_allowed", | ||
| format!("Method {} not allowed for {}", method, path), |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple clones are performed on every request inside the closure. The router_clone.clone() on line 108 is cloning an Arc<Router> which is inexpensive, but path_clone.clone() and method_clone.clone() create additional allocations for each request. Consider moving these into captured variables in the outer scope to avoid repeated cloning per request.
| let path_clone = path.clone(); | |
| let method_clone = method.clone(); | |
| let routing_handler: BoxedNext = Arc::new(move |mut req: Request| { | |
| let router = router_clone.clone(); | |
| let path = path_clone.clone(); | |
| let method = method_clone.clone(); | |
| Box::pin(async move { | |
| match router.match_route(&path, &method) { | |
| RouteMatch::Found { handler, params } => { | |
| // Set path params on the request | |
| req.set_path_params(params); | |
| handler(req).await | |
| } | |
| RouteMatch::NotFound => { | |
| ApiError::not_found(format!("No route found for {} {}", method, path)) | |
| .into_response() | |
| } | |
| RouteMatch::MethodNotAllowed { allowed } => { | |
| let allowed_str: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect(); | |
| let mut response = ApiError::new( | |
| StatusCode::METHOD_NOT_ALLOWED, | |
| "method_not_allowed", | |
| format!("Method {} not allowed for {}", method, path), | |
| let path_arc = std::sync::Arc::new(path.clone()); | |
| let method_arc = std::sync::Arc::new(method.clone()); | |
| let routing_handler: BoxedNext = Arc::new(move |mut req: Request| { | |
| let router = router_clone.clone(); | |
| let path = std::sync::Arc::clone(&path_arc); | |
| let method = std::sync::Arc::clone(&method_arc); | |
| Box::pin(async move { | |
| match router.match_route(&*path, &*method) { | |
| RouteMatch::Found { handler, params } => { | |
| // Set path params on the request | |
| req.set_path_params(params); | |
| handler(req).await | |
| } | |
| RouteMatch::NotFound => { | |
| ApiError::not_found(format!( | |
| "No route found for {} {}", | |
| method.as_ref(), | |
| path.as_ref() | |
| )) | |
| .into_response() | |
| } | |
| RouteMatch::MethodNotAllowed { allowed } => { | |
| let allowed_str: Vec<&str> = allowed.iter().map(|m| m.as_str()).collect(); | |
| let mut response = ApiError::new( | |
| StatusCode::METHOD_NOT_ALLOWED, | |
| "method_not_allowed", | |
| format!( | |
| "Method {} not allowed for {}", | |
| method.as_ref(), | |
| path.as_ref() | |
| ), |
| response | ||
| .headers_mut() | ||
| .insert(header::ALLOW, allowed_str.join(", ").parse().unwrap()); | ||
| response |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using .unwrap() on the header value parse could panic if the allowed methods string is malformed. While this is unlikely, consider using .expect() with a descriptive message or handling the error gracefully to prevent potential panics in production.
| response | |
| .headers_mut() | |
| .insert(header::ALLOW, allowed_str.join(", ").parse().unwrap()); | |
| response | |
| let allowed_header_value = allowed_str.join(", "); | |
| if let Ok(header_value) = allowed_header_value.parse() { | |
| response | |
| .headers_mut() | |
| .insert(header::ALLOW, header_value); | |
| } else { | |
| error!( | |
| allowed_methods = %allowed_header_value, | |
| "Failed to parse Allow header value; skipping header insertion" | |
| ); | |
| } | |
| response |
Bump the minimum supported Rust version to 1.78 in Cargo.toml and remove the MSRV check job from the CI workflow. This streamlines CI and aligns the project with the new Rust version requirement.
This pull request refactors the request routing and middleware handling in the core server logic to improve middleware flexibility and correctness. The main change is deferring route matching until after middleware has executed, allowing middleware (such as CORS) to intercept requests before routes are resolved. Additionally, path parameters are now set on the request object only after a route is matched.
Routing and middleware execution changes:
crates/rustapi-core/src/server.rs, [1] [2]crates/rustapi-core/src/request.rs, [1];crates/rustapi-core/src/server.rs, [2]crates/rustapi-core/src/server.rs, crates/rustapi-core/src/server.rsL91-R120)crates/rustapi-core/src/server.rs, crates/rustapi-core/src/server.rsL108-R143)Error handling and response logic:
crates/rustapi-core/src/server.rs, [1] [2]