diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c9df0d..2989519 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 32e7776..a64bc8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/rustapi-core/src/request.rs b/crates/rustapi-core/src/request.rs index 762ce03..8f909be 100644 --- a/crates/rustapi-core/src/request.rs +++ b/crates/rustapi-core/src/request.rs @@ -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. diff --git a/crates/rustapi-core/src/server.rs b/crates/rustapi-core/src/server.rs index d5536a0..b3cf511 100644 --- a/crates/rustapi-core/src/server.rs +++ b/crates/rustapi-core/src/server.rs @@ -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), + ) + .into_response(); + response + .headers_mut() + .insert(header::ALLOW, allowed_str.join(", ").parse().unwrap()); + response + } + } + }) as std::pin::Pin< Box + 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); diff --git a/crates/rustapi-rs/src/lib.rs b/crates/rustapi-rs/src/lib.rs index 989dd90..18ef9fd 100644 --- a/crates/rustapi-rs/src/lib.rs +++ b/crates/rustapi-rs/src/lib.rs @@ -8,7 +8,7 @@ //! //! ## Quick Start //! -//! ```rust +//! ```rust,no_run //! use rustapi_rs::prelude::*; //! //! #[derive(Serialize, Schema)]