Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,6 @@ jobs:
env:
RUSTDOCFLAGS: -D warnings

msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install MSRV Rust
uses: dtolnay/rust-toolchain@1.75.0
- name: Check
run: cargo check --workspace

semver:
name: SemVer Checks
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ homepage = "https://github.com/Tuntii/RustAPI"
documentation = "https://docs.rs/rustapi-rs"
keywords = ["web", "framework", "api", "rest", "http"]
categories = ["web-programming::http-server"]
rust-version = "1.75"
rust-version = "1.78"

[workspace.dependencies]
# Async runtime
Expand Down
5 changes: 5 additions & 0 deletions crates/rustapi-core/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ impl Request {
&self.state
}

/// Set path parameters (used internally after route matching)
pub(crate) fn set_path_params(&mut self, params: PathParams) {
self.path_params = params;
}

/// Create a test request from an http::Request
///
/// This is useful for testing middleware and extractors.
Expand Down
73 changes: 39 additions & 34 deletions crates/rustapi-core/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,54 +88,59 @@ async fn handle_request(
// Convert hyper request to our Request type first
let (parts, body) = req.into_parts();

// Match the route to get path params
let (handler, params) = match router.match_route(&path, &method) {
RouteMatch::Found { handler, params } => (handler.clone(), params),
RouteMatch::NotFound => {
let response = ApiError::not_found(format!("No route found for {} {}", method, path))
.into_response();
log_request(&method, &path, response.status(), start);
return 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),
)
.into_response();

response
.headers_mut()
.insert(header::ALLOW, allowed_str.join(", ").parse().unwrap());
log_request(&method, &path, response.status(), start);
return response;
}
};

// Build Request (initially streaming)
// Build Request with empty path params (will be set after route matching)
let request = Request::new(
parts,
crate::request::BodyVariant::Streaming(body),
router.state_ref(),
params,
crate::path_params::PathParams::new(),
);

// Apply request interceptors (in registration order)
let request = interceptors.intercept_request(request);

// Create the final handler as a BoxedNext
let final_handler: BoxedNext = Arc::new(move |req: Request| {
let handler = handler.clone();
Box::pin(async move { handler(req).await })
// Create the routing handler that does route matching inside the middleware chain
// This allows CORS and other middleware to intercept requests BEFORE route matching
let router_clone = router.clone();
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),
Comment on lines +105 to +127
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
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()
),

Copilot uses AI. Check for mistakes.
)
.into_response();
response
.headers_mut()
.insert(header::ALLOW, allowed_str.join(", ").parse().unwrap());
response
Comment on lines +130 to +133
Copy link

Copilot AI Jan 15, 2026

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
}
}
})
as std::pin::Pin<
Box<dyn std::future::Future<Output = crate::response::Response> + Send + 'static>,
>
});

// Execute through middleware stack
let response = layers.execute(request, final_handler).await;
// Execute through middleware stack - middleware runs FIRST, then routing
let response = layers.execute(request, routing_handler).await;

// Apply response interceptors (in reverse registration order)
let response = interceptors.intercept_response(response);
Expand Down
2 changes: 1 addition & 1 deletion crates/rustapi-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//!
//! ## Quick Start
//!
//! ```rust
//! ```rust,no_run
//! use rustapi_rs::prelude::*;
//!
//! #[derive(Serialize, Schema)]
Expand Down
Loading