Skip to main content

fastapi_core/
middleware.rs

1//! Middleware abstraction for request/response processing.
2//!
3//! This module provides a flexible middleware system that allows:
4//! - Pre-processing requests before handlers run
5//! - Post-processing responses after handlers complete
6//! - Short-circuiting to return early without calling handlers
7//! - Composable middleware stacks with defined ordering
8//!
9//! # Design Philosophy
10//!
11//! The middleware system follows these principles:
12//! - **Zero-cost when empty**: No overhead if no middleware is configured
13//! - **Async-native**: All hooks are async for I/O operations
14//! - **Cancel-aware**: Integrates with asupersync's cancellation
15//! - **Composable**: Middleware can be stacked and layered
16//!
17//! # Ordering Semantics
18//!
19//! Middleware executes in a specific order:
20//! 1. `before` hooks run in **registration order** (first registered, first run)
21//! 2. Handler executes
22//! 3. `after` hooks run in **reverse order** (last registered, first run)
23//!
24//! This creates an "onion" model where the first middleware wraps everything:
25//!
26//! ```text
27//! Request → MW1.before → MW2.before → MW3.before → Handler
28//!                                                     ↓
29//! Response ← MW1.after ← MW2.after ← MW3.after ← Response
30//! ```
31//!
32//! # Example
33//!
34//! ```ignore
35//! use fastapi_core::middleware::{Middleware, ControlFlow};
36//! use fastapi_core::{Request, Response, RequestContext};
37//!
38//! struct LoggingMiddleware;
39//!
40//! impl Middleware for LoggingMiddleware {
41//!     async fn before(&self, ctx: &RequestContext, req: &Request) -> ControlFlow {
42//!         println!("Request: {} {}", req.method(), req.path());
43//!         ControlFlow::Continue
44//!     }
45//!
46//!     async fn after(&self, _ctx: &RequestContext, _req: &Request, resp: Response) -> Response {
47//!         println!("Response: {}", resp.status().as_u16());
48//!         resp
49//!     }
50//! }
51//! ```
52
53use std::collections::HashSet;
54use std::future::Future;
55use std::ops::ControlFlow as StdControlFlow;
56use std::pin::Pin;
57use std::sync::Arc;
58use std::time::Instant;
59
60use crate::context::RequestContext;
61use crate::dependency::DependencyOverrides;
62use crate::logging::{LogConfig, RequestLogger};
63use crate::request::{Body, Request};
64use crate::response::Response;
65
66/// A boxed future for async middleware operations.
67pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
68
69/// Control flow for middleware `before` hooks.
70///
71/// Determines whether request processing should continue to the handler
72/// or short-circuit with an early response.
73#[derive(Debug)]
74pub enum ControlFlow {
75    /// Continue processing - call the next middleware or handler.
76    Continue,
77    /// Short-circuit - return this response immediately without calling the handler.
78    ///
79    /// Subsequent `before` hooks and the handler will NOT run.
80    /// However, `after` hooks for middleware that already ran their `before` WILL run.
81    Break(Response),
82}
83
84impl ControlFlow {
85    /// Returns `true` if this is `Continue`.
86    #[must_use]
87    pub fn is_continue(&self) -> bool {
88        matches!(self, Self::Continue)
89    }
90
91    /// Returns `true` if this is `Break`.
92    #[must_use]
93    pub fn is_break(&self) -> bool {
94        matches!(self, Self::Break(_))
95    }
96}
97
98impl From<ControlFlow> for StdControlFlow<Response, ()> {
99    fn from(cf: ControlFlow) -> Self {
100        match cf {
101            ControlFlow::Continue => StdControlFlow::Continue(()),
102            ControlFlow::Break(r) => StdControlFlow::Break(r),
103        }
104    }
105}
106
107/// The core middleware trait.
108///
109/// Middleware wraps request handling with pre-processing and post-processing hooks.
110/// Implementations must be thread-safe (`Send + Sync`) as middleware may be shared
111/// across concurrent requests.
112///
113/// # Implementation Guide
114///
115/// - **`before`**: Inspect/modify the request, optionally short-circuit
116/// - **`after`**: Inspect/modify the response
117///
118/// Both methods have default implementations that do nothing, so you can
119/// implement only what you need.
120///
121/// # Cancel-Safety
122///
123/// Middleware should check `ctx.checkpoint()` for long operations to support
124/// graceful cancellation when clients disconnect or timeouts occur.
125///
126/// # Example: Request Timing
127///
128/// ```ignore
129/// use std::time::Instant;
130/// use fastapi_core::middleware::{Middleware, ControlFlow};
131///
132/// struct TimingMiddleware;
133///
134/// impl Middleware for TimingMiddleware {
135///     async fn before(&self, ctx: &RequestContext, req: &mut Request) -> ControlFlow {
136///         // Store start time in request extensions (future feature)
137///         ControlFlow::Continue
138///     }
139///
140///     async fn after(&self, _ctx: &RequestContext, _req: &Request, mut resp: Response) -> Response {
141///         // Add timing header
142///         resp = resp.header("X-Response-Time", b"42ms".to_vec());
143///         resp
144///     }
145/// }
146/// ```
147pub trait Middleware: Send + Sync {
148    /// Called before the handler executes.
149    ///
150    /// # Parameters
151    ///
152    /// - `ctx`: Request context with cancellation support
153    /// - `req`: Mutable request that can be inspected or modified
154    ///
155    /// # Returns
156    ///
157    /// - `ControlFlow::Continue` to proceed to the next middleware/handler
158    /// - `ControlFlow::Break(response)` to short-circuit and return immediately
159    ///
160    /// # Default Implementation
161    ///
162    /// Returns `ControlFlow::Continue` (no-op).
163    fn before<'a>(
164        &'a self,
165        _ctx: &'a RequestContext,
166        _req: &'a mut Request,
167    ) -> BoxFuture<'a, ControlFlow> {
168        Box::pin(async { ControlFlow::Continue })
169    }
170
171    /// Called after the handler executes.
172    ///
173    /// # Parameters
174    ///
175    /// - `ctx`: Request context with cancellation support
176    /// - `req`: The request (read-only at this point)
177    /// - `response`: The response from the handler or previous `after` hooks
178    ///
179    /// # Returns
180    ///
181    /// The response to pass to the next `after` hook or to return to the client.
182    ///
183    /// # Default Implementation
184    ///
185    /// Returns the response unchanged (no-op).
186    fn after<'a>(
187        &'a self,
188        _ctx: &'a RequestContext,
189        _req: &'a Request,
190        response: Response,
191    ) -> BoxFuture<'a, Response> {
192        Box::pin(async move { response })
193    }
194
195    /// Returns the middleware name for debugging and logging.
196    ///
197    /// Override this to provide a meaningful name for your middleware.
198    fn name(&self) -> &'static str {
199        std::any::type_name::<Self>()
200    }
201}
202
203/// A handler that processes requests into responses.
204///
205/// This trait abstracts over handler functions, allowing middleware to wrap
206/// any type that can handle requests.
207pub trait Handler: Send + Sync {
208    /// Process a request and return a response.
209    fn call<'a>(&'a self, ctx: &'a RequestContext, req: &'a mut Request)
210    -> BoxFuture<'a, Response>;
211
212    /// Optional dependency overrides to apply when building request contexts.
213    ///
214    /// Default implementation returns `None`, which means no overrides.
215    fn dependency_overrides(&self) -> Option<Arc<DependencyOverrides>> {
216        None
217    }
218}
219
220/// Implement Handler for async functions.
221///
222/// This allows any async function with the signature
223/// `async fn(&RequestContext, &mut Request) -> Response` to be used as a handler.
224impl<F, Fut> Handler for F
225where
226    F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync,
227    Fut: Future<Output = Response> + Send + 'static,
228{
229    fn call<'a>(
230        &'a self,
231        ctx: &'a RequestContext,
232        req: &'a mut Request,
233    ) -> BoxFuture<'a, Response> {
234        let fut = self(ctx, req);
235        Box::pin(fut)
236    }
237}
238
239/// Delegate `Handler` to an `Arc`-wrapped handler.
240///
241/// This is a convenience for building apps behind `Arc` (common in tests and when
242/// cloning shared handlers).
243impl<H: Handler + ?Sized> Handler for Arc<H> {
244    fn call<'a>(
245        &'a self,
246        ctx: &'a RequestContext,
247        req: &'a mut Request,
248    ) -> BoxFuture<'a, Response> {
249        (**self).call(ctx, req)
250    }
251
252    fn dependency_overrides(&self) -> Option<Arc<DependencyOverrides>> {
253        (**self).dependency_overrides()
254    }
255}
256
257/// A stack of middleware that wraps a handler.
258///
259/// The stack executes middleware in order:
260/// 1. `before` hooks run first-to-last (registration order)
261/// 2. Handler executes (if no middleware short-circuited)
262/// 3. `after` hooks run last-to-first (reverse order)
263///
264/// # Example
265///
266/// ```ignore
267/// let mut stack = MiddlewareStack::new();
268/// stack.push(LoggingMiddleware);
269/// stack.push(AuthMiddleware);
270/// stack.push(CorsMiddleware);
271///
272/// let response = stack.execute(&handler, &ctx, &mut request).await;
273/// ```
274#[derive(Default)]
275pub struct MiddlewareStack {
276    middleware: Vec<Arc<dyn Middleware>>,
277}
278
279impl MiddlewareStack {
280    /// Creates an empty middleware stack.
281    #[must_use]
282    pub fn new() -> Self {
283        Self {
284            middleware: Vec::new(),
285        }
286    }
287
288    /// Creates a middleware stack with pre-allocated capacity.
289    #[must_use]
290    pub fn with_capacity(capacity: usize) -> Self {
291        Self {
292            middleware: Vec::with_capacity(capacity),
293        }
294    }
295
296    /// Adds middleware to the end of the stack.
297    ///
298    /// Middleware added first will have its `before` run first and `after` run last.
299    pub fn push<M: Middleware + 'static>(&mut self, middleware: M) {
300        self.middleware.push(Arc::new(middleware));
301    }
302
303    /// Adds middleware wrapped in an Arc.
304    ///
305    /// Useful for sharing middleware across multiple stacks.
306    pub fn push_arc(&mut self, middleware: Arc<dyn Middleware>) {
307        self.middleware.push(middleware);
308    }
309
310    /// Returns the number of middleware in the stack.
311    #[must_use]
312    pub fn len(&self) -> usize {
313        self.middleware.len()
314    }
315
316    /// Returns `true` if the stack is empty.
317    #[must_use]
318    pub fn is_empty(&self) -> bool {
319        self.middleware.is_empty()
320    }
321
322    /// Executes the middleware stack with the given handler.
323    ///
324    /// # Execution Order
325    ///
326    /// 1. Each middleware's `before` hook runs in order
327    /// 2. If any `before` returns `Break`, skip remaining middleware and handler
328    /// 3. Handler executes
329    /// 4. Each middleware's `after` hook runs in reverse order
330    ///
331    /// # Short-Circuit Behavior
332    ///
333    /// If middleware N calls `Break(response)`:
334    /// - Middleware N+1..end `before` hooks do NOT run
335    /// - Handler does NOT run
336    /// - Middleware 0..N `after` hooks STILL run (in reverse: N, N-1, ..., 0)
337    ///
338    /// This ensures cleanup middleware (like timing or logging) always runs.
339    pub async fn execute<H: Handler>(
340        &self,
341        handler: &H,
342        ctx: &RequestContext,
343        req: &mut Request,
344    ) -> Response {
345        // Track which middleware ran their `before` hook
346        let mut ran_before_count = 0;
347
348        // Run before hooks in order
349        for mw in &self.middleware {
350            let _ = ctx.checkpoint();
351            match mw.before(ctx, req).await {
352                ControlFlow::Continue => {
353                    ran_before_count += 1;
354                }
355                ControlFlow::Break(response) => {
356                    // Short-circuit: run after hooks for middleware that already ran
357                    return self
358                        .run_after_hooks(ctx, req, response, ran_before_count)
359                        .await;
360                }
361            }
362        }
363
364        // All before hooks passed, call the handler
365        let _ = ctx.checkpoint();
366        let response = handler.call(ctx, req).await;
367
368        // Run after hooks in reverse order
369        self.run_after_hooks(ctx, req, response, ran_before_count)
370            .await
371    }
372
373    /// Runs after hooks for middleware that ran their before hook.
374    async fn run_after_hooks(
375        &self,
376        ctx: &RequestContext,
377        req: &Request,
378        mut response: Response,
379        count: usize,
380    ) -> Response {
381        // Run in reverse order (last middleware's after runs first)
382        for mw in self.middleware[..count].iter().rev() {
383            let _ = ctx.checkpoint();
384            response = mw.after(ctx, req, response).await;
385        }
386        response
387    }
388}
389
390/// A layer that can wrap handlers with middleware.
391///
392/// This provides a more functional composition style similar to Tower's Layer trait.
393///
394/// # Example
395///
396/// ```ignore
397/// let layer = Layer::new(LoggingMiddleware);
398/// let wrapped = layer.wrap(my_handler);
399/// ```
400pub struct Layer<M> {
401    middleware: M,
402}
403
404impl<M: Middleware + Clone> Layer<M> {
405    /// Creates a new layer with the given middleware.
406    pub fn new(middleware: M) -> Self {
407        Self { middleware }
408    }
409
410    /// Wraps a handler with this layer's middleware.
411    pub fn wrap<H: Handler>(&self, handler: H) -> Layered<M, H> {
412        Layered {
413            middleware: self.middleware.clone(),
414            inner: handler,
415        }
416    }
417}
418
419/// A handler wrapped with middleware via a Layer.
420pub struct Layered<M, H> {
421    middleware: M,
422    inner: H,
423}
424
425impl<M: Middleware, H: Handler> Handler for Layered<M, H> {
426    fn call<'a>(
427        &'a self,
428        ctx: &'a RequestContext,
429        req: &'a mut Request,
430    ) -> BoxFuture<'a, Response> {
431        Box::pin(async move {
432            // Run before hook
433            let _ = ctx.checkpoint();
434            match self.middleware.before(ctx, req).await {
435                ControlFlow::Continue => {
436                    // Call inner handler
437                    let _ = ctx.checkpoint();
438                    let response = self.inner.call(ctx, req).await;
439                    // Run after hook
440                    let _ = ctx.checkpoint();
441                    self.middleware.after(ctx, req, response).await
442                }
443                ControlFlow::Break(response) => {
444                    // Short-circuit: still run after for this middleware
445                    let _ = ctx.checkpoint();
446                    self.middleware.after(ctx, req, response).await
447                }
448            }
449        })
450    }
451}
452
453// ============================================================================
454// Common Middleware Implementations
455// ============================================================================
456
457/// No-op middleware that does nothing.
458///
459/// Useful as a placeholder or for testing.
460#[derive(Debug, Clone, Copy, Default)]
461pub struct NoopMiddleware;
462
463impl Middleware for NoopMiddleware {
464    fn name(&self) -> &'static str {
465        "Noop"
466    }
467}
468
469/// Middleware that adds a custom header to all responses.
470///
471/// # Example
472///
473/// ```ignore
474/// // Add X-Powered-By header to all responses
475/// let mw = AddResponseHeader::new("X-Powered-By", "fastapi_rust");
476/// stack.push(mw);
477/// ```
478#[derive(Debug, Clone)]
479pub struct AddResponseHeader {
480    name: String,
481    value: Vec<u8>,
482}
483
484impl AddResponseHeader {
485    /// Creates a new middleware that adds the specified header to responses.
486    pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
487        Self {
488            name: name.into(),
489            value: value.into(),
490        }
491    }
492}
493
494impl Middleware for AddResponseHeader {
495    fn after<'a>(
496        &'a self,
497        _ctx: &'a RequestContext,
498        _req: &'a Request,
499        response: Response,
500    ) -> BoxFuture<'a, Response> {
501        let name = self.name.clone();
502        let value = self.value.clone();
503        Box::pin(async move { response.header(name, value) })
504    }
505
506    fn name(&self) -> &'static str {
507        "AddResponseHeader"
508    }
509}
510
511/// Middleware that requires a specific header to be present.
512///
513/// Returns 400 Bad Request if the header is missing.
514///
515/// # Example
516///
517/// ```ignore
518/// // Require X-Api-Key header
519/// let mw = RequireHeader::new("X-Api-Key");
520/// stack.push(mw);
521/// ```
522#[derive(Debug, Clone)]
523pub struct RequireHeader {
524    name: String,
525}
526
527impl RequireHeader {
528    /// Creates a new middleware that requires the specified header.
529    pub fn new(name: impl Into<String>) -> Self {
530        Self { name: name.into() }
531    }
532}
533
534impl Middleware for RequireHeader {
535    fn before<'a>(
536        &'a self,
537        _ctx: &'a RequestContext,
538        req: &'a mut Request,
539    ) -> BoxFuture<'a, ControlFlow> {
540        let has_header = req.headers().get(&self.name).is_some();
541        let name = self.name.clone();
542        Box::pin(async move {
543            if has_header {
544                ControlFlow::Continue
545            } else {
546                let body = format!("Missing required header: {name}");
547                ControlFlow::Break(
548                    Response::with_status(crate::response::StatusCode::BAD_REQUEST)
549                        .header("content-type", b"text/plain".to_vec())
550                        .body(crate::response::ResponseBody::Bytes(body.into_bytes())),
551                )
552            }
553        })
554    }
555
556    fn name(&self) -> &'static str {
557        "RequireHeader"
558    }
559}
560
561/// Middleware that limits request processing based on path prefix.
562///
563/// Only allows requests to paths starting with the specified prefix.
564/// Other requests receive a 404 Not Found response.
565///
566/// # Example
567///
568/// ```ignore
569/// // Only allow requests to /api/*
570/// let mw = PathPrefixFilter::new("/api");
571/// stack.push(mw);
572/// ```
573#[derive(Debug, Clone)]
574pub struct PathPrefixFilter {
575    prefix: String,
576}
577
578impl PathPrefixFilter {
579    /// Creates a new middleware that only allows requests with the specified path prefix.
580    pub fn new(prefix: impl Into<String>) -> Self {
581        Self {
582            prefix: prefix.into(),
583        }
584    }
585}
586
587impl Middleware for PathPrefixFilter {
588    fn before<'a>(
589        &'a self,
590        _ctx: &'a RequestContext,
591        req: &'a mut Request,
592    ) -> BoxFuture<'a, ControlFlow> {
593        let path_matches = req.path().starts_with(&self.prefix);
594        Box::pin(async move {
595            if path_matches {
596                ControlFlow::Continue
597            } else {
598                ControlFlow::Break(Response::with_status(
599                    crate::response::StatusCode::NOT_FOUND,
600                ))
601            }
602        })
603    }
604
605    fn name(&self) -> &'static str {
606        "PathPrefixFilter"
607    }
608}
609
610/// Middleware that sets response status code based on a condition.
611///
612/// This is useful for implementing health checks or conditional responses.
613#[derive(Debug, Clone)]
614pub struct ConditionalStatus<F>
615where
616    F: Fn(&Request) -> bool + Send + Sync,
617{
618    condition: F,
619    status_if_true: crate::response::StatusCode,
620    status_if_false: crate::response::StatusCode,
621}
622
623impl<F> ConditionalStatus<F>
624where
625    F: Fn(&Request) -> bool + Send + Sync,
626{
627    /// Creates a new conditional status middleware.
628    ///
629    /// If the condition returns true, the response gets `status_if_true`.
630    /// Otherwise, it gets `status_if_false`.
631    pub fn new(
632        condition: F,
633        status_if_true: crate::response::StatusCode,
634        status_if_false: crate::response::StatusCode,
635    ) -> Self {
636        Self {
637            condition,
638            status_if_true,
639            status_if_false,
640        }
641    }
642}
643
644impl<F> Middleware for ConditionalStatus<F>
645where
646    F: Fn(&Request) -> bool + Send + Sync,
647{
648    fn after<'a>(
649        &'a self,
650        _ctx: &'a RequestContext,
651        req: &'a Request,
652        response: Response,
653    ) -> BoxFuture<'a, Response> {
654        let matches = (self.condition)(req);
655        let status = if matches {
656            self.status_if_true
657        } else {
658            self.status_if_false
659        };
660        Box::pin(async move { Response::with_status(status).body(response.body_ref().into()) })
661    }
662
663    fn name(&self) -> &'static str {
664        "ConditionalStatus"
665    }
666}
667
668// ============================================================================
669// CORS Middleware
670// ============================================================================
671
672/// Origin matching pattern for CORS.
673#[derive(Debug, Clone)]
674pub enum OriginPattern {
675    /// Allow any origin.
676    Any,
677    /// Exact match.
678    Exact(String),
679    /// Wildcard match (supports `*`).
680    Wildcard(String),
681    /// Simple regex match (supports `^`, `$`, `.`, `*`).
682    Regex(String),
683}
684
685impl OriginPattern {
686    fn matches(&self, origin: &str) -> bool {
687        match self {
688            Self::Any => true,
689            Self::Exact(value) => value == origin,
690            Self::Wildcard(pattern) => wildcard_match(pattern, origin),
691            Self::Regex(pattern) => regex_match(pattern, origin),
692        }
693    }
694}
695
696/// Cross-Origin Resource Sharing (CORS) configuration.
697///
698/// Controls which origins, methods, and headers are allowed for
699/// cross-origin requests. By default, no origins are allowed.
700///
701/// # Defaults
702///
703/// | Setting | Default |
704/// |---------|---------|
705/// | `allow_any_origin` | `false` |
706/// | `allow_credentials` | `false` |
707/// | `allowed_methods` | GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD |
708/// | `allowed_headers` | none |
709/// | `expose_headers` | none |
710/// | `max_age` | none |
711///
712/// # Security: Credentials and Wildcards
713///
714/// According to the CORS specification (Fetch Standard), when credentials
715/// mode is enabled (`allow_credentials: true`), the following headers
716/// **cannot** use the `*` wildcard value:
717///
718/// - `Access-Control-Allow-Origin` (must echo the specific origin)
719/// - `Access-Control-Allow-Headers` (must list specific headers)
720/// - `Access-Control-Allow-Methods` (must list specific methods)
721/// - `Access-Control-Expose-Headers` (must list specific headers)
722///
723/// This implementation enforces this: when `allow_credentials(true)` is
724/// combined with `allow_any_origin()`, the response echoes back the
725/// specific request origin instead of returning `*`.
726///
727/// # Example
728///
729/// ```ignore
730/// use fastapi_core::Cors;
731///
732/// // Secure: specific origin with credentials
733/// let cors = Cors::new()
734///     .allow_origin("https://myapp.example.com")
735///     .allow_credentials(true)
736///     .expose_headers(["X-Request-Id"]);
737///
738/// // Also secure: any origin echoes back specific origin when credentials enabled
739/// // (not recommended - prefer explicit origins for security)
740/// let cors = Cors::new()
741///     .allow_any_origin()
742///     .allow_credentials(true);
743/// ```
744#[derive(Debug, Clone)]
745pub struct CorsConfig {
746    allow_any_origin: bool,
747    allow_credentials: bool,
748    allowed_methods: Vec<crate::request::Method>,
749    allowed_headers: Vec<String>,
750    expose_headers: Vec<String>,
751    max_age: Option<u32>,
752    origins: Vec<OriginPattern>,
753}
754
755impl Default for CorsConfig {
756    fn default() -> Self {
757        Self {
758            allow_any_origin: false,
759            allow_credentials: false,
760            allowed_methods: vec![
761                crate::request::Method::Get,
762                crate::request::Method::Post,
763                crate::request::Method::Put,
764                crate::request::Method::Patch,
765                crate::request::Method::Delete,
766                crate::request::Method::Options,
767                crate::request::Method::Head,
768            ],
769            allowed_headers: Vec::new(),
770            expose_headers: Vec::new(),
771            max_age: None,
772            origins: Vec::new(),
773        }
774    }
775}
776
777/// CORS middleware.
778#[derive(Debug, Clone)]
779pub struct Cors {
780    config: CorsConfig,
781}
782
783impl Cors {
784    /// Create a new CORS middleware with default configuration.
785    #[must_use]
786    pub fn new() -> Self {
787        Self {
788            config: CorsConfig::default(),
789        }
790    }
791
792    /// Replace the configuration entirely.
793    #[must_use]
794    pub fn config(mut self, config: CorsConfig) -> Self {
795        self.config = config;
796        self
797    }
798
799    /// Allow any origin.
800    #[must_use]
801    pub fn allow_any_origin(mut self) -> Self {
802        self.config.allow_any_origin = true;
803        self
804    }
805
806    /// Allow a single exact origin.
807    #[must_use]
808    pub fn allow_origin(mut self, origin: impl Into<String>) -> Self {
809        self.config
810            .origins
811            .push(OriginPattern::Exact(origin.into()));
812        self
813    }
814
815    /// Allow a wildcard origin pattern (supports `*`).
816    #[must_use]
817    pub fn allow_origin_wildcard(mut self, pattern: impl Into<String>) -> Self {
818        self.config
819            .origins
820            .push(OriginPattern::Wildcard(pattern.into()));
821        self
822    }
823
824    /// Allow a simple regex origin pattern (supports `^`, `$`, `.`, `*`).
825    #[must_use]
826    pub fn allow_origin_regex(mut self, pattern: impl Into<String>) -> Self {
827        self.config
828            .origins
829            .push(OriginPattern::Regex(pattern.into()));
830        self
831    }
832
833    /// Allow credentials for CORS responses.
834    #[must_use]
835    pub fn allow_credentials(mut self, allow: bool) -> Self {
836        self.config.allow_credentials = allow;
837        self
838    }
839
840    /// Override allowed HTTP methods for preflight.
841    #[must_use]
842    pub fn allow_methods<I>(mut self, methods: I) -> Self
843    where
844        I: IntoIterator<Item = crate::request::Method>,
845    {
846        self.config.allowed_methods = methods.into_iter().collect();
847        self
848    }
849
850    /// Override allowed headers for preflight.
851    #[must_use]
852    pub fn allow_headers<I, S>(mut self, headers: I) -> Self
853    where
854        I: IntoIterator<Item = S>,
855        S: Into<String>,
856    {
857        self.config.allowed_headers = headers.into_iter().map(Into::into).collect();
858        self
859    }
860
861    /// Add exposed headers for responses.
862    #[must_use]
863    pub fn expose_headers<I, S>(mut self, headers: I) -> Self
864    where
865        I: IntoIterator<Item = S>,
866        S: Into<String>,
867    {
868        self.config.expose_headers = headers.into_iter().map(Into::into).collect();
869        self
870    }
871
872    /// Set the preflight max-age in seconds.
873    #[must_use]
874    pub fn max_age(mut self, seconds: u32) -> Self {
875        self.config.max_age = Some(seconds);
876        self
877    }
878
879    fn is_origin_allowed(&self, origin: &str) -> bool {
880        if self.config.allow_any_origin {
881            return true;
882        }
883        self.config
884            .origins
885            .iter()
886            .any(|pattern| pattern.matches(origin))
887    }
888
889    fn allow_origin_value(&self, origin: &str) -> Option<String> {
890        if !self.is_origin_allowed(origin) {
891            return None;
892        }
893        if self.config.allow_any_origin && !self.config.allow_credentials {
894            Some("*".to_string())
895        } else {
896            Some(origin.to_string())
897        }
898    }
899
900    fn allow_methods_value(&self) -> String {
901        self.config
902            .allowed_methods
903            .iter()
904            .map(|method| method.as_str())
905            .collect::<Vec<_>>()
906            .join(", ")
907    }
908
909    fn allow_headers_value(&self, request: &Request) -> Option<String> {
910        if self.config.allowed_headers.is_empty() {
911            // No allowed headers configured — do NOT reflect the request's
912            // Access-Control-Request-Headers back, as that effectively allows
913            // arbitrary headers. Return None so the header is omitted entirely,
914            // meaning only CORS-safelisted request headers are permitted.
915            return None;
916        }
917
918        // Check for wildcard "*" — if any entry is wildcard, reflect request
919        // headers (standard CORS wildcard behavior when credentials are not
920        // in use). When credentials are enabled, wildcard is NOT valid per
921        // the Fetch spec, so we reflect the requested headers instead.
922        if self.config.allowed_headers.iter().any(|h| h == "*") {
923            if self.config.allow_credentials {
924                // With credentials, we cannot use literal "*" so reflect
925                // the request's headers as an explicit allow list.
926                return request
927                    .headers()
928                    .get("access-control-request-headers")
929                    .and_then(|value| std::str::from_utf8(value).ok())
930                    .map(ToString::to_string);
931            }
932            return Some("*".to_string());
933        }
934
935        Some(self.config.allowed_headers.join(", "))
936    }
937
938    fn apply_common_headers(&self, mut response: Response, origin: &str) -> Response {
939        if let Some(allow_origin) = self.allow_origin_value(origin) {
940            let is_wildcard = allow_origin == "*";
941            response = response.header("access-control-allow-origin", allow_origin.into_bytes());
942            if !is_wildcard {
943                response = response.header("vary", b"Origin".to_vec());
944            }
945            if self.config.allow_credentials {
946                response = response.header("access-control-allow-credentials", b"true".to_vec());
947            }
948            if !self.config.expose_headers.is_empty() {
949                response = response.header(
950                    "access-control-expose-headers",
951                    self.config.expose_headers.join(", ").into_bytes(),
952                );
953            }
954        }
955        response
956    }
957}
958
959impl Default for Cors {
960    fn default() -> Self {
961        Self::new()
962    }
963}
964
965#[derive(Debug, Clone)]
966struct CorsOrigin(String);
967
968impl Middleware for Cors {
969    fn before<'a>(
970        &'a self,
971        _ctx: &'a RequestContext,
972        req: &'a mut Request,
973    ) -> BoxFuture<'a, ControlFlow> {
974        let origin = req
975            .headers()
976            .get("origin")
977            .and_then(|value| std::str::from_utf8(value).ok())
978            .map(ToString::to_string);
979
980        let Some(origin) = origin else {
981            return Box::pin(async { ControlFlow::Continue });
982        };
983
984        if !self.is_origin_allowed(&origin) {
985            let is_preflight = req.method() == crate::request::Method::Options
986                && req.headers().get("access-control-request-method").is_some();
987            if is_preflight {
988                return Box::pin(async {
989                    ControlFlow::Break(Response::with_status(
990                        crate::response::StatusCode::FORBIDDEN,
991                    ))
992                });
993            }
994            return Box::pin(async { ControlFlow::Continue });
995        }
996
997        let is_preflight = req.method() == crate::request::Method::Options
998            && req.headers().get("access-control-request-method").is_some();
999
1000        if is_preflight {
1001            let mut response = Response::no_content();
1002            response = self.apply_common_headers(response, &origin);
1003            response = response.header(
1004                "access-control-allow-methods",
1005                self.allow_methods_value().into_bytes(),
1006            );
1007
1008            if let Some(value) = self.allow_headers_value(req) {
1009                response = response.header("access-control-allow-headers", value.into_bytes());
1010            }
1011
1012            if let Some(max_age) = self.config.max_age {
1013                response =
1014                    response.header("access-control-max-age", max_age.to_string().into_bytes());
1015            }
1016
1017            return Box::pin(async move { ControlFlow::Break(response) });
1018        }
1019
1020        req.insert_extension(CorsOrigin(origin));
1021        Box::pin(async { ControlFlow::Continue })
1022    }
1023
1024    fn after<'a>(
1025        &'a self,
1026        _ctx: &'a RequestContext,
1027        req: &'a Request,
1028        response: Response,
1029    ) -> BoxFuture<'a, Response> {
1030        let origin = req.get_extension::<CorsOrigin>().map(|v| v.0.clone());
1031        Box::pin(async move {
1032            if let Some(origin) = origin {
1033                return self.apply_common_headers(response, &origin);
1034            }
1035            response
1036        })
1037    }
1038
1039    fn name(&self) -> &'static str {
1040        "Cors"
1041    }
1042}
1043
1044fn wildcard_match(pattern: &str, value: &str) -> bool {
1045    // Simple glob matcher for '*'
1046    let mut pat_chars = pattern.chars().peekable();
1047    let mut val_chars = value.chars().peekable();
1048    let mut star = None;
1049    let mut match_after_star = None;
1050
1051    while let Some(p) = pat_chars.next() {
1052        match p {
1053            '*' => {
1054                star = Some(pat_chars.clone());
1055                match_after_star = Some(val_chars.clone());
1056            }
1057            _ => {
1058                if let Some(v) = val_chars.next() {
1059                    if p != v {
1060                        if let (Some(pat_backup), Some(val_backup)) =
1061                            (star.clone(), match_after_star.clone())
1062                        {
1063                            pat_chars = pat_backup;
1064                            val_chars = val_backup;
1065                            val_chars.next();
1066                            match_after_star = Some(val_chars.clone());
1067                            continue;
1068                        }
1069                        return false;
1070                    }
1071                } else {
1072                    return false;
1073                }
1074            }
1075        }
1076    }
1077
1078    // Consume trailing '*' in pattern
1079    if pat_chars.peek().is_none() && val_chars.peek().is_none() {
1080        return true;
1081    }
1082
1083    if let Some(pat_backup) = star {
1084        if val_chars.peek().is_none() {
1085            let trailing = pat_backup;
1086            for ch in trailing {
1087                if ch != '*' {
1088                    return false;
1089                }
1090            }
1091            return true;
1092        }
1093    }
1094
1095    val_chars.peek().is_none()
1096}
1097
1098fn regex_match(pattern: &str, value: &str) -> bool {
1099    // Minimal regex engine: supports ^, $, ., *
1100    let pat = pattern.as_bytes();
1101    let text = value.as_bytes();
1102
1103    if pat.first() == Some(&b'^') {
1104        return regex_match_here(&pat[1..], text);
1105    }
1106
1107    let mut i = 0;
1108    loop {
1109        if regex_match_here(pat, &text[i..]) {
1110            return true;
1111        }
1112        if i == text.len() {
1113            break;
1114        }
1115        i += 1;
1116    }
1117    false
1118}
1119
1120fn regex_match_here(pattern: &[u8], text: &[u8]) -> bool {
1121    if pattern.is_empty() {
1122        return true;
1123    }
1124    if pattern == b"$" {
1125        return text.is_empty();
1126    }
1127    if pattern.len() >= 2 && pattern[1] == b'*' {
1128        return regex_match_star(pattern[0], &pattern[2..], text);
1129    }
1130    if !text.is_empty() && (pattern[0] == b'.' || pattern[0] == text[0]) {
1131        return regex_match_here(&pattern[1..], &text[1..]);
1132    }
1133    false
1134}
1135
1136fn regex_match_star(ch: u8, pattern: &[u8], text: &[u8]) -> bool {
1137    let mut i = 0;
1138    loop {
1139        if regex_match_here(pattern, &text[i..]) {
1140            return true;
1141        }
1142        if i == text.len() {
1143            return false;
1144        }
1145        if ch != b'.' && text[i] != ch {
1146            return false;
1147        }
1148        i += 1;
1149    }
1150}
1151
1152// ============================================================================
1153// Request/Response Logging Middleware
1154// ============================================================================
1155
1156/// Middleware that logs requests and responses with configurable redaction.
1157#[derive(Debug, Clone)]
1158pub struct RequestResponseLogger {
1159    log_config: LogConfig,
1160    redact_headers: HashSet<String>,
1161    log_request_headers: bool,
1162    log_response_headers: bool,
1163    log_body: bool,
1164    max_body_bytes: usize,
1165}
1166
1167impl Default for RequestResponseLogger {
1168    fn default() -> Self {
1169        Self {
1170            log_config: LogConfig::production(),
1171            redact_headers: default_redacted_headers(),
1172            log_request_headers: true,
1173            log_response_headers: true,
1174            log_body: false,
1175            max_body_bytes: 1024,
1176        }
1177    }
1178}
1179
1180impl RequestResponseLogger {
1181    /// Create a new logger middleware with defaults.
1182    #[must_use]
1183    pub fn new() -> Self {
1184        Self::default()
1185    }
1186
1187    /// Override the logging configuration.
1188    #[must_use]
1189    pub fn log_config(mut self, config: LogConfig) -> Self {
1190        self.log_config = config;
1191        self
1192    }
1193
1194    /// Enable or disable request header logging.
1195    #[must_use]
1196    pub fn log_request_headers(mut self, enabled: bool) -> Self {
1197        self.log_request_headers = enabled;
1198        self
1199    }
1200
1201    /// Enable or disable response header logging.
1202    #[must_use]
1203    pub fn log_response_headers(mut self, enabled: bool) -> Self {
1204        self.log_response_headers = enabled;
1205        self
1206    }
1207
1208    /// Enable or disable request/response body logging.
1209    #[must_use]
1210    pub fn log_body(mut self, enabled: bool) -> Self {
1211        self.log_body = enabled;
1212        self
1213    }
1214
1215    /// Set the maximum number of body bytes to include in logs.
1216    #[must_use]
1217    pub fn max_body_bytes(mut self, max: usize) -> Self {
1218        self.max_body_bytes = max;
1219        self
1220    }
1221
1222    /// Add a header name to redact (case-insensitive).
1223    #[must_use]
1224    pub fn redact_header(mut self, name: impl Into<String>) -> Self {
1225        self.redact_headers.insert(name.into().to_ascii_lowercase());
1226        self
1227    }
1228}
1229
1230#[derive(Debug, Clone)]
1231struct RequestStart(Instant);
1232
1233impl Middleware for RequestResponseLogger {
1234    fn before<'a>(
1235        &'a self,
1236        ctx: &'a RequestContext,
1237        req: &'a mut Request,
1238    ) -> BoxFuture<'a, ControlFlow> {
1239        let logger = RequestLogger::new(ctx, self.log_config.clone());
1240        req.insert_extension(RequestStart(Instant::now()));
1241
1242        let method = req.method();
1243        let path = req.path();
1244        let query = req.query();
1245        let body_bytes = body_len(req.body());
1246
1247        logger.info_with_fields("request", |entry| {
1248            let mut entry = entry
1249                .field("method", method)
1250                .field("path", path)
1251                .field("body_bytes", body_bytes);
1252
1253            if let Some(q) = query {
1254                entry = entry.field("query", q);
1255            }
1256
1257            if self.log_request_headers {
1258                let headers = format_headers(req.headers().iter(), &self.redact_headers);
1259                entry = entry.field("headers", headers);
1260            }
1261
1262            if self.log_body {
1263                if let Some(body) = preview_body(req.body(), self.max_body_bytes) {
1264                    entry = entry.field("body", body);
1265                }
1266            }
1267
1268            entry
1269        });
1270
1271        Box::pin(async { ControlFlow::Continue })
1272    }
1273
1274    fn after<'a>(
1275        &'a self,
1276        ctx: &'a RequestContext,
1277        req: &'a Request,
1278        response: Response,
1279    ) -> BoxFuture<'a, Response> {
1280        let logger = RequestLogger::new(ctx, self.log_config.clone());
1281        let duration = req
1282            .get_extension::<RequestStart>()
1283            .map(|start| start.0.elapsed())
1284            .unwrap_or_default();
1285
1286        let status = response.status();
1287        let body_bytes = response.body_ref().len();
1288
1289        logger.info_with_fields("response", |entry| {
1290            let mut entry = entry
1291                .field("status", status.as_u16())
1292                .field("duration_us", duration.as_micros())
1293                .field("body_bytes", body_bytes);
1294
1295            if self.log_response_headers {
1296                let headers = format_response_headers(response.headers(), &self.redact_headers);
1297                entry = entry.field("headers", headers);
1298            }
1299
1300            if self.log_body {
1301                if let Some(body) = preview_response_body(response.body_ref(), self.max_body_bytes)
1302                {
1303                    entry = entry.field("body", body);
1304                }
1305            }
1306
1307            entry
1308        });
1309
1310        Box::pin(async move { response })
1311    }
1312
1313    fn name(&self) -> &'static str {
1314        "RequestResponseLogger"
1315    }
1316}
1317
1318fn default_redacted_headers() -> HashSet<String> {
1319    [
1320        "authorization",
1321        "proxy-authorization",
1322        "cookie",
1323        "set-cookie",
1324    ]
1325    .iter()
1326    .map(ToString::to_string)
1327    .collect()
1328}
1329
1330fn body_len(body: &Body) -> usize {
1331    match body {
1332        Body::Empty => 0,
1333        Body::Bytes(bytes) => bytes.len(),
1334        Body::Stream { content_length, .. } => content_length.unwrap_or(0),
1335    }
1336}
1337
1338fn preview_body(body: &Body, max_bytes: usize) -> Option<String> {
1339    if max_bytes == 0 {
1340        return None;
1341    }
1342    match body {
1343        Body::Empty => None,
1344        Body::Bytes(bytes) => {
1345            if bytes.is_empty() {
1346                None
1347            } else {
1348                Some(format_bytes(bytes, max_bytes))
1349            }
1350        }
1351        Body::Stream { .. } => None,
1352    }
1353}
1354
1355fn preview_response_body(body: &crate::response::ResponseBody, max_bytes: usize) -> Option<String> {
1356    if max_bytes == 0 {
1357        return None;
1358    }
1359    match body {
1360        crate::response::ResponseBody::Empty => None,
1361        crate::response::ResponseBody::Bytes(bytes) => {
1362            if bytes.is_empty() {
1363                None
1364            } else {
1365                Some(format_bytes(bytes, max_bytes))
1366            }
1367        }
1368        crate::response::ResponseBody::Stream(_) => None,
1369    }
1370}
1371
1372fn format_headers<'a>(
1373    headers: impl Iterator<Item = (&'a str, &'a [u8])>,
1374    redacted: &HashSet<String>,
1375) -> String {
1376    let mut out = String::new();
1377    for (idx, (name, value)) in headers.enumerate() {
1378        if idx > 0 {
1379            out.push_str(", ");
1380        }
1381        out.push_str(name);
1382        out.push('=');
1383
1384        let lowered = name.to_ascii_lowercase();
1385        if redacted.contains(&lowered) {
1386            out.push_str("<redacted>");
1387            continue;
1388        }
1389
1390        match std::str::from_utf8(value) {
1391            Ok(text) => out.push_str(text),
1392            Err(_) => out.push_str("<binary>"),
1393        }
1394    }
1395    out
1396}
1397
1398fn format_response_headers(headers: &[(String, Vec<u8>)], redacted: &HashSet<String>) -> String {
1399    format_headers(
1400        headers
1401            .iter()
1402            .map(|(name, value)| (name.as_str(), value.as_slice())),
1403        redacted,
1404    )
1405}
1406
1407fn format_bytes(bytes: &[u8], max_bytes: usize) -> String {
1408    let limit = max_bytes.min(bytes.len());
1409    match std::str::from_utf8(&bytes[..limit]) {
1410        Ok(text) => {
1411            let mut output = text.to_string();
1412            if bytes.len() > max_bytes {
1413                output.push_str("...");
1414            }
1415            output
1416        }
1417        Err(_) => format!("<{} bytes binary>", bytes.len()),
1418    }
1419}
1420
1421// Helper for ResponseBody conversion
1422impl From<&crate::response::ResponseBody> for crate::response::ResponseBody {
1423    fn from(body: &crate::response::ResponseBody) -> Self {
1424        match body {
1425            crate::response::ResponseBody::Empty => crate::response::ResponseBody::Empty,
1426            crate::response::ResponseBody::Bytes(b) => {
1427                crate::response::ResponseBody::Bytes(b.clone())
1428            }
1429            crate::response::ResponseBody::Stream(_) => crate::response::ResponseBody::Empty,
1430        }
1431    }
1432}
1433
1434// ============================================================================
1435// Request ID Middleware
1436// ============================================================================
1437
1438/// A request ID that was extracted or generated for the current request.
1439///
1440/// This is stored in request extensions and can be retrieved by handlers
1441/// or other middleware for logging and tracing.
1442#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1443pub struct RequestId(pub String);
1444
1445impl RequestId {
1446    /// Creates a new request ID with the given value.
1447    #[must_use]
1448    pub fn new(id: impl Into<String>) -> Self {
1449        Self(id.into())
1450    }
1451
1452    /// Returns the request ID as a string slice.
1453    #[must_use]
1454    pub fn as_str(&self) -> &str {
1455        &self.0
1456    }
1457
1458    /// Generates a new unique request ID.
1459    ///
1460    /// Uses a simple format: timestamp-counter for uniqueness without
1461    /// requiring external UUID dependencies.
1462    #[must_use]
1463    pub fn generate() -> Self {
1464        use std::sync::atomic::{AtomicU64, Ordering};
1465        use std::time::{SystemTime, UNIX_EPOCH};
1466
1467        static COUNTER: AtomicU64 = AtomicU64::new(0);
1468
1469        let timestamp = SystemTime::now()
1470            .duration_since(UNIX_EPOCH)
1471            .map(|d| d.as_micros() as u64)
1472            .unwrap_or(0);
1473        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
1474
1475        // Format: hex timestamp + full counter for unique IDs without collisions
1476        Self(format!("{:x}-{:x}", timestamp, counter))
1477    }
1478}
1479
1480impl std::fmt::Display for RequestId {
1481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1482        write!(f, "{}", self.0)
1483    }
1484}
1485
1486impl From<String> for RequestId {
1487    fn from(s: String) -> Self {
1488        Self(s)
1489    }
1490}
1491
1492impl From<&str> for RequestId {
1493    fn from(s: &str) -> Self {
1494        Self(s.to_string())
1495    }
1496}
1497
1498/// Configuration for request ID middleware.
1499#[derive(Debug, Clone)]
1500pub struct RequestIdConfig {
1501    /// Header name to read/write request ID (default: "x-request-id").
1502    pub header_name: String,
1503    /// Whether to accept request ID from client (default: true).
1504    pub accept_from_client: bool,
1505    /// Whether to add request ID to response headers (default: true).
1506    pub add_to_response: bool,
1507    /// Maximum length of client-provided request ID (default: 128).
1508    pub max_client_id_length: usize,
1509}
1510
1511impl Default for RequestIdConfig {
1512    fn default() -> Self {
1513        Self {
1514            header_name: "x-request-id".to_string(),
1515            accept_from_client: true,
1516            add_to_response: true,
1517            max_client_id_length: 128,
1518        }
1519    }
1520}
1521
1522impl RequestIdConfig {
1523    /// Creates a new configuration with defaults.
1524    #[must_use]
1525    pub fn new() -> Self {
1526        Self::default()
1527    }
1528
1529    /// Sets the header name for request ID.
1530    #[must_use]
1531    pub fn header_name(mut self, name: impl Into<String>) -> Self {
1532        self.header_name = name.into();
1533        self
1534    }
1535
1536    /// Sets whether to accept request ID from client.
1537    #[must_use]
1538    pub fn accept_from_client(mut self, accept: bool) -> Self {
1539        self.accept_from_client = accept;
1540        self
1541    }
1542
1543    /// Sets whether to add request ID to response.
1544    #[must_use]
1545    pub fn add_to_response(mut self, add: bool) -> Self {
1546        self.add_to_response = add;
1547        self
1548    }
1549
1550    /// Sets the maximum length for client-provided request IDs.
1551    #[must_use]
1552    pub fn max_client_id_length(mut self, max: usize) -> Self {
1553        self.max_client_id_length = max;
1554        self
1555    }
1556}
1557
1558/// Middleware that adds unique request IDs to requests and responses.
1559///
1560/// This middleware:
1561/// 1. Checks for an existing X-Request-ID header from the client
1562/// 2. If present and valid, uses it; otherwise generates a new ID
1563/// 3. Stores the ID in request extensions for handlers to access
1564/// 4. Adds the ID to response headers
1565///
1566/// # Example
1567///
1568/// ```ignore
1569/// use fastapi_core::middleware::RequestIdMiddleware;
1570///
1571/// let mut stack = MiddlewareStack::new();
1572/// stack.push(RequestIdMiddleware::new());
1573///
1574/// // In your handler:
1575/// async fn handler(ctx: &RequestContext, req: &Request) -> Response {
1576///     if let Some(request_id) = req.get_extension::<RequestId>() {
1577///         println!("Request ID: {}", request_id);
1578///     }
1579///     Response::ok()
1580/// }
1581/// ```
1582#[derive(Debug, Clone)]
1583pub struct RequestIdMiddleware {
1584    config: RequestIdConfig,
1585}
1586
1587impl Default for RequestIdMiddleware {
1588    fn default() -> Self {
1589        Self::new()
1590    }
1591}
1592
1593impl RequestIdMiddleware {
1594    /// Creates a new request ID middleware with default configuration.
1595    #[must_use]
1596    pub fn new() -> Self {
1597        Self {
1598            config: RequestIdConfig::default(),
1599        }
1600    }
1601
1602    /// Creates a new request ID middleware with the given configuration.
1603    #[must_use]
1604    pub fn with_config(config: RequestIdConfig) -> Self {
1605        Self { config }
1606    }
1607
1608    /// Extracts or generates a request ID for the given request.
1609    fn get_or_generate_id(&self, req: &Request) -> RequestId {
1610        if self.config.accept_from_client {
1611            if let Some(header_value) = req.headers().get(&self.config.header_name) {
1612                if let Ok(client_id) = std::str::from_utf8(header_value) {
1613                    // Validate length and basic content
1614                    if !client_id.is_empty()
1615                        && client_id.len() <= self.config.max_client_id_length
1616                        && is_valid_request_id(client_id)
1617                    {
1618                        return RequestId::new(client_id);
1619                    }
1620                }
1621            }
1622        }
1623        RequestId::generate()
1624    }
1625}
1626
1627/// Validates that a request ID contains only safe characters.
1628fn is_valid_request_id(id: &str) -> bool {
1629    !id.is_empty()
1630        && id
1631            .chars()
1632            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
1633}
1634
1635impl Middleware for RequestIdMiddleware {
1636    fn before<'a>(
1637        &'a self,
1638        _ctx: &'a RequestContext,
1639        req: &'a mut Request,
1640    ) -> BoxFuture<'a, ControlFlow> {
1641        let request_id = self.get_or_generate_id(req);
1642        req.insert_extension(request_id);
1643        Box::pin(async { ControlFlow::Continue })
1644    }
1645
1646    fn after<'a>(
1647        &'a self,
1648        _ctx: &'a RequestContext,
1649        req: &'a Request,
1650        response: Response,
1651    ) -> BoxFuture<'a, Response> {
1652        if !self.config.add_to_response {
1653            return Box::pin(async move { response });
1654        }
1655
1656        let request_id = req.get_extension::<RequestId>().cloned();
1657        let header_name = self.config.header_name.clone();
1658
1659        Box::pin(async move {
1660            if let Some(id) = request_id {
1661                response.header(header_name, id.0.into_bytes())
1662            } else {
1663                response
1664            }
1665        })
1666    }
1667
1668    fn name(&self) -> &'static str {
1669        "RequestId"
1670    }
1671}
1672
1673// ============================================================================
1674// Security Headers Middleware
1675// ============================================================================
1676
1677/// X-Frame-Options header value.
1678///
1679/// Controls whether the page can be displayed in a frame.
1680#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1681pub enum XFrameOptions {
1682    /// Prevents any domain from framing the content.
1683    Deny,
1684    /// Allows the current site to frame the content.
1685    SameOrigin,
1686}
1687
1688impl XFrameOptions {
1689    fn as_bytes(self) -> &'static [u8] {
1690        match self {
1691            Self::Deny => b"DENY",
1692            Self::SameOrigin => b"SAMEORIGIN",
1693        }
1694    }
1695}
1696
1697/// Referrer-Policy header value.
1698///
1699/// Controls how much referrer information should be included with requests.
1700#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1701pub enum ReferrerPolicy {
1702    /// No referrer information is sent.
1703    NoReferrer,
1704    /// Only send origin when protocol security level stays the same.
1705    NoReferrerWhenDowngrade,
1706    /// Only send the origin (not the path).
1707    Origin,
1708    /// Only send origin for cross-origin requests.
1709    OriginWhenCrossOrigin,
1710    /// Send the origin, path, and query string for same-origin requests only.
1711    SameOrigin,
1712    /// Only send origin if protocol security level stays the same.
1713    StrictOrigin,
1714    /// Send full referrer for same-origin, origin only for cross-origin if secure.
1715    StrictOriginWhenCrossOrigin,
1716    /// Send the full referrer (not recommended).
1717    UnsafeUrl,
1718}
1719
1720impl ReferrerPolicy {
1721    fn as_bytes(self) -> &'static [u8] {
1722        match self {
1723            Self::NoReferrer => b"no-referrer",
1724            Self::NoReferrerWhenDowngrade => b"no-referrer-when-downgrade",
1725            Self::Origin => b"origin",
1726            Self::OriginWhenCrossOrigin => b"origin-when-cross-origin",
1727            Self::SameOrigin => b"same-origin",
1728            Self::StrictOrigin => b"strict-origin",
1729            Self::StrictOriginWhenCrossOrigin => b"strict-origin-when-cross-origin",
1730            Self::UnsafeUrl => b"unsafe-url",
1731        }
1732    }
1733}
1734
1735/// Configuration for the Security Headers middleware.
1736///
1737/// All headers are optional. Set a value to `Some(...)` to include the header,
1738/// or `None` to skip it.
1739///
1740/// # Defaults
1741///
1742/// The default configuration provides secure defaults:
1743/// - `X-Content-Type-Options: nosniff`
1744/// - `X-Frame-Options: DENY`
1745/// - `X-XSS-Protection: 0` (disabled as modern browsers have built-in protection)
1746/// - `Referrer-Policy: strict-origin-when-cross-origin`
1747///
1748/// # Example
1749///
1750/// ```ignore
1751/// use fastapi_core::middleware::{SecurityHeadersConfig, XFrameOptions, ReferrerPolicy};
1752///
1753/// let config = SecurityHeadersConfig::default()
1754///     .x_frame_options(XFrameOptions::SameOrigin)
1755///     .content_security_policy("default-src 'self'")
1756///     .hsts(31536000, true);  // 1 year, includeSubDomains
1757/// ```
1758#[derive(Debug, Clone)]
1759pub struct SecurityHeadersConfig {
1760    /// X-Content-Type-Options header.
1761    /// Default: `Some("nosniff")`
1762    pub x_content_type_options: Option<&'static str>,
1763    /// X-Frame-Options header.
1764    /// Default: `Some(XFrameOptions::Deny)`
1765    pub x_frame_options: Option<XFrameOptions>,
1766    /// X-XSS-Protection header.
1767    /// Default: `Some("0")` (disabled - modern browsers have built-in protection)
1768    ///
1769    /// Note: This header is largely obsolete. Setting it to "0" is recommended
1770    /// to prevent potential security issues in older browsers.
1771    pub x_xss_protection: Option<&'static str>,
1772    /// Content-Security-Policy header.
1773    /// Default: `None` (should be configured based on your application)
1774    pub content_security_policy: Option<String>,
1775    /// Strict-Transport-Security (HSTS) header.
1776    /// Tuple of (max_age_seconds, include_sub_domains, preload)
1777    /// Default: `None` (only set this for HTTPS-only sites)
1778    pub hsts: Option<(u64, bool, bool)>,
1779    /// Referrer-Policy header.
1780    /// Default: `Some(ReferrerPolicy::StrictOriginWhenCrossOrigin)`
1781    pub referrer_policy: Option<ReferrerPolicy>,
1782    /// Permissions-Policy header (formerly Feature-Policy).
1783    /// Default: `None` (should be configured based on your application)
1784    pub permissions_policy: Option<String>,
1785}
1786
1787impl Default for SecurityHeadersConfig {
1788    fn default() -> Self {
1789        Self {
1790            x_content_type_options: Some("nosniff"),
1791            x_frame_options: Some(XFrameOptions::Deny),
1792            x_xss_protection: Some("0"),
1793            content_security_policy: None,
1794            hsts: None,
1795            referrer_policy: Some(ReferrerPolicy::StrictOriginWhenCrossOrigin),
1796            permissions_policy: None,
1797        }
1798    }
1799}
1800
1801impl SecurityHeadersConfig {
1802    /// Creates a new configuration with secure defaults.
1803    #[must_use]
1804    pub fn new() -> Self {
1805        Self::default()
1806    }
1807
1808    /// Creates an empty configuration (no headers).
1809    #[must_use]
1810    pub fn none() -> Self {
1811        Self {
1812            x_content_type_options: None,
1813            x_frame_options: None,
1814            x_xss_protection: None,
1815            content_security_policy: None,
1816            hsts: None,
1817            referrer_policy: None,
1818            permissions_policy: None,
1819        }
1820    }
1821
1822    /// Creates a strict configuration for high-security applications.
1823    ///
1824    /// Includes:
1825    /// - All default headers
1826    /// - HSTS with 1 year max-age and includeSubDomains
1827    /// - A basic CSP that only allows same-origin resources
1828    #[must_use]
1829    pub fn strict() -> Self {
1830        Self {
1831            x_content_type_options: Some("nosniff"),
1832            x_frame_options: Some(XFrameOptions::Deny),
1833            x_xss_protection: Some("0"),
1834            content_security_policy: Some("default-src 'self'".to_string()),
1835            hsts: Some((31536000, true, false)), // 1 year, includeSubDomains
1836            referrer_policy: Some(ReferrerPolicy::NoReferrer),
1837            permissions_policy: Some("geolocation=(), camera=(), microphone=()".to_string()),
1838        }
1839    }
1840
1841    /// Sets the X-Content-Type-Options header.
1842    #[must_use]
1843    pub fn x_content_type_options(mut self, value: Option<&'static str>) -> Self {
1844        self.x_content_type_options = value;
1845        self
1846    }
1847
1848    /// Sets the X-Frame-Options header.
1849    #[must_use]
1850    pub fn x_frame_options(mut self, value: Option<XFrameOptions>) -> Self {
1851        self.x_frame_options = value;
1852        self
1853    }
1854
1855    /// Sets the X-XSS-Protection header.
1856    #[must_use]
1857    pub fn x_xss_protection(mut self, value: Option<&'static str>) -> Self {
1858        self.x_xss_protection = value;
1859        self
1860    }
1861
1862    /// Sets the Content-Security-Policy header.
1863    #[must_use]
1864    pub fn content_security_policy(mut self, value: impl Into<String>) -> Self {
1865        self.content_security_policy = Some(value.into());
1866        self
1867    }
1868
1869    /// Clears the Content-Security-Policy header.
1870    #[must_use]
1871    pub fn no_content_security_policy(mut self) -> Self {
1872        self.content_security_policy = None;
1873        self
1874    }
1875
1876    /// Sets the Strict-Transport-Security (HSTS) header.
1877    ///
1878    /// # Arguments
1879    ///
1880    /// - `max_age`: Maximum time (in seconds) the browser should remember HTTPS
1881    /// - `include_sub_domains`: Whether to apply to all subdomains
1882    /// - `preload`: Whether to include in browser preload lists (use with caution)
1883    ///
1884    /// # Warning
1885    ///
1886    /// Only enable HSTS for sites that are HTTPS-only. Enabling HSTS incorrectly
1887    /// can make your site inaccessible.
1888    #[must_use]
1889    pub fn hsts(mut self, max_age: u64, include_sub_domains: bool, preload: bool) -> Self {
1890        self.hsts = Some((max_age, include_sub_domains, preload));
1891        self
1892    }
1893
1894    /// Clears the HSTS header.
1895    #[must_use]
1896    pub fn no_hsts(mut self) -> Self {
1897        self.hsts = None;
1898        self
1899    }
1900
1901    /// Sets the Referrer-Policy header.
1902    #[must_use]
1903    pub fn referrer_policy(mut self, value: Option<ReferrerPolicy>) -> Self {
1904        self.referrer_policy = value;
1905        self
1906    }
1907
1908    /// Sets the Permissions-Policy header.
1909    #[must_use]
1910    pub fn permissions_policy(mut self, value: impl Into<String>) -> Self {
1911        self.permissions_policy = Some(value.into());
1912        self
1913    }
1914
1915    /// Clears the Permissions-Policy header.
1916    #[must_use]
1917    pub fn no_permissions_policy(mut self) -> Self {
1918        self.permissions_policy = None;
1919        self
1920    }
1921
1922    /// Builds the HSTS header value.
1923    fn build_hsts_value(&self) -> Option<String> {
1924        self.hsts.map(|(max_age, include_sub, preload)| {
1925            let mut value = format!("max-age={}", max_age);
1926            if include_sub {
1927                value.push_str("; includeSubDomains");
1928            }
1929            if preload {
1930                value.push_str("; preload");
1931            }
1932            value
1933        })
1934    }
1935}
1936
1937/// Middleware that adds security-related HTTP headers to responses.
1938///
1939/// This middleware helps protect against common web vulnerabilities by setting
1940/// appropriate security headers. It's recommended for all web applications.
1941///
1942/// # Headers
1943///
1944/// - **X-Content-Type-Options**: Prevents MIME type sniffing
1945/// - **X-Frame-Options**: Controls iframe embedding (clickjacking protection)
1946/// - **X-XSS-Protection**: Legacy XSS filter control (disabled by default)
1947/// - **Content-Security-Policy**: Controls resource loading
1948/// - **Strict-Transport-Security**: Enforces HTTPS
1949/// - **Referrer-Policy**: Controls referrer information
1950/// - **Permissions-Policy**: Controls browser features
1951///
1952/// # Example
1953///
1954/// ```ignore
1955/// use fastapi_core::middleware::{SecurityHeaders, SecurityHeadersConfig};
1956///
1957/// // Use defaults
1958/// let mw = SecurityHeaders::new();
1959///
1960/// // Custom configuration
1961/// let config = SecurityHeadersConfig::default()
1962///     .content_security_policy("default-src 'self'; img-src *")
1963///     .hsts(86400, false, false);  // 1 day
1964///
1965/// let mw = SecurityHeaders::with_config(config);
1966/// ```
1967#[derive(Debug, Clone)]
1968pub struct SecurityHeaders {
1969    config: SecurityHeadersConfig,
1970}
1971
1972impl Default for SecurityHeaders {
1973    fn default() -> Self {
1974        Self::new()
1975    }
1976}
1977
1978impl SecurityHeaders {
1979    /// Creates a new middleware with default configuration.
1980    #[must_use]
1981    pub fn new() -> Self {
1982        Self {
1983            config: SecurityHeadersConfig::default(),
1984        }
1985    }
1986
1987    /// Creates a new middleware with custom configuration.
1988    #[must_use]
1989    pub fn with_config(config: SecurityHeadersConfig) -> Self {
1990        Self { config }
1991    }
1992
1993    /// Creates a middleware with strict security settings.
1994    #[must_use]
1995    pub fn strict() -> Self {
1996        Self {
1997            config: SecurityHeadersConfig::strict(),
1998        }
1999    }
2000}
2001
2002impl Middleware for SecurityHeaders {
2003    fn after<'a>(
2004        &'a self,
2005        _ctx: &'a RequestContext,
2006        _req: &'a Request,
2007        response: Response,
2008    ) -> BoxFuture<'a, Response> {
2009        let config = self.config.clone();
2010        Box::pin(async move {
2011            let mut resp = response;
2012
2013            // X-Content-Type-Options
2014            if let Some(value) = config.x_content_type_options {
2015                resp = resp.header("X-Content-Type-Options", value.as_bytes().to_vec());
2016            }
2017
2018            // X-Frame-Options
2019            if let Some(value) = config.x_frame_options {
2020                resp = resp.header("X-Frame-Options", value.as_bytes().to_vec());
2021            }
2022
2023            // X-XSS-Protection
2024            if let Some(value) = config.x_xss_protection {
2025                resp = resp.header("X-XSS-Protection", value.as_bytes().to_vec());
2026            }
2027
2028            // Content-Security-Policy
2029            if let Some(ref value) = config.content_security_policy {
2030                resp = resp.header("Content-Security-Policy", value.as_bytes().to_vec());
2031            }
2032
2033            // Strict-Transport-Security
2034            if let Some(ref hsts_value) = config.build_hsts_value() {
2035                resp = resp.header("Strict-Transport-Security", hsts_value.as_bytes().to_vec());
2036            }
2037
2038            // Referrer-Policy
2039            if let Some(value) = config.referrer_policy {
2040                resp = resp.header("Referrer-Policy", value.as_bytes().to_vec());
2041            }
2042
2043            // Permissions-Policy
2044            if let Some(ref value) = config.permissions_policy {
2045                resp = resp.header("Permissions-Policy", value.as_bytes().to_vec());
2046            }
2047
2048            resp
2049        })
2050    }
2051
2052    fn name(&self) -> &'static str {
2053        "SecurityHeaders"
2054    }
2055}
2056
2057// ============================================================================
2058// CSRF Protection Middleware
2059// ============================================================================
2060
2061/// CSRF token stored in request extensions.
2062///
2063/// Middleware stores this after generating or validating a token,
2064/// allowing handlers to access the current CSRF token.
2065#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2066pub struct CsrfToken(pub String);
2067
2068impl CsrfToken {
2069    /// Creates a new CSRF token with the given value.
2070    #[must_use]
2071    pub fn new(token: impl Into<String>) -> Self {
2072        Self(token.into())
2073    }
2074
2075    /// Returns the token as a string slice.
2076    #[must_use]
2077    pub fn as_str(&self) -> &str {
2078        &self.0
2079    }
2080
2081    /// Generates a new unique CSRF token using cryptographic randomness.
2082    ///
2083    /// Pulls 32 random bytes from the OS CSPRNG via `getrandom` (dispatches to
2084    /// `getrandom(2)` / `/dev/urandom` on Unix and `BCryptGenRandom` on Windows).
2085    ///
2086    /// # Panics
2087    ///
2088    /// Panics if the OS CSPRNG is unavailable. CSRF tokens MUST be
2089    /// cryptographically unpredictable - there is no safe fallback.
2090    #[must_use]
2091    pub fn generate() -> Self {
2092        let mut bytes = [0u8; 32];
2093        if let Err(err) = getrandom::fill(&mut bytes) {
2094            panic!(
2095                "FATAL: OS cryptographically secure random source is unavailable ({err}). \
2096                 CSRF token generation requires a CSPRNG. Cannot safely generate CSRF tokens \
2097                 without cryptographic entropy."
2098            );
2099        }
2100        Self(Self::bytes_to_hex(&bytes))
2101    }
2102
2103    fn bytes_to_hex(bytes: &[u8]) -> String {
2104        use std::fmt::Write;
2105        let mut s = String::with_capacity(bytes.len() * 2);
2106        for b in bytes {
2107            let _ = write!(s, "{b:02x}");
2108        }
2109        s
2110    }
2111}
2112
2113impl std::fmt::Display for CsrfToken {
2114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2115        f.write_str(&self.0)
2116    }
2117}
2118
2119impl From<&str> for CsrfToken {
2120    fn from(s: &str) -> Self {
2121        Self(s.to_string())
2122    }
2123}
2124
2125/// CSRF protection mode.
2126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2127pub enum CsrfMode {
2128    /// Double-submit cookie pattern: token in cookie must match token in header.
2129    /// This is the default and most common pattern.
2130    #[default]
2131    DoubleSubmit,
2132    /// Require token in header only (for APIs where cookies are not used).
2133    HeaderOnly,
2134}
2135
2136/// Configuration for CSRF protection middleware.
2137#[derive(Debug, Clone)]
2138pub struct CsrfConfig {
2139    /// Cookie name for CSRF token (default: "csrf_token").
2140    pub cookie_name: String,
2141    /// Header name for CSRF token (default: "x-csrf-token").
2142    pub header_name: String,
2143    /// CSRF protection mode (default: DoubleSubmit).
2144    pub mode: CsrfMode,
2145    /// Whether to rotate token on each request (default: false).
2146    pub rotate_token: bool,
2147    /// Whether in production mode (affects Secure cookie flag).
2148    pub production: bool,
2149    /// Custom error message for CSRF failures.
2150    pub error_message: Option<String>,
2151}
2152
2153impl Default for CsrfConfig {
2154    fn default() -> Self {
2155        Self {
2156            cookie_name: "csrf_token".to_string(),
2157            header_name: "x-csrf-token".to_string(),
2158            mode: CsrfMode::DoubleSubmit,
2159            rotate_token: false,
2160            production: true,
2161            error_message: None,
2162        }
2163    }
2164}
2165
2166impl CsrfConfig {
2167    /// Creates a new configuration with defaults.
2168    #[must_use]
2169    pub fn new() -> Self {
2170        Self::default()
2171    }
2172
2173    /// Sets the cookie name for CSRF token.
2174    #[must_use]
2175    pub fn cookie_name(mut self, name: impl Into<String>) -> Self {
2176        self.cookie_name = name.into();
2177        self
2178    }
2179
2180    /// Sets the header name for CSRF token.
2181    #[must_use]
2182    pub fn header_name(mut self, name: impl Into<String>) -> Self {
2183        self.header_name = name.into();
2184        self
2185    }
2186
2187    /// Sets the CSRF protection mode.
2188    #[must_use]
2189    pub fn mode(mut self, mode: CsrfMode) -> Self {
2190        self.mode = mode;
2191        self
2192    }
2193
2194    /// Enables token rotation on each request.
2195    #[must_use]
2196    pub fn rotate_token(mut self, rotate: bool) -> Self {
2197        self.rotate_token = rotate;
2198        self
2199    }
2200
2201    /// Sets production mode (affects Secure cookie flag).
2202    #[must_use]
2203    pub fn production(mut self, production: bool) -> Self {
2204        self.production = production;
2205        self
2206    }
2207
2208    /// Sets a custom error message for CSRF failures.
2209    #[must_use]
2210    pub fn error_message(mut self, message: impl Into<String>) -> Self {
2211        self.error_message = Some(message.into());
2212        self
2213    }
2214}
2215
2216/// CSRF protection middleware.
2217///
2218/// Implements protection against Cross-Site Request Forgery attacks using
2219/// the double-submit cookie pattern by default.
2220///
2221/// # How It Works
2222///
2223/// 1. For safe methods (GET, HEAD, OPTIONS, TRACE): generates a CSRF token
2224///    and sets it in a cookie if not present.
2225/// 2. For state-changing methods (POST, PUT, DELETE, PATCH): validates that
2226///    the token in the header matches the token in the cookie.
2227///
2228/// # Example
2229///
2230/// ```ignore
2231/// use fastapi_core::middleware::{CsrfMiddleware, CsrfConfig};
2232///
2233/// let mut stack = MiddlewareStack::new();
2234/// stack.push(CsrfMiddleware::new());
2235///
2236/// // Or with custom configuration:
2237/// let csrf = CsrfMiddleware::with_config(
2238///     CsrfConfig::new()
2239///         .header_name("X-XSRF-Token")
2240///         .cookie_name("XSRF-TOKEN")
2241///         .production(false)
2242/// );
2243/// stack.push(csrf);
2244/// ```
2245#[derive(Debug, Clone)]
2246pub struct CsrfMiddleware {
2247    config: CsrfConfig,
2248}
2249
2250impl Default for CsrfMiddleware {
2251    fn default() -> Self {
2252        Self::new()
2253    }
2254}
2255
2256impl CsrfMiddleware {
2257    /// Creates a new CSRF middleware with default configuration.
2258    #[must_use]
2259    pub fn new() -> Self {
2260        Self {
2261            config: CsrfConfig::default(),
2262        }
2263    }
2264
2265    /// Creates a new CSRF middleware with the given configuration.
2266    #[must_use]
2267    pub fn with_config(config: CsrfConfig) -> Self {
2268        Self { config }
2269    }
2270
2271    /// Checks if the HTTP method is safe (does not modify state).
2272    fn is_safe_method(method: crate::request::Method) -> bool {
2273        matches!(
2274            method,
2275            crate::request::Method::Get
2276                | crate::request::Method::Head
2277                | crate::request::Method::Options
2278                | crate::request::Method::Trace
2279        )
2280    }
2281
2282    /// Extracts the CSRF token from the cookie header.
2283    fn get_cookie_token(&self, req: &Request) -> Option<String> {
2284        let cookie_header = req.headers().get("cookie")?;
2285        let cookie_str = std::str::from_utf8(cookie_header).ok()?;
2286
2287        // Parse cookie header: "name1=value1; name2=value2"
2288        for part in cookie_str.split(';') {
2289            let part = part.trim();
2290            if let Some((name, value)) = part.split_once('=') {
2291                if name.trim() == self.config.cookie_name {
2292                    return Some(value.trim().to_string());
2293                }
2294            }
2295        }
2296        None
2297    }
2298
2299    /// Extracts the CSRF token from the request header.
2300    fn get_header_token(&self, req: &Request) -> Option<String> {
2301        let header_value = req.headers().get(&self.config.header_name)?;
2302        std::str::from_utf8(header_value)
2303            .ok()
2304            .map(|s| s.trim().to_string())
2305    }
2306
2307    /// Validates the CSRF token for state-changing requests.
2308    fn validate_token(&self, req: &Request) -> Result<Option<CsrfToken>, Response> {
2309        let header_token = self.get_header_token(req);
2310
2311        match self.config.mode {
2312            CsrfMode::DoubleSubmit => {
2313                let cookie_token = self.get_cookie_token(req);
2314
2315                match (header_token, cookie_token) {
2316                    (Some(header), Some(cookie))
2317                        if !header.is_empty()
2318                            && crate::password::constant_time_eq(
2319                                header.as_bytes(),
2320                                cookie.as_bytes(),
2321                            ) =>
2322                    {
2323                        Ok(Some(CsrfToken::new(header)))
2324                    }
2325                    (None, _) | (_, None) => Err(self.csrf_error_response("CSRF token missing")),
2326                    _ => Err(self.csrf_error_response("CSRF token mismatch")),
2327                }
2328            }
2329            CsrfMode::HeaderOnly => match header_token {
2330                Some(token) if !token.is_empty() => Ok(Some(CsrfToken::new(token))),
2331                _ => Err(self.csrf_error_response("CSRF token missing in header")),
2332            },
2333        }
2334    }
2335
2336    /// Creates a 403 Forbidden response for CSRF failures.
2337    fn csrf_error_response(&self, default_message: &str) -> Response {
2338        let message = self
2339            .config
2340            .error_message
2341            .as_deref()
2342            .unwrap_or(default_message);
2343
2344        // Create a FastAPI-compatible error response using serde_json
2345        // to properly escape header_name and message values.
2346        let detail = serde_json::json!({
2347            "detail": [{
2348                "type": "csrf_error",
2349                "loc": ["header", self.config.header_name],
2350                "msg": message,
2351            }]
2352        });
2353        let body = detail.to_string();
2354
2355        Response::with_status(crate::response::StatusCode::FORBIDDEN)
2356            .header("content-type", b"application/json".to_vec())
2357            .body(crate::response::ResponseBody::Bytes(body.into_bytes()))
2358    }
2359
2360    /// Creates the Set-Cookie header value for a CSRF token.
2361    fn make_set_cookie_header_value(cookie_name: &str, token: &str, production: bool) -> Vec<u8> {
2362        let mut cookie = format!("{}={}; Path=/; SameSite=Strict", cookie_name, token);
2363
2364        if production {
2365            cookie.push_str("; Secure");
2366        }
2367
2368        // Note: HttpOnly is NOT set - CSRF cookies must be readable by JavaScript
2369
2370        cookie.into_bytes()
2371    }
2372}
2373
2374impl Middleware for CsrfMiddleware {
2375    fn before<'a>(
2376        &'a self,
2377        _ctx: &'a RequestContext,
2378        req: &'a mut Request,
2379    ) -> BoxFuture<'a, ControlFlow> {
2380        Box::pin(async move {
2381            if Self::is_safe_method(req.method()) {
2382                // Safe methods: generate token if not present
2383                let existing_token = self.get_cookie_token(req);
2384                let token = existing_token
2385                    .map(CsrfToken::new)
2386                    .unwrap_or_else(CsrfToken::generate);
2387                req.insert_extension(token);
2388                ControlFlow::Continue
2389            } else {
2390                // State-changing methods: validate token
2391                match self.validate_token(req) {
2392                    Ok(Some(token)) => {
2393                        req.insert_extension(token);
2394                        ControlFlow::Continue
2395                    }
2396                    Ok(None) => ControlFlow::Continue,
2397                    Err(response) => ControlFlow::Break(response),
2398                }
2399            }
2400        })
2401    }
2402
2403    fn after<'a>(
2404        &'a self,
2405        _ctx: &'a RequestContext,
2406        req: &'a Request,
2407        response: Response,
2408    ) -> BoxFuture<'a, Response> {
2409        let config = self.config.clone();
2410        let is_safe = Self::is_safe_method(req.method());
2411        let existing_cookie_token = self.get_cookie_token(req);
2412        let token = req.get_extension::<CsrfToken>().cloned();
2413
2414        Box::pin(async move {
2415            // Set cookie for safe methods if:
2416            // 1. No cookie exists yet, or
2417            // 2. Token rotation is enabled
2418            if is_safe {
2419                let should_set_cookie = existing_cookie_token.is_none() || config.rotate_token;
2420
2421                if should_set_cookie {
2422                    if let Some(token) = token {
2423                        let cookie_value = Self::make_set_cookie_header_value(
2424                            &config.cookie_name,
2425                            token.as_str(),
2426                            config.production,
2427                        );
2428                        return response.header("set-cookie", cookie_value);
2429                    }
2430                }
2431            }
2432            response
2433        })
2434    }
2435
2436    fn name(&self) -> &'static str {
2437        "CSRF"
2438    }
2439}
2440
2441// ============================================================================
2442// Compression Middleware (requires "compression" feature)
2443// ============================================================================
2444
2445/// Configuration for response compression.
2446///
2447/// Controls when and how responses are compressed using gzip.
2448///
2449/// # Example
2450///
2451/// ```ignore
2452/// use fastapi_core::middleware::{CompressionMiddleware, CompressionConfig};
2453///
2454/// // Use defaults (min size 1024, level 6)
2455/// let mw = CompressionMiddleware::new();
2456///
2457/// // Custom configuration
2458/// let config = CompressionConfig::new()
2459///     .min_size(512)
2460///     .level(9);  // Maximum compression
2461/// let mw = CompressionMiddleware::with_config(config);
2462/// ```
2463#[cfg(feature = "compression")]
2464#[derive(Debug, Clone)]
2465pub struct CompressionConfig {
2466    /// Minimum response size in bytes to compress.
2467    /// Responses smaller than this are not compressed.
2468    /// Default: 1024 bytes (1 KB)
2469    pub min_size: usize,
2470    /// Compression level (1-9).
2471    /// 1 = fastest, 9 = best compression, 6 = balanced (default)
2472    pub level: u32,
2473    /// Content types that are already compressed and should be skipped.
2474    /// Default includes common compressed formats.
2475    pub skip_content_types: Vec<&'static str>,
2476}
2477
2478#[cfg(feature = "compression")]
2479impl Default for CompressionConfig {
2480    fn default() -> Self {
2481        Self {
2482            min_size: 1024,
2483            level: 6,
2484            skip_content_types: vec![
2485                // Images (already compressed)
2486                "image/jpeg",
2487                "image/png",
2488                "image/gif",
2489                "image/webp",
2490                "image/avif",
2491                // Video/Audio (already compressed)
2492                "video/",
2493                "audio/",
2494                // Archives (already compressed)
2495                "application/zip",
2496                "application/gzip",
2497                "application/x-gzip",
2498                "application/x-bzip2",
2499                "application/x-xz",
2500                "application/x-7z-compressed",
2501                "application/x-rar-compressed",
2502                // Other compressed formats
2503                "application/pdf",
2504                "application/woff",
2505                "application/woff2",
2506                "font/woff",
2507                "font/woff2",
2508            ],
2509        }
2510    }
2511}
2512
2513#[cfg(feature = "compression")]
2514impl CompressionConfig {
2515    /// Creates a new configuration with default values.
2516    #[must_use]
2517    pub fn new() -> Self {
2518        Self::default()
2519    }
2520
2521    /// Sets the minimum response size to compress.
2522    ///
2523    /// Responses smaller than this threshold will not be compressed,
2524    /// as compression overhead may exceed the savings.
2525    #[must_use]
2526    pub fn min_size(mut self, size: usize) -> Self {
2527        self.min_size = size;
2528        self
2529    }
2530
2531    /// Sets the compression level (1-9).
2532    ///
2533    /// - 1: Fastest compression, lowest ratio
2534    /// - 6: Balanced (default)
2535    /// - 9: Best compression ratio, slowest
2536    ///
2537    /// Values outside 1-9 are clamped.
2538    #[must_use]
2539    pub fn level(mut self, level: u32) -> Self {
2540        self.level = level.clamp(1, 9);
2541        self
2542    }
2543
2544    /// Adds a content type to skip during compression.
2545    ///
2546    /// Content types can be exact matches or prefixes (e.g., "video/" matches all video types).
2547    #[must_use]
2548    pub fn skip_content_type(mut self, content_type: &'static str) -> Self {
2549        self.skip_content_types.push(content_type);
2550        self
2551    }
2552
2553    /// Checks if the given content type should be skipped.
2554    fn should_skip_content_type(&self, content_type: &str) -> bool {
2555        let ct_lower = content_type.to_ascii_lowercase();
2556        for skip in &self.skip_content_types {
2557            if skip.ends_with('/') {
2558                // Prefix match (e.g., "video/" matches "video/mp4")
2559                if ct_lower.starts_with(*skip) {
2560                    return true;
2561                }
2562            } else {
2563                // Exact match (with optional charset)
2564                if ct_lower == *skip || ct_lower.starts_with(&format!("{skip};")) {
2565                    return true;
2566                }
2567            }
2568        }
2569        false
2570    }
2571}
2572
2573/// Middleware that compresses responses using gzip.
2574///
2575/// This middleware inspects the `Accept-Encoding` header and compresses
2576/// eligible responses with gzip. Compression is skipped for:
2577/// - Responses smaller than `min_size`
2578/// - Responses with already-compressed content types
2579/// - Responses that already have a `Content-Encoding` header
2580/// - Clients that don't accept gzip
2581///
2582/// # Example
2583///
2584/// ```ignore
2585/// use fastapi_core::middleware::{CompressionMiddleware, CompressionConfig, MiddlewareStack};
2586///
2587/// let mut stack = MiddlewareStack::new();
2588///
2589/// // Default configuration
2590/// stack.push(CompressionMiddleware::new());
2591///
2592/// // Or with custom settings
2593/// let config = CompressionConfig::new()
2594///     .min_size(256)   // Compress smaller responses
2595///     .level(9);       // Maximum compression
2596/// stack.push(CompressionMiddleware::with_config(config));
2597/// ```
2598///
2599/// # Headers
2600///
2601/// When compression is applied:
2602/// - `Content-Encoding: gzip` is added
2603/// - `Vary: Accept-Encoding` is added (for caching)
2604/// - `Content-Length` is updated to reflect compressed size
2605#[cfg(feature = "compression")]
2606#[derive(Debug, Clone)]
2607pub struct CompressionMiddleware {
2608    config: CompressionConfig,
2609}
2610
2611#[cfg(feature = "compression")]
2612impl Default for CompressionMiddleware {
2613    fn default() -> Self {
2614        Self::new()
2615    }
2616}
2617
2618#[cfg(feature = "compression")]
2619impl CompressionMiddleware {
2620    /// Creates compression middleware with default configuration.
2621    #[must_use]
2622    pub fn new() -> Self {
2623        Self {
2624            config: CompressionConfig::default(),
2625        }
2626    }
2627
2628    /// Creates compression middleware with custom configuration.
2629    #[must_use]
2630    pub fn with_config(config: CompressionConfig) -> Self {
2631        Self { config }
2632    }
2633
2634    /// Checks if the client accepts gzip encoding.
2635    fn accepts_gzip(req: &Request) -> bool {
2636        if let Some(accept_encoding) = req.headers().get("accept-encoding") {
2637            if let Ok(value) = std::str::from_utf8(accept_encoding) {
2638                // Parse Accept-Encoding header
2639                // Examples: "gzip", "gzip, deflate", "gzip;q=1.0, identity;q=0.5"
2640                for part in value.split(',') {
2641                    let encoding = part.trim().split(';').next().unwrap_or("").trim();
2642                    if encoding.eq_ignore_ascii_case("gzip") {
2643                        return true;
2644                    }
2645                    // Also accept "*" which means any encoding
2646                    if encoding == "*" {
2647                        return true;
2648                    }
2649                }
2650            }
2651        }
2652        false
2653    }
2654
2655    /// Gets the Content-Type from response headers.
2656    fn get_content_type(headers: &[(String, Vec<u8>)]) -> Option<String> {
2657        for (name, value) in headers {
2658            if name.eq_ignore_ascii_case("content-type") {
2659                return std::str::from_utf8(value).ok().map(String::from);
2660            }
2661        }
2662        None
2663    }
2664
2665    /// Checks if response already has Content-Encoding header.
2666    fn has_content_encoding(headers: &[(String, Vec<u8>)]) -> bool {
2667        headers
2668            .iter()
2669            .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"))
2670    }
2671
2672    /// Compresses data using gzip.
2673    fn compress_gzip(data: &[u8], level: u32) -> Result<Vec<u8>, std::io::Error> {
2674        use flate2::Compression;
2675        use flate2::write::GzEncoder;
2676        use std::io::Write;
2677
2678        let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level));
2679        encoder.write_all(data)?;
2680        encoder.finish()
2681    }
2682}
2683
2684#[cfg(feature = "compression")]
2685impl Middleware for CompressionMiddleware {
2686    fn after<'a>(
2687        &'a self,
2688        _ctx: &'a RequestContext,
2689        req: &'a Request,
2690        response: Response,
2691    ) -> BoxFuture<'a, Response> {
2692        let config = self.config.clone();
2693
2694        Box::pin(async move {
2695            // Check if client accepts gzip
2696            if !Self::accepts_gzip(req) {
2697                return response;
2698            }
2699
2700            // Decompose response to inspect body
2701            let (status, headers, body) = response.into_parts();
2702
2703            // Check if already compressed
2704            if Self::has_content_encoding(&headers) {
2705                return Response::with_status(status)
2706                    .body(body)
2707                    .rebuild_with_headers(headers);
2708            }
2709
2710            // Get body bytes (only compress Bytes variant, not streaming)
2711            let body_bytes = match body {
2712                crate::response::ResponseBody::Bytes(bytes) => bytes,
2713                other => {
2714                    // Can't compress Empty or Stream bodies
2715                    return Response::with_status(status)
2716                        .body(other)
2717                        .rebuild_with_headers(headers);
2718                }
2719            };
2720
2721            // Check minimum size
2722            if body_bytes.len() < config.min_size {
2723                return Response::with_status(status)
2724                    .body(crate::response::ResponseBody::Bytes(body_bytes))
2725                    .rebuild_with_headers(headers);
2726            }
2727
2728            // Check content type
2729            if let Some(content_type) = Self::get_content_type(&headers) {
2730                if config.should_skip_content_type(&content_type) {
2731                    return Response::with_status(status)
2732                        .body(crate::response::ResponseBody::Bytes(body_bytes))
2733                        .rebuild_with_headers(headers);
2734                }
2735            }
2736
2737            // Compress the body
2738            match Self::compress_gzip(&body_bytes, config.level) {
2739                Ok(compressed) => {
2740                    // Only use compressed if it's actually smaller
2741                    if compressed.len() >= body_bytes.len() {
2742                        return Response::with_status(status)
2743                            .body(crate::response::ResponseBody::Bytes(body_bytes))
2744                            .rebuild_with_headers(headers);
2745                    }
2746
2747                    // Build response with compression headers
2748                    let mut resp = Response::with_status(status)
2749                        .body(crate::response::ResponseBody::Bytes(compressed));
2750
2751                    // Copy original headers (except content-length)
2752                    for (name, value) in headers {
2753                        if !name.eq_ignore_ascii_case("content-length") {
2754                            resp = resp.header(name, value);
2755                        }
2756                    }
2757
2758                    // Add compression headers
2759                    resp = resp.header("Content-Encoding", b"gzip".to_vec());
2760                    resp = resp.header("Vary", b"Accept-Encoding".to_vec());
2761
2762                    resp
2763                }
2764                Err(_) => {
2765                    // Compression failed, return original
2766                    Response::with_status(status)
2767                        .body(crate::response::ResponseBody::Bytes(body_bytes))
2768                        .rebuild_with_headers(headers)
2769                }
2770            }
2771        })
2772    }
2773
2774    fn name(&self) -> &'static str {
2775        "Compression"
2776    }
2777}
2778
2779// ---------------------------------------------------------------------------
2780// Rate Limiting Middleware
2781// ---------------------------------------------------------------------------
2782
2783use parking_lot::Mutex;
2784use std::collections::HashMap as StdHashMap;
2785use std::time::Duration;
2786
2787/// Rate limiting algorithm.
2788#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2789pub enum RateLimitAlgorithm {
2790    /// Token bucket: steady refill rate, allows short bursts.
2791    TokenBucket,
2792    /// Fixed window: resets at the start of each interval.
2793    FixedWindow,
2794    /// Sliding window: weighted combination of current and previous window.
2795    SlidingWindow,
2796}
2797
2798/// Result of a rate limit check.
2799#[derive(Debug, Clone)]
2800pub struct RateLimitResult {
2801    /// Whether the request is allowed.
2802    pub allowed: bool,
2803    /// Maximum requests per window.
2804    pub limit: u64,
2805    /// Remaining requests in the current window.
2806    pub remaining: u64,
2807    /// Seconds until the window resets.
2808    pub reset_after_secs: u64,
2809}
2810
2811/// Extracts a rate limit key from a request.
2812///
2813/// Different extractors allow rate limiting by different criteria:
2814/// IP address, API key header, path, or custom logic.
2815pub trait KeyExtractor: Send + Sync {
2816    /// Extract the key string from the request.
2817    ///
2818    /// Returns `None` if no key can be extracted (request is not rate-limited).
2819    fn extract_key(&self, req: &Request) -> Option<String>;
2820}
2821
2822/// The remote address (peer IP) of the TCP connection.
2823///
2824/// This should be set by the HTTP server layer as a request extension to enable
2825/// secure IP-based rate limiting. Unlike `X-Forwarded-For` headers, this value
2826/// cannot be spoofed by clients.
2827///
2828/// # Example
2829///
2830/// ```ignore
2831/// // In your HTTP server code:
2832/// use fastapi_core::middleware::RemoteAddr;
2833/// use std::net::IpAddr;
2834///
2835/// // When accepting a connection:
2836/// let peer_addr: IpAddr = socket.peer_addr()?.ip();
2837/// request.insert_extension(RemoteAddr(peer_addr));
2838/// ```
2839#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2840pub struct RemoteAddr(pub std::net::IpAddr);
2841
2842impl std::fmt::Display for RemoteAddr {
2843    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2844        write!(f, "{}", self.0)
2845    }
2846}
2847
2848/// Rate limit by the actual TCP connection IP address.
2849///
2850/// This is the **secure** way to do IP-based rate limiting. It uses the
2851/// `RemoteAddr` extension set by the HTTP server, which represents the actual
2852/// TCP peer address and cannot be spoofed by clients.
2853///
2854/// # Prerequisites
2855///
2856/// Your HTTP server must set the `RemoteAddr` extension on each request:
2857///
2858/// ```ignore
2859/// request.insert_extension(RemoteAddr(peer_addr.ip()));
2860/// ```
2861///
2862/// If `RemoteAddr` is not set, this extractor returns `None` (request is not rate-limited).
2863///
2864/// # Security
2865///
2866/// This extractor is safe to use without a reverse proxy, as it relies on the
2867/// TCP connection's peer address rather than client-supplied headers.
2868#[derive(Debug, Clone)]
2869pub struct ConnectedIpKeyExtractor;
2870
2871impl KeyExtractor for ConnectedIpKeyExtractor {
2872    fn extract_key(&self, req: &Request) -> Option<String> {
2873        req.get_extension::<RemoteAddr>().map(ToString::to_string)
2874    }
2875}
2876
2877/// Rate limit by client IP address from `X-Forwarded-For` or `X-Real-IP` headers.
2878///
2879/// # Security Warning
2880///
2881/// **This extractor trusts client-supplied headers, which can be spoofed!**
2882///
2883/// Only use this extractor when:
2884/// 1. Your application runs behind a trusted reverse proxy (nginx, Cloudflare, etc.)
2885/// 2. The proxy is configured to set/override these headers
2886/// 3. Clients cannot connect directly to your application
2887///
2888/// For direct client connections, use [`ConnectedIpKeyExtractor`] instead.
2889///
2890/// # How Proxies Work
2891///
2892/// When a request passes through proxies:
2893/// - `X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip`
2894/// - The first IP is typically the original client
2895/// - Each proxy appends its own IP
2896///
2897/// This extractor takes the **first** IP from `X-Forwarded-For`, which is correct
2898/// only if your trusted proxy always sets/overwrites this header.
2899///
2900/// # Fallback Behavior
2901///
2902/// Falls back to `"unknown"` when no IP header is present, which means all such
2903/// requests share the same rate limit bucket. This may not be desirable in
2904/// production - consider using [`TrustedProxyIpKeyExtractor`] for better control.
2905#[derive(Debug, Clone)]
2906pub struct IpKeyExtractor;
2907
2908impl KeyExtractor for IpKeyExtractor {
2909    fn extract_key(&self, req: &Request) -> Option<String> {
2910        // Try X-Forwarded-For first, then X-Real-IP, then fall back
2911        if let Some(forwarded) = req.headers().get("x-forwarded-for") {
2912            if let Ok(s) = std::str::from_utf8(forwarded) {
2913                // Take the first IP (client IP) from the chain
2914                if let Some(ip) = s.split(',').next() {
2915                    return Some(ip.trim().to_string());
2916                }
2917            }
2918        }
2919        if let Some(real_ip) = req.headers().get("x-real-ip") {
2920            if let Ok(s) = std::str::from_utf8(real_ip) {
2921                return Some(s.trim().to_string());
2922            }
2923        }
2924        Some("unknown".to_string())
2925    }
2926}
2927
2928/// Rate limit by client IP with trusted proxy validation.
2929///
2930/// This is a **secure** IP extractor that only trusts `X-Forwarded-For` headers
2931/// when the immediate upstream (TCP peer) is a known trusted proxy.
2932///
2933/// # How It Works
2934///
2935/// 1. If `RemoteAddr` extension is set and matches a trusted proxy CIDR:
2936///    - Extract client IP from `X-Forwarded-For` (first IP in chain)
2937/// 2. If `RemoteAddr` is set but NOT a trusted proxy:
2938///    - Use the `RemoteAddr` directly (the client connected directly)
2939/// 3. If `RemoteAddr` is not set:
2940///    - Returns `None` (request is not rate-limited) - safer than guessing
2941///
2942/// # Example
2943///
2944/// ```ignore
2945/// use fastapi_core::middleware::{TrustedProxyIpKeyExtractor, RateLimitMiddleware};
2946///
2947/// let extractor = TrustedProxyIpKeyExtractor::new()
2948///     .trust_cidr("10.0.0.0/8")      // Internal network
2949///     .trust_cidr("172.16.0.0/12")   // Docker default
2950///     .trust_loopback();              // localhost
2951///
2952/// let rate_limiter = RateLimitMiddleware::builder()
2953///     .requests(100)
2954///     .per(Duration::from_secs(60))
2955///     .key_extractor(extractor)
2956///     .build();
2957/// ```
2958#[derive(Debug, Clone)]
2959pub struct TrustedProxyIpKeyExtractor {
2960    /// List of trusted proxy CIDRs (stored as (ip, prefix_len))
2961    trusted_cidrs: Vec<(std::net::IpAddr, u8)>,
2962}
2963
2964impl TrustedProxyIpKeyExtractor {
2965    /// Create a new trusted proxy IP extractor with no trusted proxies.
2966    #[must_use]
2967    pub fn new() -> Self {
2968        Self {
2969            trusted_cidrs: Vec::new(),
2970        }
2971    }
2972
2973    /// Add a trusted CIDR range (e.g., "10.0.0.0/8", "192.168.1.0/24").
2974    ///
2975    /// # Panics
2976    ///
2977    /// Panics if the CIDR string is invalid.
2978    #[must_use]
2979    pub fn trust_cidr(mut self, cidr: &str) -> Self {
2980        let (ip, prefix) = parse_cidr(cidr).expect("invalid CIDR notation");
2981        self.trusted_cidrs.push((ip, prefix));
2982        self
2983    }
2984
2985    /// Trust loopback addresses (127.0.0.0/8 for IPv4, ::1/128 for IPv6).
2986    #[must_use]
2987    pub fn trust_loopback(mut self) -> Self {
2988        self.trusted_cidrs.push((
2989            std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 0)),
2990            8,
2991        ));
2992        self.trusted_cidrs
2993            .push((std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), 128));
2994        self
2995    }
2996
2997    /// Check if an IP is within any trusted CIDR range.
2998    fn is_trusted(&self, ip: std::net::IpAddr) -> bool {
2999        self.trusted_cidrs
3000            .iter()
3001            .any(|(cidr_ip, prefix)| ip_in_cidr(ip, *cidr_ip, *prefix))
3002    }
3003
3004    /// Extract client IP from X-Forwarded-For header.
3005    fn extract_from_header(&self, req: &Request) -> Option<String> {
3006        if let Some(forwarded) = req.headers().get("x-forwarded-for") {
3007            if let Ok(s) = std::str::from_utf8(forwarded) {
3008                if let Some(ip) = s.split(',').next() {
3009                    return Some(ip.trim().to_string());
3010                }
3011            }
3012        }
3013        if let Some(real_ip) = req.headers().get("x-real-ip") {
3014            if let Ok(s) = std::str::from_utf8(real_ip) {
3015                return Some(s.trim().to_string());
3016            }
3017        }
3018        None
3019    }
3020}
3021
3022impl Default for TrustedProxyIpKeyExtractor {
3023    fn default() -> Self {
3024        Self::new()
3025    }
3026}
3027
3028impl KeyExtractor for TrustedProxyIpKeyExtractor {
3029    fn extract_key(&self, req: &Request) -> Option<String> {
3030        let remote = req.get_extension::<RemoteAddr>()?;
3031
3032        if self.is_trusted(remote.0) {
3033            // Request came from trusted proxy - use header value
3034            self.extract_from_header(req)
3035                .or_else(|| Some(remote.to_string()))
3036        } else {
3037            // Request came directly from client - use connection IP
3038            Some(remote.to_string())
3039        }
3040    }
3041}
3042
3043/// Parse a CIDR string like "192.168.1.0/24" into (ip, prefix_length).
3044fn parse_cidr(cidr: &str) -> Option<(std::net::IpAddr, u8)> {
3045    let (ip_str, prefix_str) = cidr.split_once('/')?;
3046    let ip: std::net::IpAddr = ip_str.parse().ok()?;
3047    let prefix: u8 = prefix_str.parse().ok()?;
3048
3049    // Validate prefix length
3050    let max_prefix = match ip {
3051        std::net::IpAddr::V4(_) => 32,
3052        std::net::IpAddr::V6(_) => 128,
3053    };
3054    if prefix > max_prefix {
3055        return None;
3056    }
3057
3058    Some((ip, prefix))
3059}
3060
3061/// Check if an IP address is within a CIDR range.
3062fn ip_in_cidr(ip: std::net::IpAddr, cidr_ip: std::net::IpAddr, prefix: u8) -> bool {
3063    match (ip, cidr_ip) {
3064        (std::net::IpAddr::V4(ip), std::net::IpAddr::V4(cidr)) => {
3065            if prefix == 0 {
3066                return true;
3067            }
3068            let ip_bits = u32::from(ip);
3069            let cidr_bits = u32::from(cidr);
3070            let mask = !0u32 << (32 - prefix);
3071            (ip_bits & mask) == (cidr_bits & mask)
3072        }
3073        (std::net::IpAddr::V6(ip), std::net::IpAddr::V6(cidr)) => {
3074            if prefix == 0 {
3075                return true;
3076            }
3077            let ip_bits = u128::from(ip);
3078            let cidr_bits = u128::from(cidr);
3079            let mask = !0u128 << (128 - prefix);
3080            (ip_bits & mask) == (cidr_bits & mask)
3081        }
3082        _ => false, // IPv4 vs IPv6 mismatch
3083    }
3084}
3085
3086/// Rate limit by a specific header value (e.g., `X-API-Key`).
3087#[derive(Debug, Clone)]
3088pub struct HeaderKeyExtractor {
3089    header_name: String,
3090}
3091
3092impl HeaderKeyExtractor {
3093    /// Create a new header key extractor.
3094    #[must_use]
3095    pub fn new(header_name: impl Into<String>) -> Self {
3096        Self {
3097            header_name: header_name.into(),
3098        }
3099    }
3100}
3101
3102impl KeyExtractor for HeaderKeyExtractor {
3103    fn extract_key(&self, req: &Request) -> Option<String> {
3104        req.headers()
3105            .get(&self.header_name)
3106            .and_then(|v| std::str::from_utf8(v).ok())
3107            .map(str::to_string)
3108    }
3109}
3110
3111/// Rate limit by request path.
3112#[derive(Debug, Clone)]
3113pub struct PathKeyExtractor;
3114
3115impl KeyExtractor for PathKeyExtractor {
3116    fn extract_key(&self, req: &Request) -> Option<String> {
3117        Some(req.path().to_string())
3118    }
3119}
3120
3121/// A composite key extractor that combines multiple extractors.
3122///
3123/// Keys from all extractors are joined with `:` to form a composite key.
3124/// If any extractor returns `None`, that part is omitted.
3125pub struct CompositeKeyExtractor {
3126    extractors: Vec<Box<dyn KeyExtractor>>,
3127}
3128
3129impl CompositeKeyExtractor {
3130    /// Create a composite key extractor from multiple extractors.
3131    #[must_use]
3132    pub fn new(extractors: Vec<Box<dyn KeyExtractor>>) -> Self {
3133        Self { extractors }
3134    }
3135}
3136
3137impl KeyExtractor for CompositeKeyExtractor {
3138    fn extract_key(&self, req: &Request) -> Option<String> {
3139        let parts: Vec<String> = self
3140            .extractors
3141            .iter()
3142            .filter_map(|e| e.extract_key(req))
3143            .collect();
3144        if parts.is_empty() {
3145            None
3146        } else {
3147            Some(parts.join(":"))
3148        }
3149    }
3150}
3151
3152/// Token bucket state for a single key.
3153#[derive(Debug, Clone)]
3154struct TokenBucketState {
3155    tokens: f64,
3156    last_refill: Instant,
3157    last_seen: Instant,
3158    stale_after: Duration,
3159}
3160
3161/// Fixed window state for a single key.
3162#[derive(Debug, Clone)]
3163struct FixedWindowState {
3164    count: u64,
3165    window_start: Instant,
3166    last_seen: Instant,
3167    stale_after: Duration,
3168}
3169
3170/// Sliding window state for a single key.
3171#[derive(Debug, Clone)]
3172struct SlidingWindowState {
3173    current_count: u64,
3174    previous_count: u64,
3175    current_window_start: Instant,
3176    last_seen: Instant,
3177    stale_after: Duration,
3178}
3179
3180/// Default maximum number of distinct keys retained by one rate-limit algorithm.
3181pub const DEFAULT_RATE_LIMIT_MAX_KEYS: usize = 65_536;
3182
3183const RATE_LIMIT_SWEEP_INTERVAL: Duration = Duration::from_secs(1);
3184
3185struct BoundedRateLimitMap<T> {
3186    entries: StdHashMap<String, T>,
3187    last_sweep: Instant,
3188}
3189
3190impl<T> BoundedRateLimitMap<T> {
3191    fn new(now: Instant) -> Self {
3192        Self {
3193            entries: StdHashMap::new(),
3194            last_sweep: now,
3195        }
3196    }
3197
3198    fn ensure_key<F, S>(
3199        &mut self,
3200        key: &str,
3201        max_keys: usize,
3202        now: Instant,
3203        mut is_stale: S,
3204        create: F,
3205    ) -> bool
3206    where
3207        F: FnOnce() -> T,
3208        S: FnMut(&T) -> bool,
3209    {
3210        if self.entries.contains_key(key) {
3211            return true;
3212        }
3213
3214        if self.entries.len() >= max_keys
3215            && now.saturating_duration_since(self.last_sweep) >= RATE_LIMIT_SWEEP_INTERVAL
3216        {
3217            self.entries.retain(|_, state| !is_stale(state));
3218            self.last_sweep = now;
3219        }
3220
3221        if self.entries.len() >= max_keys {
3222            return false;
3223        }
3224
3225        self.entries.entry(key.to_string()).or_insert_with(create);
3226        true
3227    }
3228}
3229
3230/// In-memory rate limit store.
3231///
3232/// Uses bounded `HashMap`s protected by `Mutex`es for thread-safe access.
3233/// Each algorithm retains at most `max_keys` distinct keys. When a map is full,
3234/// stale entries are reclaimed no more than once per second; if a map
3235/// remains full, unseen keys fail closed instead of evicting live buckets.
3236/// Suitable for single-process deployments. For distributed systems,
3237/// implement a custom store using Redis or similar.
3238pub struct InMemoryRateLimitStore {
3239    max_keys: usize,
3240    token_buckets: Mutex<BoundedRateLimitMap<TokenBucketState>>,
3241    fixed_windows: Mutex<BoundedRateLimitMap<FixedWindowState>>,
3242    sliding_windows: Mutex<BoundedRateLimitMap<SlidingWindowState>>,
3243}
3244
3245impl InMemoryRateLimitStore {
3246    /// Create a new in-memory store with [`DEFAULT_RATE_LIMIT_MAX_KEYS`] per algorithm.
3247    #[must_use]
3248    pub fn new() -> Self {
3249        Self::with_max_keys(DEFAULT_RATE_LIMIT_MAX_KEYS)
3250    }
3251
3252    /// Create an in-memory store with an explicit per-algorithm key bound.
3253    ///
3254    /// A bound of zero fails closed for every keyed request.
3255    #[must_use]
3256    pub fn with_max_keys(max_keys: usize) -> Self {
3257        let now = Instant::now();
3258        Self {
3259            max_keys,
3260            token_buckets: Mutex::new(BoundedRateLimitMap::new(now)),
3261            fixed_windows: Mutex::new(BoundedRateLimitMap::new(now)),
3262            sliding_windows: Mutex::new(BoundedRateLimitMap::new(now)),
3263        }
3264    }
3265
3266    fn cleanup_window(window: Duration) -> Duration {
3267        if window.is_zero() {
3268            RATE_LIMIT_SWEEP_INTERVAL
3269        } else {
3270            window
3271        }
3272    }
3273
3274    fn saturated_result(max_requests: u64, window: Duration) -> RateLimitResult {
3275        let retry_window = Self::cleanup_window(window);
3276        let reset_after_secs = retry_window
3277            .as_secs()
3278            .saturating_add(u64::from(retry_window.subsec_nanos() > 0))
3279            .max(1);
3280        RateLimitResult {
3281            allowed: false,
3282            limit: max_requests,
3283            remaining: 0,
3284            reset_after_secs,
3285        }
3286    }
3287
3288    #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
3289    fn check_token_bucket(
3290        &self,
3291        key: &str,
3292        max_tokens: u64,
3293        refill_rate: f64,
3294        window: Duration,
3295        now: Instant,
3296    ) -> RateLimitResult {
3297        let mut buckets = self.token_buckets.lock();
3298        let cleanup_window = Self::cleanup_window(window);
3299
3300        if !buckets.ensure_key(
3301            key,
3302            self.max_keys,
3303            now,
3304            |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3305            || TokenBucketState {
3306                tokens: max_tokens as f64,
3307                last_refill: now,
3308                last_seen: now,
3309                stale_after: cleanup_window,
3310            },
3311        ) {
3312            return Self::saturated_result(max_tokens, window);
3313        }
3314        let Some(state) = buckets.entries.get_mut(key) else {
3315            return Self::saturated_result(max_tokens, window);
3316        };
3317
3318        // Refill tokens based on elapsed time
3319        let elapsed = now.duration_since(state.last_refill);
3320        let refill = elapsed.as_secs_f64() * refill_rate;
3321        state.tokens = (state.tokens + refill).min(max_tokens as f64);
3322        state.last_refill = now;
3323        state.last_seen = now;
3324        state.stale_after = cleanup_window;
3325
3326        if state.tokens >= 1.0 {
3327            state.tokens -= 1.0;
3328            RateLimitResult {
3329                allowed: true,
3330                limit: max_tokens,
3331                remaining: state.tokens as u64,
3332                reset_after_secs: if state.tokens < max_tokens as f64 {
3333                    ((max_tokens as f64 - state.tokens) / refill_rate).ceil() as u64
3334                } else {
3335                    window.as_secs()
3336                },
3337            }
3338        } else {
3339            let wait_secs = ((1.0 - state.tokens) / refill_rate).ceil() as u64;
3340            RateLimitResult {
3341                allowed: false,
3342                limit: max_tokens,
3343                remaining: 0,
3344                reset_after_secs: wait_secs,
3345            }
3346        }
3347    }
3348
3349    fn check_fixed_window(
3350        &self,
3351        key: &str,
3352        max_requests: u64,
3353        window: Duration,
3354        now: Instant,
3355    ) -> RateLimitResult {
3356        let mut windows = self.fixed_windows.lock();
3357        let cleanup_window = Self::cleanup_window(window);
3358
3359        if !windows.ensure_key(
3360            key,
3361            self.max_keys,
3362            now,
3363            |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3364            || FixedWindowState {
3365                count: 0,
3366                window_start: now,
3367                last_seen: now,
3368                stale_after: cleanup_window,
3369            },
3370        ) {
3371            return Self::saturated_result(max_requests, window);
3372        }
3373        let Some(state) = windows.entries.get_mut(key) else {
3374            return Self::saturated_result(max_requests, window);
3375        };
3376
3377        // Check if window has expired
3378        let elapsed = now.duration_since(state.window_start);
3379        if elapsed >= window {
3380            state.count = 0;
3381            state.window_start = now;
3382        }
3383        state.last_seen = now;
3384        state.stale_after = cleanup_window;
3385
3386        let remaining_time = window
3387            .checked_sub(now.duration_since(state.window_start))
3388            .unwrap_or(Duration::ZERO);
3389
3390        if state.count < max_requests {
3391            state.count += 1;
3392            RateLimitResult {
3393                allowed: true,
3394                limit: max_requests,
3395                remaining: max_requests - state.count,
3396                reset_after_secs: remaining_time.as_secs(),
3397            }
3398        } else {
3399            RateLimitResult {
3400                allowed: false,
3401                limit: max_requests,
3402                remaining: 0,
3403                reset_after_secs: remaining_time.as_secs(),
3404            }
3405        }
3406    }
3407
3408    #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
3409    fn check_sliding_window(
3410        &self,
3411        key: &str,
3412        max_requests: u64,
3413        window: Duration,
3414        now: Instant,
3415    ) -> RateLimitResult {
3416        let mut windows = self.sliding_windows.lock();
3417        let cleanup_window = Self::cleanup_window(window);
3418        let stale_after = cleanup_window.saturating_add(cleanup_window);
3419
3420        if !windows.ensure_key(
3421            key,
3422            self.max_keys,
3423            now,
3424            |state| now.saturating_duration_since(state.last_seen) >= state.stale_after,
3425            || SlidingWindowState {
3426                current_count: 0,
3427                previous_count: 0,
3428                current_window_start: now,
3429                last_seen: now,
3430                stale_after,
3431            },
3432        ) {
3433            return Self::saturated_result(max_requests, window);
3434        }
3435        let Some(state) = windows.entries.get_mut(key) else {
3436            return Self::saturated_result(max_requests, window);
3437        };
3438
3439        // Check if we need to rotate windows
3440        let elapsed = now.duration_since(state.current_window_start);
3441        if elapsed >= stale_after {
3442            // After two windows, neither retained counter overlaps the live window.
3443            state.previous_count = 0;
3444            state.current_count = 0;
3445            state.current_window_start = now;
3446        } else if elapsed >= window {
3447            state.previous_count = state.current_count;
3448            state.current_count = 0;
3449            state.current_window_start = state
3450                .current_window_start
3451                .checked_add(window)
3452                .unwrap_or(now);
3453        }
3454        state.last_seen = now;
3455        state.stale_after = stale_after;
3456
3457        // Calculate weighted count using the proportion of the previous window
3458        // that overlaps with the current sliding window
3459        let window_elapsed = now.duration_since(state.current_window_start);
3460        let window_fraction = window_elapsed.as_secs_f64() / window.as_secs_f64();
3461        let previous_weight = 1.0 - window_fraction;
3462        let weighted_count =
3463            (state.previous_count as f64 * previous_weight) + state.current_count as f64;
3464
3465        let remaining_time = window.checked_sub(window_elapsed).unwrap_or(Duration::ZERO);
3466
3467        if weighted_count < max_requests as f64 {
3468            state.current_count += 1;
3469            let new_weighted =
3470                (state.previous_count as f64 * previous_weight) + state.current_count as f64;
3471            let remaining = (max_requests as f64 - new_weighted).max(0.0) as u64;
3472            RateLimitResult {
3473                allowed: true,
3474                limit: max_requests,
3475                remaining,
3476                reset_after_secs: remaining_time.as_secs(),
3477            }
3478        } else {
3479            RateLimitResult {
3480                allowed: false,
3481                limit: max_requests,
3482                remaining: 0,
3483                reset_after_secs: remaining_time.as_secs(),
3484            }
3485        }
3486    }
3487
3488    #[allow(clippy::cast_precision_loss)]
3489    fn check_at(
3490        &self,
3491        key: &str,
3492        algorithm: RateLimitAlgorithm,
3493        max_requests: u64,
3494        window: Duration,
3495        now: Instant,
3496    ) -> RateLimitResult {
3497        if window.is_zero() {
3498            return Self::saturated_result(max_requests, window);
3499        }
3500
3501        match algorithm {
3502            RateLimitAlgorithm::TokenBucket => {
3503                let refill_rate = max_requests as f64 / window.as_secs_f64();
3504                self.check_token_bucket(key, max_requests, refill_rate, window, now)
3505            }
3506            RateLimitAlgorithm::FixedWindow => {
3507                self.check_fixed_window(key, max_requests, window, now)
3508            }
3509            RateLimitAlgorithm::SlidingWindow => {
3510                self.check_sliding_window(key, max_requests, window, now)
3511            }
3512        }
3513    }
3514
3515    /// Check and consume a request against the rate limit.
3516    pub fn check(
3517        &self,
3518        key: &str,
3519        algorithm: RateLimitAlgorithm,
3520        max_requests: u64,
3521        window: Duration,
3522    ) -> RateLimitResult {
3523        self.check_at(key, algorithm, max_requests, window, Instant::now())
3524    }
3525}
3526
3527impl Default for InMemoryRateLimitStore {
3528    fn default() -> Self {
3529        Self::new()
3530    }
3531}
3532
3533/// Configuration for the rate limiting middleware.
3534///
3535/// Controls request rate limits using token bucket or sliding window algorithms.
3536/// When the limit is exceeded, a 429 Too Many Requests response is returned.
3537///
3538/// # Defaults
3539///
3540/// | Setting | Default |
3541/// |---------|---------|
3542/// | `max_requests` | 100 |
3543/// | `window` | 60s |
3544/// | `algorithm` | `TokenBucket` |
3545/// | `max_keys` | 65,536 per algorithm |
3546/// | `include_headers` | `true` |
3547/// | `retry_message` | "Rate limit exceeded. Please retry later." |
3548///
3549/// # Response Headers (when `include_headers` is `true`)
3550///
3551/// - `X-RateLimit-Limit`: Maximum requests per window
3552/// - `X-RateLimit-Remaining`: Remaining requests in current window
3553/// - `X-RateLimit-Reset`: Seconds until window resets
3554/// - `Retry-After`: Seconds to wait (only on 429 responses)
3555///
3556/// # Example
3557///
3558/// ```ignore
3559/// use fastapi_core::middleware::{RateLimitBuilder, RateLimitAlgorithm};
3560///
3561/// let rate_limit = RateLimitBuilder::new()
3562///     .max_requests(1000)
3563///     .window_secs(3600) // 1000 req/hour
3564///     .algorithm(RateLimitAlgorithm::SlidingWindow)
3565///     .build();
3566/// ```
3567#[derive(Clone)]
3568pub struct RateLimitConfig {
3569    /// Maximum number of requests allowed per window.
3570    pub max_requests: u64,
3571    /// Time window for the rate limit.
3572    pub window: Duration,
3573    /// The algorithm to use.
3574    pub algorithm: RateLimitAlgorithm,
3575    /// Maximum number of distinct keys retained by the selected algorithm.
3576    pub max_keys: usize,
3577    /// Whether to include rate limit headers in responses.
3578    pub include_headers: bool,
3579    /// Custom message for 429 responses.
3580    pub retry_message: String,
3581}
3582
3583impl Default for RateLimitConfig {
3584    fn default() -> Self {
3585        Self {
3586            max_requests: 100,
3587            window: Duration::from_secs(60),
3588            algorithm: RateLimitAlgorithm::TokenBucket,
3589            max_keys: DEFAULT_RATE_LIMIT_MAX_KEYS,
3590            include_headers: true,
3591            retry_message: "Rate limit exceeded. Please retry later.".to_string(),
3592        }
3593    }
3594}
3595
3596/// Builder for `RateLimitConfig`.
3597pub struct RateLimitBuilder {
3598    config: RateLimitConfig,
3599    key_extractor: Option<Box<dyn KeyExtractor>>,
3600}
3601
3602impl RateLimitBuilder {
3603    /// Create a new rate limit builder with default configuration.
3604    #[must_use]
3605    pub fn new() -> Self {
3606        Self {
3607            config: RateLimitConfig::default(),
3608            key_extractor: None,
3609        }
3610    }
3611
3612    /// Set the maximum number of requests per window.
3613    #[must_use]
3614    pub fn requests(mut self, max: u64) -> Self {
3615        self.config.max_requests = max;
3616        self
3617    }
3618
3619    /// Set the time window.
3620    #[must_use]
3621    pub fn per(mut self, window: Duration) -> Self {
3622        self.config.window = window;
3623        self
3624    }
3625
3626    /// Shorthand: set the window to the given number of seconds.
3627    #[must_use]
3628    pub fn per_second(self, secs: u64) -> Self {
3629        self.per(Duration::from_secs(secs))
3630    }
3631
3632    /// Shorthand: set the window to the given number of minutes.
3633    #[must_use]
3634    pub fn per_minute(self, minutes: u64) -> Self {
3635        self.per(Duration::from_secs(minutes * 60))
3636    }
3637
3638    /// Shorthand: set the window to the given number of hours.
3639    #[must_use]
3640    pub fn per_hour(self, hours: u64) -> Self {
3641        self.per(Duration::from_secs(hours * 3600))
3642    }
3643
3644    /// Set the rate limiting algorithm.
3645    #[must_use]
3646    pub fn algorithm(mut self, algo: RateLimitAlgorithm) -> Self {
3647        self.config.algorithm = algo;
3648        self
3649    }
3650
3651    /// Set the maximum number of distinct keys retained by the selected algorithm.
3652    ///
3653    /// A bound of zero fails closed for every keyed request.
3654    #[must_use]
3655    pub fn max_keys(mut self, max_keys: usize) -> Self {
3656        self.config.max_keys = max_keys;
3657        self
3658    }
3659
3660    /// Set the key extractor.
3661    #[must_use]
3662    pub fn key_extractor(mut self, extractor: impl KeyExtractor + 'static) -> Self {
3663        self.key_extractor = Some(Box::new(extractor));
3664        self
3665    }
3666
3667    /// Whether to include rate limit headers in responses.
3668    #[must_use]
3669    pub fn include_headers(mut self, include: bool) -> Self {
3670        self.config.include_headers = include;
3671        self
3672    }
3673
3674    /// Set the custom message for 429 responses.
3675    #[must_use]
3676    pub fn retry_message(mut self, msg: impl Into<String>) -> Self {
3677        self.config.retry_message = msg.into();
3678        self
3679    }
3680
3681    /// Build the rate limiting middleware.
3682    #[must_use]
3683    pub fn build(self) -> RateLimitMiddleware {
3684        let key_extractor = self
3685            .key_extractor
3686            .unwrap_or_else(|| Box::new(IpKeyExtractor));
3687        let store = Arc::new(InMemoryRateLimitStore::with_max_keys(self.config.max_keys));
3688        RateLimitMiddleware {
3689            config: self.config,
3690            store,
3691            key_extractor: Arc::from(key_extractor),
3692        }
3693    }
3694}
3695
3696impl Default for RateLimitBuilder {
3697    fn default() -> Self {
3698        Self::new()
3699    }
3700}
3701
3702/// Extension type stored on requests to carry rate limit info to `after` hook.
3703#[derive(Debug, Clone)]
3704struct RateLimitInfo {
3705    result: RateLimitResult,
3706}
3707
3708/// Rate limiting middleware.
3709///
3710/// Tracks request rates per key and returns 429 Too Many Requests
3711/// when a client exceeds the configured limit.
3712///
3713/// # Example
3714///
3715/// ```ignore
3716/// use fastapi_core::middleware::{RateLimitMiddleware, RateLimitAlgorithm, IpKeyExtractor};
3717/// use std::time::Duration;
3718///
3719/// let rate_limiter = RateLimitMiddleware::builder()
3720///     .requests(100)
3721///     .per(Duration::from_secs(60))
3722///     .algorithm(RateLimitAlgorithm::TokenBucket)
3723///     .key_extractor(IpKeyExtractor)
3724///     .build();
3725///
3726/// let app = App::builder()
3727///     .middleware(rate_limiter)
3728///     .build();
3729/// ```
3730pub struct RateLimitMiddleware {
3731    config: RateLimitConfig,
3732    store: Arc<InMemoryRateLimitStore>,
3733    key_extractor: Arc<dyn KeyExtractor>,
3734}
3735
3736impl RateLimitMiddleware {
3737    /// Create a new rate limiter with default settings (100 requests/minute, token bucket, IP-based).
3738    #[must_use]
3739    pub fn new() -> Self {
3740        Self::builder().build()
3741    }
3742
3743    /// Create a builder for configuring the rate limiter.
3744    #[must_use]
3745    pub fn builder() -> RateLimitBuilder {
3746        RateLimitBuilder::new()
3747    }
3748
3749    /// Format a 429 response body as JSON.
3750    fn too_many_requests_body(&self, result: &RateLimitResult) -> Vec<u8> {
3751        format!(
3752            r#"{{"detail":"{}","retry_after_secs":{}}}"#,
3753            self.config.retry_message, result.reset_after_secs
3754        )
3755        .into_bytes()
3756    }
3757
3758    /// Add rate limit headers to a response.
3759    fn add_headers(&self, response: Response, result: &RateLimitResult) -> Response {
3760        response
3761            .header("X-RateLimit-Limit", result.limit.to_string().into_bytes())
3762            .header(
3763                "X-RateLimit-Remaining",
3764                result.remaining.to_string().into_bytes(),
3765            )
3766            .header(
3767                "X-RateLimit-Reset",
3768                result.reset_after_secs.to_string().into_bytes(),
3769            )
3770    }
3771}
3772
3773impl Default for RateLimitMiddleware {
3774    fn default() -> Self {
3775        Self::new()
3776    }
3777}
3778
3779impl Middleware for RateLimitMiddleware {
3780    fn before<'a>(
3781        &'a self,
3782        _ctx: &'a RequestContext,
3783        req: &'a mut Request,
3784    ) -> BoxFuture<'a, ControlFlow> {
3785        Box::pin(async move {
3786            // Extract the key for this request
3787            let Some(key) = self.key_extractor.extract_key(req) else {
3788                // No key extracted — skip rate limiting for this request
3789                return ControlFlow::Continue;
3790            };
3791
3792            // Check the rate limit
3793            let result = self.store.check(
3794                &key,
3795                self.config.algorithm,
3796                self.config.max_requests,
3797                self.config.window,
3798            );
3799
3800            if result.allowed {
3801                // Store the result for the `after` hook to add headers
3802                req.insert_extension(RateLimitInfo { result });
3803                ControlFlow::Continue
3804            } else {
3805                // Return 429 Too Many Requests
3806                let body = self.too_many_requests_body(&result);
3807                let mut response =
3808                    Response::with_status(crate::response::StatusCode::TOO_MANY_REQUESTS)
3809                        .header("Content-Type", b"application/json".to_vec())
3810                        .header(
3811                            "Retry-After",
3812                            result.reset_after_secs.to_string().into_bytes(),
3813                        )
3814                        .body(crate::response::ResponseBody::Bytes(body));
3815
3816                if self.config.include_headers {
3817                    response = self.add_headers(response, &result);
3818                }
3819
3820                ControlFlow::Break(response)
3821            }
3822        })
3823    }
3824
3825    fn after<'a>(
3826        &'a self,
3827        _ctx: &'a RequestContext,
3828        req: &'a Request,
3829        response: Response,
3830    ) -> BoxFuture<'a, Response> {
3831        Box::pin(async move {
3832            if !self.config.include_headers {
3833                return response;
3834            }
3835
3836            // Retrieve the rate limit info stored in `before`
3837            if let Some(info) = req.get_extension::<RateLimitInfo>() {
3838                self.add_headers(response, &info.result)
3839            } else {
3840                response
3841            }
3842        })
3843    }
3844
3845    fn name(&self) -> &'static str {
3846        "RateLimit"
3847    }
3848}
3849
3850// ---------------------------------------------------------------------------
3851// End Rate Limiting Middleware
3852// ---------------------------------------------------------------------------
3853
3854// ============================================================================
3855// Request Inspection Middleware (Development)
3856// ============================================================================
3857
3858/// Verbosity level for the request inspection middleware.
3859///
3860/// Controls how much detail is shown in the request/response output.
3861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3862pub enum InspectionVerbosity {
3863    /// Minimal: one-line summary per request/response.
3864    ///
3865    /// Shows: `-->  GET /path` and `<--  200 OK (12ms)`
3866    Minimal,
3867
3868    /// Normal: summary plus headers.
3869    ///
3870    /// Shows method/path, all headers (filtered), and status/timing.
3871    Normal,
3872
3873    /// Verbose: summary, headers, and body preview.
3874    ///
3875    /// Shows everything in Normal plus request/response body previews
3876    /// with JSON pretty-printing when applicable.
3877    Verbose,
3878}
3879
3880/// Development middleware that logs detailed, human-readable request/response
3881/// information using arrow-style formatting.
3882///
3883/// This middleware is designed for development and debugging. It outputs
3884/// concise inspection lines showing request flow:
3885///
3886/// ```text
3887/// -->  POST /api/users
3888///      Content-Type: application/json
3889///      Content-Length: 42
3890///      {"name": "Alice"}
3891/// <--  201 Created (12ms)
3892///      Content-Type: application/json
3893///      {"id": 1, "name": "Alice"}
3894/// ```
3895///
3896/// # Features
3897///
3898/// - **Configurable verbosity**: Minimal (one-liner), Normal (+ headers),
3899///   Verbose (+ body preview with JSON pretty-printing)
3900/// - **Slow request highlighting**: Marks requests exceeding a threshold
3901/// - **Sensitive header filtering**: Redacts authorization, cookie, etc.
3902/// - **JSON pretty-printing**: Detects JSON bodies and formats them
3903/// - **Body size limits**: Truncates large bodies to a configurable max
3904///
3905/// # Example
3906///
3907/// ```ignore
3908/// use fastapi_core::middleware::RequestInspectionMiddleware;
3909///
3910/// let inspector = RequestInspectionMiddleware::new()
3911///     .verbosity(InspectionVerbosity::Verbose)
3912///     .slow_threshold_ms(500)
3913///     .max_body_preview(4096);
3914///
3915/// let mut stack = MiddlewareStack::new();
3916/// stack.push(inspector);
3917/// ```
3918pub struct RequestInspectionMiddleware {
3919    log_config: LogConfig,
3920    verbosity: InspectionVerbosity,
3921    redact_headers: HashSet<String>,
3922    slow_threshold_ms: u64,
3923    max_body_preview: usize,
3924}
3925
3926impl Default for RequestInspectionMiddleware {
3927    fn default() -> Self {
3928        Self {
3929            log_config: LogConfig::development(),
3930            verbosity: InspectionVerbosity::Normal,
3931            redact_headers: default_redacted_headers(),
3932            slow_threshold_ms: 1000,
3933            max_body_preview: 2048,
3934        }
3935    }
3936}
3937
3938impl RequestInspectionMiddleware {
3939    /// Create a new inspection middleware with development defaults.
3940    #[must_use]
3941    pub fn new() -> Self {
3942        Self::default()
3943    }
3944
3945    /// Set the logging configuration.
3946    #[must_use]
3947    pub fn log_config(mut self, config: LogConfig) -> Self {
3948        self.log_config = config;
3949        self
3950    }
3951
3952    /// Set the verbosity level.
3953    #[must_use]
3954    pub fn verbosity(mut self, level: InspectionVerbosity) -> Self {
3955        self.verbosity = level;
3956        self
3957    }
3958
3959    /// Set the threshold (in milliseconds) above which requests are flagged as slow.
3960    #[must_use]
3961    pub fn slow_threshold_ms(mut self, ms: u64) -> Self {
3962        self.slow_threshold_ms = ms;
3963        self
3964    }
3965
3966    /// Set the maximum number of bytes to show in body previews.
3967    #[must_use]
3968    pub fn max_body_preview(mut self, max: usize) -> Self {
3969        self.max_body_preview = max;
3970        self
3971    }
3972
3973    /// Add a header name to the redaction set (case-insensitive).
3974    #[must_use]
3975    pub fn redact_header(mut self, name: impl Into<String>) -> Self {
3976        self.redact_headers.insert(name.into().to_ascii_lowercase());
3977        self
3978    }
3979
3980    /// Format a request body for display, with optional JSON pretty-printing.
3981    fn format_body_preview(&self, bytes: &[u8], content_type: Option<&[u8]>) -> Option<String> {
3982        if bytes.is_empty() || self.max_body_preview == 0 {
3983            return None;
3984        }
3985
3986        let is_json = content_type
3987            .and_then(|ct| std::str::from_utf8(ct).ok())
3988            .is_some_and(|ct| ct.contains("application/json"));
3989
3990        let limit = self.max_body_preview.min(bytes.len());
3991        let truncated = bytes.len() > self.max_body_preview;
3992
3993        match std::str::from_utf8(&bytes[..limit]) {
3994            Ok(text) => {
3995                if is_json {
3996                    // Attempt JSON pretty-printing on the full available text
3997                    if let Some(pretty) = try_pretty_json(text) {
3998                        let mut output = pretty;
3999                        if truncated {
4000                            output.push_str("\n     ... (truncated)");
4001                        }
4002                        return Some(output);
4003                    }
4004                }
4005                let mut output = text.to_string();
4006                if truncated {
4007                    output.push_str("...");
4008                }
4009                Some(output)
4010            }
4011            Err(_) => Some(format!("<{} bytes binary>", bytes.len())),
4012        }
4013    }
4014
4015    /// Format a response body for display.
4016    fn format_response_preview(
4017        &self,
4018        body: &crate::response::ResponseBody,
4019        content_type: Option<&[u8]>,
4020    ) -> Option<String> {
4021        match body {
4022            crate::response::ResponseBody::Empty => None,
4023            crate::response::ResponseBody::Bytes(bytes) => {
4024                self.format_body_preview(bytes, content_type)
4025            }
4026            crate::response::ResponseBody::Stream(_) => Some("<streaming body>".to_string()),
4027        }
4028    }
4029
4030    /// Build the formatted header block for display.
4031    fn format_inspection_headers<'a>(
4032        &self,
4033        headers: impl Iterator<Item = (&'a str, &'a [u8])>,
4034    ) -> String {
4035        let mut out = String::new();
4036        for (name, value) in headers {
4037            out.push_str("\n     ");
4038            out.push_str(name);
4039            out.push_str(": ");
4040
4041            let lowered = name.to_ascii_lowercase();
4042            if self.redact_headers.contains(&lowered) {
4043                out.push_str("[REDACTED]");
4044            } else {
4045                match std::str::from_utf8(value) {
4046                    Ok(text) => out.push_str(text),
4047                    Err(_) => out.push_str("<binary>"),
4048                }
4049            }
4050        }
4051        out
4052    }
4053
4054    /// Build the response header block from (String, Vec<u8>) pairs.
4055    fn format_response_inspection_headers(&self, headers: &[(String, Vec<u8>)]) -> String {
4056        self.format_inspection_headers(
4057            headers
4058                .iter()
4059                .map(|(name, value)| (name.as_str(), value.as_slice())),
4060        )
4061    }
4062}
4063
4064/// Extension type to store request start time for the inspection middleware.
4065#[derive(Debug, Clone)]
4066struct InspectionStart(Instant);
4067
4068impl Middleware for RequestInspectionMiddleware {
4069    fn before<'a>(
4070        &'a self,
4071        ctx: &'a RequestContext,
4072        req: &'a mut Request,
4073    ) -> BoxFuture<'a, ControlFlow> {
4074        let logger = RequestLogger::new(ctx, self.log_config.clone());
4075        req.insert_extension(InspectionStart(Instant::now()));
4076
4077        let method = req.method();
4078        let path = req.path();
4079        let query = req.query();
4080
4081        // Build the request line: "-->  GET /path?query"
4082        let mut request_line = format!("-->  {method} {path}");
4083        if let Some(q) = query {
4084            request_line.push('?');
4085            request_line.push_str(q);
4086        }
4087
4088        let body_size = body_len(req.body());
4089        if body_size > 0 {
4090            request_line.push_str(&format!(" ({body_size} bytes)"));
4091        }
4092
4093        match self.verbosity {
4094            InspectionVerbosity::Minimal => {
4095                logger.info(request_line);
4096            }
4097            InspectionVerbosity::Normal => {
4098                let headers = self.format_inspection_headers(req.headers().iter());
4099                logger.info(format!("{request_line}{headers}"));
4100            }
4101            InspectionVerbosity::Verbose => {
4102                let headers = self.format_inspection_headers(req.headers().iter());
4103                let content_type = req.headers().get("content-type");
4104                let body_preview = match req.body() {
4105                    Body::Empty => None,
4106                    Body::Bytes(bytes) => self.format_body_preview(bytes, content_type),
4107                    Body::Stream { .. } => None,
4108                };
4109
4110                let mut output = format!("{request_line}{headers}");
4111                if let Some(body) = body_preview {
4112                    output.push_str("\n     ");
4113                    // Indent multi-line body previews
4114                    output.push_str(&body.replace('\n', "\n     "));
4115                }
4116                logger.info(output);
4117            }
4118        }
4119
4120        Box::pin(async { ControlFlow::Continue })
4121    }
4122
4123    fn after<'a>(
4124        &'a self,
4125        ctx: &'a RequestContext,
4126        req: &'a Request,
4127        response: Response,
4128    ) -> BoxFuture<'a, Response> {
4129        let logger = RequestLogger::new(ctx, self.log_config.clone());
4130        let duration = req
4131            .get_extension::<InspectionStart>()
4132            .map(|start| start.0.elapsed())
4133            .unwrap_or_default();
4134
4135        let status = response.status();
4136        let duration_ms = duration.as_millis();
4137
4138        // Build the response line: "<--  200 OK (12ms)"
4139        let mut response_line = format!(
4140            "<--  {} {} ({duration_ms}ms)",
4141            status.as_u16(),
4142            status.canonical_reason(),
4143        );
4144
4145        // Flag slow requests
4146        if duration_ms >= u128::from(self.slow_threshold_ms) {
4147            response_line.push_str(" [SLOW]");
4148        }
4149
4150        match self.verbosity {
4151            InspectionVerbosity::Minimal => {
4152                if duration_ms >= u128::from(self.slow_threshold_ms) {
4153                    logger.warn(response_line);
4154                } else {
4155                    logger.info(response_line);
4156                }
4157            }
4158            InspectionVerbosity::Normal => {
4159                let headers = self.format_response_inspection_headers(response.headers());
4160                let output = format!("{response_line}{headers}");
4161                if duration_ms >= u128::from(self.slow_threshold_ms) {
4162                    logger.warn(output);
4163                } else {
4164                    logger.info(output);
4165                }
4166            }
4167            InspectionVerbosity::Verbose => {
4168                let headers = self.format_response_inspection_headers(response.headers());
4169
4170                // Find content-type from response headers for JSON detection
4171                let resp_content_type: Option<&[u8]> = response
4172                    .headers()
4173                    .iter()
4174                    .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
4175                    .map(|(_, value)| value.as_slice());
4176
4177                let body_preview =
4178                    self.format_response_preview(response.body_ref(), resp_content_type);
4179
4180                let mut output = format!("{response_line}{headers}");
4181                if let Some(body) = body_preview {
4182                    output.push_str("\n     ");
4183                    output.push_str(&body.replace('\n', "\n     "));
4184                }
4185
4186                if duration_ms >= u128::from(self.slow_threshold_ms) {
4187                    logger.warn(output);
4188                } else {
4189                    logger.info(output);
4190                }
4191            }
4192        }
4193
4194        Box::pin(async move { response })
4195    }
4196
4197    fn name(&self) -> &'static str {
4198        "RequestInspection"
4199    }
4200}
4201
4202/// Attempt to parse and pretty-print a JSON string.
4203///
4204/// Returns `None` if the input is not valid JSON. Uses a minimal
4205/// recursive formatter to avoid external dependencies.
4206fn try_pretty_json(input: &str) -> Option<String> {
4207    let trimmed = input.trim();
4208    if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
4209        return None;
4210    }
4211
4212    // Validate it's actual JSON by attempting a parse, then pretty-format.
4213    let mut output = String::with_capacity(trimmed.len() * 2);
4214    if json_pretty_format(trimmed, &mut output).is_ok() {
4215        Some(output)
4216    } else {
4217        None
4218    }
4219}
4220
4221/// Minimal JSON pretty-formatter without external dependencies.
4222///
4223/// Handles objects, arrays, strings, numbers, booleans, and null.
4224/// Produces 2-space indented output.
4225fn json_pretty_format(input: &str, output: &mut String) -> Result<(), ()> {
4226    let bytes = input.as_bytes();
4227    let mut pos = 0;
4228    let mut indent: usize = 0;
4229    let mut in_string = false;
4230    let mut escape_next = false;
4231
4232    while pos < bytes.len() {
4233        let ch = bytes[pos] as char;
4234
4235        if escape_next {
4236            output.push(ch);
4237            escape_next = false;
4238            pos += 1;
4239            continue;
4240        }
4241
4242        if in_string {
4243            output.push(ch);
4244            if ch == '\\' {
4245                escape_next = true;
4246            } else if ch == '"' {
4247                in_string = false;
4248            }
4249            pos += 1;
4250            continue;
4251        }
4252
4253        match ch {
4254            '"' => {
4255                in_string = true;
4256                output.push('"');
4257            }
4258            '{' | '[' => {
4259                output.push(ch);
4260                // Peek ahead: if the next non-whitespace is the closing bracket, keep compact
4261                let peek = skip_whitespace(bytes, pos + 1);
4262                let closing = if ch == '{' { '}' } else { ']' };
4263                if peek < bytes.len() && bytes[peek] as char == closing {
4264                    output.push(closing);
4265                    pos = peek + 1;
4266                    continue;
4267                }
4268                indent += 1;
4269                output.push('\n');
4270                push_indent(output, indent);
4271            }
4272            '}' | ']' => {
4273                indent = indent.saturating_sub(1);
4274                output.push('\n');
4275                push_indent(output, indent);
4276                output.push(ch);
4277            }
4278            ':' => {
4279                output.push_str(": ");
4280            }
4281            ',' => {
4282                output.push(',');
4283                output.push('\n');
4284                push_indent(output, indent);
4285            }
4286            c if c.is_ascii_whitespace() => {
4287                // Skip whitespace outside strings
4288            }
4289            _ => {
4290                output.push(ch);
4291            }
4292        }
4293
4294        pos += 1;
4295    }
4296
4297    if in_string || indent != 0 {
4298        return Err(());
4299    }
4300
4301    Ok(())
4302}
4303
4304fn skip_whitespace(bytes: &[u8], start: usize) -> usize {
4305    let mut i = start;
4306    while i < bytes.len() && (bytes[i] as char).is_ascii_whitespace() {
4307        i += 1;
4308    }
4309    i
4310}
4311
4312fn push_indent(output: &mut String, level: usize) {
4313    for _ in 0..level {
4314        output.push_str("  ");
4315    }
4316}
4317
4318// ---------------------------------------------------------------------------
4319// End Request Inspection Middleware
4320// ---------------------------------------------------------------------------
4321
4322// ===========================================================================
4323// ETag Middleware
4324// ===========================================================================
4325
4326/// Configuration for ETag generation strategy.
4327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4328pub enum ETagMode {
4329    /// Automatically generate ETag from response body hash.
4330    /// Uses FNV-1a hash for fast, consistent ETag generation.
4331    Auto,
4332    /// Expect handler to set ETag manually. Middleware only handles
4333    /// conditional request logic (If-None-Match checking).
4334    Manual,
4335    /// Disable ETag handling entirely.
4336    Disabled,
4337}
4338
4339impl Default for ETagMode {
4340    fn default() -> Self {
4341        Self::Auto
4342    }
4343}
4344
4345/// Configuration for ETag middleware.
4346#[derive(Debug, Clone)]
4347pub struct ETagConfig {
4348    /// ETag generation mode.
4349    pub mode: ETagMode,
4350    /// Generate weak ETags (W/"...") instead of strong ETags.
4351    /// Weak ETags indicate semantic equivalence, allowing minor changes.
4352    pub weak: bool,
4353    /// Minimum response body size to generate ETag.
4354    /// Responses smaller than this won't get an ETag.
4355    pub min_size: usize,
4356}
4357
4358impl Default for ETagConfig {
4359    fn default() -> Self {
4360        Self {
4361            mode: ETagMode::Auto,
4362            weak: false,
4363            min_size: 0,
4364        }
4365    }
4366}
4367
4368impl ETagConfig {
4369    /// Create a new ETag configuration with default settings.
4370    #[must_use]
4371    pub fn new() -> Self {
4372        Self::default()
4373    }
4374
4375    /// Set the ETag generation mode.
4376    #[must_use]
4377    pub fn mode(mut self, mode: ETagMode) -> Self {
4378        self.mode = mode;
4379        self
4380    }
4381
4382    /// Enable weak ETags.
4383    #[must_use]
4384    pub fn weak(mut self, weak: bool) -> Self {
4385        self.weak = weak;
4386        self
4387    }
4388
4389    /// Set minimum body size for ETag generation.
4390    #[must_use]
4391    pub fn min_size(mut self, size: usize) -> Self {
4392        self.min_size = size;
4393        self
4394    }
4395}
4396
4397/// Middleware for ETag generation and conditional request handling.
4398///
4399/// Implements HTTP caching through ETags as defined in RFC 7232.
4400///
4401/// # Features
4402///
4403/// - **Automatic ETag generation**: Computes ETag from response body hash
4404/// - **If-None-Match handling**: Returns 304 Not Modified for GET/HEAD when ETag matches
4405/// - **Weak and strong ETags**: Configurable ETag strength
4406///
4407/// # Example
4408///
4409/// ```ignore
4410/// use fastapi_core::middleware::{ETagMiddleware, ETagConfig, ETagMode};
4411///
4412/// // Default: auto-generate strong ETags
4413/// let middleware = ETagMiddleware::new();
4414///
4415/// // With custom configuration
4416/// let middleware = ETagMiddleware::with_config(
4417///     ETagConfig::new()
4418///         .mode(ETagMode::Auto)
4419///         .weak(true)
4420///         .min_size(1024)
4421/// );
4422/// ```
4423///
4424/// # Conditional Request Flow
4425///
4426/// For GET/HEAD requests with `If-None-Match` header:
4427/// 1. Generate ETag for response body
4428/// 2. Compare with client's cached ETag
4429/// 3. If match: return 304 Not Modified (empty body)
4430/// 4. If no match: return full response with ETag header
4431///
4432/// # Note on If-Match
4433///
4434/// `If-Match` handling for PUT/PATCH/DELETE is typically done at the
4435/// application level since it requires knowledge of the current resource
4436/// state before the modification occurs.
4437pub struct ETagMiddleware {
4438    config: ETagConfig,
4439}
4440
4441impl Default for ETagMiddleware {
4442    fn default() -> Self {
4443        Self::new()
4444    }
4445}
4446
4447impl ETagMiddleware {
4448    /// Create ETag middleware with default configuration.
4449    #[must_use]
4450    pub fn new() -> Self {
4451        Self {
4452            config: ETagConfig::default(),
4453        }
4454    }
4455
4456    /// Create ETag middleware with custom configuration.
4457    #[must_use]
4458    pub fn with_config(config: ETagConfig) -> Self {
4459        Self { config }
4460    }
4461
4462    /// Generate an ETag from response body bytes using FNV-1a hash.
4463    ///
4464    /// FNV-1a is chosen for:
4465    /// - Speed: Very fast for small to medium data
4466    /// - Consistency: Deterministic output
4467    /// - Simplicity: No external dependencies
4468    fn generate_etag(data: &[u8], weak: bool) -> String {
4469        // FNV-1a 64-bit hash
4470        const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
4471        const FNV_PRIME: u64 = 0x100000001b3;
4472
4473        let mut hash = FNV_OFFSET_BASIS;
4474        for &byte in data {
4475            hash ^= u64::from(byte);
4476            hash = hash.wrapping_mul(FNV_PRIME);
4477        }
4478
4479        // Format as quoted hex string
4480        if weak {
4481            format!("W/\"{:016x}\"", hash)
4482        } else {
4483            format!("\"{:016x}\"", hash)
4484        }
4485    }
4486
4487    /// Parse ETags from If-None-Match header value.
4488    ///
4489    /// Handles:
4490    /// - Single ETag: "abc123"
4491    /// - Multiple ETags: "abc123", "def456"
4492    /// - Wildcard: *
4493    /// - Weak ETags: W/"abc123"
4494    fn parse_if_none_match(value: &str) -> Vec<String> {
4495        let trimmed = value.trim();
4496
4497        // Handle wildcard
4498        if trimmed == "*" {
4499            return vec!["*".to_string()];
4500        }
4501
4502        let mut etags = Vec::new();
4503        let mut current = String::new();
4504        let mut in_quote = false;
4505        let mut prev_char = '\0';
4506
4507        for ch in trimmed.chars() {
4508            match ch {
4509                '"' if prev_char != '\\' => {
4510                    current.push(ch);
4511                    if in_quote {
4512                        // End of ETag value
4513                        let etag = current.trim().to_string();
4514                        if !etag.is_empty() {
4515                            etags.push(etag);
4516                        }
4517                        current.clear();
4518                    }
4519                    in_quote = !in_quote;
4520                }
4521                ',' if !in_quote => {
4522                    // ETag separator, already handled by quote closing
4523                    current.clear();
4524                }
4525                _ => {
4526                    current.push(ch);
4527                }
4528            }
4529            prev_char = ch;
4530        }
4531
4532        etags
4533    }
4534
4535    /// Check if two ETags match according to weak comparison rules.
4536    ///
4537    /// Weak comparison (for If-None-Match with GET/HEAD):
4538    /// - W/"a" matches W/"a"
4539    /// - W/"a" matches "a"
4540    /// - "a" matches W/"a"
4541    /// - "a" matches "a"
4542    fn etags_match_weak(etag1: &str, etag2: &str) -> bool {
4543        // Strip W/ prefix for weak comparison
4544        let e1 = Self::strip_weak_prefix(etag1);
4545        let e2 = Self::strip_weak_prefix(etag2);
4546        e1 == e2
4547    }
4548
4549    /// Strip the weak ETag prefix (W/) if present.
4550    fn strip_weak_prefix(s: &str) -> &str {
4551        if s.starts_with("W/") || s.starts_with("w/") {
4552            &s[2..]
4553        } else {
4554            s
4555        }
4556    }
4557
4558    /// Check if request method is cacheable (GET or HEAD).
4559    fn is_cacheable_method(method: crate::request::Method) -> bool {
4560        matches!(
4561            method,
4562            crate::request::Method::Get | crate::request::Method::Head
4563        )
4564    }
4565
4566    /// Get existing ETag from response headers.
4567    fn get_existing_etag(headers: &[(String, Vec<u8>)]) -> Option<String> {
4568        for (name, value) in headers {
4569            if name.eq_ignore_ascii_case("etag") {
4570                return std::str::from_utf8(value).ok().map(String::from);
4571            }
4572        }
4573        None
4574    }
4575}
4576
4577impl Middleware for ETagMiddleware {
4578    fn after<'a>(
4579        &'a self,
4580        _ctx: &'a RequestContext,
4581        req: &'a Request,
4582        response: Response,
4583    ) -> BoxFuture<'a, Response> {
4584        let config = self.config.clone();
4585
4586        Box::pin(async move {
4587            // Skip if disabled
4588            if config.mode == ETagMode::Disabled {
4589                return response;
4590            }
4591
4592            // Only handle cacheable methods
4593            if !Self::is_cacheable_method(req.method()) {
4594                return response;
4595            }
4596
4597            // Decompose response to work with parts
4598            let (status, headers, body) = response.into_parts();
4599
4600            // Check for existing ETag (for Manual mode or pre-set ETags)
4601            let existing_etag = Self::get_existing_etag(&headers);
4602
4603            // Get body bytes if available
4604            let body_bytes = match &body {
4605                crate::response::ResponseBody::Bytes(bytes) => Some(bytes.clone()),
4606                crate::response::ResponseBody::Empty => Some(Vec::new()),
4607                crate::response::ResponseBody::Stream(_) => None,
4608            };
4609
4610            // Determine the ETag to use
4611            let etag = if let Some(existing) = existing_etag {
4612                Some(existing)
4613            } else if config.mode == ETagMode::Auto {
4614                if let Some(ref bytes) = body_bytes {
4615                    if bytes.len() >= config.min_size {
4616                        Some(Self::generate_etag(bytes, config.weak))
4617                    } else {
4618                        None
4619                    }
4620                } else {
4621                    None
4622                }
4623            } else {
4624                None
4625            };
4626
4627            // Check If-None-Match header
4628            if let Some(ref etag_value) = etag {
4629                if let Some(if_none_match) = req.headers().get("if-none-match") {
4630                    if let Ok(value) = std::str::from_utf8(if_none_match) {
4631                        let client_etags = Self::parse_if_none_match(value);
4632
4633                        // Check for wildcard or matching ETag
4634                        let matches = client_etags.iter().any(|client_etag| {
4635                            client_etag == "*" || Self::etags_match_weak(client_etag, etag_value)
4636                        });
4637
4638                        if matches {
4639                            // Return 304 Not Modified with ETag header
4640                            return Response::with_status(
4641                                crate::response::StatusCode::NOT_MODIFIED,
4642                            )
4643                            .header("etag", etag_value.as_bytes().to_vec());
4644                        }
4645                    }
4646                }
4647            }
4648
4649            // Rebuild response with ETag header if we have one
4650            let mut new_response = Response::with_status(status)
4651                .body(body)
4652                .rebuild_with_headers(headers);
4653
4654            if let Some(etag_value) = etag {
4655                new_response = new_response.header("etag", etag_value.into_bytes());
4656            }
4657
4658            new_response
4659        })
4660    }
4661
4662    fn name(&self) -> &'static str {
4663        "ETagMiddleware"
4664    }
4665}
4666
4667// ===========================================================================
4668// HTTP Cache Control Middleware
4669// ===========================================================================
4670
4671/// Individual Cache-Control directives.
4672///
4673/// These directives control how responses are cached by browsers, proxies,
4674/// and CDNs. See RFC 7234 for full specification.
4675#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4676pub enum CacheDirective {
4677    /// Response may be stored by any cache.
4678    Public,
4679    /// Response may only be stored by browser cache (not shared caches like CDNs).
4680    Private,
4681    /// Response must not be stored by any cache.
4682    NoStore,
4683    /// Cache must validate with server before using cached response.
4684    NoCache,
4685    /// Cache must not transform the response (e.g., compress images).
4686    NoTransform,
4687    /// Cached response must be revalidated once it becomes stale.
4688    MustRevalidate,
4689    /// Like must-revalidate but only for shared caches.
4690    ProxyRevalidate,
4691    /// Response may be served stale if origin is unreachable.
4692    StaleIfError,
4693    /// Response may be served stale while revalidating in background.
4694    StaleWhileRevalidate,
4695    /// Only cache if explicitly told to (for shared caches).
4696    SMaxAge,
4697    /// Do not store response in persistent storage.
4698    OnlyIfCached,
4699    /// Indicates an immutable response that won't change during its freshness lifetime.
4700    Immutable,
4701}
4702
4703impl CacheDirective {
4704    /// Returns the directive as a Cache-Control header string fragment.
4705    fn as_str(self) -> &'static str {
4706        match self {
4707            Self::Public => "public",
4708            Self::Private => "private",
4709            Self::NoStore => "no-store",
4710            Self::NoCache => "no-cache",
4711            Self::NoTransform => "no-transform",
4712            Self::MustRevalidate => "must-revalidate",
4713            Self::ProxyRevalidate => "proxy-revalidate",
4714            Self::StaleIfError => "stale-if-error",
4715            Self::StaleWhileRevalidate => "stale-while-revalidate",
4716            Self::SMaxAge => "s-maxage",
4717            Self::OnlyIfCached => "only-if-cached",
4718            Self::Immutable => "immutable",
4719        }
4720    }
4721}
4722
4723/// Builder for constructing Cache-Control header values.
4724///
4725/// Provides a fluent API for building complex cache control policies.
4726///
4727/// # Example
4728///
4729/// ```ignore
4730/// use fastapi_core::middleware::CacheControlBuilder;
4731///
4732/// // Public, cacheable for 1 hour, must revalidate after
4733/// let cache = CacheControlBuilder::new()
4734///     .public()
4735///     .max_age_secs(3600)
4736///     .must_revalidate()
4737///     .build();
4738///
4739/// // Private, no caching
4740/// let no_cache = CacheControlBuilder::new()
4741///     .private()
4742///     .no_store()
4743///     .build();
4744///
4745/// // CDN-friendly: public with different browser/CDN TTLs
4746/// let cdn = CacheControlBuilder::new()
4747///     .public()
4748///     .max_age_secs(60)        // Browser caches for 1 minute
4749///     .s_maxage_secs(3600)     // CDN caches for 1 hour
4750///     .build();
4751/// ```
4752#[derive(Debug, Clone, Default)]
4753pub struct CacheControlBuilder {
4754    directives: Vec<CacheDirective>,
4755    max_age: Option<u32>,
4756    s_maxage: Option<u32>,
4757    stale_while_revalidate: Option<u32>,
4758    stale_if_error: Option<u32>,
4759}
4760
4761impl CacheControlBuilder {
4762    /// Create a new empty Cache-Control builder.
4763    #[must_use]
4764    pub fn new() -> Self {
4765        Self::default()
4766    }
4767
4768    /// Add the `public` directive - response may be cached by any cache.
4769    #[must_use]
4770    pub fn public(mut self) -> Self {
4771        self.directives.push(CacheDirective::Public);
4772        self
4773    }
4774
4775    /// Add the `private` directive - response may only be cached by browser.
4776    #[must_use]
4777    pub fn private(mut self) -> Self {
4778        self.directives.push(CacheDirective::Private);
4779        self
4780    }
4781
4782    /// Add the `no-store` directive - response must not be cached.
4783    #[must_use]
4784    pub fn no_store(mut self) -> Self {
4785        self.directives.push(CacheDirective::NoStore);
4786        self
4787    }
4788
4789    /// Add the `no-cache` directive - must revalidate before using cache.
4790    #[must_use]
4791    pub fn no_cache(mut self) -> Self {
4792        self.directives.push(CacheDirective::NoCache);
4793        self
4794    }
4795
4796    /// Add the `no-transform` directive - caches must not modify response.
4797    #[must_use]
4798    pub fn no_transform(mut self) -> Self {
4799        self.directives.push(CacheDirective::NoTransform);
4800        self
4801    }
4802
4803    /// Add the `must-revalidate` directive - cache must check origin when stale.
4804    #[must_use]
4805    pub fn must_revalidate(mut self) -> Self {
4806        self.directives.push(CacheDirective::MustRevalidate);
4807        self
4808    }
4809
4810    /// Add the `proxy-revalidate` directive - shared caches must check origin when stale.
4811    #[must_use]
4812    pub fn proxy_revalidate(mut self) -> Self {
4813        self.directives.push(CacheDirective::ProxyRevalidate);
4814        self
4815    }
4816
4817    /// Add the `immutable` directive - response won't change during freshness lifetime.
4818    #[must_use]
4819    pub fn immutable(mut self) -> Self {
4820        self.directives.push(CacheDirective::Immutable);
4821        self
4822    }
4823
4824    /// Set `max-age` directive - maximum time response is fresh (in seconds).
4825    #[must_use]
4826    pub fn max_age_secs(mut self, seconds: u32) -> Self {
4827        self.max_age = Some(seconds);
4828        self
4829    }
4830
4831    /// Set `max-age` directive from a Duration.
4832    #[must_use]
4833    pub fn max_age(self, duration: std::time::Duration) -> Self {
4834        self.max_age_secs(duration.as_secs() as u32)
4835    }
4836
4837    /// Set `s-maxage` directive - maximum time for shared caches (in seconds).
4838    #[must_use]
4839    pub fn s_maxage_secs(mut self, seconds: u32) -> Self {
4840        self.s_maxage = Some(seconds);
4841        self
4842    }
4843
4844    /// Set `s-maxage` directive from a Duration.
4845    #[must_use]
4846    pub fn s_maxage(self, duration: std::time::Duration) -> Self {
4847        self.s_maxage_secs(duration.as_secs() as u32)
4848    }
4849
4850    /// Set `stale-while-revalidate` directive - serve stale while revalidating (in seconds).
4851    #[must_use]
4852    pub fn stale_while_revalidate_secs(mut self, seconds: u32) -> Self {
4853        self.stale_while_revalidate = Some(seconds);
4854        self
4855    }
4856
4857    /// Set `stale-if-error` directive - serve stale if origin errors (in seconds).
4858    #[must_use]
4859    pub fn stale_if_error_secs(mut self, seconds: u32) -> Self {
4860        self.stale_if_error = Some(seconds);
4861        self
4862    }
4863
4864    /// Build the Cache-Control header value string.
4865    #[must_use]
4866    pub fn build(&self) -> String {
4867        let mut parts = Vec::new();
4868
4869        // Add directives
4870        for directive in &self.directives {
4871            parts.push(directive.as_str().to_string());
4872        }
4873
4874        // Add max-age
4875        if let Some(age) = self.max_age {
4876            parts.push(format!("max-age={age}"));
4877        }
4878
4879        // Add s-maxage
4880        if let Some(age) = self.s_maxage {
4881            parts.push(format!("s-maxage={age}"));
4882        }
4883
4884        // Add stale-while-revalidate
4885        if let Some(seconds) = self.stale_while_revalidate {
4886            parts.push(format!("stale-while-revalidate={seconds}"));
4887        }
4888
4889        // Add stale-if-error
4890        if let Some(seconds) = self.stale_if_error {
4891            parts.push(format!("stale-if-error={seconds}"));
4892        }
4893
4894        parts.join(", ")
4895    }
4896
4897    /// Check if this represents a no-cache policy.
4898    #[must_use]
4899    pub fn is_no_cache(&self) -> bool {
4900        self.directives.contains(&CacheDirective::NoStore)
4901            || self.directives.contains(&CacheDirective::NoCache)
4902    }
4903}
4904
4905/// Common cache control presets for typical use cases.
4906#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4907pub enum CachePreset {
4908    /// No caching: `no-store, no-cache, must-revalidate`
4909    NoCache,
4910    /// Private caching only: `private, max-age=0, must-revalidate`
4911    PrivateNoCache,
4912    /// Standard public caching: `public, max-age=3600`
4913    PublicOneHour,
4914    /// Long-term immutable: `public, max-age=31536000, immutable`
4915    Immutable,
4916    /// CDN-friendly with short browser TTL: `public, max-age=60, s-maxage=3600`
4917    CdnFriendly,
4918    /// Static assets: `public, max-age=86400`
4919    StaticAssets,
4920}
4921
4922impl CachePreset {
4923    /// Convert preset to Cache-Control header value.
4924    #[must_use]
4925    pub fn to_header_value(&self) -> String {
4926        match self {
4927            Self::NoCache => "no-store, no-cache, must-revalidate".to_string(),
4928            Self::PrivateNoCache => "private, max-age=0, must-revalidate".to_string(),
4929            Self::PublicOneHour => "public, max-age=3600".to_string(),
4930            Self::Immutable => "public, max-age=31536000, immutable".to_string(),
4931            Self::CdnFriendly => "public, max-age=60, s-maxage=3600".to_string(),
4932            Self::StaticAssets => "public, max-age=86400".to_string(),
4933        }
4934    }
4935
4936    /// Convert preset to a CacheControlBuilder for further customization.
4937    #[must_use]
4938    pub fn to_builder(&self) -> CacheControlBuilder {
4939        match self {
4940            Self::NoCache => CacheControlBuilder::new()
4941                .no_store()
4942                .no_cache()
4943                .must_revalidate(),
4944            Self::PrivateNoCache => CacheControlBuilder::new()
4945                .private()
4946                .max_age_secs(0)
4947                .must_revalidate(),
4948            Self::PublicOneHour => CacheControlBuilder::new().public().max_age_secs(3600),
4949            Self::Immutable => CacheControlBuilder::new()
4950                .public()
4951                .max_age_secs(31536000)
4952                .immutable(),
4953            Self::CdnFriendly => CacheControlBuilder::new()
4954                .public()
4955                .max_age_secs(60)
4956                .s_maxage_secs(3600),
4957            Self::StaticAssets => CacheControlBuilder::new().public().max_age_secs(86400),
4958        }
4959    }
4960}
4961
4962/// Configuration for the Cache Control middleware.
4963#[derive(Debug, Clone)]
4964pub struct CacheControlConfig {
4965    /// The Cache-Control header value to set.
4966    pub cache_control: String,
4967    /// Optional Vary header values for content negotiation.
4968    pub vary: Vec<String>,
4969    /// Whether to set Expires header (deprecated but still used).
4970    pub set_expires: bool,
4971    /// Whether to preserve existing Cache-Control headers.
4972    pub preserve_existing: bool,
4973    /// HTTP methods to apply caching to (default: GET, HEAD).
4974    pub methods: Vec<crate::request::Method>,
4975    /// Path patterns to match (empty = match all).
4976    pub path_patterns: Vec<String>,
4977    /// Status codes to cache (default: 200-299).
4978    pub cacheable_statuses: Vec<u16>,
4979}
4980
4981impl Default for CacheControlConfig {
4982    fn default() -> Self {
4983        Self {
4984            cache_control: CachePreset::NoCache.to_header_value(),
4985            vary: Vec::new(),
4986            set_expires: false,
4987            preserve_existing: true,
4988            methods: vec![crate::request::Method::Get, crate::request::Method::Head],
4989            path_patterns: Vec::new(),
4990            cacheable_statuses: (200..300).collect(),
4991        }
4992    }
4993}
4994
4995impl CacheControlConfig {
4996    /// Create a new configuration with the default no-cache policy.
4997    #[must_use]
4998    pub fn new() -> Self {
4999        Self::default()
5000    }
5001
5002    /// Create configuration from a preset.
5003    #[must_use]
5004    pub fn from_preset(preset: CachePreset) -> Self {
5005        Self {
5006            cache_control: preset.to_header_value(),
5007            ..Self::default()
5008        }
5009    }
5010
5011    /// Create configuration from a custom builder.
5012    #[must_use]
5013    pub fn from_builder(builder: CacheControlBuilder) -> Self {
5014        Self {
5015            cache_control: builder.build(),
5016            ..Self::default()
5017        }
5018    }
5019
5020    /// Set the Cache-Control header value.
5021    #[must_use]
5022    pub fn cache_control(mut self, value: impl Into<String>) -> Self {
5023        self.cache_control = value.into();
5024        self
5025    }
5026
5027    /// Add a Vary header value (for content negotiation).
5028    #[must_use]
5029    pub fn vary(mut self, header: impl Into<String>) -> Self {
5030        self.vary.push(header.into());
5031        self
5032    }
5033
5034    /// Add multiple Vary header values.
5035    #[must_use]
5036    pub fn vary_headers(mut self, headers: Vec<String>) -> Self {
5037        self.vary.extend(headers);
5038        self
5039    }
5040
5041    /// Enable setting the Expires header.
5042    #[must_use]
5043    pub fn with_expires(mut self, enable: bool) -> Self {
5044        self.set_expires = enable;
5045        self
5046    }
5047
5048    /// Whether to preserve existing Cache-Control headers.
5049    #[must_use]
5050    pub fn preserve_existing(mut self, preserve: bool) -> Self {
5051        self.preserve_existing = preserve;
5052        self
5053    }
5054
5055    /// Set the HTTP methods to apply caching to.
5056    #[must_use]
5057    pub fn methods(mut self, methods: Vec<crate::request::Method>) -> Self {
5058        self.methods = methods;
5059        self
5060    }
5061
5062    /// Set path patterns to match (glob-style).
5063    #[must_use]
5064    pub fn path_patterns(mut self, patterns: Vec<String>) -> Self {
5065        self.path_patterns = patterns;
5066        self
5067    }
5068
5069    /// Set cacheable status codes.
5070    #[must_use]
5071    pub fn cacheable_statuses(mut self, statuses: Vec<u16>) -> Self {
5072        self.cacheable_statuses = statuses;
5073        self
5074    }
5075}
5076
5077/// Middleware for setting HTTP cache control headers.
5078///
5079/// This middleware adds Cache-Control, Vary, and optionally Expires headers
5080/// to responses. It supports various caching strategies from no-cache to
5081/// aggressive caching for static assets.
5082///
5083/// # Features
5084///
5085/// - **Cache-Control directives**: Full support for RFC 7234 directives
5086/// - **Vary header**: Content negotiation support for Accept-Encoding, Accept-Language, etc.
5087/// - **Expires header**: Optional legacy header support
5088/// - **Per-route configuration**: Apply different policies via middleware stacks
5089/// - **Method filtering**: Only cache GET/HEAD by default
5090/// - **Status filtering**: Only cache successful responses
5091///
5092/// # Example
5093///
5094/// ```ignore
5095/// use fastapi_core::middleware::{CacheControlMiddleware, CacheControlConfig, CachePreset};
5096///
5097/// // No caching for API responses (default)
5098/// let api_cache = CacheControlMiddleware::new();
5099///
5100/// // Public caching for static assets
5101/// let static_cache = CacheControlMiddleware::with_preset(CachePreset::StaticAssets);
5102///
5103/// // Custom caching with Vary header
5104/// let custom_cache = CacheControlMiddleware::with_config(
5105///     CacheControlConfig::from_preset(CachePreset::PublicOneHour)
5106///         .vary("Accept-Encoding")
5107///         .vary("Accept-Language")
5108///         .with_expires(true)
5109/// );
5110///
5111/// // CDN-friendly caching
5112/// let cdn_cache = CacheControlMiddleware::with_preset(CachePreset::CdnFriendly);
5113/// ```
5114///
5115/// # Response Headers Set
5116///
5117/// | Header | Description |
5118/// |--------|-------------|
5119/// | `Cache-Control` | Main caching directive |
5120/// | `Vary` | Headers that affect caching |
5121/// | `Expires` | Legacy expiration (if enabled) |
5122///
5123pub struct CacheControlMiddleware {
5124    config: CacheControlConfig,
5125}
5126
5127impl Default for CacheControlMiddleware {
5128    fn default() -> Self {
5129        Self::new()
5130    }
5131}
5132
5133impl CacheControlMiddleware {
5134    /// Create middleware with default no-cache policy.
5135    ///
5136    /// This is the safest default - no caching unless explicitly configured.
5137    #[must_use]
5138    pub fn new() -> Self {
5139        Self {
5140            config: CacheControlConfig::default(),
5141        }
5142    }
5143
5144    /// Create middleware with a preset caching policy.
5145    #[must_use]
5146    pub fn with_preset(preset: CachePreset) -> Self {
5147        Self {
5148            config: CacheControlConfig::from_preset(preset),
5149        }
5150    }
5151
5152    /// Create middleware with custom configuration.
5153    #[must_use]
5154    pub fn with_config(config: CacheControlConfig) -> Self {
5155        Self { config }
5156    }
5157
5158    /// Check if the request method is cacheable.
5159    fn is_cacheable_method(&self, method: crate::request::Method) -> bool {
5160        self.config.methods.contains(&method)
5161    }
5162
5163    /// Check if the response status is cacheable.
5164    fn is_cacheable_status(&self, status: u16) -> bool {
5165        self.config.cacheable_statuses.contains(&status)
5166    }
5167
5168    /// Check if the path matches any configured patterns.
5169    fn matches_path(&self, path: &str) -> bool {
5170        if self.config.path_patterns.is_empty() {
5171            return true; // Match all if no patterns configured
5172        }
5173
5174        for pattern in &self.config.path_patterns {
5175            if path_matches_pattern(path, pattern) {
5176                return true;
5177            }
5178        }
5179        false
5180    }
5181
5182    /// Check if response already has a Cache-Control header.
5183    fn has_cache_control(headers: &[(String, Vec<u8>)]) -> bool {
5184        headers
5185            .iter()
5186            .any(|(name, _)| name.eq_ignore_ascii_case("cache-control"))
5187    }
5188
5189    /// Calculate Expires date from max-age value.
5190    fn calculate_expires(cache_control: &str) -> Option<String> {
5191        // Extract max-age value if present
5192        for directive in cache_control.split(',') {
5193            let directive = directive.trim();
5194            if directive.starts_with("max-age=") {
5195                if let Ok(seconds) = directive[8..].parse::<u64>() {
5196                    // Calculate expiration time
5197                    let now = std::time::SystemTime::now();
5198                    if let Some(expires) = now.checked_add(std::time::Duration::from_secs(seconds))
5199                    {
5200                        return Some(format_http_date(expires));
5201                    }
5202                }
5203            }
5204        }
5205        None
5206    }
5207}
5208
5209/// Simple path pattern matching (supports * wildcard).
5210fn path_matches_pattern(path: &str, pattern: &str) -> bool {
5211    if pattern == "*" {
5212        return true;
5213    }
5214
5215    if pattern.contains('*') {
5216        // Simple wildcard matching
5217        let parts: Vec<&str> = pattern.split('*').collect();
5218        if parts.len() == 2 {
5219            let (prefix, suffix) = (parts[0], parts[1]);
5220            return path.starts_with(prefix) && path.ends_with(suffix);
5221        }
5222        // For more complex patterns, do a simple contains check
5223        let fixed_parts: Vec<&str> = pattern.split('*').filter(|s| !s.is_empty()).collect();
5224        let mut remaining = path;
5225        for part in fixed_parts {
5226            if let Some(pos) = remaining.find(part) {
5227                remaining = &remaining[pos + part.len()..];
5228            } else {
5229                return false;
5230            }
5231        }
5232        true
5233    } else {
5234        path == pattern
5235    }
5236}
5237
5238/// Format a SystemTime as an HTTP date (RFC 7231).
5239fn format_http_date(time: std::time::SystemTime) -> String {
5240    // Use UNIX_EPOCH to calculate duration
5241    match time.duration_since(std::time::UNIX_EPOCH) {
5242        Ok(duration) => {
5243            // Calculate date components
5244            let secs = duration.as_secs();
5245            // Days since epoch
5246            let days = secs / 86400;
5247            let remaining_secs = secs % 86400;
5248            let hours = remaining_secs / 3600;
5249            let minutes = (remaining_secs % 3600) / 60;
5250            let seconds = remaining_secs % 60;
5251
5252            // Calculate day of week (Jan 1, 1970 was Thursday = 4)
5253            let day_of_week = ((days + 4) % 7) as usize;
5254            let day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
5255
5256            // Calculate date (simplified - doesn't account for leap years perfectly but good enough)
5257            let (year, month, day) = days_to_date(days);
5258            let month_names = [
5259                "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
5260            ];
5261
5262            format!(
5263                "{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
5264                day_names[day_of_week],
5265                day,
5266                month_names[(month - 1) as usize],
5267                year,
5268                hours,
5269                minutes,
5270                seconds
5271            )
5272        }
5273        Err(_) => "Thu, 01 Jan 1970 00:00:00 GMT".to_string(),
5274    }
5275}
5276
5277/// Convert days since UNIX epoch to (year, month, day).
5278fn days_to_date(days: u64) -> (u64, u64, u64) {
5279    // Simplified algorithm - works for dates 1970-2099
5280    let mut remaining_days = days;
5281    let mut year = 1970u64;
5282
5283    loop {
5284        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
5285        if remaining_days < days_in_year {
5286            break;
5287        }
5288        remaining_days -= days_in_year;
5289        year += 1;
5290    }
5291
5292    let leap = is_leap_year(year);
5293    let month_days: [u64; 12] = if leap {
5294        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5295    } else {
5296        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5297    };
5298
5299    let mut month = 1u64;
5300    for &days_in_month in &month_days {
5301        if remaining_days < days_in_month {
5302            break;
5303        }
5304        remaining_days -= days_in_month;
5305        month += 1;
5306    }
5307
5308    (year, month, remaining_days + 1)
5309}
5310
5311/// Check if a year is a leap year.
5312fn is_leap_year(year: u64) -> bool {
5313    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
5314}
5315
5316impl Middleware for CacheControlMiddleware {
5317    fn after<'a>(
5318        &'a self,
5319        _ctx: &'a RequestContext,
5320        req: &'a Request,
5321        response: Response,
5322    ) -> BoxFuture<'a, Response> {
5323        let config = self.config.clone();
5324
5325        Box::pin(async move {
5326            // Check if this request/response is cacheable
5327            if !self.is_cacheable_method(req.method()) {
5328                return response;
5329            }
5330
5331            if !self.is_cacheable_status(response.status().as_u16()) {
5332                return response;
5333            }
5334
5335            if !self.matches_path(req.path()) {
5336                return response;
5337            }
5338
5339            // Decompose response to modify headers
5340            let (status, mut headers, body) = response.into_parts();
5341
5342            // Check for existing Cache-Control header
5343            if config.preserve_existing && Self::has_cache_control(&headers) {
5344                // Reconstruct and return unchanged
5345                let mut resp = Response::with_status(status);
5346                for (name, value) in headers {
5347                    resp = resp.header(name, value);
5348                }
5349                return resp.body(body);
5350            }
5351
5352            // Add Cache-Control header
5353            headers.push((
5354                "Cache-Control".to_string(),
5355                config.cache_control.as_bytes().to_vec(),
5356            ));
5357
5358            // Add Vary header if configured
5359            if !config.vary.is_empty() {
5360                let vary_value = config.vary.join(", ");
5361                headers.push(("Vary".to_string(), vary_value.into_bytes()));
5362            }
5363
5364            // Add Expires header if configured
5365            if config.set_expires {
5366                if let Some(expires) = Self::calculate_expires(&config.cache_control) {
5367                    headers.push(("Expires".to_string(), expires.into_bytes()));
5368                }
5369            }
5370
5371            // Reconstruct response
5372            let mut resp = Response::with_status(status);
5373            for (name, value) in headers {
5374                resp = resp.header(name, value);
5375            }
5376            resp.body(body)
5377        })
5378    }
5379
5380    fn name(&self) -> &'static str {
5381        "CacheControlMiddleware"
5382    }
5383}
5384
5385// ===========================================================================
5386// End Cache Control Middleware
5387// ===========================================================================
5388
5389// ===========================================================================
5390// TRACE Method Rejection Middleware (Security)
5391// ===========================================================================
5392
5393/// Middleware that rejects HTTP TRACE requests to prevent Cross-Site Tracing (XST) attacks.
5394///
5395/// The HTTP TRACE method echoes the request back to the client, which can be exploited
5396/// in XSS attacks to steal sensitive headers like Authorization or cookies.
5397///
5398/// # Security Rationale
5399///
5400/// - TRACE can expose Authorization headers via XSS attacks
5401/// - No legitimate use case in modern APIs
5402/// - OWASP recommends disabling TRACE
5403///
5404/// # Example
5405///
5406/// ```ignore
5407/// use fastapi_core::middleware::TraceRejectionMiddleware;
5408///
5409/// let app = App::builder()
5410///     .middleware(TraceRejectionMiddleware::new())
5411///     .build();
5412/// ```
5413///
5414/// # Behavior
5415///
5416/// - Returns 405 Method Not Allowed for all TRACE requests
5417/// - Logs TRACE attempts as security events (when log_attempts is true)
5418/// - Cannot be disabled per-route (intentionally)
5419#[derive(Debug, Clone)]
5420pub struct TraceRejectionMiddleware {
5421    /// Whether to log TRACE attempts as security events.
5422    log_attempts: bool,
5423}
5424
5425impl Default for TraceRejectionMiddleware {
5426    fn default() -> Self {
5427        Self::new()
5428    }
5429}
5430
5431impl TraceRejectionMiddleware {
5432    /// Create a new TRACE rejection middleware with default settings.
5433    ///
5434    /// By default, logging of TRACE attempts is enabled.
5435    #[must_use]
5436    pub fn new() -> Self {
5437        Self { log_attempts: true }
5438    }
5439
5440    /// Configure whether to log TRACE attempts.
5441    ///
5442    /// When enabled, each TRACE request is logged as a security event
5443    /// including the remote IP (if available) and request path.
5444    #[must_use]
5445    pub fn log_attempts(mut self, log: bool) -> Self {
5446        self.log_attempts = log;
5447        self
5448    }
5449
5450    /// Create a response for rejected TRACE requests.
5451    fn rejection_response(path: &str) -> Response {
5452        let body = format!(
5453            r#"{{"detail":"HTTP TRACE method is not allowed","path":"{}"}}"#,
5454            path.replace('"', "\\\"")
5455        );
5456        Response::with_status(crate::response::StatusCode::METHOD_NOT_ALLOWED)
5457            .header("Content-Type", b"application/json".to_vec())
5458            .header(
5459                "Allow",
5460                b"GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD".to_vec(),
5461            )
5462            .body(crate::response::ResponseBody::Bytes(body.into_bytes()))
5463    }
5464}
5465
5466impl Middleware for TraceRejectionMiddleware {
5467    fn before<'a>(
5468        &'a self,
5469        _ctx: &'a RequestContext,
5470        req: &'a mut Request,
5471    ) -> BoxFuture<'a, ControlFlow> {
5472        Box::pin(async move {
5473            if req.method() == crate::request::Method::Trace {
5474                if self.log_attempts {
5475                    // Log as security event
5476                    let path = req.path();
5477                    let remote_ip = req
5478                        .headers()
5479                        .get("X-Forwarded-For")
5480                        .or_else(|| req.headers().get("X-Real-IP"))
5481                        .map(|v| String::from_utf8_lossy(v).to_string())
5482                        .unwrap_or_else(|| "unknown".to_string());
5483
5484                    eprintln!(
5485                        "[SECURITY] TRACE request blocked: path={}, remote_ip={}",
5486                        path, remote_ip
5487                    );
5488                }
5489
5490                return ControlFlow::Break(Self::rejection_response(req.path()));
5491            }
5492
5493            ControlFlow::Continue
5494        })
5495    }
5496
5497    fn name(&self) -> &'static str {
5498        "TraceRejection"
5499    }
5500}
5501
5502// ===========================================================================
5503// End TRACE Rejection Middleware
5504// ===========================================================================
5505
5506// ===========================================================================
5507// HTTPS Redirect and HSTS Middleware (Security)
5508// ===========================================================================
5509
5510/// Configuration for HTTPS redirect behavior.
5511#[derive(Debug, Clone)]
5512#[allow(clippy::struct_excessive_bools)]
5513pub struct HttpsRedirectConfig {
5514    /// Enable HTTP to HTTPS redirects.
5515    pub redirect_enabled: bool,
5516    /// Use permanent (301) or temporary (307) redirects.
5517    pub permanent_redirect: bool,
5518    /// HSTS max-age in seconds (0 = disabled).
5519    pub hsts_max_age_secs: u64,
5520    /// Include subdomains in HSTS.
5521    pub hsts_include_subdomains: bool,
5522    /// Enable HSTS preload.
5523    pub hsts_preload: bool,
5524    /// Paths to exclude from redirect (e.g., health checks).
5525    pub exclude_paths: Vec<String>,
5526    /// Port for HTTPS (default 443).
5527    pub https_port: u16,
5528}
5529
5530impl Default for HttpsRedirectConfig {
5531    fn default() -> Self {
5532        Self {
5533            redirect_enabled: true,
5534            permanent_redirect: true,      // 301
5535            hsts_max_age_secs: 31_536_000, // 1 year
5536            hsts_include_subdomains: false,
5537            hsts_preload: false,
5538            exclude_paths: Vec::new(),
5539            https_port: 443,
5540        }
5541    }
5542}
5543
5544/// Middleware that redirects HTTP requests to HTTPS and sets HSTS headers.
5545///
5546/// This middleware provides two critical security features:
5547///
5548/// 1. **HTTP to HTTPS Redirect**: Automatically redirects insecure HTTP requests
5549///    to their HTTPS equivalents, ensuring all traffic is encrypted.
5550///
5551/// 2. **HSTS (Strict Transport Security)**: Adds the `Strict-Transport-Security`
5552///    header to HTTPS responses, instructing browsers to always use HTTPS.
5553///
5554/// # Proxy Awareness
5555///
5556/// The middleware respects the `X-Forwarded-Proto` header, so it works correctly
5557/// behind reverse proxies like nginx or HAProxy. If the proxy sets this header
5558/// to "https", the request is treated as secure.
5559///
5560/// # Example
5561///
5562/// ```ignore
5563/// use fastapi_core::middleware::HttpsRedirectMiddleware;
5564///
5565/// let app = App::builder()
5566///     .middleware(HttpsRedirectMiddleware::new()
5567///         .hsts_max_age_secs(31536000)  // 1 year
5568///         .include_subdomains(true)
5569///         .preload(true)
5570///         .exclude_path("/health")
5571///         .exclude_path("/readiness"))
5572///     .build();
5573/// ```
5574///
5575/// # Configuration Options
5576///
5577/// - `redirect_enabled`: Enable/disable redirects (default: true)
5578/// - `permanent_redirect`: Use 301 (true) or 307 (false) redirects
5579/// - `hsts_max_age_secs`: HSTS max-age value in seconds
5580/// - `include_subdomains`: Apply HSTS to all subdomains
5581/// - `preload`: Mark site for HSTS preload list
5582/// - `exclude_path`: Paths that should remain accessible over HTTP
5583#[derive(Debug, Clone)]
5584pub struct HttpsRedirectMiddleware {
5585    config: HttpsRedirectConfig,
5586}
5587
5588impl Default for HttpsRedirectMiddleware {
5589    fn default() -> Self {
5590        Self::new()
5591    }
5592}
5593
5594impl HttpsRedirectMiddleware {
5595    /// Create a new HTTPS redirect middleware with default settings.
5596    #[must_use]
5597    pub fn new() -> Self {
5598        Self {
5599            config: HttpsRedirectConfig::default(),
5600        }
5601    }
5602
5603    /// Enable or disable HTTP to HTTPS redirects.
5604    #[must_use]
5605    pub fn redirect_enabled(mut self, enabled: bool) -> Self {
5606        self.config.redirect_enabled = enabled;
5607        self
5608    }
5609
5610    /// Use permanent (301) redirects instead of temporary (307).
5611    ///
5612    /// Default is true (permanent redirects).
5613    #[must_use]
5614    pub fn permanent_redirect(mut self, permanent: bool) -> Self {
5615        self.config.permanent_redirect = permanent;
5616        self
5617    }
5618
5619    /// Set the HSTS max-age in seconds.
5620    ///
5621    /// Set to 0 to disable HSTS header.
5622    /// Default is 31536000 (1 year).
5623    #[must_use]
5624    pub fn hsts_max_age_secs(mut self, secs: u64) -> Self {
5625        self.config.hsts_max_age_secs = secs;
5626        self
5627    }
5628
5629    /// Include subdomains in HSTS policy.
5630    #[must_use]
5631    pub fn include_subdomains(mut self, include: bool) -> Self {
5632        self.config.hsts_include_subdomains = include;
5633        self
5634    }
5635
5636    /// Enable HSTS preload.
5637    ///
5638    /// Only enable this if you're ready to submit your site to the
5639    /// HSTS preload list at hstspreload.org.
5640    #[must_use]
5641    pub fn preload(mut self, preload: bool) -> Self {
5642        self.config.hsts_preload = preload;
5643        self
5644    }
5645
5646    /// Add a path to exclude from redirects.
5647    ///
5648    /// Use this for health check endpoints that need to remain
5649    /// accessible over HTTP for load balancer probes.
5650    #[must_use]
5651    pub fn exclude_path(mut self, path: impl Into<String>) -> Self {
5652        self.config.exclude_paths.push(path.into());
5653        self
5654    }
5655
5656    /// Set multiple excluded paths at once.
5657    #[must_use]
5658    pub fn exclude_paths(mut self, paths: Vec<String>) -> Self {
5659        self.config.exclude_paths = paths;
5660        self
5661    }
5662
5663    /// Set the HTTPS port (default 443).
5664    #[must_use]
5665    pub fn https_port(mut self, port: u16) -> Self {
5666        self.config.https_port = port;
5667        self
5668    }
5669
5670    /// Check if the request is using HTTPS.
5671    ///
5672    /// This checks both the scheme and the X-Forwarded-Proto header
5673    /// for proxy-aware detection.
5674    fn is_secure(&self, req: &Request) -> bool {
5675        fn trim_ascii(mut bytes: &[u8]) -> &[u8] {
5676            while matches!(bytes.first(), Some(b' ' | b'\t')) {
5677                bytes = &bytes[1..];
5678            }
5679            while matches!(bytes.last(), Some(b' ' | b'\t')) {
5680                bytes = &bytes[..bytes.len() - 1];
5681            }
5682            bytes
5683        }
5684
5685        if let Some(info) = req.get_extension::<crate::request::ConnectionInfo>() {
5686            if info.is_tls {
5687                return true;
5688            }
5689        }
5690
5691        // RFC 7239 Forwarded: for=...;proto=https;host=...
5692        if let Some(forwarded) = req.headers().get("Forwarded") {
5693            if let Ok(s) = std::str::from_utf8(forwarded) {
5694                for entry in s.split(',') {
5695                    for param in entry.split(';') {
5696                        let param = param.trim();
5697                        if let Some((k, v)) = param.split_once('=') {
5698                            if k.trim().eq_ignore_ascii_case("proto") {
5699                                let proto = v.trim().trim_matches('"');
5700                                if proto.eq_ignore_ascii_case("https") {
5701                                    return true;
5702                                }
5703                            }
5704                        }
5705                    }
5706                }
5707            }
5708        }
5709
5710        // Check X-Forwarded-Proto header first (for reverse proxy)
5711        if let Some(proto) = req.headers().get("X-Forwarded-Proto") {
5712            let first = proto.split(|&b| b == b',').next().unwrap_or(proto);
5713            return trim_ascii(first).eq_ignore_ascii_case(b"https");
5714        }
5715
5716        // Check X-Forwarded-Ssl header (alternative)
5717        if let Some(ssl) = req.headers().get("X-Forwarded-Ssl") {
5718            return ssl.eq_ignore_ascii_case(b"on");
5719        }
5720
5721        // Check Front-End-Https header (Microsoft IIS)
5722        if let Some(https) = req.headers().get("Front-End-Https") {
5723            return https.eq_ignore_ascii_case(b"on");
5724        }
5725
5726        false
5727    }
5728
5729    /// Check if a path should be excluded from redirects.
5730    fn is_excluded(&self, path: &str) -> bool {
5731        self.config
5732            .exclude_paths
5733            .iter()
5734            .any(|p| path.starts_with(p))
5735    }
5736
5737    /// Build the HSTS header value.
5738    fn build_hsts_header(&self) -> Option<Vec<u8>> {
5739        if self.config.hsts_max_age_secs == 0 {
5740            return None;
5741        }
5742
5743        let mut value = format!("max-age={}", self.config.hsts_max_age_secs);
5744
5745        if self.config.hsts_include_subdomains {
5746            value.push_str("; includeSubDomains");
5747        }
5748
5749        if self.config.hsts_preload {
5750            value.push_str("; preload");
5751        }
5752
5753        Some(value.into_bytes())
5754    }
5755
5756    /// Build the redirect URL.
5757    fn build_redirect_url(&self, req: &Request) -> String {
5758        let host = req
5759            .headers()
5760            .get("Host")
5761            .map(|h| String::from_utf8_lossy(h).to_string())
5762            .unwrap_or_else(|| "localhost".to_string());
5763
5764        // Remove port from host if present
5765        let host_without_port = host.split(':').next().unwrap_or(&host);
5766
5767        let path = req.path();
5768        let query = req.query();
5769
5770        if self.config.https_port == 443 {
5771            match query {
5772                Some(q) => format!("https://{}{}?{}", host_without_port, path, q),
5773                None => format!("https://{}{}", host_without_port, path),
5774            }
5775        } else {
5776            match query {
5777                Some(q) => format!(
5778                    "https://{}:{}{}?{}",
5779                    host_without_port, self.config.https_port, path, q
5780                ),
5781                None => format!(
5782                    "https://{}:{}{}",
5783                    host_without_port, self.config.https_port, path
5784                ),
5785            }
5786        }
5787    }
5788}
5789
5790impl Middleware for HttpsRedirectMiddleware {
5791    fn before<'a>(
5792        &'a self,
5793        _ctx: &'a RequestContext,
5794        req: &'a mut Request,
5795    ) -> BoxFuture<'a, ControlFlow> {
5796        Box::pin(async move {
5797            // Skip if redirects are disabled
5798            if !self.config.redirect_enabled {
5799                return ControlFlow::Continue;
5800            }
5801
5802            // Skip if already HTTPS
5803            if self.is_secure(req) {
5804                return ControlFlow::Continue;
5805            }
5806
5807            // Skip excluded paths (e.g., health checks)
5808            if self.is_excluded(req.path()) {
5809                return ControlFlow::Continue;
5810            }
5811
5812            // Build redirect URL
5813            let redirect_url = self.build_redirect_url(req);
5814
5815            // Choose status code
5816            let status = if self.config.permanent_redirect {
5817                crate::response::StatusCode::MOVED_PERMANENTLY
5818            } else {
5819                crate::response::StatusCode::TEMPORARY_REDIRECT
5820            };
5821
5822            // Create redirect response
5823            let response = Response::with_status(status)
5824                .header("Location", redirect_url.into_bytes())
5825                .header("Content-Type", b"text/plain".to_vec())
5826                .body(crate::response::ResponseBody::Bytes(
5827                    b"Redirecting to HTTPS...".to_vec(),
5828                ));
5829
5830            ControlFlow::Break(response)
5831        })
5832    }
5833
5834    fn after<'a>(
5835        &'a self,
5836        _ctx: &'a RequestContext,
5837        req: &'a Request,
5838        response: Response,
5839    ) -> BoxFuture<'a, Response> {
5840        Box::pin(async move {
5841            // Only add HSTS to secure responses
5842            if !self.is_secure(req) {
5843                return response;
5844            }
5845
5846            // Add HSTS header if configured
5847            if let Some(hsts_value) = self.build_hsts_header() {
5848                response.header("Strict-Transport-Security", hsts_value)
5849            } else {
5850                response
5851            }
5852        })
5853    }
5854
5855    fn name(&self) -> &'static str {
5856        "HttpsRedirect"
5857    }
5858}
5859
5860// ===========================================================================
5861// End HTTPS Redirect Middleware
5862// ===========================================================================
5863
5864// ===========================================================================
5865// Response Interceptors and Transformers
5866// ===========================================================================
5867//
5868// This section provides a simplified abstraction for response-only processing.
5869// Unlike full Middleware, ResponseInterceptor only handles post-handler processing,
5870// making it lighter weight and easier to compose for response transformations.
5871
5872/// A response interceptor that processes responses after handler execution.
5873///
5874/// Unlike the full [`Middleware`] trait, `ResponseInterceptor` only handles
5875/// the post-handler phase, making it simpler to implement for response-only
5876/// processing like:
5877/// - Adding timing headers
5878/// - Transforming response bodies
5879/// - Adding debug information
5880/// - Logging response details
5881///
5882/// # Example
5883///
5884/// ```ignore
5885/// use fastapi_core::middleware::{ResponseInterceptor, ResponseInterceptorContext};
5886///
5887/// struct TimingInterceptor {
5888///     start_time: Instant,
5889/// }
5890///
5891/// impl ResponseInterceptor for TimingInterceptor {
5892///     fn intercept(&self, ctx: &ResponseInterceptorContext, response: Response) -> Response {
5893///         let elapsed = self.start_time.elapsed();
5894///         response.header("X-Response-Time", format!("{}ms", elapsed.as_millis()).into_bytes())
5895///     }
5896/// }
5897/// ```
5898pub trait ResponseInterceptor: Send + Sync {
5899    /// Process a response after the handler has executed.
5900    ///
5901    /// # Parameters
5902    ///
5903    /// - `ctx`: Context containing request information and timing data
5904    /// - `response`: The response from the handler or previous interceptors
5905    ///
5906    /// # Returns
5907    ///
5908    /// The modified response to pass to the next interceptor or return to client.
5909    fn intercept<'a>(
5910        &'a self,
5911        ctx: &'a ResponseInterceptorContext<'a>,
5912        response: Response,
5913    ) -> BoxFuture<'a, Response>;
5914
5915    /// Returns the interceptor name for debugging and logging.
5916    fn name(&self) -> &'static str {
5917        std::any::type_name::<Self>()
5918    }
5919}
5920
5921/// Context provided to response interceptors.
5922///
5923/// Contains information about the original request and timing data
5924/// that interceptors might need to process responses.
5925#[derive(Debug)]
5926pub struct ResponseInterceptorContext<'a> {
5927    /// The original request (read-only).
5928    pub request: &'a Request,
5929    /// When the request processing started.
5930    pub start_time: Instant,
5931    /// The request context for cancellation support.
5932    pub request_ctx: &'a RequestContext,
5933}
5934
5935impl<'a> ResponseInterceptorContext<'a> {
5936    /// Create a new interceptor context.
5937    pub fn new(request: &'a Request, request_ctx: &'a RequestContext, start_time: Instant) -> Self {
5938        Self {
5939            request,
5940            start_time,
5941            request_ctx,
5942        }
5943    }
5944
5945    /// Get the elapsed time since request processing started.
5946    pub fn elapsed(&self) -> std::time::Duration {
5947        self.start_time.elapsed()
5948    }
5949
5950    /// Get the elapsed time in milliseconds.
5951    pub fn elapsed_ms(&self) -> u128 {
5952        self.start_time.elapsed().as_millis()
5953    }
5954}
5955
5956/// A stack of response interceptors that run in order.
5957///
5958/// Interceptors are executed in registration order (first registered, first run).
5959/// Each interceptor receives the response from the previous one and can modify it.
5960///
5961/// # Example
5962///
5963/// ```ignore
5964/// let mut stack = ResponseInterceptorStack::new();
5965/// stack.push(TimingInterceptor);
5966/// stack.push(DebugHeadersInterceptor::new());
5967///
5968/// let response = stack.process(&ctx, response).await;
5969/// ```
5970#[derive(Default)]
5971pub struct ResponseInterceptorStack {
5972    interceptors: Vec<Arc<dyn ResponseInterceptor>>,
5973}
5974
5975impl ResponseInterceptorStack {
5976    /// Create an empty interceptor stack.
5977    #[must_use]
5978    pub fn new() -> Self {
5979        Self {
5980            interceptors: Vec::new(),
5981        }
5982    }
5983
5984    /// Create a stack with pre-allocated capacity.
5985    #[must_use]
5986    pub fn with_capacity(capacity: usize) -> Self {
5987        Self {
5988            interceptors: Vec::with_capacity(capacity),
5989        }
5990    }
5991
5992    /// Add an interceptor to the end of the stack.
5993    pub fn push<I: ResponseInterceptor + 'static>(&mut self, interceptor: I) {
5994        self.interceptors.push(Arc::new(interceptor));
5995    }
5996
5997    /// Add an Arc-wrapped interceptor.
5998    pub fn push_arc(&mut self, interceptor: Arc<dyn ResponseInterceptor>) {
5999        self.interceptors.push(interceptor);
6000    }
6001
6002    /// Return the number of interceptors in the stack.
6003    #[must_use]
6004    pub fn len(&self) -> usize {
6005        self.interceptors.len()
6006    }
6007
6008    /// Return true if the stack is empty.
6009    #[must_use]
6010    pub fn is_empty(&self) -> bool {
6011        self.interceptors.is_empty()
6012    }
6013
6014    /// Process a response through all interceptors.
6015    pub async fn process(
6016        &self,
6017        ctx: &ResponseInterceptorContext<'_>,
6018        mut response: Response,
6019    ) -> Response {
6020        for interceptor in &self.interceptors {
6021            let _ = ctx.request_ctx.checkpoint();
6022            response = interceptor.intercept(ctx, response).await;
6023        }
6024        response
6025    }
6026}
6027
6028// ---------------------------------------------------------------------------
6029// Timing Interceptor
6030// ---------------------------------------------------------------------------
6031
6032/// Interceptor that adds response timing headers.
6033///
6034/// Adds the `X-Response-Time` header with the time taken to process the request.
6035/// Optionally adds Server-Timing header for browser DevTools integration.
6036///
6037/// # Example
6038///
6039/// ```ignore
6040/// let interceptor = TimingInterceptor::new();
6041/// // Or with Server-Timing header
6042/// let interceptor = TimingInterceptor::with_server_timing("app");
6043/// ```
6044#[derive(Debug, Clone)]
6045pub struct TimingInterceptor {
6046    /// Header name for the response time (default: X-Response-Time).
6047    header_name: String,
6048    /// Whether to include Server-Timing header.
6049    include_server_timing: bool,
6050    /// The timing metric name for Server-Timing (default: "total").
6051    server_timing_name: String,
6052}
6053
6054impl Default for TimingInterceptor {
6055    fn default() -> Self {
6056        Self::new()
6057    }
6058}
6059
6060impl TimingInterceptor {
6061    /// Create a new timing interceptor with default settings.
6062    #[must_use]
6063    pub fn new() -> Self {
6064        Self {
6065            header_name: "X-Response-Time".to_string(),
6066            include_server_timing: false,
6067            server_timing_name: "total".to_string(),
6068        }
6069    }
6070
6071    /// Enable Server-Timing header with the given metric name.
6072    #[must_use]
6073    pub fn with_server_timing(mut self, metric_name: impl Into<String>) -> Self {
6074        self.include_server_timing = true;
6075        self.server_timing_name = metric_name.into();
6076        self
6077    }
6078
6079    /// Set a custom header name instead of X-Response-Time.
6080    #[must_use]
6081    pub fn header_name(mut self, name: impl Into<String>) -> Self {
6082        self.header_name = name.into();
6083        self
6084    }
6085}
6086
6087impl ResponseInterceptor for TimingInterceptor {
6088    fn intercept<'a>(
6089        &'a self,
6090        ctx: &'a ResponseInterceptorContext<'a>,
6091        response: Response,
6092    ) -> BoxFuture<'a, Response> {
6093        Box::pin(async move {
6094            let elapsed_ms = ctx.elapsed_ms();
6095            let timing_value = format!("{}ms", elapsed_ms);
6096
6097            let response = response.header(&self.header_name, timing_value.clone().into_bytes());
6098
6099            if self.include_server_timing {
6100                // Server-Timing format: name;dur=value;desc="description"
6101                let server_timing = format!("{};dur={}", self.server_timing_name, elapsed_ms);
6102                response.header("Server-Timing", server_timing.into_bytes())
6103            } else {
6104                response
6105            }
6106        })
6107    }
6108
6109    fn name(&self) -> &'static str {
6110        "TimingInterceptor"
6111    }
6112}
6113
6114// ---------------------------------------------------------------------------
6115// Debug Headers Interceptor
6116// ---------------------------------------------------------------------------
6117
6118/// Interceptor that adds debug information headers.
6119///
6120/// Useful for development/staging environments to expose internal
6121/// processing information in response headers.
6122///
6123/// # Headers Added
6124///
6125/// - `X-Debug-Request-Id`: The request ID (if available)
6126/// - `X-Debug-Handler-Time`: Handler execution time
6127/// - `X-Debug-Path`: The request path
6128/// - `X-Debug-Method`: The HTTP method
6129///
6130/// # Example
6131///
6132/// ```ignore
6133/// let interceptor = DebugInfoInterceptor::new()
6134///     .include_path(true)
6135///     .include_method(true);
6136/// ```
6137#[derive(Debug, Clone)]
6138#[allow(clippy::struct_excessive_bools)]
6139pub struct DebugInfoInterceptor {
6140    /// Include path in debug headers.
6141    include_path: bool,
6142    /// Include HTTP method in debug headers.
6143    include_method: bool,
6144    /// Include request ID in debug headers.
6145    include_request_id: bool,
6146    /// Include timing information.
6147    include_timing: bool,
6148    /// Header prefix (default: "X-Debug-").
6149    header_prefix: String,
6150}
6151
6152impl Default for DebugInfoInterceptor {
6153    fn default() -> Self {
6154        Self::new()
6155    }
6156}
6157
6158impl DebugInfoInterceptor {
6159    /// Create a new debug info interceptor with all options enabled.
6160    #[must_use]
6161    pub fn new() -> Self {
6162        Self {
6163            include_path: true,
6164            include_method: true,
6165            include_request_id: true,
6166            include_timing: true,
6167            header_prefix: "X-Debug-".to_string(),
6168        }
6169    }
6170
6171    /// Set whether to include the path.
6172    #[must_use]
6173    pub fn include_path(mut self, include: bool) -> Self {
6174        self.include_path = include;
6175        self
6176    }
6177
6178    /// Set whether to include the HTTP method.
6179    #[must_use]
6180    pub fn include_method(mut self, include: bool) -> Self {
6181        self.include_method = include;
6182        self
6183    }
6184
6185    /// Set whether to include the request ID.
6186    #[must_use]
6187    pub fn include_request_id(mut self, include: bool) -> Self {
6188        self.include_request_id = include;
6189        self
6190    }
6191
6192    /// Set whether to include timing information.
6193    #[must_use]
6194    pub fn include_timing(mut self, include: bool) -> Self {
6195        self.include_timing = include;
6196        self
6197    }
6198
6199    /// Set a custom header prefix.
6200    #[must_use]
6201    pub fn header_prefix(mut self, prefix: impl Into<String>) -> Self {
6202        self.header_prefix = prefix.into();
6203        self
6204    }
6205}
6206
6207impl ResponseInterceptor for DebugInfoInterceptor {
6208    fn intercept<'a>(
6209        &'a self,
6210        ctx: &'a ResponseInterceptorContext<'a>,
6211        response: Response,
6212    ) -> BoxFuture<'a, Response> {
6213        Box::pin(async move {
6214            let mut resp = response;
6215
6216            if self.include_path {
6217                let header_name = format!("{}Path", self.header_prefix);
6218                resp = resp.header(header_name, ctx.request.path().as_bytes().to_vec());
6219            }
6220
6221            if self.include_method {
6222                let header_name = format!("{}Method", self.header_prefix);
6223                resp = resp.header(
6224                    header_name,
6225                    ctx.request.method().as_str().as_bytes().to_vec(),
6226                );
6227            }
6228
6229            if self.include_request_id {
6230                if let Some(request_id) = ctx.request.get_extension::<RequestId>() {
6231                    let header_name = format!("{}Request-Id", self.header_prefix);
6232                    resp = resp.header(header_name, request_id.0.as_bytes().to_vec());
6233                }
6234            }
6235
6236            if self.include_timing {
6237                let header_name = format!("{}Handler-Time", self.header_prefix);
6238                let timing = format!("{}ms", ctx.elapsed_ms());
6239                resp = resp.header(header_name, timing.into_bytes());
6240            }
6241
6242            resp
6243        })
6244    }
6245
6246    fn name(&self) -> &'static str {
6247        "DebugInfoInterceptor"
6248    }
6249}
6250
6251// ---------------------------------------------------------------------------
6252// Response Body Transform
6253// ---------------------------------------------------------------------------
6254
6255/// A response transformer that applies a function to the response body.
6256///
6257/// This is useful for content transformations like:
6258/// - Minification
6259/// - Pretty-printing
6260/// - Wrapping responses
6261/// - Filtering content
6262///
6263/// # Example
6264///
6265/// ```ignore
6266/// // Wrap JSON responses in an envelope
6267/// let transformer = ResponseBodyTransform::new(|body| {
6268///     format!(r#"{{"data": {}}}"#, String::from_utf8_lossy(&body)).into_bytes()
6269/// });
6270/// ```
6271pub struct ResponseBodyTransform<F>
6272where
6273    F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6274{
6275    transform_fn: F,
6276    /// Optional content type filter - only transform if content type matches.
6277    content_type_filter: Option<String>,
6278}
6279
6280impl<F> ResponseBodyTransform<F>
6281where
6282    F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6283{
6284    /// Create a new body transformer with the given function.
6285    pub fn new(transform_fn: F) -> Self {
6286        Self {
6287            transform_fn,
6288            content_type_filter: None,
6289        }
6290    }
6291
6292    /// Only apply transformation if the response content type starts with this value.
6293    #[must_use]
6294    pub fn for_content_type(mut self, content_type: impl Into<String>) -> Self {
6295        self.content_type_filter = Some(content_type.into());
6296        self
6297    }
6298
6299    fn should_transform(&self, response: &Response) -> bool {
6300        match &self.content_type_filter {
6301            Some(filter) => response
6302                .headers()
6303                .iter()
6304                .find(|(name, _)| name.eq_ignore_ascii_case("content-type"))
6305                .and_then(|(_, ct)| std::str::from_utf8(ct).ok())
6306                .map(|ct| ct.starts_with(filter))
6307                .unwrap_or(false),
6308            None => true,
6309        }
6310    }
6311}
6312
6313impl<F> ResponseInterceptor for ResponseBodyTransform<F>
6314where
6315    F: Fn(Vec<u8>) -> Vec<u8> + Send + Sync,
6316{
6317    fn intercept<'a>(
6318        &'a self,
6319        _ctx: &'a ResponseInterceptorContext<'a>,
6320        response: Response,
6321    ) -> BoxFuture<'a, Response> {
6322        Box::pin(async move {
6323            if !self.should_transform(&response) {
6324                return response;
6325            }
6326
6327            // Extract the body bytes
6328            let body_bytes = match response.body_ref() {
6329                crate::response::ResponseBody::Empty => Vec::new(),
6330                crate::response::ResponseBody::Bytes(b) => b.clone(),
6331                crate::response::ResponseBody::Stream(_) => {
6332                    // Cannot transform streaming responses
6333                    return response;
6334                }
6335            };
6336
6337            // Apply transformation
6338            let transformed = (self.transform_fn)(body_bytes);
6339
6340            // Rebuild response with new body
6341            response.body(crate::response::ResponseBody::Bytes(transformed))
6342        })
6343    }
6344
6345    fn name(&self) -> &'static str {
6346        "ResponseBodyTransform"
6347    }
6348}
6349
6350// ---------------------------------------------------------------------------
6351// Header Transform Interceptor
6352// ---------------------------------------------------------------------------
6353
6354/// An interceptor that transforms response headers.
6355///
6356/// Allows adding, removing, or modifying headers based on the response.
6357///
6358/// # Example
6359///
6360/// ```ignore
6361/// let interceptor = HeaderTransformInterceptor::new()
6362///     .add("X-Powered-By", "fastapi_rust")
6363///     .remove("Server")
6364///     .rename("X-Request-Id", "X-Trace-Id");
6365/// ```
6366#[derive(Debug, Clone, Default)]
6367pub struct HeaderTransformInterceptor {
6368    /// Headers to add.
6369    add_headers: Vec<(String, Vec<u8>)>,
6370    /// Headers to remove.
6371    remove_headers: Vec<String>,
6372    /// Headers to rename (old_name -> new_name).
6373    rename_headers: Vec<(String, String)>,
6374}
6375
6376impl HeaderTransformInterceptor {
6377    /// Create a new header transform interceptor.
6378    #[must_use]
6379    pub fn new() -> Self {
6380        Self::default()
6381    }
6382
6383    /// Add a header to the response.
6384    #[must_use]
6385    pub fn add(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
6386        self.add_headers.push((name.into(), value.into()));
6387        self
6388    }
6389
6390    /// Remove a header from the response.
6391    #[must_use]
6392    pub fn remove(mut self, name: impl Into<String>) -> Self {
6393        self.remove_headers.push(name.into());
6394        self
6395    }
6396
6397    /// Rename a header (if it exists).
6398    #[must_use]
6399    pub fn rename(mut self, old_name: impl Into<String>, new_name: impl Into<String>) -> Self {
6400        self.rename_headers.push((old_name.into(), new_name.into()));
6401        self
6402    }
6403}
6404
6405impl ResponseInterceptor for HeaderTransformInterceptor {
6406    fn intercept<'a>(
6407        &'a self,
6408        _ctx: &'a ResponseInterceptorContext<'a>,
6409        response: Response,
6410    ) -> BoxFuture<'a, Response> {
6411        let add_headers = self.add_headers.clone();
6412        let remove_headers = self.remove_headers.clone();
6413        let rename_headers = self.rename_headers.clone();
6414
6415        Box::pin(async move {
6416            let mut resp = response;
6417
6418            // Handle renames first - get values of headers to rename
6419            for (old_name, new_name) in &rename_headers {
6420                let values: Vec<Vec<u8>> = resp
6421                    .headers()
6422                    .iter()
6423                    .filter(|(name, _)| name.eq_ignore_ascii_case(old_name))
6424                    .map(|(_, v)| v.clone())
6425                    .collect();
6426
6427                if !values.is_empty() {
6428                    resp = resp.remove_header(old_name);
6429                    for v in values {
6430                        resp = resp.header(new_name, v);
6431                    }
6432                }
6433            }
6434
6435            // Add new headers
6436            for (name, value) in add_headers {
6437                resp = resp.header(name, value);
6438            }
6439
6440            // Remove headers (case-insensitive) after renames/additions.
6441            for name in &remove_headers {
6442                resp = resp.remove_header(name);
6443            }
6444
6445            resp
6446        })
6447    }
6448
6449    fn name(&self) -> &'static str {
6450        "HeaderTransformInterceptor"
6451    }
6452}
6453
6454// ---------------------------------------------------------------------------
6455// Conditional Interceptor Wrapper
6456// ---------------------------------------------------------------------------
6457
6458/// Wrapper that applies an interceptor only when a condition is met.
6459///
6460/// # Example
6461///
6462/// ```ignore
6463/// // Only add debug headers for non-production requests
6464/// let interceptor = ConditionalInterceptor::new(
6465///     DebugInfoInterceptor::new(),
6466///     |ctx, resp| ctx.request.headers().get("X-Debug").is_some()
6467/// );
6468/// ```
6469pub struct ConditionalInterceptor<I, F>
6470where
6471    I: ResponseInterceptor,
6472    F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6473{
6474    inner: I,
6475    condition: F,
6476}
6477
6478impl<I, F> ConditionalInterceptor<I, F>
6479where
6480    I: ResponseInterceptor,
6481    F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6482{
6483    /// Create a new conditional interceptor.
6484    pub fn new(inner: I, condition: F) -> Self {
6485        Self { inner, condition }
6486    }
6487}
6488
6489impl<I, F> ResponseInterceptor for ConditionalInterceptor<I, F>
6490where
6491    I: ResponseInterceptor,
6492    F: Fn(&ResponseInterceptorContext, &Response) -> bool + Send + Sync,
6493{
6494    fn intercept<'a>(
6495        &'a self,
6496        ctx: &'a ResponseInterceptorContext<'a>,
6497        response: Response,
6498    ) -> BoxFuture<'a, Response> {
6499        Box::pin(async move {
6500            if (self.condition)(ctx, &response) {
6501                self.inner.intercept(ctx, response).await
6502            } else {
6503                response
6504            }
6505        })
6506    }
6507
6508    fn name(&self) -> &'static str {
6509        "ConditionalInterceptor"
6510    }
6511}
6512
6513// ---------------------------------------------------------------------------
6514// Error Response Transformer
6515// ---------------------------------------------------------------------------
6516
6517/// Interceptor that transforms error responses.
6518///
6519/// Useful for:
6520/// - Hiding internal error details in production
6521/// - Adding consistent error formatting
6522/// - Logging error responses
6523///
6524/// # Example
6525///
6526/// ```ignore
6527/// let interceptor = ErrorResponseTransformer::new()
6528///     .hide_details_for_status(StatusCode::INTERNAL_SERVER_ERROR)
6529///     .with_replacement_body(b"An internal error occurred".to_vec());
6530/// ```
6531#[derive(Debug, Clone)]
6532pub struct ErrorResponseTransformer {
6533    /// Status codes to transform.
6534    status_codes: HashSet<u16>,
6535    /// Replacement body for error responses.
6536    replacement_body: Option<Vec<u8>>,
6537    /// Whether to add an error ID header.
6538    add_error_id: bool,
6539}
6540
6541impl Default for ErrorResponseTransformer {
6542    fn default() -> Self {
6543        Self::new()
6544    }
6545}
6546
6547impl ErrorResponseTransformer {
6548    /// Create a new error response transformer.
6549    #[must_use]
6550    pub fn new() -> Self {
6551        Self {
6552            status_codes: HashSet::new(),
6553            replacement_body: None,
6554            add_error_id: false,
6555        }
6556    }
6557
6558    /// Hide details for the given status code.
6559    #[must_use]
6560    pub fn hide_details_for_status(mut self, status: crate::response::StatusCode) -> Self {
6561        self.status_codes.insert(status.as_u16());
6562        self
6563    }
6564
6565    /// Set the replacement body for error responses.
6566    #[must_use]
6567    pub fn with_replacement_body(mut self, body: impl Into<Vec<u8>>) -> Self {
6568        self.replacement_body = Some(body.into());
6569        self
6570    }
6571
6572    /// Enable adding an error ID header for tracking.
6573    #[must_use]
6574    pub fn add_error_id(mut self, enable: bool) -> Self {
6575        self.add_error_id = enable;
6576        self
6577    }
6578}
6579
6580impl ResponseInterceptor for ErrorResponseTransformer {
6581    fn intercept<'a>(
6582        &'a self,
6583        ctx: &'a ResponseInterceptorContext<'a>,
6584        response: Response,
6585    ) -> BoxFuture<'a, Response> {
6586        Box::pin(async move {
6587            let status_code = response.status().as_u16();
6588
6589            if !self.status_codes.contains(&status_code) {
6590                return response;
6591            }
6592
6593            let mut resp = response;
6594
6595            // Replace body if configured
6596            if let Some(ref replacement) = self.replacement_body {
6597                resp = resp.body(crate::response::ResponseBody::Bytes(replacement.clone()));
6598            }
6599
6600            // Add error ID header if enabled
6601            if self.add_error_id {
6602                // Use request ID if available, otherwise generate a simple one
6603                let error_id = ctx
6604                    .request
6605                    .get_extension::<RequestId>()
6606                    .map(|r| r.0.clone())
6607                    .unwrap_or_else(|| format!("err-{}", ctx.elapsed_ms()));
6608                resp = resp.header("X-Error-Id", error_id.into_bytes());
6609            }
6610
6611            resp
6612        })
6613    }
6614
6615    fn name(&self) -> &'static str {
6616        "ErrorResponseTransformer"
6617    }
6618}
6619
6620// ---------------------------------------------------------------------------
6621// Middleware adapter for ResponseInterceptor
6622// ---------------------------------------------------------------------------
6623
6624/// Adapter that wraps a `ResponseInterceptor` as a `Middleware`.
6625///
6626/// This allows using response interceptors in the existing middleware stack.
6627///
6628/// # Example
6629///
6630/// ```ignore
6631/// let timing = TimingInterceptor::new();
6632/// let middleware = ResponseInterceptorMiddleware::new(timing);
6633/// stack.push(middleware);
6634/// ```
6635pub struct ResponseInterceptorMiddleware<I>
6636where
6637    I: ResponseInterceptor,
6638{
6639    interceptor: I,
6640}
6641
6642impl<I> ResponseInterceptorMiddleware<I>
6643where
6644    I: ResponseInterceptor,
6645{
6646    /// Wrap a response interceptor as middleware.
6647    pub fn new(interceptor: I) -> Self {
6648        Self { interceptor }
6649    }
6650}
6651
6652impl<I> Middleware for ResponseInterceptorMiddleware<I>
6653where
6654    I: ResponseInterceptor,
6655{
6656    fn before<'a>(
6657        &'a self,
6658        _ctx: &'a RequestContext,
6659        req: &'a mut Request,
6660    ) -> BoxFuture<'a, ControlFlow> {
6661        // Store the start time in request extensions
6662        req.insert_extension(InterceptorStartTime(Instant::now()));
6663        Box::pin(async { ControlFlow::Continue })
6664    }
6665
6666    fn after<'a>(
6667        &'a self,
6668        ctx: &'a RequestContext,
6669        req: &'a Request,
6670        response: Response,
6671    ) -> BoxFuture<'a, Response> {
6672        Box::pin(async move {
6673            // Retrieve start time from extensions
6674            let start_time = req
6675                .get_extension::<InterceptorStartTime>()
6676                .map(|t| t.0)
6677                .unwrap_or_else(Instant::now);
6678
6679            let interceptor_ctx = ResponseInterceptorContext::new(req, ctx, start_time);
6680            self.interceptor.intercept(&interceptor_ctx, response).await
6681        })
6682    }
6683
6684    fn name(&self) -> &'static str {
6685        self.interceptor.name()
6686    }
6687}
6688
6689/// Internal type for storing interceptor start time in request extensions.
6690#[derive(Debug, Clone, Copy)]
6691struct InterceptorStartTime(Instant);
6692
6693// ===========================================================================
6694// End Response Interceptors and Transformers
6695// ===========================================================================
6696
6697// ===========================================================================
6698// Response Timing Metrics Collection
6699// ===========================================================================
6700//
6701// This section provides comprehensive timing metrics for monitoring:
6702// - Request duration
6703// - Time-to-first-byte (TTFB)
6704// - Server-Timing header with multiple metrics
6705// - Histogram collection for aggregation
6706// - Integration with logging
6707
6708/// A single entry in the Server-Timing header.
6709///
6710/// Each entry has a name, duration in milliseconds, and optional description.
6711///
6712/// # Server-Timing Format
6713///
6714/// ```text
6715/// Server-Timing: name;dur=value;desc="description"
6716/// ```
6717///
6718/// # Example
6719///
6720/// ```ignore
6721/// let entry = ServerTimingEntry::new("db", 42.5)
6722///     .with_description("Database query");
6723/// ```
6724#[derive(Debug, Clone)]
6725pub struct ServerTimingEntry {
6726    /// The metric name (e.g., "db", "cache", "render").
6727    name: String,
6728    /// Duration in milliseconds (supports sub-millisecond precision).
6729    duration_ms: f64,
6730    /// Optional description for the metric.
6731    description: Option<String>,
6732}
6733
6734impl ServerTimingEntry {
6735    /// Create a new Server-Timing entry.
6736    #[must_use]
6737    pub fn new(name: impl Into<String>, duration_ms: f64) -> Self {
6738        Self {
6739            name: name.into(),
6740            duration_ms,
6741            description: None,
6742        }
6743    }
6744
6745    /// Add a description to the entry.
6746    #[must_use]
6747    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
6748        self.description = Some(desc.into());
6749        self
6750    }
6751
6752    /// Format this entry for the Server-Timing header.
6753    #[must_use]
6754    pub fn to_header_value(&self) -> String {
6755        match &self.description {
6756            Some(desc) => format!(
6757                "{};dur={:.3};desc=\"{}\"",
6758                self.name, self.duration_ms, desc
6759            ),
6760            None => format!("{};dur={:.3}", self.name, self.duration_ms),
6761        }
6762    }
6763}
6764
6765/// Builder for constructing Server-Timing headers with multiple metrics.
6766///
6767/// Collects multiple timing entries and formats them as a single header value.
6768///
6769/// # Example
6770///
6771/// ```ignore
6772/// let timing = ServerTimingBuilder::new()
6773///     .add("total", 150.5)
6774///     .add_with_desc("db", 42.0, "Database queries")
6775///     .add_with_desc("cache", 5.0, "Cache lookup")
6776///     .build();
6777///
6778/// // Result: "total;dur=150.500, db;dur=42.000;desc=\"Database queries\", cache;dur=5.000;desc=\"Cache lookup\""
6779/// ```
6780#[derive(Debug, Clone, Default)]
6781pub struct ServerTimingBuilder {
6782    entries: Vec<ServerTimingEntry>,
6783}
6784
6785impl ServerTimingBuilder {
6786    /// Create a new empty builder.
6787    #[must_use]
6788    pub fn new() -> Self {
6789        Self::default()
6790    }
6791
6792    /// Add a timing entry with just a name and duration.
6793    #[must_use]
6794    pub fn add(mut self, name: impl Into<String>, duration_ms: f64) -> Self {
6795        self.entries.push(ServerTimingEntry::new(name, duration_ms));
6796        self
6797    }
6798
6799    /// Add a timing entry with a description.
6800    #[must_use]
6801    pub fn add_with_desc(
6802        mut self,
6803        name: impl Into<String>,
6804        duration_ms: f64,
6805        description: impl Into<String>,
6806    ) -> Self {
6807        self.entries
6808            .push(ServerTimingEntry::new(name, duration_ms).with_description(description));
6809        self
6810    }
6811
6812    /// Add a pre-built entry.
6813    #[must_use]
6814    pub fn add_entry(mut self, entry: ServerTimingEntry) -> Self {
6815        self.entries.push(entry);
6816        self
6817    }
6818
6819    /// Build the Server-Timing header value.
6820    #[must_use]
6821    pub fn build(&self) -> String {
6822        self.entries
6823            .iter()
6824            .map(ServerTimingEntry::to_header_value)
6825            .collect::<Vec<_>>()
6826            .join(", ")
6827    }
6828
6829    /// Return true if no entries have been added.
6830    #[must_use]
6831    pub fn is_empty(&self) -> bool {
6832        self.entries.is_empty()
6833    }
6834
6835    /// Return the number of entries.
6836    #[must_use]
6837    pub fn len(&self) -> usize {
6838        self.entries.len()
6839    }
6840}
6841
6842/// Collected timing metrics for a single request.
6843///
6844/// This struct is stored in request extensions and can be read by
6845/// interceptors or logging middleware to expose timing data.
6846///
6847/// # Usage
6848///
6849/// Handlers can access and modify timing metrics via request extensions:
6850///
6851/// ```ignore
6852/// // Add a custom timing metric
6853/// if let Some(metrics) = req.get_extension_mut::<TimingMetrics>() {
6854///     metrics.add_metric("db", db_time.as_secs_f64() * 1000.0);
6855/// }
6856/// ```
6857#[derive(Debug, Clone)]
6858pub struct TimingMetrics {
6859    /// When the request processing started.
6860    pub start_time: Instant,
6861    /// When the first byte of the response was sent (if known).
6862    pub first_byte_time: Option<Instant>,
6863    /// Custom metrics added by handlers (name -> duration_ms).
6864    pub custom_metrics: Vec<(String, f64, Option<String>)>,
6865}
6866
6867impl TimingMetrics {
6868    /// Create new timing metrics starting now.
6869    #[must_use]
6870    pub fn new() -> Self {
6871        Self {
6872            start_time: Instant::now(),
6873            first_byte_time: None,
6874            custom_metrics: Vec::new(),
6875        }
6876    }
6877
6878    /// Create timing metrics with a specific start time.
6879    #[must_use]
6880    pub fn with_start_time(start_time: Instant) -> Self {
6881        Self {
6882            start_time,
6883            first_byte_time: None,
6884            custom_metrics: Vec::new(),
6885        }
6886    }
6887
6888    /// Mark the time when the first byte of the response was sent.
6889    pub fn mark_first_byte(&mut self) {
6890        self.first_byte_time = Some(Instant::now());
6891    }
6892
6893    /// Add a custom metric (e.g., database query time).
6894    pub fn add_metric(&mut self, name: impl Into<String>, duration_ms: f64) {
6895        self.custom_metrics.push((name.into(), duration_ms, None));
6896    }
6897
6898    /// Add a custom metric with a description.
6899    pub fn add_metric_with_desc(
6900        &mut self,
6901        name: impl Into<String>,
6902        duration_ms: f64,
6903        desc: impl Into<String>,
6904    ) {
6905        self.custom_metrics
6906            .push((name.into(), duration_ms, Some(desc.into())));
6907    }
6908
6909    /// Get the total elapsed time in milliseconds.
6910    #[must_use]
6911    pub fn total_ms(&self) -> f64 {
6912        self.start_time.elapsed().as_secs_f64() * 1000.0
6913    }
6914
6915    /// Get the time-to-first-byte in milliseconds (if available).
6916    #[must_use]
6917    pub fn ttfb_ms(&self) -> Option<f64> {
6918        self.first_byte_time
6919            .map(|t| t.duration_since(self.start_time).as_secs_f64() * 1000.0)
6920    }
6921
6922    /// Build a Server-Timing header from the collected metrics.
6923    #[must_use]
6924    pub fn to_server_timing(&self) -> ServerTimingBuilder {
6925        let mut builder = ServerTimingBuilder::new().add_with_desc(
6926            "total",
6927            self.total_ms(),
6928            "Total request time",
6929        );
6930
6931        if let Some(ttfb) = self.ttfb_ms() {
6932            builder = builder.add_with_desc("ttfb", ttfb, "Time to first byte");
6933        }
6934
6935        for (name, duration, desc) in &self.custom_metrics {
6936            match desc {
6937                Some(d) => builder = builder.add_with_desc(name, *duration, d),
6938                None => builder = builder.add(name, *duration),
6939            }
6940        }
6941
6942        builder
6943    }
6944}
6945
6946impl Default for TimingMetrics {
6947    fn default() -> Self {
6948        Self::new()
6949    }
6950}
6951
6952/// Configuration for the timing metrics middleware.
6953#[derive(Debug, Clone)]
6954#[allow(clippy::struct_excessive_bools)]
6955pub struct TimingMetricsConfig {
6956    /// Whether to add the Server-Timing header.
6957    pub add_server_timing_header: bool,
6958    /// Whether to add the X-Response-Time header.
6959    pub add_response_time_header: bool,
6960    /// Custom header name for response time (default: "X-Response-Time").
6961    pub response_time_header_name: String,
6962    /// Whether to include custom metrics from handlers.
6963    pub include_custom_metrics: bool,
6964    /// Whether to include TTFB in the Server-Timing header.
6965    pub include_ttfb: bool,
6966}
6967
6968impl Default for TimingMetricsConfig {
6969    fn default() -> Self {
6970        Self {
6971            add_server_timing_header: true,
6972            add_response_time_header: true,
6973            response_time_header_name: "X-Response-Time".to_string(),
6974            include_custom_metrics: true,
6975            include_ttfb: true,
6976        }
6977    }
6978}
6979
6980impl TimingMetricsConfig {
6981    /// Create a new config with default settings.
6982    #[must_use]
6983    pub fn new() -> Self {
6984        Self::default()
6985    }
6986
6987    /// Enable or disable Server-Timing header.
6988    #[must_use]
6989    pub fn server_timing(mut self, enabled: bool) -> Self {
6990        self.add_server_timing_header = enabled;
6991        self
6992    }
6993
6994    /// Enable or disable X-Response-Time header.
6995    #[must_use]
6996    pub fn response_time(mut self, enabled: bool) -> Self {
6997        self.add_response_time_header = enabled;
6998        self
6999    }
7000
7001    /// Set a custom response time header name.
7002    #[must_use]
7003    pub fn response_time_header(mut self, name: impl Into<String>) -> Self {
7004        self.response_time_header_name = name.into();
7005        self
7006    }
7007
7008    /// Enable or disable custom metrics.
7009    #[must_use]
7010    pub fn custom_metrics(mut self, enabled: bool) -> Self {
7011        self.include_custom_metrics = enabled;
7012        self
7013    }
7014
7015    /// Enable or disable TTFB tracking.
7016    #[must_use]
7017    pub fn ttfb(mut self, enabled: bool) -> Self {
7018        self.include_ttfb = enabled;
7019        self
7020    }
7021
7022    /// Create a production-safe config (minimal headers).
7023    #[must_use]
7024    pub fn production() -> Self {
7025        Self {
7026            add_server_timing_header: false,
7027            add_response_time_header: true,
7028            response_time_header_name: "X-Response-Time".to_string(),
7029            include_custom_metrics: false,
7030            include_ttfb: false,
7031        }
7032    }
7033
7034    /// Create a development config (all timing info exposed).
7035    #[must_use]
7036    pub fn development() -> Self {
7037        Self::default()
7038    }
7039}
7040
7041/// Middleware that collects and exposes timing metrics.
7042///
7043/// This middleware:
7044/// 1. Records the request start time
7045/// 2. Injects `TimingMetrics` into request extensions for handlers to use
7046/// 3. Adds timing headers to the response
7047///
7048/// # Example
7049///
7050/// ```ignore
7051/// let timing = TimingMetricsMiddleware::new();
7052/// // Or with custom config:
7053/// let timing = TimingMetricsMiddleware::with_config(
7054///     TimingMetricsConfig::production()
7055/// );
7056///
7057/// middleware_stack.push(timing);
7058/// ```
7059#[derive(Debug, Clone)]
7060pub struct TimingMetricsMiddleware {
7061    config: TimingMetricsConfig,
7062}
7063
7064impl TimingMetricsMiddleware {
7065    /// Create a new timing metrics middleware with default config.
7066    #[must_use]
7067    pub fn new() -> Self {
7068        Self {
7069            config: TimingMetricsConfig::default(),
7070        }
7071    }
7072
7073    /// Create with a custom configuration.
7074    #[must_use]
7075    pub fn with_config(config: TimingMetricsConfig) -> Self {
7076        Self { config }
7077    }
7078
7079    /// Create a production-safe instance (minimal headers).
7080    #[must_use]
7081    pub fn production() -> Self {
7082        Self {
7083            config: TimingMetricsConfig::production(),
7084        }
7085    }
7086
7087    /// Create a development instance (all timing info exposed).
7088    #[must_use]
7089    pub fn development() -> Self {
7090        Self {
7091            config: TimingMetricsConfig::development(),
7092        }
7093    }
7094}
7095
7096impl Default for TimingMetricsMiddleware {
7097    fn default() -> Self {
7098        Self::new()
7099    }
7100}
7101
7102impl Middleware for TimingMetricsMiddleware {
7103    fn before<'a>(
7104        &'a self,
7105        _ctx: &'a RequestContext,
7106        req: &'a mut Request,
7107    ) -> BoxFuture<'a, ControlFlow> {
7108        // Store timing metrics in request extensions
7109        req.insert_extension(TimingMetrics::new());
7110        Box::pin(async { ControlFlow::Continue })
7111    }
7112
7113    fn after<'a>(
7114        &'a self,
7115        _ctx: &'a RequestContext,
7116        req: &'a Request,
7117        response: Response,
7118    ) -> BoxFuture<'a, Response> {
7119        let config = self.config.clone();
7120
7121        Box::pin(async move {
7122            let mut resp = response;
7123
7124            // Get timing metrics from extensions
7125            let metrics = req.get_extension::<TimingMetrics>();
7126
7127            match metrics {
7128                Some(metrics) => {
7129                    // Add X-Response-Time header
7130                    if config.add_response_time_header {
7131                        let timing = format!("{:.3}ms", metrics.total_ms());
7132                        resp = resp.header(&config.response_time_header_name, timing.into_bytes());
7133                    }
7134
7135                    // Add Server-Timing header
7136                    if config.add_server_timing_header {
7137                        let mut builder = ServerTimingBuilder::new().add_with_desc(
7138                            "total",
7139                            metrics.total_ms(),
7140                            "Total request time",
7141                        );
7142
7143                        // Add TTFB if available and enabled
7144                        if config.include_ttfb {
7145                            if let Some(ttfb) = metrics.ttfb_ms() {
7146                                builder = builder.add_with_desc("ttfb", ttfb, "Time to first byte");
7147                            }
7148                        }
7149
7150                        // Add custom metrics if enabled
7151                        if config.include_custom_metrics {
7152                            for (name, duration, desc) in &metrics.custom_metrics {
7153                                match desc {
7154                                    Some(d) => builder = builder.add_with_desc(name, *duration, d),
7155                                    None => builder = builder.add(name, *duration),
7156                                }
7157                            }
7158                        }
7159
7160                        let header_value = builder.build();
7161                        resp = resp.header("Server-Timing", header_value.into_bytes());
7162                    }
7163                }
7164                None => {
7165                    // No timing metrics in extensions - add basic timing
7166                    // This shouldn't happen if middleware is properly registered
7167                    if config.add_response_time_header {
7168                        resp = resp.header(&config.response_time_header_name, b"0.000ms".to_vec());
7169                    }
7170                }
7171            }
7172
7173            resp
7174        })
7175    }
7176
7177    fn name(&self) -> &'static str {
7178        "TimingMetrics"
7179    }
7180}
7181
7182/// Simple histogram bucket for collecting timing distributions.
7183///
7184/// Useful for aggregating timing data across many requests.
7185#[derive(Debug, Clone)]
7186pub struct TimingHistogramBucket {
7187    /// Upper bound for this bucket (milliseconds).
7188    pub le: f64,
7189    /// Count of observations in this bucket.
7190    pub count: u64,
7191}
7192
7193/// A histogram for collecting timing distributions.
7194///
7195/// This provides Prometheus-style histogram buckets for aggregating
7196/// timing data across many requests.
7197///
7198/// # Example
7199///
7200/// ```ignore
7201/// let mut histogram = TimingHistogram::with_buckets(vec![
7202///     1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0
7203/// ]);
7204///
7205/// histogram.observe(42.5);  // 42.5ms response time
7206/// histogram.observe(150.0);
7207///
7208/// let buckets = histogram.buckets();
7209/// let avg = histogram.mean();
7210/// ```
7211#[derive(Debug, Clone)]
7212pub struct TimingHistogram {
7213    /// Bucket upper bounds in milliseconds.
7214    bucket_bounds: Vec<f64>,
7215    /// Count per bucket.
7216    bucket_counts: Vec<u64>,
7217    /// Sum of all observed values.
7218    sum: f64,
7219    /// Total count of observations.
7220    count: u64,
7221}
7222
7223impl TimingHistogram {
7224    /// Create a histogram with the given bucket upper bounds.
7225    ///
7226    /// Bounds should be sorted in ascending order.
7227    #[must_use]
7228    pub fn with_buckets(bucket_bounds: Vec<f64>) -> Self {
7229        let bucket_counts = vec![0; bucket_bounds.len()];
7230        Self {
7231            bucket_bounds,
7232            bucket_counts,
7233            sum: 0.0,
7234            count: 0,
7235        }
7236    }
7237
7238    /// Create a histogram with default HTTP latency buckets.
7239    ///
7240    /// Buckets: 1ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s
7241    #[must_use]
7242    pub fn http_latency() -> Self {
7243        Self::with_buckets(vec![
7244            1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0,
7245        ])
7246    }
7247
7248    /// Record an observation.
7249    pub fn observe(&mut self, value_ms: f64) {
7250        self.sum += value_ms;
7251        self.count += 1;
7252
7253        // Increment bucket counts (cumulative)
7254        for (i, bound) in self.bucket_bounds.iter().enumerate() {
7255            if value_ms <= *bound {
7256                self.bucket_counts[i] += 1;
7257            }
7258        }
7259    }
7260
7261    /// Get the total count of observations.
7262    #[must_use]
7263    pub fn count(&self) -> u64 {
7264        self.count
7265    }
7266
7267    /// Get the sum of all observed values.
7268    #[must_use]
7269    pub fn sum(&self) -> f64 {
7270        self.sum
7271    }
7272
7273    /// Get the mean value.
7274    #[must_use]
7275    pub fn mean(&self) -> f64 {
7276        if self.count == 0 {
7277            0.0
7278        } else {
7279            #[allow(clippy::cast_precision_loss)]
7280            {
7281                self.sum / self.count as f64
7282            }
7283        }
7284    }
7285
7286    /// Get the bucket data.
7287    #[must_use]
7288    pub fn buckets(&self) -> Vec<TimingHistogramBucket> {
7289        self.bucket_bounds
7290            .iter()
7291            .zip(&self.bucket_counts)
7292            .map(|(&le, &count)| TimingHistogramBucket { le, count })
7293            .collect()
7294    }
7295
7296    /// Reset the histogram.
7297    pub fn reset(&mut self) {
7298        self.sum = 0.0;
7299        self.count = 0;
7300        for count in &mut self.bucket_counts {
7301            *count = 0;
7302        }
7303    }
7304}
7305
7306impl Default for TimingHistogram {
7307    fn default() -> Self {
7308        Self::http_latency()
7309    }
7310}
7311
7312// ===========================================================================
7313// End Response Timing Metrics Collection
7314// ===========================================================================
7315
7316#[cfg(test)]
7317mod timing_metrics_tests {
7318    use super::*;
7319    use crate::request::Method;
7320    use crate::response::StatusCode;
7321
7322    fn test_context() -> RequestContext {
7323        RequestContext::new(asupersync::Cx::for_testing(), 1)
7324    }
7325
7326    fn test_request() -> Request {
7327        Request::new(Method::Get, "/test")
7328    }
7329
7330    fn run_middleware_before(mw: &impl Middleware, req: &mut Request) -> ControlFlow {
7331        let ctx = test_context();
7332        futures_executor::block_on(mw.before(&ctx, req))
7333    }
7334
7335    fn run_middleware_after(mw: &impl Middleware, req: &Request, resp: Response) -> Response {
7336        let ctx = test_context();
7337        futures_executor::block_on(mw.after(&ctx, req, resp))
7338    }
7339
7340    #[test]
7341    fn server_timing_entry_basic() {
7342        let entry = ServerTimingEntry::new("db", 42.5);
7343        assert_eq!(entry.to_header_value(), "db;dur=42.500");
7344    }
7345
7346    #[test]
7347    fn server_timing_entry_with_description() {
7348        let entry = ServerTimingEntry::new("db", 42.5).with_description("Database query");
7349        assert_eq!(
7350            entry.to_header_value(),
7351            "db;dur=42.500;desc=\"Database query\""
7352        );
7353    }
7354
7355    #[test]
7356    fn server_timing_builder_single_entry() {
7357        let timing = ServerTimingBuilder::new().add("total", 150.0).build();
7358        assert_eq!(timing, "total;dur=150.000");
7359    }
7360
7361    #[test]
7362    fn server_timing_builder_multiple_entries() {
7363        let timing = ServerTimingBuilder::new()
7364            .add("total", 150.0)
7365            .add_with_desc("db", 42.0, "Database")
7366            .add("cache", 5.0)
7367            .build();
7368
7369        assert!(timing.contains("total;dur=150.000"));
7370        assert!(timing.contains("db;dur=42.000;desc=\"Database\""));
7371        assert!(timing.contains("cache;dur=5.000"));
7372        assert!(timing.contains(", ")); // Multiple entries separated by comma
7373    }
7374
7375    #[test]
7376    fn server_timing_builder_empty() {
7377        let builder = ServerTimingBuilder::new();
7378        assert!(builder.is_empty());
7379        assert_eq!(builder.len(), 0);
7380        assert_eq!(builder.build(), "");
7381    }
7382
7383    #[test]
7384    fn timing_metrics_basic() {
7385        let metrics = TimingMetrics::new();
7386        std::thread::sleep(std::time::Duration::from_millis(5));
7387
7388        let total = metrics.total_ms();
7389        assert!(total >= 5.0, "Total should be at least 5ms");
7390        assert!(metrics.ttfb_ms().is_none(), "TTFB should not be set");
7391    }
7392
7393    #[test]
7394    fn timing_metrics_custom_metrics() {
7395        let mut metrics = TimingMetrics::new();
7396        metrics.add_metric("db", 42.5);
7397        metrics.add_metric_with_desc("cache", 5.0, "Cache lookup");
7398
7399        let timing = metrics.to_server_timing();
7400        assert_eq!(timing.len(), 3); // total + 2 custom
7401
7402        let header = timing.build();
7403        assert!(header.contains("total"));
7404        assert!(header.contains("db;dur=42.500"));
7405        assert!(header.contains("cache;dur=5.000;desc=\"Cache lookup\""));
7406    }
7407
7408    #[test]
7409    fn timing_metrics_ttfb() {
7410        let mut metrics = TimingMetrics::new();
7411        std::thread::sleep(std::time::Duration::from_millis(5));
7412        metrics.mark_first_byte();
7413
7414        let ttfb = metrics.ttfb_ms().unwrap();
7415        assert!(ttfb >= 5.0, "TTFB should be at least 5ms");
7416    }
7417
7418    #[test]
7419    fn timing_metrics_config_default() {
7420        let config = TimingMetricsConfig::default();
7421        assert!(config.add_server_timing_header);
7422        assert!(config.add_response_time_header);
7423        assert!(config.include_custom_metrics);
7424        assert!(config.include_ttfb);
7425    }
7426
7427    #[test]
7428    fn timing_metrics_config_production() {
7429        let config = TimingMetricsConfig::production();
7430        assert!(!config.add_server_timing_header);
7431        assert!(config.add_response_time_header);
7432        assert!(!config.include_custom_metrics);
7433    }
7434
7435    #[test]
7436    fn timing_middleware_adds_metrics_to_request() {
7437        let mw = TimingMetricsMiddleware::new();
7438        let mut req = test_request();
7439
7440        // Before should insert TimingMetrics
7441        let result = run_middleware_before(&mw, &mut req);
7442        assert!(result.is_continue());
7443
7444        let metrics = req.get_extension::<TimingMetrics>();
7445        assert!(metrics.is_some(), "TimingMetrics should be in extensions");
7446    }
7447
7448    #[test]
7449    fn timing_middleware_adds_response_time_header() {
7450        let mw = TimingMetricsMiddleware::new();
7451        let mut req = test_request();
7452
7453        // Run before to insert TimingMetrics
7454        run_middleware_before(&mw, &mut req);
7455
7456        let resp = Response::with_status(StatusCode::OK);
7457        let result = run_middleware_after(&mw, &req, resp);
7458
7459        let has_timing = result
7460            .headers()
7461            .iter()
7462            .any(|(name, _)| name == "X-Response-Time");
7463        assert!(has_timing, "Should have X-Response-Time header");
7464    }
7465
7466    #[test]
7467    fn timing_middleware_adds_server_timing_header() {
7468        let mw = TimingMetricsMiddleware::new();
7469        let mut req = test_request();
7470
7471        run_middleware_before(&mw, &mut req);
7472
7473        let resp = Response::with_status(StatusCode::OK);
7474        let result = run_middleware_after(&mw, &req, resp);
7475
7476        let server_timing = result
7477            .headers()
7478            .iter()
7479            .find(|(name, _)| name == "Server-Timing")
7480            .map(|(_, v)| String::from_utf8_lossy(v).to_string());
7481
7482        assert!(server_timing.is_some(), "Should have Server-Timing header");
7483        let header = server_timing.unwrap();
7484        assert!(header.contains("total"), "Should have total timing");
7485    }
7486
7487    #[test]
7488    fn timing_middleware_production_mode() {
7489        let mw = TimingMetricsMiddleware::production();
7490        let mut req = test_request();
7491
7492        run_middleware_before(&mw, &mut req);
7493
7494        let resp = Response::with_status(StatusCode::OK);
7495        let result = run_middleware_after(&mw, &req, resp);
7496
7497        // Should have X-Response-Time
7498        let has_response_time = result
7499            .headers()
7500            .iter()
7501            .any(|(name, _)| name == "X-Response-Time");
7502        assert!(has_response_time);
7503
7504        // Should NOT have Server-Timing
7505        let has_server_timing = result
7506            .headers()
7507            .iter()
7508            .any(|(name, _)| name == "Server-Timing");
7509        assert!(!has_server_timing);
7510    }
7511
7512    #[test]
7513    #[allow(clippy::float_cmp)]
7514    fn timing_histogram_basic() {
7515        let mut histogram = TimingHistogram::http_latency();
7516        assert_eq!(histogram.count(), 0);
7517        assert_eq!(histogram.sum(), 0.0);
7518
7519        histogram.observe(42.0);
7520        histogram.observe(150.0);
7521        histogram.observe(5.0);
7522
7523        assert_eq!(histogram.count(), 3);
7524        assert_eq!(histogram.sum(), 197.0);
7525        assert!((histogram.mean() - 65.666).abs() < 0.01);
7526    }
7527
7528    #[test]
7529    fn timing_histogram_buckets() {
7530        let mut histogram = TimingHistogram::with_buckets(vec![10.0, 50.0, 100.0]);
7531
7532        histogram.observe(5.0); // Falls in 10 bucket
7533        histogram.observe(25.0); // Falls in 50 bucket
7534        histogram.observe(75.0); // Falls in 100 bucket
7535        histogram.observe(150.0); // Above all buckets
7536
7537        let buckets = histogram.buckets();
7538        assert_eq!(buckets.len(), 3);
7539
7540        // Buckets are cumulative
7541        assert_eq!(buckets[0].count, 1); // <= 10: 1
7542        assert_eq!(buckets[1].count, 2); // <= 50: 2
7543        assert_eq!(buckets[2].count, 3); // <= 100: 3
7544    }
7545
7546    #[test]
7547    #[allow(clippy::float_cmp)]
7548    fn timing_histogram_reset() {
7549        let mut histogram = TimingHistogram::http_latency();
7550        histogram.observe(100.0);
7551        histogram.observe(200.0);
7552
7553        assert_eq!(histogram.count(), 2);
7554
7555        histogram.reset();
7556
7557        assert_eq!(histogram.count(), 0);
7558        assert_eq!(histogram.sum(), 0.0);
7559    }
7560}
7561
7562#[cfg(test)]
7563mod response_interceptor_tests {
7564    use super::*;
7565    use crate::request::Method;
7566    use crate::response::StatusCode;
7567
7568    fn test_context() -> RequestContext {
7569        RequestContext::new(asupersync::Cx::for_testing(), 1)
7570    }
7571
7572    fn test_request() -> Request {
7573        Request::new(Method::Get, "/test")
7574    }
7575
7576    fn run_interceptor<I: ResponseInterceptor>(
7577        interceptor: &I,
7578        req: &Request,
7579        resp: Response,
7580    ) -> Response {
7581        let ctx = test_context();
7582        let start_time = Instant::now();
7583        let interceptor_ctx = ResponseInterceptorContext::new(req, &ctx, start_time);
7584        futures_executor::block_on(interceptor.intercept(&interceptor_ctx, resp))
7585    }
7586
7587    #[test]
7588    fn timing_interceptor_adds_header() {
7589        let interceptor = TimingInterceptor::new();
7590        let req = test_request();
7591        let resp = Response::with_status(StatusCode::OK);
7592
7593        let result = run_interceptor(&interceptor, &req, resp);
7594
7595        let has_timing = result
7596            .headers()
7597            .iter()
7598            .any(|(name, _)| name == "X-Response-Time");
7599        assert!(has_timing, "Should have X-Response-Time header");
7600    }
7601
7602    #[test]
7603    fn timing_interceptor_with_server_timing() {
7604        let interceptor = TimingInterceptor::new().with_server_timing("app");
7605        let req = test_request();
7606        let resp = Response::with_status(StatusCode::OK);
7607
7608        let result = run_interceptor(&interceptor, &req, resp);
7609
7610        let has_server_timing = result
7611            .headers()
7612            .iter()
7613            .any(|(name, _)| name == "Server-Timing");
7614        assert!(has_server_timing, "Should have Server-Timing header");
7615    }
7616
7617    #[test]
7618    fn timing_interceptor_custom_header_name() {
7619        let interceptor = TimingInterceptor::new().header_name("X-Custom-Time");
7620        let req = test_request();
7621        let resp = Response::with_status(StatusCode::OK);
7622
7623        let result = run_interceptor(&interceptor, &req, resp);
7624
7625        let has_custom = result
7626            .headers()
7627            .iter()
7628            .any(|(name, _)| name == "X-Custom-Time");
7629        assert!(has_custom, "Should have X-Custom-Time header");
7630    }
7631
7632    #[test]
7633    fn debug_info_interceptor_adds_headers() {
7634        let interceptor = DebugInfoInterceptor::new();
7635        let req = test_request();
7636        let resp = Response::with_status(StatusCode::OK);
7637
7638        let result = run_interceptor(&interceptor, &req, resp);
7639
7640        let has_path = result
7641            .headers()
7642            .iter()
7643            .any(|(name, _)| name == "X-Debug-Path");
7644        let has_method = result
7645            .headers()
7646            .iter()
7647            .any(|(name, _)| name == "X-Debug-Method");
7648        let has_timing = result
7649            .headers()
7650            .iter()
7651            .any(|(name, _)| name == "X-Debug-Handler-Time");
7652
7653        assert!(has_path, "Should have X-Debug-Path header");
7654        assert!(has_method, "Should have X-Debug-Method header");
7655        assert!(has_timing, "Should have X-Debug-Handler-Time header");
7656    }
7657
7658    #[test]
7659    fn debug_info_interceptor_custom_prefix() {
7660        let interceptor = DebugInfoInterceptor::new().header_prefix("X-Trace-");
7661        let req = test_request();
7662        let resp = Response::with_status(StatusCode::OK);
7663
7664        let result = run_interceptor(&interceptor, &req, resp);
7665
7666        let has_trace_path = result
7667            .headers()
7668            .iter()
7669            .any(|(name, _)| name == "X-Trace-Path");
7670        assert!(has_trace_path, "Should have X-Trace-Path header");
7671    }
7672
7673    #[test]
7674    fn debug_info_interceptor_selective_options() {
7675        let interceptor = DebugInfoInterceptor::new()
7676            .include_path(true)
7677            .include_method(false)
7678            .include_timing(false)
7679            .include_request_id(false);
7680        let req = test_request();
7681        let resp = Response::with_status(StatusCode::OK);
7682
7683        let result = run_interceptor(&interceptor, &req, resp);
7684
7685        let has_path = result
7686            .headers()
7687            .iter()
7688            .any(|(name, _)| name == "X-Debug-Path");
7689        let has_method = result
7690            .headers()
7691            .iter()
7692            .any(|(name, _)| name == "X-Debug-Method");
7693
7694        assert!(has_path, "Should have X-Debug-Path header");
7695        assert!(!has_method, "Should NOT have X-Debug-Method header");
7696    }
7697
7698    #[test]
7699    fn header_transform_adds_headers() {
7700        let interceptor = HeaderTransformInterceptor::new()
7701            .add("X-Powered-By", b"fastapi_rust".to_vec())
7702            .add("X-Version", b"1.0".to_vec());
7703        let req = test_request();
7704        let resp = Response::with_status(StatusCode::OK);
7705
7706        let result = run_interceptor(&interceptor, &req, resp);
7707
7708        let has_powered_by = result
7709            .headers()
7710            .iter()
7711            .any(|(name, _)| name == "X-Powered-By");
7712        let has_version = result.headers().iter().any(|(name, _)| name == "X-Version");
7713
7714        assert!(has_powered_by, "Should have X-Powered-By header");
7715        assert!(has_version, "Should have X-Version header");
7716    }
7717
7718    #[test]
7719    fn response_body_transform_modifies_body() {
7720        let transformer = ResponseBodyTransform::new(|body| {
7721            let mut result = b"[".to_vec();
7722            result.extend_from_slice(&body);
7723            result.extend_from_slice(b"]");
7724            result
7725        });
7726        let req = test_request();
7727        let resp = Response::with_status(StatusCode::OK)
7728            .body(crate::response::ResponseBody::Bytes(b"hello".to_vec()));
7729
7730        let result = run_interceptor(&transformer, &req, resp);
7731
7732        match result.body_ref() {
7733            crate::response::ResponseBody::Bytes(b) => {
7734                assert_eq!(b, b"[hello]");
7735            }
7736            _ => panic!("Expected bytes body"),
7737        }
7738    }
7739
7740    #[test]
7741    fn response_body_transform_with_content_type_filter() {
7742        let transformer =
7743            ResponseBodyTransform::new(|_| b"transformed".to_vec()).for_content_type("text/plain");
7744        let req = test_request();
7745
7746        // JSON response should NOT be transformed
7747        let json_resp = Response::with_status(StatusCode::OK)
7748            .header("content-type", b"application/json".to_vec())
7749            .body(crate::response::ResponseBody::Bytes(b"original".to_vec()));
7750
7751        let result = run_interceptor(&transformer, &req, json_resp);
7752
7753        match result.body_ref() {
7754            crate::response::ResponseBody::Bytes(b) => {
7755                assert_eq!(b, b"original", "JSON should not be transformed");
7756            }
7757            _ => panic!("Expected bytes body"),
7758        }
7759
7760        // Plain text response SHOULD be transformed
7761        let text_resp = Response::with_status(StatusCode::OK)
7762            .header("content-type", b"text/plain".to_vec())
7763            .body(crate::response::ResponseBody::Bytes(b"original".to_vec()));
7764
7765        let result = run_interceptor(&transformer, &req, text_resp);
7766
7767        match result.body_ref() {
7768            crate::response::ResponseBody::Bytes(b) => {
7769                assert_eq!(b, b"transformed", "Text should be transformed");
7770            }
7771            _ => panic!("Expected bytes body"),
7772        }
7773    }
7774
7775    #[test]
7776    fn error_response_transformer_hides_details() {
7777        let transformer = ErrorResponseTransformer::new()
7778            .hide_details_for_status(StatusCode::INTERNAL_SERVER_ERROR)
7779            .with_replacement_body(b"An error occurred");
7780
7781        let req = test_request();
7782
7783        // 500 response should be transformed
7784        let error_resp = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR).body(
7785            crate::response::ResponseBody::Bytes(b"Sensitive error details".to_vec()),
7786        );
7787
7788        let result = run_interceptor(&transformer, &req, error_resp);
7789
7790        match result.body_ref() {
7791            crate::response::ResponseBody::Bytes(b) => {
7792                assert_eq!(b, b"An error occurred");
7793            }
7794            _ => panic!("Expected bytes body"),
7795        }
7796
7797        // 200 response should NOT be transformed
7798        let ok_resp = Response::with_status(StatusCode::OK)
7799            .body(crate::response::ResponseBody::Bytes(b"Success".to_vec()));
7800
7801        let result = run_interceptor(&transformer, &req, ok_resp);
7802
7803        match result.body_ref() {
7804            crate::response::ResponseBody::Bytes(b) => {
7805                assert_eq!(b, b"Success");
7806            }
7807            _ => panic!("Expected bytes body"),
7808        }
7809    }
7810
7811    #[test]
7812    fn response_interceptor_stack_chains_interceptors() {
7813        let mut stack = ResponseInterceptorStack::new();
7814        stack.push(TimingInterceptor::new());
7815        stack.push(HeaderTransformInterceptor::new().add("X-Extra", b"value".to_vec()));
7816
7817        let req = test_request();
7818        let resp = Response::with_status(StatusCode::OK);
7819
7820        let ctx = test_context();
7821        let start_time = Instant::now();
7822        let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7823        let result = futures_executor::block_on(stack.process(&interceptor_ctx, resp));
7824
7825        let has_timing = result
7826            .headers()
7827            .iter()
7828            .any(|(name, _)| name == "X-Response-Time");
7829        let has_extra = result.headers().iter().any(|(name, _)| name == "X-Extra");
7830
7831        assert!(
7832            has_timing,
7833            "Should have timing header from first interceptor"
7834        );
7835        assert!(
7836            has_extra,
7837            "Should have extra header from second interceptor"
7838        );
7839    }
7840
7841    #[test]
7842    fn response_interceptor_stack_empty_is_noop() {
7843        let stack = ResponseInterceptorStack::new();
7844        assert!(stack.is_empty());
7845        assert_eq!(stack.len(), 0);
7846
7847        let req = test_request();
7848        let resp = Response::with_status(StatusCode::OK)
7849            .body(crate::response::ResponseBody::Bytes(b"unchanged".to_vec()));
7850
7851        let ctx = test_context();
7852        let start_time = Instant::now();
7853        let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7854        let result = futures_executor::block_on(stack.process(&interceptor_ctx, resp));
7855
7856        match result.body_ref() {
7857            crate::response::ResponseBody::Bytes(b) => {
7858                assert_eq!(b, b"unchanged");
7859            }
7860            _ => panic!("Expected bytes body"),
7861        }
7862    }
7863
7864    #[test]
7865    fn interceptor_context_provides_timing() {
7866        let ctx = test_context();
7867        let req = test_request();
7868        let start_time = Instant::now();
7869        std::thread::sleep(std::time::Duration::from_millis(5));
7870
7871        let interceptor_ctx = ResponseInterceptorContext::new(&req, &ctx, start_time);
7872
7873        assert!(
7874            interceptor_ctx.elapsed_ms() >= 5,
7875            "Elapsed time should be at least 5ms"
7876        );
7877        assert!(interceptor_ctx.elapsed().as_millis() >= 5);
7878    }
7879
7880    #[test]
7881    fn conditional_interceptor_applies_conditionally() {
7882        // Only add header if response is 200 OK
7883        let inner = HeaderTransformInterceptor::new().add("X-Success", b"true".to_vec());
7884        let conditional =
7885            ConditionalInterceptor::new(inner, |_ctx, resp| resp.status().as_u16() == 200);
7886
7887        let req = test_request();
7888
7889        // 200 response should get the header
7890        let ok_resp = Response::with_status(StatusCode::OK);
7891        let result = run_interceptor(&conditional, &req, ok_resp);
7892        let has_success = result.headers().iter().any(|(name, _)| name == "X-Success");
7893        assert!(has_success, "200 response should get X-Success header");
7894
7895        // 404 response should NOT get the header
7896        let not_found = Response::with_status(StatusCode::NOT_FOUND);
7897        let result = run_interceptor(&conditional, &req, not_found);
7898        let has_success = result.headers().iter().any(|(name, _)| name == "X-Success");
7899        assert!(!has_success, "404 response should NOT get X-Success header");
7900    }
7901}
7902
7903#[cfg(test)]
7904mod cache_control_tests {
7905    use super::*;
7906    use crate::request::Method;
7907    use crate::response::StatusCode;
7908
7909    fn test_context() -> RequestContext {
7910        RequestContext::new(asupersync::Cx::for_testing(), 1)
7911    }
7912
7913    fn run_after(mw: &CacheControlMiddleware, req: &Request, resp: Response) -> Response {
7914        let ctx = test_context();
7915        let fut = mw.after(&ctx, req, resp);
7916        futures_executor::block_on(fut)
7917    }
7918
7919    #[test]
7920    fn cache_directive_as_str_works() {
7921        assert_eq!(CacheDirective::Public.as_str(), "public");
7922        assert_eq!(CacheDirective::Private.as_str(), "private");
7923        assert_eq!(CacheDirective::NoStore.as_str(), "no-store");
7924        assert_eq!(CacheDirective::NoCache.as_str(), "no-cache");
7925        assert_eq!(CacheDirective::MustRevalidate.as_str(), "must-revalidate");
7926        assert_eq!(CacheDirective::Immutable.as_str(), "immutable");
7927    }
7928
7929    #[test]
7930    fn cache_control_builder_basic() {
7931        let cc = CacheControlBuilder::new()
7932            .public()
7933            .max_age_secs(3600)
7934            .build();
7935        assert!(cc.contains("public"));
7936        assert!(cc.contains("max-age=3600"));
7937    }
7938
7939    #[test]
7940    fn cache_control_builder_complex() {
7941        let cc = CacheControlBuilder::new()
7942            .public()
7943            .max_age_secs(60)
7944            .s_maxage_secs(3600)
7945            .stale_while_revalidate_secs(86400)
7946            .build();
7947        assert!(cc.contains("public"));
7948        assert!(cc.contains("max-age=60"));
7949        assert!(cc.contains("s-maxage=3600"));
7950        assert!(cc.contains("stale-while-revalidate=86400"));
7951    }
7952
7953    #[test]
7954    fn cache_control_builder_no_cache() {
7955        let cc = CacheControlBuilder::new()
7956            .no_store()
7957            .no_cache()
7958            .must_revalidate()
7959            .build();
7960        assert!(cc.contains("no-store"));
7961        assert!(cc.contains("no-cache"));
7962        assert!(cc.contains("must-revalidate"));
7963    }
7964
7965    #[test]
7966    fn cache_preset_no_cache() {
7967        let value = CachePreset::NoCache.to_header_value();
7968        assert!(value.contains("no-store"));
7969        assert!(value.contains("no-cache"));
7970        assert!(value.contains("must-revalidate"));
7971    }
7972
7973    #[test]
7974    fn cache_preset_immutable() {
7975        let value = CachePreset::Immutable.to_header_value();
7976        assert!(value.contains("public"));
7977        assert!(value.contains("max-age=31536000"));
7978        assert!(value.contains("immutable"));
7979    }
7980
7981    #[test]
7982    fn cache_preset_static_assets() {
7983        let value = CachePreset::StaticAssets.to_header_value();
7984        assert!(value.contains("public"));
7985        assert!(value.contains("max-age=86400"));
7986    }
7987
7988    #[test]
7989    fn middleware_adds_cache_control_header() {
7990        let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
7991        let req = Request::new(Method::Get, "/api/test");
7992        let resp = Response::with_status(StatusCode::OK);
7993
7994        let result = run_after(&mw, &req, resp);
7995        let headers = result.headers();
7996        let cc_header = headers
7997            .iter()
7998            .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
7999        assert!(
8000            cc_header.is_some(),
8001            "Cache-Control header should be present"
8002        );
8003        let (_, value) = cc_header.unwrap();
8004        let value_str = String::from_utf8_lossy(value);
8005        assert!(value_str.contains("public"));
8006        assert!(value_str.contains("max-age=3600"));
8007    }
8008
8009    #[test]
8010    fn middleware_skips_post_requests() {
8011        let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
8012        let req = Request::new(Method::Post, "/api/test");
8013        let resp = Response::with_status(StatusCode::OK);
8014
8015        let result = run_after(&mw, &req, resp);
8016        let headers = result.headers();
8017        let cc_header = headers
8018            .iter()
8019            .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
8020        assert!(
8021            cc_header.is_none(),
8022            "Cache-Control should not be added for POST"
8023        );
8024    }
8025
8026    #[test]
8027    fn middleware_skips_error_responses() {
8028        let mw = CacheControlMiddleware::with_preset(CachePreset::PublicOneHour);
8029        let req = Request::new(Method::Get, "/api/test");
8030        let resp = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
8031
8032        let result = run_after(&mw, &req, resp);
8033        let headers = result.headers();
8034        let cc_header = headers
8035            .iter()
8036            .find(|(name, _)| name.eq_ignore_ascii_case("cache-control"));
8037        assert!(
8038            cc_header.is_none(),
8039            "Cache-Control should not be added for error responses"
8040        );
8041    }
8042
8043    #[test]
8044    fn middleware_with_vary_header() {
8045        let mw = CacheControlMiddleware::with_config(
8046            CacheControlConfig::from_preset(CachePreset::PublicOneHour)
8047                .vary("Accept-Encoding")
8048                .vary("Accept-Language"),
8049        );
8050        let req = Request::new(Method::Get, "/api/test");
8051        let resp = Response::with_status(StatusCode::OK);
8052
8053        let result = run_after(&mw, &req, resp);
8054        let headers = result.headers();
8055        let vary_header = headers
8056            .iter()
8057            .find(|(name, _)| name.eq_ignore_ascii_case("vary"));
8058        assert!(vary_header.is_some(), "Vary header should be present");
8059        let (_, value) = vary_header.unwrap();
8060        let value_str = String::from_utf8_lossy(value);
8061        assert!(value_str.contains("Accept-Encoding"));
8062        assert!(value_str.contains("Accept-Language"));
8063    }
8064
8065    #[test]
8066    fn middleware_preserves_existing_cache_control() {
8067        let mw = CacheControlMiddleware::with_config(
8068            CacheControlConfig::from_preset(CachePreset::PublicOneHour).preserve_existing(true),
8069        );
8070        let req = Request::new(Method::Get, "/api/test");
8071        let resp =
8072            Response::with_status(StatusCode::OK).header("Cache-Control", b"max-age=60".to_vec());
8073
8074        let result = run_after(&mw, &req, resp);
8075        let headers = result.headers();
8076        let cc_headers: Vec<_> = headers
8077            .iter()
8078            .filter(|(name, _)| name.eq_ignore_ascii_case("cache-control"))
8079            .collect();
8080        // Should only have the original header, not add a new one
8081        assert_eq!(cc_headers.len(), 1);
8082        let (_, value) = cc_headers[0];
8083        let value_str = String::from_utf8_lossy(value);
8084        assert_eq!(value_str, "max-age=60");
8085    }
8086
8087    #[test]
8088    fn path_pattern_matching_exact() {
8089        assert!(path_matches_pattern("/api/users", "/api/users"));
8090        assert!(!path_matches_pattern("/api/users", "/api/items"));
8091    }
8092
8093    #[test]
8094    fn path_pattern_matching_wildcard() {
8095        assert!(path_matches_pattern("/api/users/123", "/api/users/*"));
8096        assert!(path_matches_pattern("/static/css/style.css", "/static/*"));
8097        assert!(path_matches_pattern("/anything", "*"));
8098    }
8099
8100    #[test]
8101    fn date_formatting_works() {
8102        // Test that format_http_date doesn't panic and produces valid format
8103        let now = std::time::SystemTime::now();
8104        let formatted = format_http_date(now);
8105        // Should contain GMT
8106        assert!(formatted.ends_with(" GMT"));
8107        // Should have day name
8108        let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
8109        assert!(days.iter().any(|d| formatted.starts_with(d)));
8110    }
8111
8112    #[test]
8113    fn leap_year_detection() {
8114        assert!(!is_leap_year(1900)); // Divisible by 100 but not 400
8115        assert!(is_leap_year(2000)); // Divisible by 400
8116        assert!(is_leap_year(2024)); // Divisible by 4 but not 100
8117        assert!(!is_leap_year(2023)); // Not divisible by 4
8118    }
8119}
8120
8121// ===========================================================================
8122// TRACE Rejection Middleware Tests
8123// ===========================================================================
8124
8125#[cfg(test)]
8126mod trace_rejection_tests {
8127    use super::*;
8128    use crate::request::Method;
8129    use crate::response::StatusCode;
8130
8131    fn test_context() -> RequestContext {
8132        RequestContext::new(asupersync::Cx::for_testing(), 1)
8133    }
8134
8135    fn run_before(mw: &TraceRejectionMiddleware, req: &mut Request) -> ControlFlow {
8136        let ctx = test_context();
8137        let fut = mw.before(&ctx, req);
8138        futures_executor::block_on(fut)
8139    }
8140
8141    fn find_header<'a>(headers: &'a [(String, Vec<u8>)], name: &str) -> Option<&'a [u8]> {
8142        headers
8143            .iter()
8144            .find(|(n, _)| n.eq_ignore_ascii_case(name))
8145            .map(|(_, v)| v.as_slice())
8146    }
8147
8148    #[test]
8149    fn trace_request_rejected() {
8150        let mw = TraceRejectionMiddleware::new();
8151        let mut req = Request::new(Method::Trace, "/");
8152
8153        let result = run_before(&mw, &mut req);
8154
8155        match result {
8156            ControlFlow::Break(response) => {
8157                assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
8158            }
8159            ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8160        }
8161    }
8162
8163    #[test]
8164    fn trace_request_with_path() {
8165        let mw = TraceRejectionMiddleware::new();
8166        let mut req = Request::new(Method::Trace, "/api/users/123");
8167
8168        let result = run_before(&mw, &mut req);
8169
8170        match result {
8171            ControlFlow::Break(response) => {
8172                assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
8173            }
8174            ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8175        }
8176    }
8177
8178    #[test]
8179    fn get_request_allowed() {
8180        let mw = TraceRejectionMiddleware::new();
8181        let mut req = Request::new(Method::Get, "/");
8182
8183        let result = run_before(&mw, &mut req);
8184
8185        match result {
8186            ControlFlow::Continue => {} // Expected
8187            ControlFlow::Break(_) => panic!("GET request should be allowed"),
8188        }
8189    }
8190
8191    #[test]
8192    fn post_request_allowed() {
8193        let mw = TraceRejectionMiddleware::new();
8194        let mut req = Request::new(Method::Post, "/api/users");
8195
8196        let result = run_before(&mw, &mut req);
8197
8198        match result {
8199            ControlFlow::Continue => {} // Expected
8200            ControlFlow::Break(_) => panic!("POST request should be allowed"),
8201        }
8202    }
8203
8204    #[test]
8205    fn put_request_allowed() {
8206        let mw = TraceRejectionMiddleware::new();
8207        let mut req = Request::new(Method::Put, "/api/users/1");
8208
8209        let result = run_before(&mw, &mut req);
8210
8211        match result {
8212            ControlFlow::Continue => {} // Expected
8213            ControlFlow::Break(_) => panic!("PUT request should be allowed"),
8214        }
8215    }
8216
8217    #[test]
8218    fn delete_request_allowed() {
8219        let mw = TraceRejectionMiddleware::new();
8220        let mut req = Request::new(Method::Delete, "/api/users/1");
8221
8222        let result = run_before(&mw, &mut req);
8223
8224        match result {
8225            ControlFlow::Continue => {} // Expected
8226            ControlFlow::Break(_) => panic!("DELETE request should be allowed"),
8227        }
8228    }
8229
8230    #[test]
8231    fn patch_request_allowed() {
8232        let mw = TraceRejectionMiddleware::new();
8233        let mut req = Request::new(Method::Patch, "/api/users/1");
8234
8235        let result = run_before(&mw, &mut req);
8236
8237        match result {
8238            ControlFlow::Continue => {} // Expected
8239            ControlFlow::Break(_) => panic!("PATCH request should be allowed"),
8240        }
8241    }
8242
8243    #[test]
8244    fn options_request_allowed() {
8245        let mw = TraceRejectionMiddleware::new();
8246        let mut req = Request::new(Method::Options, "/api/users");
8247
8248        let result = run_before(&mw, &mut req);
8249
8250        match result {
8251            ControlFlow::Continue => {} // Expected
8252            ControlFlow::Break(_) => panic!("OPTIONS request should be allowed"),
8253        }
8254    }
8255
8256    #[test]
8257    fn head_request_allowed() {
8258        let mw = TraceRejectionMiddleware::new();
8259        let mut req = Request::new(Method::Head, "/");
8260
8261        let result = run_before(&mw, &mut req);
8262
8263        match result {
8264            ControlFlow::Continue => {} // Expected
8265            ControlFlow::Break(_) => panic!("HEAD request should be allowed"),
8266        }
8267    }
8268
8269    #[test]
8270    fn response_includes_allow_header() {
8271        let mw = TraceRejectionMiddleware::new();
8272        let mut req = Request::new(Method::Trace, "/");
8273
8274        let result = run_before(&mw, &mut req);
8275
8276        match result {
8277            ControlFlow::Break(response) => {
8278                let allow_header = find_header(response.headers(), "Allow");
8279                assert!(
8280                    allow_header.is_some(),
8281                    "Response should include Allow header"
8282                );
8283            }
8284            ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8285        }
8286    }
8287
8288    #[test]
8289    fn response_has_json_content_type() {
8290        let mw = TraceRejectionMiddleware::new();
8291        let mut req = Request::new(Method::Trace, "/");
8292
8293        let result = run_before(&mw, &mut req);
8294
8295        match result {
8296            ControlFlow::Break(response) => {
8297                let ct_header = find_header(response.headers(), "Content-Type");
8298                assert_eq!(ct_header, Some(b"application/json".as_slice()));
8299            }
8300            ControlFlow::Continue => panic!("TRACE request should have been rejected"),
8301        }
8302    }
8303
8304    #[test]
8305    fn default_enables_logging() {
8306        let mw = TraceRejectionMiddleware::new();
8307        assert!(mw.log_attempts);
8308    }
8309
8310    #[test]
8311    fn log_attempts_can_be_disabled() {
8312        let mw = TraceRejectionMiddleware::new().log_attempts(false);
8313        assert!(!mw.log_attempts);
8314    }
8315
8316    #[test]
8317    fn middleware_name() {
8318        let mw = TraceRejectionMiddleware::new();
8319        assert_eq!(mw.name(), "TraceRejection");
8320    }
8321
8322    #[test]
8323    fn default_impl() {
8324        let mw = TraceRejectionMiddleware::default();
8325        assert!(mw.log_attempts);
8326    }
8327}
8328
8329// ===========================================================================
8330// End TRACE Rejection Middleware Tests
8331// ===========================================================================
8332
8333// ===========================================================================
8334// HTTPS Redirect Middleware Tests
8335// ===========================================================================
8336
8337#[cfg(test)]
8338mod https_redirect_tests {
8339    use super::*;
8340    use crate::request::Method;
8341    use crate::response::StatusCode;
8342
8343    fn test_context() -> RequestContext {
8344        RequestContext::new(asupersync::Cx::for_testing(), 1)
8345    }
8346
8347    fn run_before(mw: &HttpsRedirectMiddleware, req: &mut Request) -> ControlFlow {
8348        let ctx = test_context();
8349        let fut = mw.before(&ctx, req);
8350        futures_executor::block_on(fut)
8351    }
8352
8353    fn run_after(mw: &HttpsRedirectMiddleware, req: &Request, resp: Response) -> Response {
8354        let ctx = test_context();
8355        let fut = mw.after(&ctx, req, resp);
8356        futures_executor::block_on(fut)
8357    }
8358
8359    fn find_header<'a>(headers: &'a [(String, Vec<u8>)], name: &str) -> Option<&'a [u8]> {
8360        headers
8361            .iter()
8362            .find(|(n, _)| n.eq_ignore_ascii_case(name))
8363            .map(|(_, v)| v.as_slice())
8364    }
8365
8366    #[test]
8367    fn http_request_redirected() {
8368        let mw = HttpsRedirectMiddleware::new();
8369        let mut req = Request::new(Method::Get, "/");
8370        req.headers_mut().insert("Host", b"example.com".to_vec());
8371
8372        let result = run_before(&mw, &mut req);
8373
8374        match result {
8375            ControlFlow::Break(response) => {
8376                assert_eq!(response.status(), StatusCode::MOVED_PERMANENTLY);
8377                let location = find_header(response.headers(), "Location");
8378                assert_eq!(location, Some(b"https://example.com/".as_slice()));
8379            }
8380            ControlFlow::Continue => panic!("HTTP request should be redirected"),
8381        }
8382    }
8383
8384    #[test]
8385    fn http_request_with_path_and_query() {
8386        let mw = HttpsRedirectMiddleware::new();
8387        let mut req = Request::new(Method::Get, "/api/users?page=1");
8388        req.headers_mut().insert("Host", b"example.com".to_vec());
8389
8390        let result = run_before(&mw, &mut req);
8391
8392        match result {
8393            ControlFlow::Break(response) => {
8394                let location = find_header(response.headers(), "Location");
8395                assert_eq!(
8396                    location,
8397                    Some(b"https://example.com/api/users?page=1".as_slice())
8398                );
8399            }
8400            ControlFlow::Continue => panic!("HTTP request should be redirected"),
8401        }
8402    }
8403
8404    #[test]
8405    fn https_request_not_redirected() {
8406        let mw = HttpsRedirectMiddleware::new();
8407        let mut req = Request::new(Method::Get, "/");
8408        req.headers_mut().insert("Host", b"example.com".to_vec());
8409        req.headers_mut()
8410            .insert("X-Forwarded-Proto", b"https".to_vec());
8411
8412        let result = run_before(&mw, &mut req);
8413
8414        match result {
8415            ControlFlow::Continue => {} // Expected
8416            ControlFlow::Break(_) => panic!("HTTPS request should not be redirected"),
8417        }
8418    }
8419
8420    #[test]
8421    fn x_forwarded_ssl_recognized() {
8422        let mw = HttpsRedirectMiddleware::new();
8423        let mut req = Request::new(Method::Get, "/");
8424        req.headers_mut().insert("Host", b"example.com".to_vec());
8425        req.headers_mut().insert("X-Forwarded-Ssl", b"on".to_vec());
8426
8427        let result = run_before(&mw, &mut req);
8428
8429        match result {
8430            ControlFlow::Continue => {} // Expected
8431            ControlFlow::Break(_) => panic!("Request with X-Forwarded-Ssl=on should not redirect"),
8432        }
8433    }
8434
8435    #[test]
8436    fn excluded_path_not_redirected() {
8437        let mw = HttpsRedirectMiddleware::new().exclude_path("/health");
8438        let mut req = Request::new(Method::Get, "/health");
8439        req.headers_mut().insert("Host", b"example.com".to_vec());
8440
8441        let result = run_before(&mw, &mut req);
8442
8443        match result {
8444            ControlFlow::Continue => {} // Expected
8445            ControlFlow::Break(_) => panic!("Excluded path should not be redirected"),
8446        }
8447    }
8448
8449    #[test]
8450    fn excluded_path_prefix_matches() {
8451        let mw = HttpsRedirectMiddleware::new().exclude_path("/health");
8452        let mut req = Request::new(Method::Get, "/health/live");
8453        req.headers_mut().insert("Host", b"example.com".to_vec());
8454
8455        let result = run_before(&mw, &mut req);
8456
8457        match result {
8458            ControlFlow::Continue => {} // Expected
8459            ControlFlow::Break(_) => panic!("Path with excluded prefix should not be redirected"),
8460        }
8461    }
8462
8463    #[test]
8464    fn temporary_redirect_option() {
8465        let mw = HttpsRedirectMiddleware::new().permanent_redirect(false);
8466        let mut req = Request::new(Method::Get, "/");
8467        req.headers_mut().insert("Host", b"example.com".to_vec());
8468
8469        let result = run_before(&mw, &mut req);
8470
8471        match result {
8472            ControlFlow::Break(response) => {
8473                assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT);
8474            }
8475            ControlFlow::Continue => panic!("HTTP request should be redirected"),
8476        }
8477    }
8478
8479    #[test]
8480    fn redirect_disabled() {
8481        let mw = HttpsRedirectMiddleware::new().redirect_enabled(false);
8482        let mut req = Request::new(Method::Get, "/");
8483        req.headers_mut().insert("Host", b"example.com".to_vec());
8484
8485        let result = run_before(&mw, &mut req);
8486
8487        match result {
8488            ControlFlow::Continue => {} // Expected
8489            ControlFlow::Break(_) => panic!("Redirects are disabled, should continue"),
8490        }
8491    }
8492
8493    #[test]
8494    fn hsts_header_on_https_response() {
8495        let mw = HttpsRedirectMiddleware::new();
8496        let mut req = Request::new(Method::Get, "/");
8497        req.headers_mut()
8498            .insert("X-Forwarded-Proto", b"https".to_vec());
8499
8500        let response = Response::with_status(StatusCode::OK);
8501        let result = run_after(&mw, &req, response);
8502
8503        let hsts = find_header(result.headers(), "Strict-Transport-Security");
8504        assert!(
8505            hsts.is_some(),
8506            "HSTS header should be present on HTTPS response"
8507        );
8508        let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8509        assert!(hsts_str.contains("max-age=31536000"));
8510    }
8511
8512    #[test]
8513    fn hsts_header_not_on_http_response() {
8514        let mw = HttpsRedirectMiddleware::new().redirect_enabled(false);
8515        let req = Request::new(Method::Get, "/");
8516        // No X-Forwarded-Proto, so this is HTTP
8517
8518        let response = Response::with_status(StatusCode::OK);
8519        let result = run_after(&mw, &req, response);
8520
8521        let hsts = find_header(result.headers(), "Strict-Transport-Security");
8522        assert!(hsts.is_none(), "HSTS header should not be on HTTP response");
8523    }
8524
8525    #[test]
8526    fn hsts_with_include_subdomains() {
8527        let mw = HttpsRedirectMiddleware::new().include_subdomains(true);
8528        let mut req = Request::new(Method::Get, "/");
8529        req.headers_mut()
8530            .insert("X-Forwarded-Proto", b"https".to_vec());
8531
8532        let response = Response::with_status(StatusCode::OK);
8533        let result = run_after(&mw, &req, response);
8534
8535        let hsts = find_header(result.headers(), "Strict-Transport-Security");
8536        let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8537        assert!(hsts_str.contains("includeSubDomains"));
8538    }
8539
8540    #[test]
8541    fn hsts_with_preload() {
8542        let mw = HttpsRedirectMiddleware::new().preload(true);
8543        let mut req = Request::new(Method::Get, "/");
8544        req.headers_mut()
8545            .insert("X-Forwarded-Proto", b"https".to_vec());
8546
8547        let response = Response::with_status(StatusCode::OK);
8548        let result = run_after(&mw, &req, response);
8549
8550        let hsts = find_header(result.headers(), "Strict-Transport-Security");
8551        let hsts_str = String::from_utf8_lossy(hsts.unwrap());
8552        assert!(hsts_str.contains("preload"));
8553    }
8554
8555    #[test]
8556    fn hsts_disabled_with_zero_max_age() {
8557        let mw = HttpsRedirectMiddleware::new().hsts_max_age_secs(0);
8558        let mut req = Request::new(Method::Get, "/");
8559        req.headers_mut()
8560            .insert("X-Forwarded-Proto", b"https".to_vec());
8561
8562        let response = Response::with_status(StatusCode::OK);
8563        let result = run_after(&mw, &req, response);
8564
8565        let hsts = find_header(result.headers(), "Strict-Transport-Security");
8566        assert!(hsts.is_none(), "HSTS should be disabled with max-age=0");
8567    }
8568
8569    #[test]
8570    fn custom_https_port() {
8571        let mw = HttpsRedirectMiddleware::new().https_port(8443);
8572        let mut req = Request::new(Method::Get, "/");
8573        req.headers_mut().insert("Host", b"example.com".to_vec());
8574
8575        let result = run_before(&mw, &mut req);
8576
8577        match result {
8578            ControlFlow::Break(response) => {
8579                let location = find_header(response.headers(), "Location");
8580                assert_eq!(location, Some(b"https://example.com:8443/".as_slice()));
8581            }
8582            ControlFlow::Continue => panic!("HTTP request should be redirected"),
8583        }
8584    }
8585
8586    #[test]
8587    fn host_with_port_stripped() {
8588        let mw = HttpsRedirectMiddleware::new();
8589        let mut req = Request::new(Method::Get, "/");
8590        req.headers_mut()
8591            .insert("Host", b"example.com:8080".to_vec());
8592
8593        let result = run_before(&mw, &mut req);
8594
8595        match result {
8596            ControlFlow::Break(response) => {
8597                let location = find_header(response.headers(), "Location");
8598                // Port should be stripped from host, using default 443
8599                assert_eq!(location, Some(b"https://example.com/".as_slice()));
8600            }
8601            ControlFlow::Continue => panic!("HTTP request should be redirected"),
8602        }
8603    }
8604
8605    #[test]
8606    fn middleware_name() {
8607        let mw = HttpsRedirectMiddleware::new();
8608        assert_eq!(mw.name(), "HttpsRedirect");
8609    }
8610
8611    #[test]
8612    fn default_impl() {
8613        let mw = HttpsRedirectMiddleware::default();
8614        assert!(mw.config.redirect_enabled);
8615        assert!(mw.config.permanent_redirect);
8616        assert_eq!(mw.config.hsts_max_age_secs, 31_536_000);
8617    }
8618
8619    #[test]
8620    fn config_builder() {
8621        let mw = HttpsRedirectMiddleware::new()
8622            .redirect_enabled(false)
8623            .permanent_redirect(false)
8624            .hsts_max_age_secs(86400)
8625            .include_subdomains(true)
8626            .preload(true)
8627            .https_port(8443);
8628
8629        assert!(!mw.config.redirect_enabled);
8630        assert!(!mw.config.permanent_redirect);
8631        assert_eq!(mw.config.hsts_max_age_secs, 86400);
8632        assert!(mw.config.hsts_include_subdomains);
8633        assert!(mw.config.hsts_preload);
8634        assert_eq!(mw.config.https_port, 8443);
8635    }
8636
8637    #[test]
8638    fn exclude_paths_method() {
8639        let mw = HttpsRedirectMiddleware::new()
8640            .exclude_paths(vec!["/health".to_string(), "/ready".to_string()]);
8641
8642        assert_eq!(mw.config.exclude_paths.len(), 2);
8643        assert!(mw.config.exclude_paths.contains(&"/health".to_string()));
8644        assert!(mw.config.exclude_paths.contains(&"/ready".to_string()));
8645    }
8646}
8647
8648// ===========================================================================
8649// End HTTPS Redirect Middleware Tests
8650// ===========================================================================
8651
8652// ===========================================================================
8653// End ETag Middleware
8654// ===========================================================================
8655
8656#[cfg(test)]
8657mod tests {
8658    use super::*;
8659    use crate::response::{ResponseBody, StatusCode};
8660
8661    // Test middleware that adds a header
8662    #[allow(dead_code)]
8663    struct AddHeaderMiddleware {
8664        name: &'static str,
8665        value: &'static [u8],
8666    }
8667
8668    impl Middleware for AddHeaderMiddleware {
8669        fn after<'a>(
8670            &'a self,
8671            _ctx: &'a RequestContext,
8672            _req: &'a Request,
8673            response: Response,
8674        ) -> BoxFuture<'a, Response> {
8675            Box::pin(async move { response.header(self.name, self.value.to_vec()) })
8676        }
8677    }
8678
8679    // Test middleware that short-circuits
8680    #[allow(dead_code)]
8681    struct BlockingMiddleware;
8682
8683    impl Middleware for BlockingMiddleware {
8684        fn before<'a>(
8685            &'a self,
8686            _ctx: &'a RequestContext,
8687            _req: &'a mut Request,
8688        ) -> BoxFuture<'a, ControlFlow> {
8689            Box::pin(async {
8690                ControlFlow::Break(
8691                    Response::with_status(StatusCode::FORBIDDEN)
8692                        .body(ResponseBody::Bytes(b"blocked".to_vec())),
8693                )
8694            })
8695        }
8696    }
8697
8698    // Test middleware that tracks calls
8699    #[allow(dead_code)]
8700    struct TrackingMiddleware {
8701        before_count: std::sync::atomic::AtomicUsize,
8702        after_count: std::sync::atomic::AtomicUsize,
8703    }
8704
8705    #[allow(dead_code)]
8706    impl TrackingMiddleware {
8707        fn new() -> Self {
8708            Self {
8709                before_count: std::sync::atomic::AtomicUsize::new(0),
8710                after_count: std::sync::atomic::AtomicUsize::new(0),
8711            }
8712        }
8713
8714        fn before_count(&self) -> usize {
8715            self.before_count.load(std::sync::atomic::Ordering::SeqCst)
8716        }
8717
8718        fn after_count(&self) -> usize {
8719            self.after_count.load(std::sync::atomic::Ordering::SeqCst)
8720        }
8721    }
8722
8723    impl Middleware for TrackingMiddleware {
8724        fn before<'a>(
8725            &'a self,
8726            _ctx: &'a RequestContext,
8727            _req: &'a mut Request,
8728        ) -> BoxFuture<'a, ControlFlow> {
8729            self.before_count
8730                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8731            Box::pin(async { ControlFlow::Continue })
8732        }
8733
8734        fn after<'a>(
8735            &'a self,
8736            _ctx: &'a RequestContext,
8737            _req: &'a Request,
8738            response: Response,
8739        ) -> BoxFuture<'a, Response> {
8740            self.after_count
8741                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
8742            Box::pin(async move { response })
8743        }
8744    }
8745
8746    #[test]
8747    fn control_flow_variants() {
8748        let cont = ControlFlow::Continue;
8749        assert!(cont.is_continue());
8750        assert!(!cont.is_break());
8751
8752        let brk = ControlFlow::Break(Response::ok());
8753        assert!(!brk.is_continue());
8754        assert!(brk.is_break());
8755    }
8756
8757    #[test]
8758    fn middleware_stack_empty() {
8759        let stack = MiddlewareStack::new();
8760        assert!(stack.is_empty());
8761        assert_eq!(stack.len(), 0);
8762    }
8763
8764    #[test]
8765    fn middleware_stack_push() {
8766        let mut stack = MiddlewareStack::new();
8767        stack.push(NoopMiddleware);
8768        stack.push(NoopMiddleware);
8769        assert_eq!(stack.len(), 2);
8770        assert!(!stack.is_empty());
8771    }
8772
8773    #[test]
8774    fn noop_middleware_name() {
8775        let mw = NoopMiddleware;
8776        assert_eq!(mw.name(), "Noop");
8777    }
8778
8779    #[test]
8780    fn logging_redacts_sensitive_headers() {
8781        let mut headers = crate::request::Headers::new();
8782        headers.insert("Authorization", b"secret".to_vec());
8783        headers.insert("X-Request-Id", b"abc123".to_vec());
8784
8785        let redacted = super::default_redacted_headers();
8786        let formatted = super::format_headers(headers.iter(), &redacted);
8787
8788        assert!(formatted.contains("authorization=<redacted>"));
8789        assert!(formatted.contains("x-request-id=abc123"));
8790    }
8791
8792    #[test]
8793    fn logging_body_truncation() {
8794        let body = b"abcdef";
8795        let preview = super::format_bytes(body, 4);
8796        assert_eq!(preview, "abcd...");
8797
8798        let preview_full = super::format_bytes(body, 10);
8799        assert_eq!(preview_full, "abcdef");
8800    }
8801
8802    fn test_context() -> RequestContext {
8803        let cx = asupersync::Cx::for_testing();
8804        RequestContext::new(cx, 1)
8805    }
8806
8807    fn header_value(response: &Response, name: &str) -> Option<String> {
8808        response
8809            .headers()
8810            .iter()
8811            .find(|(n, _)| n.eq_ignore_ascii_case(name))
8812            .and_then(|(_, v)| std::str::from_utf8(v).ok())
8813            .map(ToString::to_string)
8814    }
8815
8816    #[test]
8817    fn cors_exact_origin_allows() {
8818        let cors = Cors::new().allow_origin("https://example.com");
8819        let ctx = test_context();
8820        let mut req = Request::new(crate::request::Method::Get, "/");
8821        req.headers_mut()
8822            .insert("origin", b"https://example.com".to_vec());
8823
8824        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8825        assert!(matches!(result, ControlFlow::Continue));
8826
8827        let response = Response::ok().body(ResponseBody::Bytes(b"ok".to_vec()));
8828        let response = futures_executor::block_on(cors.after(&ctx, &req, response));
8829
8830        assert_eq!(
8831            header_value(&response, "access-control-allow-origin"),
8832            Some("https://example.com".to_string())
8833        );
8834        assert_eq!(header_value(&response, "vary"), Some("Origin".to_string()));
8835    }
8836
8837    #[test]
8838    fn cors_wildcard_origin_allows() {
8839        let cors = Cors::new().allow_origin_wildcard("https://*.example.com");
8840        let ctx = test_context();
8841        let mut req = Request::new(crate::request::Method::Get, "/");
8842        req.headers_mut()
8843            .insert("origin", b"https://api.example.com".to_vec());
8844
8845        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8846        assert!(matches!(result, ControlFlow::Continue));
8847    }
8848
8849    #[test]
8850    fn cors_regex_origin_allows() {
8851        let cors = Cors::new().allow_origin_regex(r"^https://.*\.example\.com$");
8852        let ctx = test_context();
8853        let mut req = Request::new(crate::request::Method::Get, "/");
8854        req.headers_mut()
8855            .insert("origin", b"https://svc.example.com".to_vec());
8856
8857        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8858        assert!(matches!(result, ControlFlow::Continue));
8859    }
8860
8861    #[test]
8862    fn cors_preflight_handled() {
8863        let cors = Cors::new()
8864            .allow_any_origin()
8865            .allow_headers(["x-test", "content-type"])
8866            .max_age(600);
8867        let ctx = test_context();
8868        let mut req = Request::new(crate::request::Method::Options, "/");
8869        req.headers_mut()
8870            .insert("origin", b"https://example.com".to_vec());
8871        req.headers_mut()
8872            .insert("access-control-request-method", b"POST".to_vec());
8873        req.headers_mut().insert(
8874            "access-control-request-headers",
8875            b"x-test, content-type".to_vec(),
8876        );
8877
8878        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8879        let ControlFlow::Break(response) = result else {
8880            panic!("expected preflight break");
8881        };
8882
8883        assert_eq!(response.status().as_u16(), 204);
8884        assert_eq!(
8885            header_value(&response, "access-control-allow-origin"),
8886            Some("*".to_string())
8887        );
8888        assert_eq!(
8889            header_value(&response, "access-control-allow-methods"),
8890            Some("GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD".to_string())
8891        );
8892        assert_eq!(
8893            header_value(&response, "access-control-allow-headers"),
8894            Some("x-test, content-type".to_string())
8895        );
8896        assert_eq!(
8897            header_value(&response, "access-control-max-age"),
8898            Some("600".to_string())
8899        );
8900    }
8901
8902    #[test]
8903    fn cors_credentials_echo_origin() {
8904        let cors = Cors::new().allow_any_origin().allow_credentials(true);
8905        let ctx = test_context();
8906        let mut req = Request::new(crate::request::Method::Get, "/");
8907        req.headers_mut()
8908            .insert("origin", b"https://example.com".to_vec());
8909
8910        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8911        assert!(matches!(result, ControlFlow::Continue));
8912
8913        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
8914        assert_eq!(
8915            header_value(&response, "access-control-allow-origin"),
8916            Some("https://example.com".to_string())
8917        );
8918        assert_eq!(
8919            header_value(&response, "access-control-allow-credentials"),
8920            Some("true".to_string())
8921        );
8922    }
8923
8924    // CORS Spec Compliance Tests (bd-l1qe)
8925    // According to the Fetch Standard, when credentials mode is true,
8926    // the Access-Control-Allow-Origin header MUST NOT be "*".
8927
8928    #[test]
8929    fn cors_spec_compliance_credentials_never_wildcard_origin() {
8930        // When credentials are enabled, Access-Control-Allow-Origin
8931        // must echo the specific origin, never "*"
8932        let cors = Cors::new().allow_any_origin().allow_credentials(true);
8933        let ctx = test_context();
8934
8935        // Test with various origins
8936        for origin in &[
8937            "https://example.com",
8938            "https://api.example.com",
8939            "http://localhost:3000",
8940        ] {
8941            let mut req = Request::new(crate::request::Method::Get, "/");
8942            req.headers_mut()
8943                .insert("origin", origin.as_bytes().to_vec());
8944
8945            futures_executor::block_on(cors.before(&ctx, &mut req));
8946            let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
8947
8948            let allow_origin = header_value(&response, "access-control-allow-origin");
8949            assert_eq!(
8950                allow_origin,
8951                Some((*origin).to_string()),
8952                "With credentials enabled, Access-Control-Allow-Origin must echo '{}', not '*'",
8953                origin
8954            );
8955            assert_ne!(
8956                allow_origin,
8957                Some("*".to_string()),
8958                "CORS spec violation: credentials + wildcard origin is forbidden"
8959            );
8960        }
8961    }
8962
8963    #[test]
8964    fn cors_spec_compliance_preflight_with_credentials() {
8965        // Preflight response with credentials should also echo origin, not "*"
8966        let cors = Cors::new()
8967            .allow_any_origin()
8968            .allow_credentials(true)
8969            .allow_headers(["content-type", "x-custom-header"]);
8970        let ctx = test_context();
8971
8972        let mut req = Request::new(crate::request::Method::Options, "/");
8973        req.headers_mut()
8974            .insert("origin", b"https://example.com".to_vec());
8975        req.headers_mut()
8976            .insert("access-control-request-method", b"POST".to_vec());
8977        req.headers_mut()
8978            .insert("access-control-request-headers", b"content-type".to_vec());
8979
8980        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
8981        let ControlFlow::Break(response) = result else {
8982            panic!("expected preflight break");
8983        };
8984
8985        // Verify Access-Control-Allow-Origin is NOT "*" with credentials
8986        let allow_origin = header_value(&response, "access-control-allow-origin");
8987        assert_eq!(allow_origin, Some("https://example.com".to_string()));
8988        assert_ne!(
8989            allow_origin,
8990            Some("*".to_string()),
8991            "CORS spec violation: preflight with credentials must not use wildcard origin"
8992        );
8993
8994        // Verify credentials header is set
8995        assert_eq!(
8996            header_value(&response, "access-control-allow-credentials"),
8997            Some("true".to_string())
8998        );
8999    }
9000
9001    #[test]
9002    fn cors_spec_without_credentials_allows_wildcard() {
9003        // When credentials are NOT enabled, "*" is allowed for Access-Control-Allow-Origin
9004        let cors = Cors::new().allow_any_origin();
9005        let ctx = test_context();
9006        let mut req = Request::new(crate::request::Method::Get, "/");
9007        req.headers_mut()
9008            .insert("origin", b"https://example.com".to_vec());
9009
9010        futures_executor::block_on(cors.before(&ctx, &mut req));
9011        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9012
9013        // Without credentials, wildcard IS allowed
9014        assert_eq!(
9015            header_value(&response, "access-control-allow-origin"),
9016            Some("*".to_string())
9017        );
9018        // Should NOT have credentials header
9019        assert!(header_value(&response, "access-control-allow-credentials").is_none());
9020    }
9021
9022    #[test]
9023    fn cors_disallowed_preflight_forbidden() {
9024        let cors = Cors::new().allow_origin("https://good.example");
9025        let ctx = test_context();
9026        let mut req = Request::new(crate::request::Method::Options, "/");
9027        req.headers_mut()
9028            .insert("origin", b"https://evil.example".to_vec());
9029        req.headers_mut()
9030            .insert("access-control-request-method", b"GET".to_vec());
9031
9032        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9033        let ControlFlow::Break(response) = result else {
9034            panic!("expected forbidden preflight");
9035        };
9036        assert_eq!(response.status().as_u16(), 403);
9037    }
9038
9039    #[test]
9040    fn cors_simple_request_disallowed_origin_no_headers() {
9041        // Non-preflight request from disallowed origin should proceed but not get CORS headers
9042        let cors = Cors::new().allow_origin("https://good.example");
9043        let ctx = test_context();
9044        let mut req = Request::new(crate::request::Method::Get, "/");
9045        req.headers_mut()
9046            .insert("origin", b"https://evil.example".to_vec());
9047
9048        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9049        // Simple requests proceed (browser will block based on missing headers)
9050        assert!(matches!(result, ControlFlow::Continue));
9051
9052        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9053        // No CORS headers should be added for disallowed origin
9054        assert!(header_value(&response, "access-control-allow-origin").is_none());
9055    }
9056
9057    #[test]
9058    fn cors_expose_headers_configuration() {
9059        let cors = Cors::new()
9060            .allow_any_origin()
9061            .expose_headers(["x-custom-header", "x-another-header"]);
9062        let ctx = test_context();
9063        let mut req = Request::new(crate::request::Method::Get, "/");
9064        req.headers_mut()
9065            .insert("origin", b"https://example.com".to_vec());
9066
9067        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9068        assert!(matches!(result, ControlFlow::Continue));
9069
9070        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9071        assert_eq!(
9072            header_value(&response, "access-control-expose-headers"),
9073            Some("x-custom-header, x-another-header".to_string())
9074        );
9075    }
9076
9077    #[test]
9078    fn cors_any_origin_sets_wildcard() {
9079        let cors = Cors::new().allow_any_origin();
9080        let ctx = test_context();
9081        let mut req = Request::new(crate::request::Method::Get, "/");
9082        req.headers_mut()
9083            .insert("origin", b"https://any-site.com".to_vec());
9084
9085        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9086        assert!(matches!(result, ControlFlow::Continue));
9087
9088        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9089        assert_eq!(
9090            header_value(&response, "access-control-allow-origin"),
9091            Some("*".to_string())
9092        );
9093    }
9094
9095    #[test]
9096    fn cors_config_allows_method_override() {
9097        // Test that allow_methods overrides defaults
9098        let cors = Cors::new()
9099            .allow_any_origin()
9100            .allow_methods([crate::request::Method::Get, crate::request::Method::Post]);
9101        let ctx = test_context();
9102        let mut req = Request::new(crate::request::Method::Options, "/");
9103        req.headers_mut()
9104            .insert("origin", b"https://example.com".to_vec());
9105        req.headers_mut()
9106            .insert("access-control-request-method", b"POST".to_vec());
9107
9108        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9109        let ControlFlow::Break(response) = result else {
9110            panic!("expected preflight break");
9111        };
9112        assert_eq!(
9113            header_value(&response, "access-control-allow-methods"),
9114            Some("GET, POST".to_string())
9115        );
9116    }
9117
9118    #[test]
9119    fn cors_no_origin_header_skips_cors() {
9120        // Request without Origin header should not get CORS headers
9121        let cors = Cors::new().allow_any_origin();
9122        let ctx = test_context();
9123        let mut req = Request::new(crate::request::Method::Get, "/");
9124
9125        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9126        assert!(matches!(result, ControlFlow::Continue));
9127
9128        let response = futures_executor::block_on(cors.after(&ctx, &req, Response::ok()));
9129        assert!(header_value(&response, "access-control-allow-origin").is_none());
9130    }
9131
9132    #[test]
9133    fn cors_middleware_name() {
9134        let cors = Cors::new();
9135        assert_eq!(cors.name(), "Cors");
9136    }
9137
9138    #[test]
9139    fn cors_empty_allowed_headers_does_not_reflect_request_headers() {
9140        // When allowed_headers is empty (default), the CORS middleware should
9141        // NOT reflect the client's Access-Control-Request-Headers back. That
9142        // would effectively allow arbitrary headers — a security risk.
9143        let cors = Cors::new().allow_any_origin(); // default: allowed_headers = []
9144        let ctx = test_context();
9145        let mut req = Request::new(crate::request::Method::Options, "/api");
9146        req.headers_mut()
9147            .insert("origin", b"https://example.com".to_vec());
9148        req.headers_mut()
9149            .insert("access-control-request-method", b"GET".to_vec());
9150        req.headers_mut().insert(
9151            "access-control-request-headers",
9152            b"x-evil-custom, authorization".to_vec(),
9153        );
9154
9155        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9156        if let ControlFlow::Break(response) = result {
9157            // Preflight response should NOT have access-control-allow-headers
9158            // when no allowed_headers are configured.
9159            assert_eq!(
9160                header_value(&response, "access-control-allow-headers"),
9161                None,
9162                "Empty allowed_headers must not reflect request headers"
9163            );
9164        } else {
9165            panic!("Preflight should have been handled (Break)");
9166        }
9167    }
9168
9169    #[test]
9170    fn cors_explicit_allowed_headers_returned_in_preflight() {
9171        let cors = Cors::new()
9172            .allow_any_origin()
9173            .allow_headers(["x-token", "content-type"]);
9174        let ctx = test_context();
9175        let mut req = Request::new(crate::request::Method::Options, "/api");
9176        req.headers_mut()
9177            .insert("origin", b"https://example.com".to_vec());
9178        req.headers_mut()
9179            .insert("access-control-request-method", b"POST".to_vec());
9180
9181        let result = futures_executor::block_on(cors.before(&ctx, &mut req));
9182        if let ControlFlow::Break(response) = result {
9183            let headers_val = header_value(&response, "access-control-allow-headers");
9184            assert!(headers_val.is_some());
9185            let val = headers_val.unwrap();
9186            assert!(val.contains("x-token"));
9187            assert!(val.contains("content-type"));
9188        } else {
9189            panic!("Preflight should have been handled (Break)");
9190        }
9191    }
9192
9193    // =========================================================================
9194    // Request ID Middleware tests
9195    // =========================================================================
9196
9197    #[test]
9198    fn request_id_generates_unique_ids() {
9199        let id1 = RequestId::generate();
9200        let id2 = RequestId::generate();
9201        let id3 = RequestId::generate();
9202
9203        assert_ne!(id1, id2);
9204        assert_ne!(id2, id3);
9205        assert_ne!(id1, id3);
9206
9207        // IDs should be non-empty
9208        assert!(!id1.as_str().is_empty());
9209        assert!(!id2.as_str().is_empty());
9210        assert!(!id3.as_str().is_empty());
9211    }
9212
9213    #[test]
9214    fn request_id_display() {
9215        let id = RequestId::new("test-request-123");
9216        assert_eq!(format!("{}", id), "test-request-123");
9217    }
9218
9219    #[test]
9220    fn request_id_from_string() {
9221        let id: RequestId = "my-id".into();
9222        assert_eq!(id.as_str(), "my-id");
9223
9224        let id2: RequestId = String::from("my-id-2").into();
9225        assert_eq!(id2.as_str(), "my-id-2");
9226    }
9227
9228    #[test]
9229    fn request_id_config_defaults() {
9230        let config = RequestIdConfig::default();
9231        assert_eq!(config.header_name, "x-request-id");
9232        assert!(config.accept_from_client);
9233        assert!(config.add_to_response);
9234        assert_eq!(config.max_client_id_length, 128);
9235    }
9236
9237    #[test]
9238    fn request_id_config_builder() {
9239        let config = RequestIdConfig::new()
9240            .header_name("X-Trace-ID")
9241            .accept_from_client(false)
9242            .add_to_response(false)
9243            .max_client_id_length(64);
9244
9245        assert_eq!(config.header_name, "X-Trace-ID");
9246        assert!(!config.accept_from_client);
9247        assert!(!config.add_to_response);
9248        assert_eq!(config.max_client_id_length, 64);
9249    }
9250
9251    #[test]
9252    fn request_id_middleware_generates_id() {
9253        let middleware = RequestIdMiddleware::new();
9254        let ctx = test_context();
9255        let mut req = Request::new(crate::request::Method::Get, "/");
9256
9257        let result = futures_executor::block_on(middleware.before(&ctx, &mut req));
9258        assert!(matches!(result, ControlFlow::Continue));
9259
9260        let stored_id = req.get_extension::<RequestId>();
9261        assert!(stored_id.is_some());
9262        assert!(!stored_id.unwrap().as_str().is_empty());
9263    }
9264
9265    #[test]
9266    fn request_id_middleware_accepts_client_id() {
9267        let middleware = RequestIdMiddleware::new();
9268        let ctx = test_context();
9269        let mut req = Request::new(crate::request::Method::Get, "/");
9270        req.headers_mut()
9271            .insert("x-request-id", b"client-provided-id-123".to_vec());
9272
9273        futures_executor::block_on(middleware.before(&ctx, &mut req));
9274
9275        let stored_id = req.get_extension::<RequestId>().unwrap();
9276        assert_eq!(stored_id.as_str(), "client-provided-id-123");
9277    }
9278
9279    #[test]
9280    fn request_id_middleware_rejects_invalid_client_id() {
9281        let middleware = RequestIdMiddleware::new();
9282        let ctx = test_context();
9283
9284        // Test with invalid characters
9285        let mut req = Request::new(crate::request::Method::Get, "/");
9286        req.headers_mut()
9287            .insert("x-request-id", b"invalid<script>id".to_vec());
9288
9289        futures_executor::block_on(middleware.before(&ctx, &mut req));
9290
9291        let stored_id = req.get_extension::<RequestId>().unwrap();
9292        // Should have generated a new ID instead of using the invalid one
9293        assert_ne!(stored_id.as_str(), "invalid<script>id");
9294    }
9295
9296    #[test]
9297    fn request_id_middleware_rejects_too_long_client_id() {
9298        let config = RequestIdConfig::new().max_client_id_length(10);
9299        let middleware = RequestIdMiddleware::with_config(config);
9300        let ctx = test_context();
9301
9302        let mut req = Request::new(crate::request::Method::Get, "/");
9303        req.headers_mut()
9304            .insert("x-request-id", b"this-id-is-way-too-long".to_vec());
9305
9306        futures_executor::block_on(middleware.before(&ctx, &mut req));
9307
9308        let stored_id = req.get_extension::<RequestId>().unwrap();
9309        // Should have generated a new ID instead of using the too-long one
9310        assert_ne!(stored_id.as_str(), "this-id-is-way-too-long");
9311    }
9312
9313    #[test]
9314    fn request_id_middleware_adds_to_response() {
9315        let middleware = RequestIdMiddleware::new();
9316        let ctx = test_context();
9317        let mut req = Request::new(crate::request::Method::Get, "/");
9318
9319        futures_executor::block_on(middleware.before(&ctx, &mut req));
9320        let stored_id = req.get_extension::<RequestId>().unwrap().clone();
9321
9322        let response = Response::ok();
9323        let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9324
9325        let header = header_value(&response, "x-request-id");
9326        assert_eq!(header, Some(stored_id.0));
9327    }
9328
9329    #[test]
9330    fn request_id_middleware_respects_add_to_response_false() {
9331        let config = RequestIdConfig::new().add_to_response(false);
9332        let middleware = RequestIdMiddleware::with_config(config);
9333        let ctx = test_context();
9334        let mut req = Request::new(crate::request::Method::Get, "/");
9335
9336        futures_executor::block_on(middleware.before(&ctx, &mut req));
9337
9338        let response = Response::ok();
9339        let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9340
9341        let header = header_value(&response, "x-request-id");
9342        assert!(header.is_none());
9343    }
9344
9345    #[test]
9346    fn request_id_middleware_respects_accept_from_client_false() {
9347        let config = RequestIdConfig::new().accept_from_client(false);
9348        let middleware = RequestIdMiddleware::with_config(config);
9349        let ctx = test_context();
9350        let mut req = Request::new(crate::request::Method::Get, "/");
9351        req.headers_mut()
9352            .insert("x-request-id", b"client-id".to_vec());
9353
9354        futures_executor::block_on(middleware.before(&ctx, &mut req));
9355
9356        let stored_id = req.get_extension::<RequestId>().unwrap();
9357        // Should ignore client ID and generate new one
9358        assert_ne!(stored_id.as_str(), "client-id");
9359    }
9360
9361    #[test]
9362    fn request_id_middleware_custom_header_name() {
9363        let config = RequestIdConfig::new().header_name("X-Trace-ID");
9364        let middleware = RequestIdMiddleware::with_config(config);
9365        let ctx = test_context();
9366        let mut req = Request::new(crate::request::Method::Get, "/");
9367        req.headers_mut()
9368            .insert("X-Trace-ID", b"trace-123".to_vec());
9369
9370        futures_executor::block_on(middleware.before(&ctx, &mut req));
9371
9372        let stored_id = req.get_extension::<RequestId>().unwrap();
9373        assert_eq!(stored_id.as_str(), "trace-123");
9374
9375        let response = Response::ok();
9376        let response = futures_executor::block_on(middleware.after(&ctx, &req, response));
9377
9378        let header = header_value(&response, "X-Trace-ID");
9379        assert_eq!(header, Some("trace-123".to_string()));
9380    }
9381
9382    #[test]
9383    fn is_valid_request_id_accepts_valid() {
9384        assert!(super::is_valid_request_id("abc123"));
9385        assert!(super::is_valid_request_id("request-id-123"));
9386        assert!(super::is_valid_request_id("request_id_123"));
9387        assert!(super::is_valid_request_id("request.id.123"));
9388        assert!(super::is_valid_request_id("ABC123"));
9389        assert!(super::is_valid_request_id("a-b_c.D"));
9390    }
9391
9392    #[test]
9393    fn is_valid_request_id_rejects_invalid() {
9394        assert!(!super::is_valid_request_id(""));
9395        assert!(!super::is_valid_request_id("id with spaces"));
9396        assert!(!super::is_valid_request_id("id<script>"));
9397        assert!(!super::is_valid_request_id("id\nwith\nnewlines"));
9398        assert!(!super::is_valid_request_id("id;with;semicolons"));
9399        assert!(!super::is_valid_request_id("id/with/slashes"));
9400    }
9401
9402    #[test]
9403    fn request_id_middleware_name() {
9404        let middleware = RequestIdMiddleware::new();
9405        assert_eq!(middleware.name(), "RequestId");
9406    }
9407
9408    // =========================================================================
9409    // Middleware Stack Execution Order Tests
9410    // =========================================================================
9411
9412    /// Test middleware that records when its before/after hooks run
9413    struct OrderTrackingMiddleware {
9414        id: &'static str,
9415        log: Arc<std::sync::Mutex<Vec<String>>>,
9416    }
9417
9418    impl OrderTrackingMiddleware {
9419        fn new(id: &'static str, log: Arc<std::sync::Mutex<Vec<String>>>) -> Self {
9420            Self { id, log }
9421        }
9422    }
9423
9424    impl Middleware for OrderTrackingMiddleware {
9425        fn before<'a>(
9426            &'a self,
9427            _ctx: &'a RequestContext,
9428            _req: &'a mut Request,
9429        ) -> BoxFuture<'a, ControlFlow> {
9430            self.log.lock().unwrap().push(format!("{}.before", self.id));
9431            Box::pin(async { ControlFlow::Continue })
9432        }
9433
9434        fn after<'a>(
9435            &'a self,
9436            _ctx: &'a RequestContext,
9437            _req: &'a Request,
9438            response: Response,
9439        ) -> BoxFuture<'a, Response> {
9440            self.log.lock().unwrap().push(format!("{}.after", self.id));
9441            Box::pin(async move { response })
9442        }
9443    }
9444
9445    /// Test middleware that short-circuits with a configurable condition
9446    struct ConditionalBreakMiddleware {
9447        id: &'static str,
9448        should_break: bool,
9449        log: Arc<std::sync::Mutex<Vec<String>>>,
9450    }
9451
9452    impl ConditionalBreakMiddleware {
9453        fn new(
9454            id: &'static str,
9455            should_break: bool,
9456            log: Arc<std::sync::Mutex<Vec<String>>>,
9457        ) -> Self {
9458            Self {
9459                id,
9460                should_break,
9461                log,
9462            }
9463        }
9464    }
9465
9466    impl Middleware for ConditionalBreakMiddleware {
9467        fn before<'a>(
9468            &'a self,
9469            _ctx: &'a RequestContext,
9470            _req: &'a mut Request,
9471        ) -> BoxFuture<'a, ControlFlow> {
9472            self.log.lock().unwrap().push(format!("{}.before", self.id));
9473            let should_break = self.should_break;
9474            Box::pin(async move {
9475                if should_break {
9476                    ControlFlow::Break(
9477                        Response::with_status(StatusCode::FORBIDDEN)
9478                            .body(ResponseBody::Bytes(b"blocked".to_vec())),
9479                    )
9480                } else {
9481                    ControlFlow::Continue
9482                }
9483            })
9484        }
9485
9486        fn after<'a>(
9487            &'a self,
9488            _ctx: &'a RequestContext,
9489            _req: &'a Request,
9490            response: Response,
9491        ) -> BoxFuture<'a, Response> {
9492            self.log.lock().unwrap().push(format!("{}.after", self.id));
9493            Box::pin(async move { response })
9494        }
9495    }
9496
9497    /// Simple test handler that returns 200 OK
9498    struct OkHandler;
9499
9500    impl Handler for OkHandler {
9501        fn call<'a>(
9502            &'a self,
9503            _ctx: &'a RequestContext,
9504            _req: &'a mut Request,
9505        ) -> BoxFuture<'a, Response> {
9506            Box::pin(async move { Response::ok().body(ResponseBody::Bytes(b"handler".to_vec())) })
9507        }
9508    }
9509
9510    /// Handler that checks for a header injected by middleware.
9511    struct CheckHeaderHandler;
9512
9513    impl Handler for CheckHeaderHandler {
9514        fn call<'a>(
9515            &'a self,
9516            _ctx: &'a RequestContext,
9517            req: &'a mut Request,
9518        ) -> BoxFuture<'a, Response> {
9519            let has_header = req.headers().get("X-Modified-By").is_some();
9520            Box::pin(async move {
9521                if has_header {
9522                    Response::ok().body(ResponseBody::Bytes(b"header-present".to_vec()))
9523                } else {
9524                    Response::with_status(StatusCode::BAD_REQUEST)
9525                }
9526            })
9527        }
9528    }
9529
9530    /// Handler that returns an error status.
9531    struct ErrorHandler;
9532
9533    impl Handler for ErrorHandler {
9534        fn call<'a>(
9535            &'a self,
9536            _ctx: &'a RequestContext,
9537            _req: &'a mut Request,
9538        ) -> BoxFuture<'a, Response> {
9539            Box::pin(async move { Response::with_status(StatusCode::INTERNAL_SERVER_ERROR) })
9540        }
9541    }
9542
9543    #[test]
9544    fn middleware_stack_executes_in_correct_order() {
9545        // Verify the "onion" model: before hooks run first-to-last,
9546        // after hooks run last-to-first
9547        let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9548
9549        let mut stack = MiddlewareStack::new();
9550        stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9551        stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9552        stack.push(OrderTrackingMiddleware::new("mw3", log.clone()));
9553
9554        let ctx = test_context();
9555        let mut req = Request::new(crate::request::Method::Get, "/");
9556
9557        futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9558
9559        let calls = log.lock().unwrap().clone();
9560        assert_eq!(
9561            calls,
9562            vec![
9563                "mw1.before",
9564                "mw2.before",
9565                "mw3.before",
9566                "mw3.after",
9567                "mw2.after",
9568                "mw1.after",
9569            ]
9570        );
9571    }
9572
9573    #[test]
9574    fn middleware_stack_short_circuit_skips_later_middleware() {
9575        // When middleware 2 breaks, middleware 3's before should NOT run
9576        // But middleware 1 and 2's after hooks should still run
9577        let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9578
9579        let mut stack = MiddlewareStack::new();
9580        stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9581        stack.push(ConditionalBreakMiddleware::new("mw2", true, log.clone()));
9582        stack.push(OrderTrackingMiddleware::new("mw3", log.clone()));
9583
9584        let ctx = test_context();
9585        let mut req = Request::new(crate::request::Method::Get, "/");
9586
9587        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9588
9589        // Should get 403 from the break
9590        assert_eq!(response.status().as_u16(), 403);
9591
9592        let calls = log.lock().unwrap().clone();
9593        assert_eq!(
9594            calls,
9595            vec![
9596                "mw1.before",
9597                "mw2.before",
9598                // mw3.before NOT called because mw2 broke
9599                // mw2.after NOT called because it was the one that broke (ran_before_count = 1)
9600                "mw1.after",
9601            ]
9602        );
9603    }
9604
9605    #[test]
9606    fn middleware_stack_first_middleware_breaks() {
9607        // When the first middleware breaks, no other middleware should run
9608        let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9609
9610        let mut stack = MiddlewareStack::new();
9611        stack.push(ConditionalBreakMiddleware::new("mw1", true, log.clone()));
9612        stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9613
9614        let ctx = test_context();
9615        let mut req = Request::new(crate::request::Method::Get, "/");
9616
9617        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9618
9619        assert_eq!(response.status().as_u16(), 403);
9620
9621        let calls = log.lock().unwrap().clone();
9622        assert_eq!(calls, vec!["mw1.before"]);
9623        // No after hooks because ran_before_count = 0
9624    }
9625
9626    #[test]
9627    fn middleware_stack_last_middleware_breaks() {
9628        // When the last middleware breaks, all previous after hooks should run
9629        let log = Arc::new(std::sync::Mutex::new(Vec::new()));
9630
9631        let mut stack = MiddlewareStack::new();
9632        stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
9633        stack.push(OrderTrackingMiddleware::new("mw2", log.clone()));
9634        stack.push(ConditionalBreakMiddleware::new("mw3", true, log.clone()));
9635
9636        let ctx = test_context();
9637        let mut req = Request::new(crate::request::Method::Get, "/");
9638
9639        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9640
9641        assert_eq!(response.status().as_u16(), 403);
9642
9643        let calls = log.lock().unwrap().clone();
9644        assert_eq!(
9645            calls,
9646            vec![
9647                "mw1.before",
9648                "mw2.before",
9649                "mw3.before",
9650                // mw3 broke, so only mw1 and mw2 after hooks run
9651                "mw2.after",
9652                "mw1.after",
9653            ]
9654        );
9655    }
9656
9657    #[test]
9658    fn middleware_stack_empty_executes_handler_directly() {
9659        let stack = MiddlewareStack::new();
9660        let ctx = test_context();
9661        let mut req = Request::new(crate::request::Method::Get, "/");
9662
9663        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
9664
9665        assert_eq!(response.status().as_u16(), 200);
9666    }
9667
9668    #[test]
9669    fn middleware_stack_with_capacity() {
9670        let stack = MiddlewareStack::with_capacity(10);
9671        assert!(stack.is_empty());
9672        assert_eq!(stack.len(), 0);
9673    }
9674
9675    #[test]
9676    fn middleware_stack_push_arc() {
9677        let mut stack = MiddlewareStack::new();
9678        let mw: Arc<dyn Middleware> = Arc::new(NoopMiddleware);
9679        stack.push_arc(mw);
9680        assert_eq!(stack.len(), 1);
9681    }
9682
9683    // =========================================================================
9684    // AddResponseHeader Middleware Tests
9685    // =========================================================================
9686
9687    #[test]
9688    fn add_response_header_adds_header() {
9689        let mw = AddResponseHeader::new("X-Custom", b"custom-value".to_vec());
9690        let ctx = test_context();
9691        let req = Request::new(crate::request::Method::Get, "/");
9692
9693        let response = Response::ok();
9694        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9695
9696        assert_eq!(
9697            header_value(&response, "X-Custom"),
9698            Some("custom-value".to_string())
9699        );
9700    }
9701
9702    #[test]
9703    fn add_response_header_preserves_existing_headers() {
9704        let mw = AddResponseHeader::new("X-New", b"new".to_vec());
9705        let ctx = test_context();
9706        let req = Request::new(crate::request::Method::Get, "/");
9707
9708        let response = Response::ok().header("X-Existing", b"existing".to_vec());
9709        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9710
9711        assert_eq!(
9712            header_value(&response, "X-Existing"),
9713            Some("existing".to_string())
9714        );
9715        assert_eq!(header_value(&response, "X-New"), Some("new".to_string()));
9716    }
9717
9718    #[test]
9719    fn add_response_header_name() {
9720        let mw = AddResponseHeader::new("X-Test", b"test".to_vec());
9721        assert_eq!(mw.name(), "AddResponseHeader");
9722    }
9723
9724    // =========================================================================
9725    // RequireHeader Middleware Tests
9726    // =========================================================================
9727
9728    #[test]
9729    fn require_header_allows_with_header() {
9730        let mw = RequireHeader::new("X-Api-Key");
9731        let ctx = test_context();
9732        let mut req = Request::new(crate::request::Method::Get, "/");
9733        req.headers_mut()
9734            .insert("X-Api-Key", b"secret-key".to_vec());
9735
9736        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9737        assert!(matches!(result, ControlFlow::Continue));
9738    }
9739
9740    #[test]
9741    fn require_header_blocks_without_header() {
9742        let mw = RequireHeader::new("X-Api-Key");
9743        let ctx = test_context();
9744        let mut req = Request::new(crate::request::Method::Get, "/");
9745
9746        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9747
9748        match result {
9749            ControlFlow::Break(response) => {
9750                assert_eq!(response.status().as_u16(), 400);
9751            }
9752            ControlFlow::Continue => panic!("Expected Break, got Continue"),
9753        }
9754    }
9755
9756    #[test]
9757    fn require_header_name() {
9758        let mw = RequireHeader::new("X-Test");
9759        assert_eq!(mw.name(), "RequireHeader");
9760    }
9761
9762    // =========================================================================
9763    // PathPrefixFilter Middleware Tests
9764    // =========================================================================
9765
9766    #[test]
9767    fn path_prefix_filter_allows_matching_path() {
9768        let mw = PathPrefixFilter::new("/api");
9769        let ctx = test_context();
9770        let mut req = Request::new(crate::request::Method::Get, "/api/users");
9771
9772        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9773        assert!(matches!(result, ControlFlow::Continue));
9774    }
9775
9776    #[test]
9777    fn path_prefix_filter_allows_exact_prefix() {
9778        let mw = PathPrefixFilter::new("/api");
9779        let ctx = test_context();
9780        let mut req = Request::new(crate::request::Method::Get, "/api");
9781
9782        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9783        assert!(matches!(result, ControlFlow::Continue));
9784    }
9785
9786    #[test]
9787    fn path_prefix_filter_blocks_non_matching_path() {
9788        let mw = PathPrefixFilter::new("/api");
9789        let ctx = test_context();
9790        let mut req = Request::new(crate::request::Method::Get, "/admin/users");
9791
9792        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
9793
9794        match result {
9795            ControlFlow::Break(response) => {
9796                assert_eq!(response.status().as_u16(), 404);
9797            }
9798            ControlFlow::Continue => panic!("Expected Break, got Continue"),
9799        }
9800    }
9801
9802    #[test]
9803    fn path_prefix_filter_name() {
9804        let mw = PathPrefixFilter::new("/api");
9805        assert_eq!(mw.name(), "PathPrefixFilter");
9806    }
9807
9808    // =========================================================================
9809    // ConditionalStatus Middleware Tests
9810    // =========================================================================
9811
9812    #[test]
9813    fn conditional_status_applies_true_status() {
9814        let mw = ConditionalStatus::new(
9815            |req| req.path() == "/health",
9816            StatusCode::OK,
9817            StatusCode::NOT_FOUND,
9818        );
9819        let ctx = test_context();
9820        let req = Request::new(crate::request::Method::Get, "/health");
9821        let response = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
9822
9823        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9824        assert_eq!(response.status().as_u16(), 200);
9825    }
9826
9827    #[test]
9828    fn conditional_status_applies_false_status() {
9829        let mw = ConditionalStatus::new(
9830            |req| req.path() == "/health",
9831            StatusCode::OK,
9832            StatusCode::NOT_FOUND,
9833        );
9834        let ctx = test_context();
9835        let req = Request::new(crate::request::Method::Get, "/other");
9836        let response = Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
9837
9838        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
9839        assert_eq!(response.status().as_u16(), 404);
9840    }
9841
9842    #[test]
9843    fn conditional_status_name() {
9844        let mw = ConditionalStatus::new(|_| true, StatusCode::OK, StatusCode::NOT_FOUND);
9845        assert_eq!(mw.name(), "ConditionalStatus");
9846    }
9847
9848    // =========================================================================
9849    // Layer and Layered Tests
9850    // =========================================================================
9851
9852    #[derive(Clone)]
9853    struct LayerTestMiddleware {
9854        prefix: String,
9855    }
9856
9857    impl LayerTestMiddleware {
9858        fn new(prefix: impl Into<String>) -> Self {
9859            Self {
9860                prefix: prefix.into(),
9861            }
9862        }
9863    }
9864
9865    impl Middleware for LayerTestMiddleware {
9866        fn after<'a>(
9867            &'a self,
9868            _ctx: &'a RequestContext,
9869            _req: &'a Request,
9870            response: Response,
9871        ) -> BoxFuture<'a, Response> {
9872            let prefix = self.prefix.clone();
9873            Box::pin(async move { response.header("X-Layer", prefix.into_bytes()) })
9874        }
9875    }
9876
9877    #[test]
9878    fn layer_wraps_handler() {
9879        let layer = Layer::new(LayerTestMiddleware::new("wrapped"));
9880        let wrapped = layer.wrap(OkHandler);
9881
9882        let ctx = test_context();
9883        let mut req = Request::new(crate::request::Method::Get, "/");
9884
9885        let response = futures_executor::block_on(wrapped.call(&ctx, &mut req));
9886
9887        assert_eq!(response.status().as_u16(), 200);
9888        assert_eq!(
9889            header_value(&response, "X-Layer"),
9890            Some("wrapped".to_string())
9891        );
9892    }
9893
9894    #[test]
9895    fn layered_handles_break() {
9896        #[derive(Clone)]
9897        struct BreakingMiddleware;
9898
9899        impl Middleware for BreakingMiddleware {
9900            fn before<'a>(
9901                &'a self,
9902                _ctx: &'a RequestContext,
9903                _req: &'a mut Request,
9904            ) -> BoxFuture<'a, ControlFlow> {
9905                Box::pin(async {
9906                    ControlFlow::Break(Response::with_status(StatusCode::UNAUTHORIZED))
9907                })
9908            }
9909
9910            fn after<'a>(
9911                &'a self,
9912                _ctx: &'a RequestContext,
9913                _req: &'a Request,
9914                response: Response,
9915            ) -> BoxFuture<'a, Response> {
9916                Box::pin(async move { response.header("X-After", b"ran".to_vec()) })
9917            }
9918        }
9919
9920        let layer = Layer::new(BreakingMiddleware);
9921        let wrapped = layer.wrap(OkHandler);
9922
9923        let ctx = test_context();
9924        let mut req = Request::new(crate::request::Method::Get, "/");
9925
9926        let response = futures_executor::block_on(wrapped.call(&ctx, &mut req));
9927
9928        // Should get 401 from break
9929        assert_eq!(response.status().as_u16(), 401);
9930        // After hook should still run
9931        assert_eq!(header_value(&response, "X-After"), Some("ran".to_string()));
9932    }
9933
9934    // =========================================================================
9935    // RequestResponseLogger Tests
9936    // =========================================================================
9937
9938    #[test]
9939    fn request_response_logger_default() {
9940        let logger = RequestResponseLogger::default();
9941        assert!(logger.log_request_headers);
9942        assert!(logger.log_response_headers);
9943        assert!(!logger.log_body);
9944        assert_eq!(logger.max_body_bytes, 1024);
9945    }
9946
9947    #[test]
9948    fn request_response_logger_builder() {
9949        let logger = RequestResponseLogger::new()
9950            .log_request_headers(false)
9951            .log_response_headers(false)
9952            .log_body(true)
9953            .max_body_bytes(2048)
9954            .redact_header("x-secret");
9955
9956        assert!(!logger.log_request_headers);
9957        assert!(!logger.log_response_headers);
9958        assert!(logger.log_body);
9959        assert_eq!(logger.max_body_bytes, 2048);
9960        assert!(logger.redact_headers.contains("x-secret"));
9961    }
9962
9963    #[test]
9964    fn request_response_logger_name() {
9965        let logger = RequestResponseLogger::new();
9966        assert_eq!(logger.name(), "RequestResponseLogger");
9967    }
9968
9969    // =========================================================================
9970    // Integration Tests with Handlers
9971    // =========================================================================
9972
9973    #[test]
9974    fn middleware_stack_modifies_request_for_handler() {
9975        /// Middleware that adds a header that the handler can see
9976        struct RequestModifier;
9977
9978        impl Middleware for RequestModifier {
9979            fn before<'a>(
9980                &'a self,
9981                _ctx: &'a RequestContext,
9982                req: &'a mut Request,
9983            ) -> BoxFuture<'a, ControlFlow> {
9984                req.headers_mut()
9985                    .insert("X-Modified-By", b"middleware".to_vec());
9986                Box::pin(async { ControlFlow::Continue })
9987            }
9988        }
9989
9990        let mut stack = MiddlewareStack::new();
9991        stack.push(RequestModifier);
9992
9993        let ctx = test_context();
9994        let mut req = Request::new(crate::request::Method::Get, "/");
9995
9996        let response =
9997            futures_executor::block_on(stack.execute(&CheckHeaderHandler, &ctx, &mut req));
9998
9999        assert_eq!(response.status().as_u16(), 200);
10000    }
10001
10002    #[test]
10003    fn middleware_stack_multiple_response_modifications() {
10004        let mut stack = MiddlewareStack::new();
10005        stack.push(AddResponseHeader::new("X-First", b"1".to_vec()));
10006        stack.push(AddResponseHeader::new("X-Second", b"2".to_vec()));
10007        stack.push(AddResponseHeader::new("X-Third", b"3".to_vec()));
10008
10009        let ctx = test_context();
10010        let mut req = Request::new(crate::request::Method::Get, "/");
10011
10012        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10013
10014        // All headers should be present (after hooks run in reverse)
10015        assert_eq!(header_value(&response, "X-First"), Some("1".to_string()));
10016        assert_eq!(header_value(&response, "X-Second"), Some("2".to_string()));
10017        assert_eq!(header_value(&response, "X-Third"), Some("3".to_string()));
10018    }
10019
10020    #[test]
10021    fn middleware_stack_handler_receives_response_after_break() {
10022        // Verify that when middleware breaks, the response body is from the break
10023        let mut stack = MiddlewareStack::new();
10024        stack.push(ConditionalBreakMiddleware::new(
10025            "breaker",
10026            true,
10027            Arc::new(std::sync::Mutex::new(Vec::new())),
10028        ));
10029
10030        let ctx = test_context();
10031        let mut req = Request::new(crate::request::Method::Get, "/");
10032
10033        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10034
10035        assert_eq!(response.status().as_u16(), 403);
10036        // Body should be from the breaking middleware, not the handler
10037        match response.body_ref() {
10038            ResponseBody::Bytes(b) => assert_eq!(b, b"blocked"),
10039            _ => panic!("Expected Bytes body"),
10040        }
10041    }
10042
10043    // =========================================================================
10044    // Error Propagation Tests
10045    // =========================================================================
10046
10047    #[test]
10048    fn middleware_after_can_change_status() {
10049        struct StatusChanger;
10050
10051        impl Middleware for StatusChanger {
10052            fn after<'a>(
10053                &'a self,
10054                _ctx: &'a RequestContext,
10055                _req: &'a Request,
10056                _response: Response,
10057            ) -> BoxFuture<'a, Response> {
10058                Box::pin(async { Response::with_status(StatusCode::SERVICE_UNAVAILABLE) })
10059            }
10060        }
10061
10062        let mut stack = MiddlewareStack::new();
10063        stack.push(StatusChanger);
10064
10065        let ctx = test_context();
10066        let mut req = Request::new(crate::request::Method::Get, "/");
10067
10068        let response = futures_executor::block_on(stack.execute(&OkHandler, &ctx, &mut req));
10069
10070        // Should be changed by after hook
10071        assert_eq!(response.status().as_u16(), 503);
10072    }
10073
10074    #[test]
10075    fn middleware_after_runs_even_on_error_status() {
10076        let log = Arc::new(std::sync::Mutex::new(Vec::new()));
10077        let mut stack = MiddlewareStack::new();
10078        stack.push(OrderTrackingMiddleware::new("mw1", log.clone()));
10079
10080        let ctx = test_context();
10081        let mut req = Request::new(crate::request::Method::Get, "/");
10082
10083        let response = futures_executor::block_on(stack.execute(&ErrorHandler, &ctx, &mut req));
10084
10085        assert_eq!(response.status().as_u16(), 500);
10086
10087        let calls = log.lock().unwrap().clone();
10088        // After should run even when handler returns error status
10089        assert_eq!(calls, vec!["mw1.before", "mw1.after"]);
10090    }
10091
10092    // =========================================================================
10093    // Wildcard and Regex Matching Tests
10094    // =========================================================================
10095
10096    #[test]
10097    fn wildcard_match_simple() {
10098        assert!(super::wildcard_match("*.example.com", "api.example.com"));
10099        assert!(super::wildcard_match("*.example.com", "www.example.com"));
10100        assert!(!super::wildcard_match("*.example.com", "example.com"));
10101    }
10102
10103    #[test]
10104    fn wildcard_match_suffix_pattern() {
10105        // Wildcard at start with fixed suffix - primary use case for CORS
10106        assert!(super::wildcard_match("*.txt", "file.txt"));
10107        assert!(super::wildcard_match("*.txt", "document.txt"));
10108        assert!(!super::wildcard_match("*.txt", "file.doc"));
10109        assert!(super::wildcard_match("*-suffix", "any-suffix"));
10110    }
10111
10112    #[test]
10113    fn wildcard_match_no_wildcard() {
10114        assert!(super::wildcard_match("exact", "exact"));
10115        assert!(!super::wildcard_match("exact", "different"));
10116    }
10117
10118    #[test]
10119    fn regex_match_anchored() {
10120        assert!(super::regex_match("^hello$", "hello"));
10121        assert!(!super::regex_match("^hello$", "hello world"));
10122        assert!(!super::regex_match("^hello$", "say hello"));
10123    }
10124
10125    #[test]
10126    fn regex_match_dot_wildcard() {
10127        assert!(super::regex_match("h.llo", "hello"));
10128        assert!(super::regex_match("h.llo", "hallo"));
10129    }
10130
10131    #[test]
10132    fn regex_match_star() {
10133        assert!(super::regex_match("hel*o", "hello"));
10134        assert!(super::regex_match("hel*o", "helo"));
10135        assert!(super::regex_match("hel*o", "hellllllo"));
10136    }
10137
10138    // =========================================================================
10139    // Middleware Trait Default Implementation Tests
10140    // =========================================================================
10141
10142    #[test]
10143    fn middleware_default_before_continues() {
10144        struct DefaultBefore;
10145        impl Middleware for DefaultBefore {}
10146
10147        let mw = DefaultBefore;
10148        let ctx = test_context();
10149        let mut req = Request::new(crate::request::Method::Get, "/");
10150
10151        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
10152        assert!(matches!(result, ControlFlow::Continue));
10153    }
10154
10155    #[test]
10156    fn middleware_default_after_passes_through() {
10157        struct DefaultAfter;
10158        impl Middleware for DefaultAfter {}
10159
10160        let mw = DefaultAfter;
10161        let ctx = test_context();
10162        let req = Request::new(crate::request::Method::Get, "/");
10163        let response = Response::with_status(StatusCode::CREATED);
10164
10165        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10166        assert_eq!(result.status().as_u16(), 201);
10167    }
10168
10169    #[test]
10170    fn middleware_default_name_is_type_name() {
10171        struct MyCustomMiddleware;
10172        impl Middleware for MyCustomMiddleware {}
10173
10174        let mw = MyCustomMiddleware;
10175        assert!(mw.name().contains("MyCustomMiddleware"));
10176    }
10177
10178    // =========================================================================
10179    // Security Headers Middleware Tests
10180    // =========================================================================
10181
10182    #[test]
10183    fn security_headers_default_config() {
10184        let config = SecurityHeadersConfig::default();
10185        assert_eq!(config.x_content_type_options, Some("nosniff"));
10186        assert_eq!(config.x_frame_options, Some(XFrameOptions::Deny));
10187        assert_eq!(config.x_xss_protection, Some("0"));
10188        assert!(config.content_security_policy.is_none());
10189        assert!(config.hsts.is_none());
10190        assert_eq!(
10191            config.referrer_policy,
10192            Some(ReferrerPolicy::StrictOriginWhenCrossOrigin)
10193        );
10194        assert!(config.permissions_policy.is_none());
10195    }
10196
10197    #[test]
10198    fn security_headers_none_config() {
10199        let config = SecurityHeadersConfig::none();
10200        assert!(config.x_content_type_options.is_none());
10201        assert!(config.x_frame_options.is_none());
10202        assert!(config.x_xss_protection.is_none());
10203        assert!(config.content_security_policy.is_none());
10204        assert!(config.hsts.is_none());
10205        assert!(config.referrer_policy.is_none());
10206        assert!(config.permissions_policy.is_none());
10207    }
10208
10209    #[test]
10210    fn security_headers_strict_config() {
10211        let config = SecurityHeadersConfig::strict();
10212        assert_eq!(config.x_content_type_options, Some("nosniff"));
10213        assert_eq!(config.x_frame_options, Some(XFrameOptions::Deny));
10214        assert_eq!(
10215            config.content_security_policy,
10216            Some("default-src 'self'".to_string())
10217        );
10218        assert_eq!(config.hsts, Some((31536000, true, false)));
10219        assert_eq!(config.referrer_policy, Some(ReferrerPolicy::NoReferrer));
10220        assert!(config.permissions_policy.is_some());
10221    }
10222
10223    #[test]
10224    fn security_headers_config_builder() {
10225        let config = SecurityHeadersConfig::new()
10226            .x_frame_options(Some(XFrameOptions::SameOrigin))
10227            .content_security_policy("default-src 'self'")
10228            .hsts(86400, false, false)
10229            .referrer_policy(Some(ReferrerPolicy::Origin));
10230
10231        assert_eq!(config.x_frame_options, Some(XFrameOptions::SameOrigin));
10232        assert_eq!(
10233            config.content_security_policy,
10234            Some("default-src 'self'".to_string())
10235        );
10236        assert_eq!(config.hsts, Some((86400, false, false)));
10237        assert_eq!(config.referrer_policy, Some(ReferrerPolicy::Origin));
10238    }
10239
10240    #[test]
10241    fn security_headers_hsts_value_format() {
10242        // Basic HSTS
10243        let config = SecurityHeadersConfig::none().hsts(3600, false, false);
10244        assert_eq!(config.build_hsts_value(), Some("max-age=3600".to_string()));
10245
10246        // With includeSubDomains
10247        let config = SecurityHeadersConfig::none().hsts(3600, true, false);
10248        assert_eq!(
10249            config.build_hsts_value(),
10250            Some("max-age=3600; includeSubDomains".to_string())
10251        );
10252
10253        // With preload
10254        let config = SecurityHeadersConfig::none().hsts(3600, false, true);
10255        assert_eq!(
10256            config.build_hsts_value(),
10257            Some("max-age=3600; preload".to_string())
10258        );
10259
10260        // With both
10261        let config = SecurityHeadersConfig::none().hsts(3600, true, true);
10262        assert_eq!(
10263            config.build_hsts_value(),
10264            Some("max-age=3600; includeSubDomains; preload".to_string())
10265        );
10266    }
10267
10268    #[test]
10269    fn security_headers_middleware_adds_default_headers() {
10270        let mw = SecurityHeaders::new();
10271        let ctx = test_context();
10272        let req = Request::new(crate::request::Method::Get, "/");
10273        let response = Response::ok();
10274
10275        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10276
10277        // Check that default headers are present
10278        assert!(header_value(&result, "X-Content-Type-Options").is_some());
10279        assert!(header_value(&result, "X-Frame-Options").is_some());
10280        assert!(header_value(&result, "X-XSS-Protection").is_some());
10281        assert!(header_value(&result, "Referrer-Policy").is_some());
10282
10283        // Check that optional headers are NOT present by default
10284        assert!(header_value(&result, "Content-Security-Policy").is_none());
10285        assert!(header_value(&result, "Strict-Transport-Security").is_none());
10286        assert!(header_value(&result, "Permissions-Policy").is_none());
10287    }
10288
10289    #[test]
10290    fn security_headers_middleware_with_csp() {
10291        let config = SecurityHeadersConfig::new()
10292            .content_security_policy("default-src 'self'; script-src 'self' 'unsafe-inline'");
10293        let mw = SecurityHeaders::with_config(config);
10294        let ctx = test_context();
10295        let req = Request::new(crate::request::Method::Get, "/");
10296        let response = Response::ok();
10297
10298        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10299
10300        let csp = header_value(&result, "Content-Security-Policy");
10301        assert!(csp.is_some());
10302        assert_eq!(
10303            csp.unwrap(),
10304            "default-src 'self'; script-src 'self' 'unsafe-inline'"
10305        );
10306    }
10307
10308    #[test]
10309    fn security_headers_middleware_with_hsts() {
10310        let config = SecurityHeadersConfig::new().hsts(31536000, true, false);
10311        let mw = SecurityHeaders::with_config(config);
10312        let ctx = test_context();
10313        let req = Request::new(crate::request::Method::Get, "/");
10314        let response = Response::ok();
10315
10316        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10317
10318        let hsts = header_value(&result, "Strict-Transport-Security");
10319        assert!(hsts.is_some());
10320        assert_eq!(hsts.unwrap(), "max-age=31536000; includeSubDomains");
10321    }
10322
10323    #[test]
10324    fn security_headers_middleware_name() {
10325        let mw = SecurityHeaders::new();
10326        assert_eq!(mw.name(), "SecurityHeaders");
10327    }
10328
10329    #[test]
10330    fn x_frame_options_values() {
10331        assert_eq!(XFrameOptions::Deny.as_bytes(), b"DENY");
10332        assert_eq!(XFrameOptions::SameOrigin.as_bytes(), b"SAMEORIGIN");
10333    }
10334
10335    #[test]
10336    fn referrer_policy_values() {
10337        assert_eq!(ReferrerPolicy::NoReferrer.as_bytes(), b"no-referrer");
10338        assert_eq!(
10339            ReferrerPolicy::NoReferrerWhenDowngrade.as_bytes(),
10340            b"no-referrer-when-downgrade"
10341        );
10342        assert_eq!(ReferrerPolicy::Origin.as_bytes(), b"origin");
10343        assert_eq!(
10344            ReferrerPolicy::OriginWhenCrossOrigin.as_bytes(),
10345            b"origin-when-cross-origin"
10346        );
10347        assert_eq!(ReferrerPolicy::SameOrigin.as_bytes(), b"same-origin");
10348        assert_eq!(ReferrerPolicy::StrictOrigin.as_bytes(), b"strict-origin");
10349        assert_eq!(
10350            ReferrerPolicy::StrictOriginWhenCrossOrigin.as_bytes(),
10351            b"strict-origin-when-cross-origin"
10352        );
10353        assert_eq!(ReferrerPolicy::UnsafeUrl.as_bytes(), b"unsafe-url");
10354    }
10355
10356    #[test]
10357    fn security_headers_strict_preset() {
10358        let mw = SecurityHeaders::strict();
10359        let ctx = test_context();
10360        let req = Request::new(crate::request::Method::Get, "/");
10361        let response = Response::ok();
10362
10363        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
10364
10365        // All headers should be present with strict config
10366        assert!(header_value(&result, "X-Content-Type-Options").is_some());
10367        assert!(header_value(&result, "X-Frame-Options").is_some());
10368        assert!(header_value(&result, "Content-Security-Policy").is_some());
10369        assert!(header_value(&result, "Strict-Transport-Security").is_some());
10370        assert!(header_value(&result, "Referrer-Policy").is_some());
10371        assert!(header_value(&result, "Permissions-Policy").is_some());
10372    }
10373
10374    #[test]
10375    fn security_headers_config_clearing_methods() {
10376        let config = SecurityHeadersConfig::strict()
10377            .no_content_security_policy()
10378            .no_hsts()
10379            .no_permissions_policy();
10380
10381        assert!(config.content_security_policy.is_none());
10382        assert!(config.hsts.is_none());
10383        assert!(config.permissions_policy.is_none());
10384    }
10385
10386    // =========================================================================
10387    // CSRF Middleware Tests
10388    // =========================================================================
10389
10390    #[test]
10391    fn csrf_token_generate_produces_unique_tokens() {
10392        let token1 = CsrfToken::generate();
10393        let token2 = CsrfToken::generate();
10394        assert_ne!(token1, token2);
10395        assert!(!token1.as_str().is_empty());
10396        assert!(!token2.as_str().is_empty());
10397    }
10398
10399    #[test]
10400    fn csrf_token_display() {
10401        let token = CsrfToken::new("test-token-123");
10402        assert_eq!(format!("{}", token), "test-token-123");
10403    }
10404
10405    #[test]
10406    fn csrf_config_defaults() {
10407        let config = CsrfConfig::default();
10408        assert_eq!(config.cookie_name, "csrf_token");
10409        assert_eq!(config.header_name, "x-csrf-token");
10410        assert_eq!(config.mode, CsrfMode::DoubleSubmit);
10411        assert!(!config.rotate_token);
10412        assert!(config.production);
10413        assert!(config.error_message.is_none());
10414    }
10415
10416    #[test]
10417    fn csrf_config_builder() {
10418        let config = CsrfConfig::new()
10419            .cookie_name("XSRF-TOKEN")
10420            .header_name("X-XSRF-Token")
10421            .mode(CsrfMode::HeaderOnly)
10422            .rotate_token(true)
10423            .production(false)
10424            .error_message("Custom CSRF error");
10425
10426        assert_eq!(config.cookie_name, "XSRF-TOKEN");
10427        assert_eq!(config.header_name, "X-XSRF-Token");
10428        assert_eq!(config.mode, CsrfMode::HeaderOnly);
10429        assert!(config.rotate_token);
10430        assert!(!config.production);
10431        assert_eq!(config.error_message, Some("Custom CSRF error".to_string()));
10432    }
10433
10434    #[test]
10435    fn csrf_middleware_allows_get_without_token() {
10436        let csrf = CsrfMiddleware::new();
10437        let ctx = test_context();
10438        let mut req = Request::new(crate::request::Method::Get, "/");
10439
10440        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10441        assert!(result.is_continue());
10442        // Token should be generated and stored
10443        assert!(req.get_extension::<CsrfToken>().is_some());
10444    }
10445
10446    #[test]
10447    fn csrf_middleware_allows_head_without_token() {
10448        let csrf = CsrfMiddleware::new();
10449        let ctx = test_context();
10450        let mut req = Request::new(crate::request::Method::Head, "/");
10451
10452        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10453        assert!(result.is_continue());
10454    }
10455
10456    #[test]
10457    fn csrf_middleware_allows_options_without_token() {
10458        let csrf = CsrfMiddleware::new();
10459        let ctx = test_context();
10460        let mut req = Request::new(crate::request::Method::Options, "/");
10461
10462        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10463        assert!(result.is_continue());
10464    }
10465
10466    #[test]
10467    fn csrf_middleware_blocks_post_without_token() {
10468        let csrf = CsrfMiddleware::new();
10469        let ctx = test_context();
10470        let mut req = Request::new(crate::request::Method::Post, "/");
10471
10472        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10473        assert!(result.is_break());
10474
10475        if let ControlFlow::Break(response) = result {
10476            assert_eq!(response.status(), StatusCode::FORBIDDEN);
10477        }
10478    }
10479
10480    #[test]
10481    fn csrf_middleware_blocks_put_without_token() {
10482        let csrf = CsrfMiddleware::new();
10483        let ctx = test_context();
10484        let mut req = Request::new(crate::request::Method::Put, "/");
10485
10486        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10487        assert!(result.is_break());
10488    }
10489
10490    #[test]
10491    fn csrf_middleware_blocks_delete_without_token() {
10492        let csrf = CsrfMiddleware::new();
10493        let ctx = test_context();
10494        let mut req = Request::new(crate::request::Method::Delete, "/");
10495
10496        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10497        assert!(result.is_break());
10498    }
10499
10500    #[test]
10501    fn csrf_middleware_blocks_patch_without_token() {
10502        let csrf = CsrfMiddleware::new();
10503        let ctx = test_context();
10504        let mut req = Request::new(crate::request::Method::Patch, "/");
10505
10506        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10507        assert!(result.is_break());
10508    }
10509
10510    #[test]
10511    fn csrf_middleware_allows_post_with_matching_tokens() {
10512        let csrf = CsrfMiddleware::new();
10513        let ctx = test_context();
10514        let mut req = Request::new(crate::request::Method::Post, "/");
10515
10516        // Set matching cookie and header
10517        let token = "valid-csrf-token-12345";
10518        req.headers_mut()
10519            .insert("cookie", format!("csrf_token={}", token).into_bytes());
10520        req.headers_mut()
10521            .insert("x-csrf-token", token.as_bytes().to_vec());
10522
10523        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10524        assert!(result.is_continue());
10525
10526        // Token should be stored in extensions
10527        let stored_token = req.get_extension::<CsrfToken>().unwrap();
10528        assert_eq!(stored_token.as_str(), token);
10529    }
10530
10531    #[test]
10532    fn csrf_middleware_blocks_post_with_mismatched_tokens() {
10533        let csrf = CsrfMiddleware::new();
10534        let ctx = test_context();
10535        let mut req = Request::new(crate::request::Method::Post, "/");
10536
10537        // Set mismatched cookie and header
10538        req.headers_mut()
10539            .insert("cookie", b"csrf_token=token-in-cookie".to_vec());
10540        req.headers_mut()
10541            .insert("x-csrf-token", b"different-token".to_vec());
10542
10543        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10544        assert!(result.is_break());
10545
10546        if let ControlFlow::Break(response) = result {
10547            assert_eq!(response.status(), StatusCode::FORBIDDEN);
10548        }
10549    }
10550
10551    #[test]
10552    fn csrf_middleware_blocks_post_with_header_only_in_double_submit_mode() {
10553        let csrf = CsrfMiddleware::new();
10554        let ctx = test_context();
10555        let mut req = Request::new(crate::request::Method::Post, "/");
10556
10557        // Only header, no cookie
10558        req.headers_mut()
10559            .insert("x-csrf-token", b"some-token".to_vec());
10560
10561        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10562        assert!(result.is_break());
10563    }
10564
10565    #[test]
10566    fn csrf_middleware_blocks_post_with_cookie_only_in_double_submit_mode() {
10567        let csrf = CsrfMiddleware::new();
10568        let ctx = test_context();
10569        let mut req = Request::new(crate::request::Method::Post, "/");
10570
10571        // Only cookie, no header
10572        req.headers_mut()
10573            .insert("cookie", b"csrf_token=some-token".to_vec());
10574
10575        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10576        assert!(result.is_break());
10577    }
10578
10579    #[test]
10580    fn csrf_middleware_header_only_mode_accepts_header_token() {
10581        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10582        let ctx = test_context();
10583        let mut req = Request::new(crate::request::Method::Post, "/");
10584
10585        req.headers_mut()
10586            .insert("x-csrf-token", b"valid-token".to_vec());
10587
10588        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10589        assert!(result.is_continue());
10590    }
10591
10592    #[test]
10593    fn csrf_middleware_header_only_mode_rejects_empty_header() {
10594        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10595        let ctx = test_context();
10596        let mut req = Request::new(crate::request::Method::Post, "/");
10597
10598        req.headers_mut().insert("x-csrf-token", b"".to_vec());
10599
10600        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10601        assert!(result.is_break());
10602    }
10603
10604    #[test]
10605    fn csrf_middleware_sets_cookie_on_get() {
10606        let csrf = CsrfMiddleware::new();
10607        let ctx = test_context();
10608        let mut req = Request::new(crate::request::Method::Get, "/");
10609
10610        // Run before to generate token
10611        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10612
10613        // Run after to set cookie
10614        let response = Response::ok();
10615        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10616
10617        // Check Set-Cookie header
10618        let cookie_value = header_value(&result, "set-cookie");
10619        assert!(cookie_value.is_some());
10620
10621        let cookie_value = cookie_value.unwrap();
10622        assert!(cookie_value.starts_with("csrf_token="));
10623        assert!(cookie_value.contains("SameSite=Strict"));
10624        assert!(cookie_value.contains("Secure")); // Production mode
10625    }
10626
10627    #[test]
10628    fn csrf_middleware_no_secure_in_dev_mode() {
10629        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().production(false));
10630        let ctx = test_context();
10631        let mut req = Request::new(crate::request::Method::Get, "/");
10632
10633        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10634
10635        let response = Response::ok();
10636        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10637
10638        let cookie_value = header_value(&result, "set-cookie").unwrap();
10639        assert!(!cookie_value.contains("Secure")); // No Secure in dev mode
10640    }
10641
10642    #[test]
10643    fn csrf_middleware_does_not_set_cookie_if_already_present() {
10644        let csrf = CsrfMiddleware::new();
10645        let ctx = test_context();
10646        let mut req = Request::new(crate::request::Method::Get, "/");
10647
10648        // Cookie already present
10649        req.headers_mut()
10650            .insert("cookie", b"csrf_token=existing-token".to_vec());
10651
10652        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10653
10654        let response = Response::ok();
10655        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10656
10657        // Should not set a new cookie
10658        assert!(header_value(&result, "set-cookie").is_none());
10659    }
10660
10661    #[test]
10662    fn csrf_middleware_rotates_token_when_configured() {
10663        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(true));
10664        let ctx = test_context();
10665        let mut req = Request::new(crate::request::Method::Get, "/");
10666
10667        // Cookie already present
10668        req.headers_mut()
10669            .insert("cookie", b"csrf_token=old-token".to_vec());
10670
10671        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10672
10673        let response = Response::ok();
10674        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
10675
10676        // Should set a new cookie even though one exists
10677        assert!(header_value(&result, "set-cookie").is_some());
10678    }
10679
10680    #[test]
10681    fn csrf_middleware_custom_header_name() {
10682        let csrf = CsrfMiddleware::with_config(
10683            CsrfConfig::new()
10684                .header_name("X-XSRF-Token")
10685                .cookie_name("XSRF-TOKEN"),
10686        );
10687        let ctx = test_context();
10688        let mut req = Request::new(crate::request::Method::Post, "/");
10689
10690        let token = "custom-token-value";
10691        req.headers_mut()
10692            .insert("cookie", format!("XSRF-TOKEN={}", token).into_bytes());
10693        req.headers_mut()
10694            .insert("x-xsrf-token", token.as_bytes().to_vec());
10695
10696        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10697        assert!(result.is_continue());
10698    }
10699
10700    #[test]
10701    fn csrf_middleware_error_response_is_json() {
10702        let csrf = CsrfMiddleware::new();
10703        let ctx = test_context();
10704        let mut req = Request::new(crate::request::Method::Post, "/");
10705
10706        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10707
10708        if let ControlFlow::Break(response) = result {
10709            let content_type = header_value(&response, "content-type");
10710            assert_eq!(content_type, Some("application/json".to_string()));
10711
10712            // Check body contains proper error structure
10713            if let ResponseBody::Bytes(body) = response.body_ref() {
10714                let body_str = std::str::from_utf8(body).unwrap();
10715                assert!(body_str.contains("csrf_error"));
10716                assert!(body_str.contains("x-csrf-token"));
10717            } else {
10718                panic!("Expected Bytes body");
10719            }
10720        } else {
10721            panic!("Expected Break");
10722        }
10723    }
10724
10725    #[test]
10726    fn csrf_middleware_custom_error_message() {
10727        let csrf = CsrfMiddleware::with_config(
10728            CsrfConfig::new().error_message("Access denied: invalid security token"),
10729        );
10730        let ctx = test_context();
10731        let mut req = Request::new(crate::request::Method::Post, "/");
10732
10733        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10734
10735        if let ControlFlow::Break(response) = result {
10736            if let ResponseBody::Bytes(body) = response.body_ref() {
10737                let body_str = std::str::from_utf8(body).unwrap();
10738                assert!(body_str.contains("Access denied: invalid security token"));
10739            }
10740        }
10741    }
10742
10743    #[test]
10744    fn csrf_middleware_name() {
10745        let csrf = CsrfMiddleware::new();
10746        assert_eq!(csrf.name(), "CSRF");
10747    }
10748
10749    #[test]
10750    fn csrf_middleware_parses_cookie_with_multiple_cookies() {
10751        let csrf = CsrfMiddleware::new();
10752        let ctx = test_context();
10753        let mut req = Request::new(crate::request::Method::Post, "/");
10754
10755        // Multiple cookies in the header
10756        let token = "the-csrf-token";
10757        req.headers_mut().insert(
10758            "cookie",
10759            format!("session=abc123; csrf_token={}; user=test", token).into_bytes(),
10760        );
10761        req.headers_mut()
10762            .insert("x-csrf-token", token.as_bytes().to_vec());
10763
10764        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10765        assert!(result.is_continue());
10766    }
10767
10768    #[test]
10769    fn csrf_middleware_handles_empty_token_value() {
10770        let csrf = CsrfMiddleware::new();
10771        let ctx = test_context();
10772        let mut req = Request::new(crate::request::Method::Post, "/");
10773
10774        // Empty token values
10775        req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10776        req.headers_mut().insert("x-csrf-token", b"".to_vec());
10777
10778        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10779        assert!(result.is_break()); // Should reject empty tokens
10780    }
10781
10782    // ---- Comprehensive CSRF tests (bd-3v0c) ----
10783
10784    #[test]
10785    fn csrf_token_generate_many_unique() {
10786        // Generate many tokens and verify all are unique
10787        let mut tokens = std::collections::HashSet::new();
10788        for _ in 0..100 {
10789            let token = CsrfToken::generate();
10790            assert!(
10791                tokens.insert(token.0.clone()),
10792                "Duplicate token generated: {}",
10793                token.0
10794            );
10795        }
10796        assert_eq!(tokens.len(), 100);
10797    }
10798
10799    #[test]
10800    fn csrf_token_generate_format_is_hex() {
10801        let token = CsrfToken::generate();
10802        let s = token.as_str();
10803        // Token should be all hex characters, at least 64 chars (32 bytes from urandom)
10804        assert!(
10805            s.len() >= 64,
10806            "Expected at least 64 hex characters, got {} in '{s}'",
10807            s.len()
10808        );
10809        assert!(
10810            s.chars().all(|c| c.is_ascii_hexdigit()),
10811            "Non-hex character in token: {s}"
10812        );
10813    }
10814
10815    #[test]
10816    fn csrf_token_generate_minimum_length() {
10817        let token = CsrfToken::generate();
10818        // 32 bytes from urandom = 64 hex chars
10819        assert!(
10820            token.as_str().len() >= 64,
10821            "Token too short: {} (len={})",
10822            token.as_str(),
10823            token.as_str().len()
10824        );
10825    }
10826
10827    #[test]
10828    fn csrf_token_from_str() {
10829        let token: CsrfToken = "my-token".into();
10830        assert_eq!(token.as_str(), "my-token");
10831        assert_eq!(token.0, "my-token");
10832    }
10833
10834    #[test]
10835    fn csrf_token_clone_eq() {
10836        let t1 = CsrfToken::new("abc");
10837        let t2 = t1.clone();
10838        assert_eq!(t1, t2);
10839        assert_eq!(t1.as_str(), t2.as_str());
10840    }
10841
10842    #[test]
10843    fn csrf_middleware_allows_trace_without_token() {
10844        let csrf = CsrfMiddleware::new();
10845        let ctx = test_context();
10846        let mut req = Request::new(crate::request::Method::Trace, "/");
10847
10848        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10849        assert!(result.is_continue());
10850        // Token should be generated
10851        assert!(req.get_extension::<CsrfToken>().is_some());
10852    }
10853
10854    #[test]
10855    fn csrf_safe_method_generates_token_into_extension() {
10856        let csrf = CsrfMiddleware::new();
10857        let ctx = test_context();
10858
10859        for method in [
10860            crate::request::Method::Get,
10861            crate::request::Method::Head,
10862            crate::request::Method::Options,
10863            crate::request::Method::Trace,
10864        ] {
10865            let mut req = Request::new(method, "/test");
10866            let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10867            assert!(result.is_continue());
10868            let token = req.get_extension::<CsrfToken>().expect("token missing");
10869            assert!(!token.as_str().is_empty());
10870        }
10871    }
10872
10873    #[test]
10874    fn csrf_safe_method_preserves_existing_cookie_token() {
10875        let csrf = CsrfMiddleware::new();
10876        let ctx = test_context();
10877        let mut req = Request::new(crate::request::Method::Get, "/");
10878        req.headers_mut()
10879            .insert("cookie", b"csrf_token=my-existing-token".to_vec());
10880
10881        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
10882
10883        // Extension should contain the existing cookie token, not a new one
10884        let token = req.get_extension::<CsrfToken>().unwrap();
10885        assert_eq!(token.as_str(), "my-existing-token");
10886    }
10887
10888    #[test]
10889    fn csrf_valid_post_stores_token_in_extension() {
10890        let csrf = CsrfMiddleware::new();
10891        let ctx = test_context();
10892        let mut req = Request::new(crate::request::Method::Post, "/submit");
10893
10894        let tk = "valid-token-xyz";
10895        req.headers_mut()
10896            .insert("cookie", format!("csrf_token={}", tk).into_bytes());
10897        req.headers_mut()
10898            .insert("x-csrf-token", tk.as_bytes().to_vec());
10899
10900        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10901        assert!(result.is_continue());
10902        let stored = req.get_extension::<CsrfToken>().unwrap();
10903        assert_eq!(stored.as_str(), tk);
10904    }
10905
10906    #[test]
10907    fn csrf_double_submit_both_empty_strings_rejected() {
10908        let csrf = CsrfMiddleware::new();
10909        let ctx = test_context();
10910        let mut req = Request::new(crate::request::Method::Post, "/");
10911
10912        // Both cookie and header have empty string values
10913        req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10914        req.headers_mut().insert("x-csrf-token", b"".to_vec());
10915
10916        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10917        assert!(result.is_break());
10918    }
10919
10920    #[test]
10921    fn csrf_double_submit_matching_empty_rejected() {
10922        // Even if both are technically "equal" (empty), should reject
10923        let csrf = CsrfMiddleware::new();
10924        let ctx = test_context();
10925        let mut req = Request::new(crate::request::Method::Post, "/");
10926
10927        req.headers_mut().insert("cookie", b"csrf_token=".to_vec());
10928        req.headers_mut().insert("x-csrf-token", b"".to_vec());
10929
10930        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10931        assert!(
10932            result.is_break(),
10933            "Empty matching tokens should be rejected"
10934        );
10935    }
10936
10937    #[test]
10938    fn csrf_header_only_mode_does_not_need_cookie() {
10939        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10940        let ctx = test_context();
10941        let mut req = Request::new(crate::request::Method::Post, "/");
10942
10943        // Header only, no cookie
10944        req.headers_mut()
10945            .insert("x-csrf-token", b"header-only-token".to_vec());
10946
10947        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10948        assert!(result.is_continue());
10949        let token = req.get_extension::<CsrfToken>().unwrap();
10950        assert_eq!(token.as_str(), "header-only-token");
10951    }
10952
10953    #[test]
10954    fn csrf_header_only_mode_ignores_mismatched_cookie() {
10955        // In HeaderOnly mode, the cookie value is irrelevant
10956        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10957        let ctx = test_context();
10958        let mut req = Request::new(crate::request::Method::Post, "/");
10959
10960        req.headers_mut()
10961            .insert("cookie", b"csrf_token=different-value".to_vec());
10962        req.headers_mut()
10963            .insert("x-csrf-token", b"header-value".to_vec());
10964
10965        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10966        assert!(result.is_continue(), "HeaderOnly should ignore cookie");
10967    }
10968
10969    #[test]
10970    fn csrf_header_only_mode_rejects_no_header() {
10971        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10972        let ctx = test_context();
10973        let mut req = Request::new(crate::request::Method::Post, "/");
10974        // No header at all
10975        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10976        assert!(result.is_break());
10977    }
10978
10979    #[test]
10980    fn csrf_header_only_error_message_mentions_header() {
10981        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().mode(CsrfMode::HeaderOnly));
10982        let ctx = test_context();
10983        let mut req = Request::new(crate::request::Method::Post, "/");
10984
10985        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
10986        if let ControlFlow::Break(response) = result {
10987            if let ResponseBody::Bytes(body) = response.body_ref() {
10988                let body_str = std::str::from_utf8(body).unwrap();
10989                assert!(
10990                    body_str.contains("missing in header"),
10991                    "Expected 'missing in header' in: {}",
10992                    body_str
10993                );
10994            }
10995        } else {
10996            panic!("Expected Break");
10997        }
10998    }
10999
11000    #[test]
11001    fn csrf_mismatch_error_differs_from_missing_error() {
11002        let csrf = CsrfMiddleware::new();
11003        let ctx = test_context();
11004
11005        // Missing: no header or cookie
11006        let mut req_missing = Request::new(crate::request::Method::Post, "/");
11007        let missing_result = futures_executor::block_on(csrf.before(&ctx, &mut req_missing));
11008        let missing_body = match missing_result {
11009            ControlFlow::Break(r) => match r.body_ref() {
11010                ResponseBody::Bytes(b) => std::str::from_utf8(b).unwrap().to_string(),
11011                ResponseBody::Empty | ResponseBody::Stream(_) => panic!("Expected Bytes"),
11012            },
11013            ControlFlow::Continue => panic!("Expected Break"),
11014        };
11015
11016        // Mismatch: both present but different
11017        let mut req_mismatch = Request::new(crate::request::Method::Post, "/");
11018        req_mismatch
11019            .headers_mut()
11020            .insert("cookie", b"csrf_token=aaa".to_vec());
11021        req_mismatch
11022            .headers_mut()
11023            .insert("x-csrf-token", b"bbb".to_vec());
11024        let mismatch_result = futures_executor::block_on(csrf.before(&ctx, &mut req_mismatch));
11025        let mismatch_body = match mismatch_result {
11026            ControlFlow::Break(r) => match r.body_ref() {
11027                ResponseBody::Bytes(b) => std::str::from_utf8(b).unwrap().to_string(),
11028                ResponseBody::Empty | ResponseBody::Stream(_) => panic!("Expected Bytes"),
11029            },
11030            ControlFlow::Continue => panic!("Expected Break"),
11031        };
11032
11033        // Error messages should differ
11034        assert_ne!(
11035            missing_body, mismatch_body,
11036            "Missing vs mismatch should have different error messages"
11037        );
11038        assert!(missing_body.contains("missing"));
11039        assert!(mismatch_body.contains("mismatch"));
11040    }
11041
11042    #[test]
11043    fn csrf_cookie_not_httponly() {
11044        // CSRF cookies MUST be readable by JavaScript (no HttpOnly)
11045        let csrf = CsrfMiddleware::new();
11046        let ctx = test_context();
11047        let mut req = Request::new(crate::request::Method::Get, "/");
11048
11049        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11050        let response = Response::ok();
11051        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11052
11053        let cookie_value = header_value(&result, "set-cookie").unwrap();
11054        assert!(
11055            !cookie_value.to_lowercase().contains("httponly"),
11056            "CSRF cookie must NOT be HttpOnly (needs JS access), got: {}",
11057            cookie_value
11058        );
11059    }
11060
11061    #[test]
11062    fn csrf_cookie_has_path_slash() {
11063        let csrf = CsrfMiddleware::new();
11064        let ctx = test_context();
11065        let mut req = Request::new(crate::request::Method::Get, "/");
11066
11067        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11068        let response = Response::ok();
11069        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11070
11071        let cookie_value = header_value(&result, "set-cookie").unwrap();
11072        assert!(
11073            cookie_value.contains("Path=/"),
11074            "Cookie should have Path=/, got: {}",
11075            cookie_value
11076        );
11077    }
11078
11079    #[test]
11080    fn csrf_cookie_has_samesite_strict() {
11081        let csrf = CsrfMiddleware::new();
11082        let ctx = test_context();
11083        let mut req = Request::new(crate::request::Method::Get, "/");
11084
11085        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11086        let response = Response::ok();
11087        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11088
11089        let cookie_value = header_value(&result, "set-cookie").unwrap();
11090        assert!(
11091            cookie_value.contains("SameSite=Strict"),
11092            "Cookie should have SameSite=Strict, got: {}",
11093            cookie_value
11094        );
11095    }
11096
11097    #[test]
11098    fn csrf_production_mode_sets_secure_flag() {
11099        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().production(true));
11100        let ctx = test_context();
11101        let mut req = Request::new(crate::request::Method::Get, "/");
11102
11103        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11104        let response = Response::ok();
11105        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11106
11107        let cookie_value = header_value(&result, "set-cookie").unwrap();
11108        assert!(
11109            cookie_value.contains("Secure"),
11110            "Production cookie must have Secure flag, got: {}",
11111            cookie_value
11112        );
11113    }
11114
11115    #[test]
11116    fn csrf_no_set_cookie_on_post_response() {
11117        // Set-Cookie should only be added for safe methods, not POST
11118        let csrf = CsrfMiddleware::new();
11119        let ctx = test_context();
11120        let mut req = Request::new(crate::request::Method::Post, "/");
11121
11122        let token = "valid-token";
11123        req.headers_mut()
11124            .insert("cookie", format!("csrf_token={}", token).into_bytes());
11125        req.headers_mut()
11126            .insert("x-csrf-token", token.as_bytes().to_vec());
11127
11128        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11129        let response = Response::ok();
11130        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11131
11132        assert!(
11133            header_value(&result, "set-cookie").is_none(),
11134            "POST response should not set CSRF cookie"
11135        );
11136    }
11137
11138    #[test]
11139    fn csrf_head_method_sets_cookie() {
11140        let csrf = CsrfMiddleware::new();
11141        let ctx = test_context();
11142        let mut req = Request::new(crate::request::Method::Head, "/");
11143
11144        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11145        let response = Response::ok();
11146        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11147
11148        assert!(
11149            header_value(&result, "set-cookie").is_some(),
11150            "HEAD response should set CSRF cookie"
11151        );
11152    }
11153
11154    #[test]
11155    fn csrf_options_method_sets_cookie() {
11156        let csrf = CsrfMiddleware::new();
11157        let ctx = test_context();
11158        let mut req = Request::new(crate::request::Method::Options, "/");
11159
11160        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11161        let response = Response::ok();
11162        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11163
11164        assert!(
11165            header_value(&result, "set-cookie").is_some(),
11166            "OPTIONS response should set CSRF cookie"
11167        );
11168    }
11169
11170    #[test]
11171    fn csrf_rotation_produces_different_token_in_cookie() {
11172        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(true));
11173        let ctx = test_context();
11174        let mut req = Request::new(crate::request::Method::Get, "/");
11175
11176        let old_token = "old-token-value";
11177        req.headers_mut()
11178            .insert("cookie", format!("csrf_token={}", old_token).into_bytes());
11179
11180        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11181        let response = Response::ok();
11182        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11183
11184        let cookie_value = header_value(&result, "set-cookie").unwrap();
11185        // When rotation is enabled, old token is reused from cookie parse, but
11186        // the cookie IS set (which the before phase stored in extension).
11187        // The existing token from cookie is used, so cookie_value will contain old_token.
11188        // This verifies the Set-Cookie is emitted even with an existing cookie.
11189        assert!(cookie_value.starts_with("csrf_token="));
11190    }
11191
11192    #[test]
11193    fn csrf_no_rotation_skips_set_cookie_when_present() {
11194        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().rotate_token(false));
11195        let ctx = test_context();
11196        let mut req = Request::new(crate::request::Method::Get, "/");
11197
11198        req.headers_mut()
11199            .insert("cookie", b"csrf_token=existing".to_vec());
11200
11201        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11202        let response = Response::ok();
11203        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11204
11205        assert!(
11206            header_value(&result, "set-cookie").is_none(),
11207            "Without rotation, should not re-set existing cookie"
11208        );
11209    }
11210
11211    #[test]
11212    fn csrf_custom_cookie_name_in_set_cookie_response() {
11213        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().cookie_name("XSRF-TOKEN"));
11214        let ctx = test_context();
11215        let mut req = Request::new(crate::request::Method::Get, "/");
11216
11217        let _ = futures_executor::block_on(csrf.before(&ctx, &mut req));
11218        let response = Response::ok();
11219        let result = futures_executor::block_on(csrf.after(&ctx, &req, response));
11220
11221        let cookie_value = header_value(&result, "set-cookie").unwrap();
11222        assert!(
11223            cookie_value.starts_with("XSRF-TOKEN="),
11224            "Custom cookie name should appear in Set-Cookie, got: {}",
11225            cookie_value
11226        );
11227    }
11228
11229    #[test]
11230    fn csrf_custom_header_name_validated() {
11231        let csrf = CsrfMiddleware::with_config(
11232            CsrfConfig::new()
11233                .header_name("X-Custom-CSRF")
11234                .cookie_name("my_csrf"),
11235        );
11236        let ctx = test_context();
11237        let mut req = Request::new(crate::request::Method::Post, "/");
11238
11239        let token = "custom-tok";
11240        req.headers_mut()
11241            .insert("cookie", format!("my_csrf={}", token).into_bytes());
11242        req.headers_mut()
11243            .insert("x-custom-csrf", token.as_bytes().to_vec());
11244
11245        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11246        assert!(result.is_continue());
11247    }
11248
11249    #[test]
11250    fn csrf_custom_header_name_wrong_header_rejected() {
11251        let csrf = CsrfMiddleware::with_config(CsrfConfig::new().header_name("X-Custom-CSRF"));
11252        let ctx = test_context();
11253        let mut req = Request::new(crate::request::Method::Post, "/");
11254
11255        let token = "some-token";
11256        req.headers_mut()
11257            .insert("cookie", format!("csrf_token={}", token).into_bytes());
11258        // Using default header name instead of custom one
11259        req.headers_mut()
11260            .insert("x-csrf-token", token.as_bytes().to_vec());
11261
11262        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11263        assert!(result.is_break(), "Wrong header name should be rejected");
11264    }
11265
11266    #[test]
11267    fn csrf_cookie_parsing_multiple_cookies_picks_correct() {
11268        let csrf = CsrfMiddleware::new();
11269        let ctx = test_context();
11270        let mut req = Request::new(crate::request::Method::Post, "/");
11271
11272        let token = "correct-csrf";
11273        req.headers_mut().insert(
11274            "cookie",
11275            format!("session=abc; other=xyz; csrf_token={}; tracking=123", token).into_bytes(),
11276        );
11277        req.headers_mut()
11278            .insert("x-csrf-token", token.as_bytes().to_vec());
11279
11280        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11281        assert!(result.is_continue());
11282    }
11283
11284    #[test]
11285    fn csrf_cookie_parsing_spaces_around_semicolons() {
11286        let csrf = CsrfMiddleware::new();
11287        let ctx = test_context();
11288        let mut req = Request::new(crate::request::Method::Post, "/");
11289
11290        let token = "spaced-token";
11291        req.headers_mut().insert(
11292            "cookie",
11293            format!("session=abc ;  csrf_token={}  ; other=xyz", token).into_bytes(),
11294        );
11295        req.headers_mut()
11296            .insert("x-csrf-token", token.as_bytes().to_vec());
11297
11298        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11299        assert!(result.is_continue());
11300    }
11301
11302    #[test]
11303    fn csrf_error_response_status_is_403() {
11304        let csrf = CsrfMiddleware::new();
11305        let ctx = test_context();
11306
11307        // Test all state-changing methods return 403
11308        for method in [
11309            crate::request::Method::Post,
11310            crate::request::Method::Put,
11311            crate::request::Method::Delete,
11312            crate::request::Method::Patch,
11313        ] {
11314            let mut req = Request::new(method, "/");
11315            let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11316            match result {
11317                ControlFlow::Break(response) => {
11318                    assert_eq!(
11319                        response.status(),
11320                        StatusCode::FORBIDDEN,
11321                        "Expected 403 for {:?}",
11322                        method
11323                    );
11324                }
11325                ControlFlow::Continue => panic!("Expected Break for {:?}", method),
11326            }
11327        }
11328    }
11329
11330    #[test]
11331    fn csrf_error_body_json_structure() {
11332        let csrf = CsrfMiddleware::new();
11333        let ctx = test_context();
11334        let mut req = Request::new(crate::request::Method::Post, "/");
11335
11336        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11337        if let ControlFlow::Break(response) = result {
11338            if let ResponseBody::Bytes(body) = response.body_ref() {
11339                let body_str = std::str::from_utf8(body).unwrap();
11340                // Verify JSON structure
11341                let parsed: serde_json::Value = serde_json::from_str(body_str)
11342                    .unwrap_or_else(|e| panic!("Invalid JSON: {}: {}", body_str, e));
11343                assert!(parsed["detail"].is_array());
11344                let detail = &parsed["detail"][0];
11345                assert_eq!(detail["type"], "csrf_error");
11346                assert!(detail["loc"].is_array());
11347                assert_eq!(detail["loc"][0], "header");
11348                assert_eq!(detail["loc"][1], "x-csrf-token");
11349                assert!(detail["msg"].is_string());
11350            } else {
11351                panic!("Expected Bytes body");
11352            }
11353        } else {
11354            panic!("Expected Break");
11355        }
11356    }
11357
11358    #[test]
11359    fn csrf_default_trait() {
11360        let csrf = CsrfMiddleware::default();
11361        assert_eq!(csrf.name(), "CSRF");
11362        // Should behave identically to new()
11363        let ctx = test_context();
11364        let mut req = Request::new(crate::request::Method::Get, "/");
11365        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11366        assert!(result.is_continue());
11367    }
11368
11369    #[test]
11370    fn csrf_mode_default_is_double_submit() {
11371        assert_eq!(CsrfMode::default(), CsrfMode::DoubleSubmit);
11372    }
11373
11374    #[test]
11375    fn csrf_double_submit_both_present_same_non_empty_passes() {
11376        // Explicit test of the core double-submit pattern
11377        let csrf = CsrfMiddleware::new();
11378        let ctx = test_context();
11379
11380        let token = "a1b2c3d4e5f6";
11381        let mut req = Request::new(crate::request::Method::Delete, "/resource/1");
11382        req.headers_mut()
11383            .insert("cookie", format!("csrf_token={}", token).into_bytes());
11384        req.headers_mut()
11385            .insert("x-csrf-token", token.as_bytes().to_vec());
11386
11387        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11388        assert!(result.is_continue());
11389    }
11390
11391    #[test]
11392    fn csrf_double_submit_case_sensitive() {
11393        // Token comparison should be case-sensitive
11394        let csrf = CsrfMiddleware::new();
11395        let ctx = test_context();
11396        let mut req = Request::new(crate::request::Method::Post, "/");
11397
11398        req.headers_mut()
11399            .insert("cookie", b"csrf_token=AbCdEf".to_vec());
11400        req.headers_mut().insert("x-csrf-token", b"abcdef".to_vec());
11401
11402        let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11403        assert!(
11404            result.is_break(),
11405            "Token comparison should be case-sensitive"
11406        );
11407    }
11408
11409    #[test]
11410    fn csrf_token_cookie_extractor_reads_csrf_cookie() {
11411        // Test that CsrfTokenCookie works as a cookie name marker
11412        use crate::extract::{CookieName, CsrfTokenCookie};
11413        assert_eq!(CsrfTokenCookie::NAME, "csrf_token");
11414    }
11415
11416    #[test]
11417    fn csrf_make_set_cookie_header_value_production() {
11418        let value = CsrfMiddleware::make_set_cookie_header_value("csrf_token", "tok123", true);
11419        let s = std::str::from_utf8(&value).unwrap();
11420        assert!(s.contains("csrf_token=tok123"));
11421        assert!(s.contains("Path=/"));
11422        assert!(s.contains("SameSite=Strict"));
11423        assert!(s.contains("Secure"));
11424        assert!(!s.to_lowercase().contains("httponly"));
11425    }
11426
11427    #[test]
11428    fn csrf_make_set_cookie_header_value_development() {
11429        let value = CsrfMiddleware::make_set_cookie_header_value("csrf_token", "tok123", false);
11430        let s = std::str::from_utf8(&value).unwrap();
11431        assert!(s.contains("csrf_token=tok123"));
11432        assert!(s.contains("Path=/"));
11433        assert!(s.contains("SameSite=Strict"));
11434        assert!(!s.contains("Secure"));
11435    }
11436
11437    #[test]
11438    fn csrf_before_after_full_cycle_get_then_post() {
11439        // Simulate a full CSRF flow: GET sets cookie, POST uses it
11440        let csrf = CsrfMiddleware::new();
11441        let ctx = test_context();
11442
11443        // Step 1: GET request - generates token and sets cookie
11444        let mut get_req = Request::new(crate::request::Method::Get, "/form");
11445        let _ = futures_executor::block_on(csrf.before(&ctx, &mut get_req));
11446        let get_response = Response::ok();
11447        let get_result = futures_executor::block_on(csrf.after(&ctx, &get_req, get_response));
11448
11449        let set_cookie = header_value(&get_result, "set-cookie").expect("GET should set cookie");
11450        // Extract token value from "csrf_token=<value>; Path=/; ..."
11451        let token_value = set_cookie
11452            .strip_prefix("csrf_token=")
11453            .unwrap()
11454            .split(';')
11455            .next()
11456            .unwrap();
11457        assert!(!token_value.is_empty());
11458
11459        // Step 2: POST request - uses the token from cookie + header
11460        let mut post_req = Request::new(crate::request::Method::Post, "/form");
11461        post_req
11462            .headers_mut()
11463            .insert("cookie", format!("csrf_token={}", token_value).into_bytes());
11464        post_req
11465            .headers_mut()
11466            .insert("x-csrf-token", token_value.as_bytes().to_vec());
11467
11468        let result = futures_executor::block_on(csrf.before(&ctx, &mut post_req));
11469        assert!(result.is_continue(), "POST with valid token should pass");
11470    }
11471
11472    #[test]
11473    fn csrf_all_state_changing_methods_require_token() {
11474        let csrf = CsrfMiddleware::new();
11475        let ctx = test_context();
11476
11477        for method in [
11478            crate::request::Method::Post,
11479            crate::request::Method::Put,
11480            crate::request::Method::Delete,
11481            crate::request::Method::Patch,
11482        ] {
11483            let mut req = Request::new(method, "/resource");
11484            let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11485            assert!(
11486                result.is_break(),
11487                "{:?} without token should be rejected",
11488                method
11489            );
11490        }
11491    }
11492
11493    #[test]
11494    fn csrf_all_safe_methods_pass_without_token() {
11495        let csrf = CsrfMiddleware::new();
11496        let ctx = test_context();
11497
11498        for method in [
11499            crate::request::Method::Get,
11500            crate::request::Method::Head,
11501            crate::request::Method::Options,
11502            crate::request::Method::Trace,
11503        ] {
11504            let mut req = Request::new(method, "/resource");
11505            let result = futures_executor::block_on(csrf.before(&ctx, &mut req));
11506            assert!(
11507                result.is_continue(),
11508                "{:?} should be allowed without token",
11509                method
11510            );
11511        }
11512    }
11513
11514    // =========================================================================
11515    // Middleware Stack Ordering Tests (Onion Model)
11516    // =========================================================================
11517
11518    /// Middleware that records execution order to a shared Vec.
11519    /// Used to verify the onion model (before in order, after in reverse).
11520    #[derive(Clone)]
11521    struct OrderRecordingMiddleware {
11522        id: &'static str,
11523        log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11524    }
11525
11526    impl OrderRecordingMiddleware {
11527        fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11528            Self { id, log }
11529        }
11530    }
11531
11532    impl Middleware for OrderRecordingMiddleware {
11533        fn before<'a>(
11534            &'a self,
11535            _ctx: &'a RequestContext,
11536            _req: &'a mut Request,
11537        ) -> BoxFuture<'a, ControlFlow> {
11538            let id = self.id;
11539            let log = self.log.clone();
11540            Box::pin(async move {
11541                log.lock().unwrap().push(format!("{id}:before"));
11542                ControlFlow::Continue
11543            })
11544        }
11545
11546        fn after<'a>(
11547            &'a self,
11548            _ctx: &'a RequestContext,
11549            _req: &'a Request,
11550            response: Response,
11551        ) -> BoxFuture<'a, Response> {
11552            let id = self.id;
11553            let log = self.log.clone();
11554            Box::pin(async move {
11555                log.lock().unwrap().push(format!("{id}:after"));
11556                response
11557            })
11558        }
11559
11560        fn name(&self) -> &'static str {
11561            "OrderRecording"
11562        }
11563    }
11564
11565    /// Middleware that short-circuits in its before hook.
11566    struct ShortCircuitMiddleware {
11567        id: &'static str,
11568        log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11569    }
11570
11571    impl ShortCircuitMiddleware {
11572        fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11573            Self { id, log }
11574        }
11575    }
11576
11577    impl Middleware for ShortCircuitMiddleware {
11578        fn before<'a>(
11579            &'a self,
11580            _ctx: &'a RequestContext,
11581            _req: &'a mut Request,
11582        ) -> BoxFuture<'a, ControlFlow> {
11583            let id = self.id;
11584            let log = self.log.clone();
11585            Box::pin(async move {
11586                log.lock().unwrap().push(format!("{id}:before:break"));
11587                ControlFlow::Break(
11588                    Response::with_status(StatusCode::FORBIDDEN)
11589                        .body(ResponseBody::Bytes(b"short-circuited".to_vec())),
11590                )
11591            })
11592        }
11593
11594        fn after<'a>(
11595            &'a self,
11596            _ctx: &'a RequestContext,
11597            _req: &'a Request,
11598            response: Response,
11599        ) -> BoxFuture<'a, Response> {
11600            let id = self.id;
11601            let log = self.log.clone();
11602            Box::pin(async move {
11603                log.lock().unwrap().push(format!("{id}:after"));
11604                response
11605            })
11606        }
11607
11608        fn name(&self) -> &'static str {
11609            "ShortCircuit"
11610        }
11611    }
11612
11613    /// Simple handler that records when it runs.
11614    struct RecordingHandler {
11615        log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11616    }
11617
11618    impl RecordingHandler {
11619        fn new(log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11620            Self { log }
11621        }
11622    }
11623
11624    impl Handler for RecordingHandler {
11625        fn call<'a>(
11626            &'a self,
11627            _ctx: &'a RequestContext,
11628            _req: &'a mut Request,
11629        ) -> BoxFuture<'a, Response> {
11630            let log = self.log.clone();
11631            Box::pin(async move {
11632                log.lock().unwrap().push("handler".to_string());
11633                Response::ok().body(ResponseBody::Bytes(b"ok".to_vec()))
11634            })
11635        }
11636    }
11637
11638    #[test]
11639    fn middleware_stack_three_middleware_onion_order() {
11640        // Test that three middleware follow the onion model:
11641        // Before hooks run in order: 1 -> 2 -> 3
11642        // After hooks run in reverse: 3 -> 2 -> 1
11643        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11644
11645        let mut stack = MiddlewareStack::new();
11646        stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11647        stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11648        stack.push(OrderRecordingMiddleware::new("mw3", log.clone()));
11649
11650        let handler = RecordingHandler::new(log.clone());
11651        let ctx = test_context();
11652        let mut req = Request::new(crate::request::Method::Get, "/");
11653
11654        let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11655
11656        let execution_log = log.lock().unwrap().clone();
11657        assert_eq!(
11658            execution_log,
11659            vec![
11660                "mw1:before",
11661                "mw2:before",
11662                "mw3:before",
11663                "handler",
11664                "mw3:after",
11665                "mw2:after",
11666                "mw1:after",
11667            ]
11668        );
11669    }
11670
11671    #[test]
11672    fn middleware_stack_short_circuit_runs_prior_after_hooks() {
11673        // When middleware 2 short-circuits:
11674        // - mw1:before runs (returns Continue, count=1)
11675        // - mw2:before short-circuits (returns Break, count stays at 1)
11676        // - mw3:before does NOT run
11677        // - handler does NOT run
11678        // - Only middleware that successfully completed before (mw1) have after run
11679        // - mw1:after runs
11680        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11681
11682        let mut stack = MiddlewareStack::new();
11683        stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11684        stack.push(ShortCircuitMiddleware::new("mw2", log.clone()));
11685        stack.push(OrderRecordingMiddleware::new("mw3", log.clone()));
11686
11687        let handler = RecordingHandler::new(log.clone());
11688        let ctx = test_context();
11689        let mut req = Request::new(crate::request::Method::Get, "/");
11690
11691        let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11692
11693        // Should return the short-circuit response
11694        assert_eq!(response.status().as_u16(), 403);
11695
11696        let execution_log = log.lock().unwrap().clone();
11697        // Note: mw2's after hook does NOT run because it didn't return Continue
11698        // Only middleware that successfully completed before (returned Continue) have after run
11699        assert_eq!(
11700            execution_log,
11701            vec!["mw1:before", "mw2:before:break", "mw1:after",]
11702        );
11703    }
11704
11705    #[test]
11706    fn middleware_stack_first_middleware_short_circuits() {
11707        // When the first middleware short-circuits:
11708        // - mw1:before short-circuits (returns Break, count=0)
11709        // - No after hooks run (count=0)
11710        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11711
11712        let mut stack = MiddlewareStack::new();
11713        stack.push(ShortCircuitMiddleware::new("mw1", log.clone()));
11714        stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11715
11716        let handler = RecordingHandler::new(log.clone());
11717        let ctx = test_context();
11718        let mut req = Request::new(crate::request::Method::Get, "/");
11719
11720        let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11721        assert_eq!(response.status().as_u16(), 403);
11722
11723        let execution_log = log.lock().unwrap().clone();
11724        // No after hooks run because no middleware returned Continue
11725        assert_eq!(execution_log, vec!["mw1:before:break",]);
11726    }
11727
11728    #[test]
11729    fn middleware_stack_empty_runs_handler_only() {
11730        // Empty stack should just run the handler (onion ordering variant)
11731        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11732
11733        let stack = MiddlewareStack::new();
11734        let handler = RecordingHandler::new(log.clone());
11735        let ctx = test_context();
11736        let mut req = Request::new(crate::request::Method::Get, "/");
11737
11738        let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11739        assert_eq!(response.status().as_u16(), 200);
11740
11741        let execution_log = log.lock().unwrap().clone();
11742        assert_eq!(execution_log, vec!["handler"]);
11743    }
11744
11745    #[test]
11746    fn middleware_stack_single_middleware_ordering() {
11747        // Single middleware should have before -> handler -> after
11748        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11749
11750        let mut stack = MiddlewareStack::new();
11751        stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11752
11753        let handler = RecordingHandler::new(log.clone());
11754        let ctx = test_context();
11755        let mut req = Request::new(crate::request::Method::Get, "/");
11756
11757        let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11758
11759        let execution_log = log.lock().unwrap().clone();
11760        assert_eq!(execution_log, vec!["mw1:before", "handler", "mw1:after",]);
11761    }
11762
11763    #[test]
11764    fn middleware_stack_five_middleware_onion_order() {
11765        // Test with five middleware for a longer chain
11766        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11767
11768        let mut stack = MiddlewareStack::new();
11769        stack.push(OrderRecordingMiddleware::new("a", log.clone()));
11770        stack.push(OrderRecordingMiddleware::new("b", log.clone()));
11771        stack.push(OrderRecordingMiddleware::new("c", log.clone()));
11772        stack.push(OrderRecordingMiddleware::new("d", log.clone()));
11773        stack.push(OrderRecordingMiddleware::new("e", log.clone()));
11774
11775        let handler = RecordingHandler::new(log.clone());
11776        let ctx = test_context();
11777        let mut req = Request::new(crate::request::Method::Get, "/");
11778
11779        let _response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11780
11781        let execution_log = log.lock().unwrap().clone();
11782        assert_eq!(
11783            execution_log,
11784            vec![
11785                "a:before", "b:before", "c:before", "d:before", "e:before", "handler", "e:after",
11786                "d:after", "c:after", "b:after", "a:after",
11787            ]
11788        );
11789    }
11790
11791    #[test]
11792    fn middleware_stack_short_circuit_at_end_runs_prior_afters() {
11793        // When the last middleware short-circuits:
11794        // - mw1:before runs (Continue, count=1)
11795        // - mw2:before runs (Continue, count=2)
11796        // - mw3:before short-circuits (Break, count stays at 2)
11797        // - handler does NOT run
11798        // - After hooks run for mw1 and mw2 only (they returned Continue)
11799        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11800
11801        let mut stack = MiddlewareStack::new();
11802        stack.push(OrderRecordingMiddleware::new("mw1", log.clone()));
11803        stack.push(OrderRecordingMiddleware::new("mw2", log.clone()));
11804        stack.push(ShortCircuitMiddleware::new("mw3", log.clone()));
11805
11806        let handler = RecordingHandler::new(log.clone());
11807        let ctx = test_context();
11808        let mut req = Request::new(crate::request::Method::Get, "/");
11809
11810        let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11811        assert_eq!(response.status().as_u16(), 403);
11812
11813        let execution_log = log.lock().unwrap().clone();
11814        // mw3's after hook does NOT run because it didn't return Continue
11815        assert_eq!(
11816            execution_log,
11817            vec![
11818                "mw1:before",
11819                "mw2:before",
11820                "mw3:before:break",
11821                "mw2:after",
11822                "mw1:after",
11823            ]
11824        );
11825    }
11826
11827    /// Middleware that modifies the request in before and response in after.
11828    struct ModifyingMiddleware {
11829        id: &'static str,
11830        log: std::sync::Arc<std::sync::Mutex<Vec<String>>>,
11831    }
11832
11833    impl ModifyingMiddleware {
11834        fn new(id: &'static str, log: std::sync::Arc<std::sync::Mutex<Vec<String>>>) -> Self {
11835            Self { id, log }
11836        }
11837    }
11838
11839    impl Middleware for ModifyingMiddleware {
11840        fn before<'a>(
11841            &'a self,
11842            _ctx: &'a RequestContext,
11843            req: &'a mut Request,
11844        ) -> BoxFuture<'a, ControlFlow> {
11845            let id = self.id;
11846            let log = self.log.clone();
11847            Box::pin(async move {
11848                // Add a header to track middleware order
11849                req.headers_mut()
11850                    .insert(format!("x-{id}-before"), b"true".to_vec());
11851                log.lock().unwrap().push(format!("{id}:before"));
11852                ControlFlow::Continue
11853            })
11854        }
11855
11856        fn after<'a>(
11857            &'a self,
11858            _ctx: &'a RequestContext,
11859            _req: &'a Request,
11860            response: Response,
11861        ) -> BoxFuture<'a, Response> {
11862            let id = self.id;
11863            let log = self.log.clone();
11864            Box::pin(async move {
11865                log.lock().unwrap().push(format!("{id}:after"));
11866                // Add a header to the response
11867                response.header(format!("x-{id}-after"), b"true".to_vec())
11868            })
11869        }
11870
11871        fn name(&self) -> &'static str {
11872            "Modifying"
11873        }
11874    }
11875
11876    #[test]
11877    fn middleware_stack_modifications_accumulate_correctly() {
11878        // Test that request modifications in before hooks accumulate,
11879        // and response modifications in after hooks accumulate
11880        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11881
11882        let mut stack = MiddlewareStack::new();
11883        stack.push(ModifyingMiddleware::new("mw1", log.clone()));
11884        stack.push(ModifyingMiddleware::new("mw2", log.clone()));
11885        stack.push(ModifyingMiddleware::new("mw3", log.clone()));
11886
11887        let handler = RecordingHandler::new(log.clone());
11888        let ctx = test_context();
11889        let mut req = Request::new(crate::request::Method::Get, "/");
11890
11891        let response = futures_executor::block_on(stack.execute(&handler, &ctx, &mut req));
11892
11893        // Check that all after hooks added their headers
11894        assert!(header_value(&response, "x-mw1-after").is_some());
11895        assert!(header_value(&response, "x-mw2-after").is_some());
11896        assert!(header_value(&response, "x-mw3-after").is_some());
11897
11898        // Check that the request was modified by all before hooks
11899        assert!(req.headers().contains("x-mw1-before"));
11900        assert!(req.headers().contains("x-mw2-before"));
11901        assert!(req.headers().contains("x-mw3-before"));
11902    }
11903
11904    #[test]
11905    fn layer_wrap_maintains_middleware_order() {
11906        // Test that Layer::wrap creates a Layered handler that maintains before->after ordering
11907        let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
11908
11909        // Create a layer with our recording middleware
11910        let layer = Layer::new(OrderRecordingMiddleware::new("layer", log.clone()));
11911
11912        // Wrap the recording handler
11913        let handler = RecordingHandler::new(log.clone());
11914        let layered_handler = layer.wrap(handler);
11915
11916        let ctx = test_context();
11917        let mut req = Request::new(crate::request::Method::Get, "/");
11918
11919        // Execute the layered handler directly (not via middleware stack)
11920        let _response = futures_executor::block_on(layered_handler.call(&ctx, &mut req));
11921
11922        let execution_log = log.lock().unwrap().clone();
11923        assert_eq!(
11924            execution_log,
11925            vec!["layer:before", "handler", "layer:after",]
11926        );
11927    }
11928}
11929
11930// ============================================================================
11931// Compression Middleware Tests (requires "compression" feature)
11932// ============================================================================
11933
11934#[cfg(all(test, feature = "compression"))]
11935mod compression_tests {
11936    use super::*;
11937    use crate::request::Method;
11938    use crate::response::ResponseBody;
11939
11940    fn test_context() -> RequestContext {
11941        RequestContext::new(asupersync::Cx::for_testing(), 1)
11942    }
11943
11944    #[test]
11945    fn compression_config_defaults() {
11946        let config = CompressionConfig::default();
11947        assert_eq!(config.min_size, 1024);
11948        assert_eq!(config.level, 6);
11949        assert!(!config.skip_content_types.is_empty());
11950    }
11951
11952    #[test]
11953    fn compression_config_builder() {
11954        let config = CompressionConfig::new().min_size(512).level(9);
11955        assert_eq!(config.min_size, 512);
11956        assert_eq!(config.level, 9);
11957    }
11958
11959    #[test]
11960    fn compression_level_clamped() {
11961        let config = CompressionConfig::new().level(100);
11962        assert_eq!(config.level, 9);
11963
11964        let config = CompressionConfig::new().level(0);
11965        assert_eq!(config.level, 1);
11966    }
11967
11968    #[test]
11969    fn skip_content_type_exact_match() {
11970        let config = CompressionConfig::default();
11971        assert!(config.should_skip_content_type("image/jpeg"));
11972        assert!(config.should_skip_content_type("image/jpeg; charset=utf-8"));
11973        assert!(!config.should_skip_content_type("text/html"));
11974    }
11975
11976    #[test]
11977    fn skip_content_type_prefix_match() {
11978        let config = CompressionConfig::default();
11979        // "video/" prefix should match any video type
11980        assert!(config.should_skip_content_type("video/mp4"));
11981        assert!(config.should_skip_content_type("video/webm"));
11982        assert!(config.should_skip_content_type("audio/mpeg"));
11983    }
11984
11985    #[test]
11986    fn compression_skips_small_responses() {
11987        let middleware = CompressionMiddleware::new();
11988        let ctx = test_context();
11989
11990        // Create request with Accept-Encoding: gzip
11991        let mut req = Request::new(Method::Get, "/");
11992        req.headers_mut()
11993            .insert("accept-encoding", b"gzip".to_vec());
11994
11995        // Create a small response (less than 1024 bytes)
11996        let response = Response::ok()
11997            .header("content-type", b"text/plain".to_vec())
11998            .body(ResponseBody::Bytes(b"Hello, World!".to_vec()));
11999
12000        // Run the after hook
12001        let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12002
12003        // Should NOT be compressed (too small)
12004        let has_encoding = result
12005            .headers()
12006            .iter()
12007            .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12008        assert!(!has_encoding, "Small response should not be compressed");
12009    }
12010
12011    #[test]
12012    fn compression_works_for_large_responses() {
12013        let config = CompressionConfig::new().min_size(10); // Lower threshold
12014        let middleware = CompressionMiddleware::with_config(config);
12015        let ctx = test_context();
12016
12017        // Create request with Accept-Encoding: gzip
12018        let mut req = Request::new(Method::Get, "/");
12019        req.headers_mut()
12020            .insert("accept-encoding", b"gzip".to_vec());
12021
12022        // Create a response with repetitive content (compresses well)
12023        let body = "Hello, World! ".repeat(100);
12024        let original_size = body.len();
12025
12026        let response = Response::ok()
12027            .header("content-type", b"text/plain".to_vec())
12028            .body(ResponseBody::Bytes(body.into_bytes()));
12029
12030        // Run the after hook
12031        let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12032
12033        // Should be compressed
12034        let encoding = result
12035            .headers()
12036            .iter()
12037            .find(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12038        assert!(encoding.is_some(), "Large response should be compressed");
12039
12040        let (_, value) = encoding.unwrap();
12041        assert_eq!(value, b"gzip");
12042
12043        // Check Vary header
12044        let vary = result
12045            .headers()
12046            .iter()
12047            .find(|(name, _)| name.eq_ignore_ascii_case("vary"));
12048        assert!(vary.is_some(), "Should have Vary header");
12049
12050        // Verify compressed size is smaller
12051        if let ResponseBody::Bytes(compressed) = result.body_ref() {
12052            assert!(
12053                compressed.len() < original_size,
12054                "Compressed size should be smaller"
12055            );
12056        } else {
12057            panic!("Expected Bytes body");
12058        }
12059    }
12060
12061    #[test]
12062    fn compression_skips_without_accept_encoding() {
12063        let config = CompressionConfig::new().min_size(10);
12064        let middleware = CompressionMiddleware::with_config(config);
12065        let ctx = test_context();
12066
12067        // Create request WITHOUT Accept-Encoding
12068        let req = Request::new(Method::Get, "/");
12069
12070        let body = "Hello, World! ".repeat(100);
12071        let response = Response::ok()
12072            .header("content-type", b"text/plain".to_vec())
12073            .body(ResponseBody::Bytes(body.into_bytes()));
12074
12075        let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12076
12077        // Should NOT be compressed (no Accept-Encoding)
12078        let has_encoding = result
12079            .headers()
12080            .iter()
12081            .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12082        assert!(!has_encoding, "Should not compress without Accept-Encoding");
12083    }
12084
12085    #[test]
12086    fn compression_skips_already_compressed_content() {
12087        let config = CompressionConfig::new().min_size(10);
12088        let middleware = CompressionMiddleware::with_config(config);
12089        let ctx = test_context();
12090
12091        // Create request with Accept-Encoding: gzip
12092        let mut req = Request::new(Method::Get, "/");
12093        req.headers_mut()
12094            .insert("accept-encoding", b"gzip".to_vec());
12095
12096        // Create response with already-compressed content type
12097        let body = "Some image data".repeat(100);
12098        let response = Response::ok()
12099            .header("content-type", b"image/jpeg".to_vec())
12100            .body(ResponseBody::Bytes(body.into_bytes()));
12101
12102        let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12103
12104        // Should NOT be compressed (image/jpeg is already compressed)
12105        let has_encoding = result
12106            .headers()
12107            .iter()
12108            .any(|(name, _)| name.eq_ignore_ascii_case("content-encoding"));
12109        assert!(
12110            !has_encoding,
12111            "Should not compress already-compressed content types"
12112        );
12113    }
12114
12115    #[test]
12116    fn compression_skips_if_already_has_content_encoding() {
12117        let config = CompressionConfig::new().min_size(10);
12118        let middleware = CompressionMiddleware::with_config(config);
12119        let ctx = test_context();
12120
12121        // Create request with Accept-Encoding: gzip
12122        let mut req = Request::new(Method::Get, "/");
12123        req.headers_mut()
12124            .insert("accept-encoding", b"gzip".to_vec());
12125
12126        // Create response that already has Content-Encoding
12127        let body = "Hello, World! ".repeat(100);
12128        let response = Response::ok()
12129            .header("content-type", b"text/plain".to_vec())
12130            .header("content-encoding", b"br".to_vec())
12131            .body(ResponseBody::Bytes(body.into_bytes()));
12132
12133        let result = futures_executor::block_on(middleware.after(&ctx, &req, response));
12134
12135        // Should NOT double-compress
12136        let encodings: Vec<_> = result
12137            .headers()
12138            .iter()
12139            .filter(|(name, _)| name.eq_ignore_ascii_case("content-encoding"))
12140            .collect();
12141
12142        // Should still have exactly one Content-Encoding header (the original br)
12143        assert_eq!(encodings.len(), 1);
12144        assert_eq!(encodings[0].1, b"br");
12145    }
12146
12147    #[test]
12148    fn accepts_gzip_parses_header_correctly() {
12149        // Test various Accept-Encoding header formats
12150
12151        // Simple gzip
12152        let mut req = Request::new(Method::Get, "/");
12153        req.headers_mut()
12154            .insert("accept-encoding", b"gzip".to_vec());
12155        assert!(CompressionMiddleware::accepts_gzip(&req));
12156
12157        // Multiple encodings
12158        let mut req = Request::new(Method::Get, "/");
12159        req.headers_mut()
12160            .insert("accept-encoding", b"deflate, gzip, br".to_vec());
12161        assert!(CompressionMiddleware::accepts_gzip(&req));
12162
12163        // With quality values
12164        let mut req = Request::new(Method::Get, "/");
12165        req.headers_mut()
12166            .insert("accept-encoding", b"gzip;q=1.0, identity;q=0.5".to_vec());
12167        assert!(CompressionMiddleware::accepts_gzip(&req));
12168
12169        // Wildcard
12170        let mut req = Request::new(Method::Get, "/");
12171        req.headers_mut().insert("accept-encoding", b"*".to_vec());
12172        assert!(CompressionMiddleware::accepts_gzip(&req));
12173
12174        // No gzip
12175        let mut req = Request::new(Method::Get, "/");
12176        req.headers_mut()
12177            .insert("accept-encoding", b"deflate, br".to_vec());
12178        assert!(!CompressionMiddleware::accepts_gzip(&req));
12179
12180        // No header
12181        let req_no_header = Request::new(Method::Get, "/");
12182        assert!(!CompressionMiddleware::accepts_gzip(&req_no_header));
12183    }
12184
12185    #[test]
12186    fn compression_middleware_name() {
12187        let middleware = CompressionMiddleware::new();
12188        assert_eq!(middleware.name(), "Compression");
12189    }
12190}
12191
12192// ============================================================================
12193// Request Inspection Middleware Tests
12194// ============================================================================
12195
12196#[cfg(test)]
12197mod request_inspection_tests {
12198    use super::*;
12199    use crate::request::Method;
12200    use crate::response::ResponseBody;
12201
12202    fn test_context() -> RequestContext {
12203        RequestContext::new(asupersync::Cx::for_testing(), 1)
12204    }
12205
12206    #[test]
12207    fn inspection_middleware_default_creates_normal_verbosity() {
12208        let mw = RequestInspectionMiddleware::new();
12209        assert_eq!(mw.verbosity, InspectionVerbosity::Normal);
12210        assert_eq!(mw.slow_threshold_ms, 1000);
12211        assert_eq!(mw.max_body_preview, 2048);
12212        assert_eq!(mw.name(), "RequestInspection");
12213    }
12214
12215    #[test]
12216    fn inspection_middleware_builder_methods() {
12217        let mw = RequestInspectionMiddleware::new()
12218            .verbosity(InspectionVerbosity::Verbose)
12219            .slow_threshold_ms(500)
12220            .max_body_preview(4096)
12221            .log_config(LogConfig::development())
12222            .redact_header("x-api-key");
12223
12224        assert_eq!(mw.verbosity, InspectionVerbosity::Verbose);
12225        assert_eq!(mw.slow_threshold_ms, 500);
12226        assert_eq!(mw.max_body_preview, 4096);
12227        assert!(mw.redact_headers.contains("x-api-key"));
12228        // Default redacted headers should still be present
12229        assert!(mw.redact_headers.contains("authorization"));
12230        assert!(mw.redact_headers.contains("cookie"));
12231    }
12232
12233    #[test]
12234    fn inspection_before_continues_processing() {
12235        let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12236        let ctx = test_context();
12237        let mut req = Request::new(Method::Post, "/api/users");
12238
12239        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12240        assert!(result.is_continue());
12241    }
12242
12243    #[test]
12244    fn inspection_after_returns_response_unchanged() {
12245        let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12246        let ctx = test_context();
12247        let mut req = Request::new(Method::Get, "/health");
12248
12249        // Run before to set the InspectionStart extension
12250        let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12251
12252        let response = Response::ok().body(ResponseBody::Bytes(b"OK".to_vec()));
12253
12254        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
12255        assert_eq!(result.status().as_u16(), 200);
12256        assert_eq!(result.body_ref().len(), 2);
12257    }
12258
12259    #[test]
12260    fn inspection_stores_start_extension() {
12261        let mw = RequestInspectionMiddleware::new();
12262        let ctx = test_context();
12263        let mut req = Request::new(Method::Get, "/");
12264
12265        let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12266
12267        // Verify the InspectionStart extension was set
12268        assert!(req.get_extension::<InspectionStart>().is_some());
12269    }
12270
12271    #[test]
12272    fn inspection_all_verbosity_levels_continue() {
12273        for verbosity in [
12274            InspectionVerbosity::Minimal,
12275            InspectionVerbosity::Normal,
12276            InspectionVerbosity::Verbose,
12277        ] {
12278            let mw = RequestInspectionMiddleware::new().verbosity(verbosity);
12279            let ctx = test_context();
12280            let mut req = Request::new(Method::Get, "/test");
12281            req.headers_mut()
12282                .insert("content-type", b"text/plain".to_vec());
12283
12284            let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12285            assert!(
12286                result.is_continue(),
12287                "Verbosity {verbosity:?} should continue"
12288            );
12289        }
12290    }
12291
12292    #[test]
12293    fn inspection_verbose_with_json_body() {
12294        let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Verbose);
12295        let ctx = test_context();
12296        let body = br#"{"name":"Alice","age":30}"#;
12297        let mut req = Request::new(Method::Post, "/api/users");
12298        req.headers_mut()
12299            .insert("content-type", b"application/json".to_vec());
12300        req.set_body(Body::Bytes(body.to_vec()));
12301
12302        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12303        assert!(result.is_continue());
12304    }
12305
12306    #[test]
12307    fn inspection_verbose_after_with_json_response() {
12308        let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Verbose);
12309        let ctx = test_context();
12310        let mut req = Request::new(Method::Get, "/api/users/1");
12311
12312        let _ = futures_executor::block_on(mw.before(&ctx, &mut req));
12313
12314        let response = Response::ok()
12315            .header("content-type", b"application/json".to_vec())
12316            .body(ResponseBody::Bytes(br#"{"id":1,"name":"Alice"}"#.to_vec()));
12317
12318        let result = futures_executor::block_on(mw.after(&ctx, &req, response));
12319        assert_eq!(result.status().as_u16(), 200);
12320    }
12321
12322    #[test]
12323    fn inspection_redacts_sensitive_headers() {
12324        let mw = RequestInspectionMiddleware::new();
12325
12326        // Verify default redacted headers are present
12327        assert!(mw.redact_headers.contains("authorization"));
12328        assert!(mw.redact_headers.contains("proxy-authorization"));
12329        assert!(mw.redact_headers.contains("cookie"));
12330        assert!(mw.redact_headers.contains("set-cookie"));
12331    }
12332
12333    #[test]
12334    fn inspection_format_headers_redacts() {
12335        let mw = RequestInspectionMiddleware::new().redact_header("x-secret");
12336
12337        let headers = vec![
12338            ("content-type", b"text/plain".as_slice()),
12339            ("x-secret", b"my-secret-value".as_slice()),
12340            ("x-normal", b"visible".as_slice()),
12341        ];
12342
12343        let output = mw.format_inspection_headers(headers.into_iter());
12344        assert!(output.contains("content-type: text/plain"));
12345        assert!(output.contains("x-secret: [REDACTED]"));
12346        assert!(output.contains("x-normal: visible"));
12347        assert!(!output.contains("my-secret-value"));
12348    }
12349
12350    #[test]
12351    fn inspection_format_body_preview_truncates() {
12352        let mw = RequestInspectionMiddleware::new().max_body_preview(10);
12353
12354        let body = b"Hello, World! This is a long body.";
12355        let result = mw.format_body_preview(body, None);
12356        assert!(result.is_some());
12357        let text = result.unwrap();
12358        assert!(text.ends_with("..."));
12359        assert!(text.len() <= 15); // 10 chars + "..."
12360    }
12361
12362    #[test]
12363    fn inspection_format_body_preview_empty() {
12364        let mw = RequestInspectionMiddleware::new();
12365        assert!(mw.format_body_preview(b"", None).is_none());
12366    }
12367
12368    #[test]
12369    fn inspection_format_body_preview_zero_max() {
12370        let mw = RequestInspectionMiddleware::new().max_body_preview(0);
12371        assert!(mw.format_body_preview(b"hello", None).is_none());
12372    }
12373
12374    #[test]
12375    fn inspection_format_body_preview_json_pretty() {
12376        let mw = RequestInspectionMiddleware::new();
12377        let body = br#"{"key":"value","num":42}"#;
12378        let ct = b"application/json".as_slice();
12379        let result = mw.format_body_preview(body, Some(ct));
12380        assert!(result.is_some());
12381        let text = result.unwrap();
12382        // Pretty-printed JSON should contain newlines
12383        assert!(text.contains('\n'));
12384        assert!(text.contains("\"key\": \"value\""));
12385    }
12386
12387    #[test]
12388    fn inspection_format_body_preview_non_json() {
12389        let mw = RequestInspectionMiddleware::new();
12390        let body = b"Hello, World!";
12391        let ct = b"text/plain".as_slice();
12392        let result = mw.format_body_preview(body, Some(ct));
12393        assert_eq!(result.unwrap(), "Hello, World!");
12394    }
12395
12396    #[test]
12397    fn inspection_format_body_preview_binary() {
12398        let mw = RequestInspectionMiddleware::new();
12399        let body: &[u8] = &[0xFF, 0xFE, 0xFD, 0x00];
12400        let result = mw.format_body_preview(body, None);
12401        assert!(result.is_some());
12402        assert!(result.unwrap().contains("binary"));
12403    }
12404
12405    #[test]
12406    fn try_pretty_json_valid_object() {
12407        let result = try_pretty_json(r#"{"a":"b","c":1}"#);
12408        assert!(result.is_some());
12409        let pretty = result.unwrap();
12410        assert!(pretty.contains('\n'));
12411        assert!(pretty.contains("  \"a\": \"b\""));
12412    }
12413
12414    #[test]
12415    fn try_pretty_json_valid_array() {
12416        let result = try_pretty_json(r"[1,2,3]");
12417        assert!(result.is_some());
12418        let pretty = result.unwrap();
12419        assert!(pretty.contains('\n'));
12420    }
12421
12422    #[test]
12423    fn try_pretty_json_empty_object() {
12424        let result = try_pretty_json("{}");
12425        assert!(result.is_some());
12426        assert_eq!(result.unwrap(), "{}");
12427    }
12428
12429    #[test]
12430    fn try_pretty_json_empty_array() {
12431        let result = try_pretty_json("[]");
12432        assert!(result.is_some());
12433        assert_eq!(result.unwrap(), "[]");
12434    }
12435
12436    #[test]
12437    fn try_pretty_json_not_json() {
12438        assert!(try_pretty_json("hello world").is_none());
12439        assert!(try_pretty_json("12345").is_none());
12440    }
12441
12442    #[test]
12443    fn try_pretty_json_nested() {
12444        let input = r#"{"user":{"name":"Alice","roles":["admin","user"]}}"#;
12445        let result = try_pretty_json(input);
12446        assert!(result.is_some());
12447        let pretty = result.unwrap();
12448        assert!(pretty.contains("\"user\":"));
12449        assert!(pretty.contains("\"name\": \"Alice\""));
12450        assert!(pretty.contains("\"roles\":"));
12451    }
12452
12453    #[test]
12454    fn try_pretty_json_with_escapes() {
12455        let input = r#"{"msg":"hello \"world\""}"#;
12456        let result = try_pretty_json(input);
12457        assert!(result.is_some());
12458        let pretty = result.unwrap();
12459        assert!(pretty.contains(r#"\"world\""#));
12460    }
12461
12462    #[test]
12463    fn inspection_name() {
12464        let mw = RequestInspectionMiddleware::new();
12465        assert_eq!(mw.name(), "RequestInspection");
12466    }
12467
12468    #[test]
12469    fn inspection_default_via_default_trait() {
12470        let mw = RequestInspectionMiddleware::default();
12471        assert_eq!(mw.verbosity, InspectionVerbosity::Normal);
12472        assert_eq!(mw.slow_threshold_ms, 1000);
12473    }
12474
12475    #[test]
12476    fn inspection_with_query_string() {
12477        let mw = RequestInspectionMiddleware::new().verbosity(InspectionVerbosity::Minimal);
12478        let ctx = test_context();
12479        let mut req = Request::new(Method::Get, "/search");
12480        req.set_query(Some("q=rust&page=1".to_string()));
12481
12482        let result = futures_executor::block_on(mw.before(&ctx, &mut req));
12483        assert!(result.is_continue());
12484    }
12485
12486    #[test]
12487    fn inspection_response_body_stream() {
12488        let mw = RequestInspectionMiddleware::new();
12489        let result = mw.format_response_preview(&ResponseBody::Empty, None);
12490        assert!(result.is_none());
12491    }
12492}
12493
12494// ============================================================================
12495// Rate Limiting Middleware Tests
12496// ============================================================================
12497
12498#[cfg(test)]
12499mod rate_limit_tests {
12500    use super::*;
12501    use crate::request::Method;
12502    use crate::response::{ResponseBody, StatusCode};
12503    use std::time::{Duration, Instant};
12504
12505    fn test_context() -> RequestContext {
12506        RequestContext::new(asupersync::Cx::for_testing(), 1)
12507    }
12508
12509    fn run_rate_limit_before(mw: &RateLimitMiddleware, req: &mut Request) -> ControlFlow {
12510        let ctx = test_context();
12511        let fut = mw.before(&ctx, req);
12512        futures_executor::block_on(fut)
12513    }
12514
12515    fn run_rate_limit_after(mw: &RateLimitMiddleware, req: &Request, resp: Response) -> Response {
12516        let ctx = test_context();
12517        let fut = mw.after(&ctx, req, resp);
12518        futures_executor::block_on(fut)
12519    }
12520
12521    fn request_with_ip(key: &str) -> Request {
12522        let mut req = Request::new(Method::Get, "/");
12523        req.headers_mut()
12524            .insert("x-forwarded-for", key.as_bytes().to_vec());
12525        req
12526    }
12527
12528    fn rate_limit_entry_keys(
12529        store: &InMemoryRateLimitStore,
12530        algorithm: RateLimitAlgorithm,
12531    ) -> Vec<String> {
12532        match algorithm {
12533            RateLimitAlgorithm::TokenBucket => {
12534                store.token_buckets.lock().entries.keys().cloned().collect()
12535            }
12536            RateLimitAlgorithm::FixedWindow => {
12537                store.fixed_windows.lock().entries.keys().cloned().collect()
12538            }
12539            RateLimitAlgorithm::SlidingWindow => store
12540                .sliding_windows
12541                .lock()
12542                .entries
12543                .keys()
12544                .cloned()
12545                .collect(),
12546        }
12547    }
12548
12549    #[test]
12550    fn rate_limit_default_allows_requests() {
12551        let mw = RateLimitMiddleware::new();
12552        let mut req = Request::new(Method::Get, "/api/test");
12553        req.headers_mut()
12554            .insert("x-forwarded-for", b"192.168.1.1".to_vec());
12555
12556        let result = run_rate_limit_before(&mw, &mut req);
12557        assert!(result.is_continue(), "first request should be allowed");
12558    }
12559
12560    #[test]
12561    fn rate_limit_fixed_window_blocks_after_limit() {
12562        let mw = RateLimitMiddleware::builder()
12563            .requests(3)
12564            .per(Duration::from_secs(60))
12565            .algorithm(RateLimitAlgorithm::FixedWindow)
12566            .key_extractor(IpKeyExtractor)
12567            .build();
12568
12569        for i in 0..3 {
12570            let mut req = Request::new(Method::Get, "/api/test");
12571            req.headers_mut()
12572                .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12573            let result = run_rate_limit_before(&mw, &mut req);
12574            assert!(
12575                result.is_continue(),
12576                "request {i} should be allowed within limit"
12577            );
12578        }
12579
12580        // Fourth request should be blocked
12581        let mut req = Request::new(Method::Get, "/api/test");
12582        req.headers_mut()
12583            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12584        let result = run_rate_limit_before(&mw, &mut req);
12585        assert!(result.is_break(), "fourth request should be blocked");
12586
12587        // Verify 429 status
12588        if let ControlFlow::Break(resp) = result {
12589            assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
12590        }
12591    }
12592
12593    #[test]
12594    fn rate_limit_different_keys_independent() {
12595        let mw = RateLimitMiddleware::builder()
12596            .requests(2)
12597            .per(Duration::from_secs(60))
12598            .algorithm(RateLimitAlgorithm::FixedWindow)
12599            .key_extractor(IpKeyExtractor)
12600            .build();
12601
12602        // Two requests from IP A
12603        for _ in 0..2 {
12604            let mut req = Request::new(Method::Get, "/");
12605            req.headers_mut()
12606                .insert("x-forwarded-for", b"1.1.1.1".to_vec());
12607            assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12608        }
12609
12610        // IP A is now exhausted
12611        let mut req = Request::new(Method::Get, "/");
12612        req.headers_mut()
12613            .insert("x-forwarded-for", b"1.1.1.1".to_vec());
12614        assert!(run_rate_limit_before(&mw, &mut req).is_break());
12615
12616        // IP B should still be fine
12617        let mut req = Request::new(Method::Get, "/");
12618        req.headers_mut()
12619            .insert("x-forwarded-for", b"2.2.2.2".to_vec());
12620        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12621    }
12622
12623    #[test]
12624    fn rate_limit_token_bucket_allows_burst() {
12625        let mw = RateLimitMiddleware::builder()
12626            .requests(5)
12627            .per(Duration::from_secs(60))
12628            .algorithm(RateLimitAlgorithm::TokenBucket)
12629            .key_extractor(IpKeyExtractor)
12630            .build();
12631
12632        // Should allow 5 rapid requests (full bucket)
12633        for i in 0..5 {
12634            let mut req = Request::new(Method::Get, "/");
12635            req.headers_mut()
12636                .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12637            let result = run_rate_limit_before(&mw, &mut req);
12638            assert!(result.is_continue(), "burst request {i} should be allowed");
12639        }
12640
12641        // 6th request should be blocked (bucket empty)
12642        let mut req = Request::new(Method::Get, "/");
12643        req.headers_mut()
12644            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12645        assert!(run_rate_limit_before(&mw, &mut req).is_break());
12646    }
12647
12648    #[test]
12649    fn rate_limit_sliding_window_basic() {
12650        let mw = RateLimitMiddleware::builder()
12651            .requests(3)
12652            .per(Duration::from_secs(60))
12653            .algorithm(RateLimitAlgorithm::SlidingWindow)
12654            .key_extractor(IpKeyExtractor)
12655            .build();
12656
12657        for i in 0..3 {
12658            let mut req = Request::new(Method::Get, "/");
12659            req.headers_mut()
12660                .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12661            assert!(
12662                run_rate_limit_before(&mw, &mut req).is_continue(),
12663                "sliding window request {i} should be allowed"
12664            );
12665        }
12666
12667        // Should block once limit reached
12668        let mut req = Request::new(Method::Get, "/");
12669        req.headers_mut()
12670            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12671        assert!(run_rate_limit_before(&mw, &mut req).is_break());
12672    }
12673
12674    #[test]
12675    fn rate_limit_header_key_extractor() {
12676        let mw = RateLimitMiddleware::builder()
12677            .requests(2)
12678            .per(Duration::from_secs(60))
12679            .algorithm(RateLimitAlgorithm::FixedWindow)
12680            .key_extractor(HeaderKeyExtractor::new("x-api-key"))
12681            .build();
12682
12683        // Two requests with same API key
12684        for _ in 0..2 {
12685            let mut req = Request::new(Method::Get, "/");
12686            req.headers_mut().insert("x-api-key", b"key-abc".to_vec());
12687            assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12688        }
12689
12690        // Same key blocked
12691        let mut req = Request::new(Method::Get, "/");
12692        req.headers_mut().insert("x-api-key", b"key-abc".to_vec());
12693        assert!(run_rate_limit_before(&mw, &mut req).is_break());
12694
12695        // Different key still allowed
12696        let mut req = Request::new(Method::Get, "/");
12697        req.headers_mut().insert("x-api-key", b"key-xyz".to_vec());
12698        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12699    }
12700
12701    #[test]
12702    fn rate_limit_path_key_extractor() {
12703        let mw = RateLimitMiddleware::builder()
12704            .requests(1)
12705            .per(Duration::from_secs(60))
12706            .algorithm(RateLimitAlgorithm::FixedWindow)
12707            .key_extractor(PathKeyExtractor)
12708            .build();
12709
12710        let mut req = Request::new(Method::Get, "/api/a");
12711        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12712
12713        // Same path is blocked
12714        let mut req = Request::new(Method::Get, "/api/a");
12715        assert!(run_rate_limit_before(&mw, &mut req).is_break());
12716
12717        // Different path is allowed
12718        let mut req = Request::new(Method::Get, "/api/b");
12719        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12720    }
12721
12722    #[test]
12723    fn rate_limit_no_key_skips_limiting() {
12724        let mw = RateLimitMiddleware::builder()
12725            .requests(1)
12726            .per(Duration::from_secs(60))
12727            .algorithm(RateLimitAlgorithm::FixedWindow)
12728            .key_extractor(HeaderKeyExtractor::new("x-api-key"))
12729            .build();
12730
12731        // Request without the header — no key extracted, should pass
12732        let mut req = Request::new(Method::Get, "/");
12733        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12734
12735        // Still passes even with many requests (no key = no limiting)
12736        for _ in 0..10 {
12737            let mut req = Request::new(Method::Get, "/");
12738            assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12739        }
12740    }
12741
12742    #[test]
12743    fn rate_limit_response_headers_on_success() {
12744        let mw = RateLimitMiddleware::builder()
12745            .requests(10)
12746            .per(Duration::from_secs(60))
12747            .algorithm(RateLimitAlgorithm::FixedWindow)
12748            .key_extractor(IpKeyExtractor)
12749            .build();
12750
12751        let mut req = Request::new(Method::Get, "/");
12752        req.headers_mut()
12753            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12754        let cf = run_rate_limit_before(&mw, &mut req);
12755        assert!(cf.is_continue());
12756
12757        let resp = Response::with_status(StatusCode::OK);
12758        let resp = run_rate_limit_after(&mw, &req, resp);
12759
12760        // Verify rate limit headers are present
12761        let headers = resp.headers();
12762        let has_limit = headers
12763            .iter()
12764            .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"));
12765        let has_remaining = headers
12766            .iter()
12767            .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-remaining"));
12768        let has_reset = headers
12769            .iter()
12770            .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-reset"));
12771
12772        assert!(has_limit, "should have X-RateLimit-Limit header");
12773        assert!(has_remaining, "should have X-RateLimit-Remaining header");
12774        assert!(has_reset, "should have X-RateLimit-Reset header");
12775
12776        // Check limit value
12777        let limit_val = headers
12778            .iter()
12779            .find(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"))
12780            .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
12781            .unwrap();
12782        assert_eq!(limit_val, "10");
12783    }
12784
12785    #[test]
12786    fn rate_limit_429_response_has_retry_after() {
12787        let mw = RateLimitMiddleware::builder()
12788            .requests(1)
12789            .per(Duration::from_secs(60))
12790            .algorithm(RateLimitAlgorithm::FixedWindow)
12791            .key_extractor(IpKeyExtractor)
12792            .build();
12793
12794        // Consume the single allowed request
12795        let mut req = Request::new(Method::Get, "/");
12796        req.headers_mut()
12797            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12798        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12799
12800        // Second request should be blocked with 429
12801        let mut req = Request::new(Method::Get, "/");
12802        req.headers_mut()
12803            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12804        let result = run_rate_limit_before(&mw, &mut req);
12805
12806        if let ControlFlow::Break(resp) = result {
12807            assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
12808
12809            // Should have Retry-After header
12810            let has_retry = resp
12811                .headers()
12812                .iter()
12813                .any(|(n, _)| n.eq_ignore_ascii_case("retry-after"));
12814            assert!(has_retry, "429 response should have Retry-After header");
12815
12816            // Should have JSON body
12817            let has_ct = resp
12818                .headers()
12819                .iter()
12820                .any(|(n, v)| n.eq_ignore_ascii_case("content-type") && v == b"application/json");
12821            assert!(has_ct, "429 response should have JSON content type");
12822        } else {
12823            panic!("expected Break(429)");
12824        }
12825    }
12826
12827    #[test]
12828    fn rate_limit_no_headers_when_disabled() {
12829        let mw = RateLimitMiddleware::builder()
12830            .requests(10)
12831            .per(Duration::from_secs(60))
12832            .algorithm(RateLimitAlgorithm::FixedWindow)
12833            .key_extractor(IpKeyExtractor)
12834            .include_headers(false)
12835            .build();
12836
12837        let mut req = Request::new(Method::Get, "/");
12838        req.headers_mut()
12839            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12840        assert!(run_rate_limit_before(&mw, &mut req).is_continue());
12841
12842        let resp = Response::with_status(StatusCode::OK);
12843        let resp = run_rate_limit_after(&mw, &req, resp);
12844
12845        let has_limit = resp
12846            .headers()
12847            .iter()
12848            .any(|(n, _)| n.eq_ignore_ascii_case("x-ratelimit-limit"));
12849        assert!(
12850            !has_limit,
12851            "should NOT have rate limit headers when disabled"
12852        );
12853    }
12854
12855    #[test]
12856    fn rate_limit_custom_retry_message() {
12857        let mw = RateLimitMiddleware::builder()
12858            .requests(1)
12859            .per(Duration::from_secs(60))
12860            .algorithm(RateLimitAlgorithm::FixedWindow)
12861            .key_extractor(IpKeyExtractor)
12862            .retry_message("Slow down, partner!")
12863            .build();
12864
12865        // Exhaust limit
12866        let mut req = Request::new(Method::Get, "/");
12867        req.headers_mut()
12868            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12869        run_rate_limit_before(&mw, &mut req);
12870
12871        // Check custom message in 429 body
12872        let mut req = Request::new(Method::Get, "/");
12873        req.headers_mut()
12874            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
12875        if let ControlFlow::Break(resp) = run_rate_limit_before(&mw, &mut req) {
12876            if let ResponseBody::Bytes(body) = resp.body_ref() {
12877                let body_str = std::str::from_utf8(body).unwrap();
12878                assert!(
12879                    body_str.contains("Slow down, partner!"),
12880                    "expected custom message in body, got: {body_str}"
12881                );
12882            } else {
12883                panic!("expected Bytes body");
12884            }
12885        } else {
12886            panic!("expected Break(429)");
12887        }
12888    }
12889
12890    #[test]
12891    fn rate_limit_ip_extractor_x_forwarded_for() {
12892        let extractor = IpKeyExtractor;
12893        let mut req = Request::new(Method::Get, "/");
12894        req.headers_mut()
12895            .insert("x-forwarded-for", b"1.2.3.4, 5.6.7.8".to_vec());
12896        assert_eq!(extractor.extract_key(&req), Some("1.2.3.4".to_string()));
12897    }
12898
12899    #[test]
12900    fn rate_limit_ip_extractor_x_real_ip() {
12901        let extractor = IpKeyExtractor;
12902        let mut req = Request::new(Method::Get, "/");
12903        req.headers_mut().insert("x-real-ip", b"9.8.7.6".to_vec());
12904        assert_eq!(extractor.extract_key(&req), Some("9.8.7.6".to_string()));
12905    }
12906
12907    #[test]
12908    fn rate_limit_ip_extractor_fallback() {
12909        let extractor = IpKeyExtractor;
12910        let req = Request::new(Method::Get, "/");
12911        assert_eq!(extractor.extract_key(&req), Some("unknown".to_string()));
12912    }
12913
12914    // Tests for secure ConnectedIpKeyExtractor (bd-u9gw)
12915    #[test]
12916    fn connected_ip_extractor_with_remote_addr() {
12917        use std::net::{IpAddr, Ipv4Addr};
12918
12919        let extractor = ConnectedIpKeyExtractor;
12920        let mut req = Request::new(Method::Get, "/");
12921        req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))));
12922
12923        assert_eq!(
12924            extractor.extract_key(&req),
12925            Some("192.168.1.100".to_string())
12926        );
12927    }
12928
12929    #[test]
12930    fn connected_ip_extractor_without_remote_addr() {
12931        let extractor = ConnectedIpKeyExtractor;
12932        let req = Request::new(Method::Get, "/");
12933
12934        // Should return None when no RemoteAddr is set
12935        assert_eq!(extractor.extract_key(&req), None);
12936    }
12937
12938    #[test]
12939    fn connected_ip_extractor_ignores_headers() {
12940        use std::net::{IpAddr, Ipv4Addr};
12941
12942        let extractor = ConnectedIpKeyExtractor;
12943        let mut req = Request::new(Method::Get, "/");
12944        req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
12945        // Add spoofed header - should be ignored
12946        req.headers_mut()
12947            .insert("x-forwarded-for", b"1.2.3.4".to_vec());
12948
12949        // Should use RemoteAddr, not the header
12950        assert_eq!(extractor.extract_key(&req), Some("10.0.0.1".to_string()));
12951    }
12952
12953    // Tests for TrustedProxyIpKeyExtractor (bd-u9gw)
12954    #[test]
12955    fn trusted_proxy_extractor_from_trusted_proxy() {
12956        use std::net::{IpAddr, Ipv4Addr};
12957
12958        let extractor = TrustedProxyIpKeyExtractor::new().trust_cidr("10.0.0.0/8");
12959
12960        let mut req = Request::new(Method::Get, "/");
12961        // Request came from trusted proxy 10.0.0.1
12962        req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
12963        // Proxy set X-Forwarded-For with real client IP
12964        req.headers_mut()
12965            .insert("x-forwarded-for", b"203.0.113.50".to_vec());
12966
12967        // Should trust the header and extract client IP
12968        assert_eq!(
12969            extractor.extract_key(&req),
12970            Some("203.0.113.50".to_string())
12971        );
12972    }
12973
12974    #[test]
12975    fn trusted_proxy_extractor_from_untrusted_direct() {
12976        use std::net::{IpAddr, Ipv4Addr};
12977
12978        let extractor = TrustedProxyIpKeyExtractor::new().trust_cidr("10.0.0.0/8");
12979
12980        let mut req = Request::new(Method::Get, "/");
12981        // Request came directly from client (not a trusted proxy)
12982        req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 50))));
12983        // Client tries to spoof X-Forwarded-For
12984        req.headers_mut()
12985            .insert("x-forwarded-for", b"1.2.3.4".to_vec());
12986
12987        // Should ignore header and use RemoteAddr
12988        assert_eq!(
12989            extractor.extract_key(&req),
12990            Some("203.0.113.50".to_string())
12991        );
12992    }
12993
12994    #[test]
12995    fn trusted_proxy_extractor_no_remote_addr() {
12996        let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
12997
12998        let mut req = Request::new(Method::Get, "/");
12999        // No RemoteAddr set - should return None (safer than guessing)
13000        req.headers_mut()
13001            .insert("x-forwarded-for", b"1.2.3.4".to_vec());
13002
13003        assert_eq!(extractor.extract_key(&req), None);
13004    }
13005
13006    #[test]
13007    fn trusted_proxy_extractor_loopback_ipv4() {
13008        use std::net::{IpAddr, Ipv4Addr};
13009
13010        let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
13011
13012        let mut req = Request::new(Method::Get, "/");
13013        req.insert_extension(RemoteAddr(IpAddr::V4(Ipv4Addr::LOCALHOST)));
13014        req.headers_mut()
13015            .insert("x-forwarded-for", b"8.8.8.8".to_vec());
13016
13017        assert_eq!(extractor.extract_key(&req), Some("8.8.8.8".to_string()));
13018    }
13019
13020    #[test]
13021    fn trusted_proxy_extractor_loopback_ipv6() {
13022        use std::net::{IpAddr, Ipv6Addr};
13023
13024        let extractor = TrustedProxyIpKeyExtractor::new().trust_loopback();
13025
13026        let mut req = Request::new(Method::Get, "/");
13027        req.insert_extension(RemoteAddr(IpAddr::V6(Ipv6Addr::LOCALHOST)));
13028        req.headers_mut()
13029            .insert("x-forwarded-for", b"8.8.8.8".to_vec());
13030
13031        assert_eq!(extractor.extract_key(&req), Some("8.8.8.8".to_string()));
13032    }
13033
13034    #[test]
13035    fn cidr_parsing() {
13036        // Valid CIDRs
13037        assert!(parse_cidr("10.0.0.0/8").is_some());
13038        assert!(parse_cidr("192.168.1.0/24").is_some());
13039        assert!(parse_cidr("0.0.0.0/0").is_some());
13040        assert!(parse_cidr("::1/128").is_some());
13041        assert!(parse_cidr("::/0").is_some());
13042
13043        // Invalid CIDRs
13044        assert!(parse_cidr("10.0.0.0/33").is_none()); // Prefix too large for IPv4
13045        assert!(parse_cidr("invalid").is_none());
13046        assert!(parse_cidr("10.0.0.0").is_none()); // Missing prefix
13047    }
13048
13049    #[test]
13050    fn ip_in_cidr_matching() {
13051        use std::net::{IpAddr, Ipv4Addr};
13052
13053        let cidr_10 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0));
13054
13055        // In range
13056        assert!(ip_in_cidr(
13057            IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
13058            cidr_10,
13059            8
13060        ));
13061        assert!(ip_in_cidr(
13062            IpAddr::V4(Ipv4Addr::new(10, 255, 255, 255)),
13063            cidr_10,
13064            8
13065        ));
13066
13067        // Out of range
13068        assert!(!ip_in_cidr(
13069            IpAddr::V4(Ipv4Addr::new(11, 0, 0, 1)),
13070            cidr_10,
13071            8
13072        ));
13073        assert!(!ip_in_cidr(
13074            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
13075            cidr_10,
13076            8
13077        ));
13078    }
13079
13080    #[test]
13081    fn rate_limit_composite_key_extractor() {
13082        let extractor =
13083            CompositeKeyExtractor::new(vec![Box::new(IpKeyExtractor), Box::new(PathKeyExtractor)]);
13084
13085        let mut req = Request::new(Method::Get, "/api/users");
13086        req.headers_mut()
13087            .insert("x-forwarded-for", b"10.0.0.1".to_vec());
13088
13089        let key = extractor.extract_key(&req);
13090        assert_eq!(key, Some("10.0.0.1:/api/users".to_string()));
13091    }
13092
13093    #[test]
13094    fn rate_limit_builder_defaults() {
13095        let mw = RateLimitMiddleware::builder().build();
13096        assert_eq!(DEFAULT_RATE_LIMIT_MAX_KEYS, 65_536);
13097        assert_eq!(mw.config.max_requests, 100);
13098        assert_eq!(mw.config.window, Duration::from_secs(60));
13099        assert_eq!(mw.config.algorithm, RateLimitAlgorithm::TokenBucket);
13100        assert_eq!(mw.config.max_keys, DEFAULT_RATE_LIMIT_MAX_KEYS);
13101        assert_eq!(mw.store.max_keys, DEFAULT_RATE_LIMIT_MAX_KEYS);
13102        assert!(mw.config.include_headers);
13103    }
13104
13105    #[test]
13106    fn rate_limit_builder_applies_custom_key_bound() {
13107        let mw = RateLimitMiddleware::builder().max_keys(7).build();
13108        assert_eq!(mw.config.max_keys, 7);
13109        assert_eq!(mw.store.max_keys, 7);
13110    }
13111
13112    #[test]
13113    fn rate_limit_store_bounds_every_algorithm_and_fails_closed() {
13114        let window = Duration::from_secs(60);
13115
13116        for algorithm in [
13117            RateLimitAlgorithm::TokenBucket,
13118            RateLimitAlgorithm::FixedWindow,
13119            RateLimitAlgorithm::SlidingWindow,
13120        ] {
13121            let store = InMemoryRateLimitStore::with_max_keys(2);
13122            assert!(store.check("resident-a", algorithm, 10, window).allowed);
13123            assert!(store.check("resident-b", algorithm, 10, window).allowed);
13124
13125            let saturated = store.check("unseen", algorithm, 10, window);
13126            assert!(!saturated.allowed, "{algorithm:?} must fail closed");
13127            assert_eq!(saturated.limit, 10);
13128            assert_eq!(saturated.remaining, 0);
13129            assert_eq!(saturated.reset_after_secs, 60);
13130
13131            let keys = rate_limit_entry_keys(&store, algorithm);
13132            assert_eq!(keys.len(), 2, "{algorithm:?} exceeded its key bound");
13133            assert!(keys.contains(&"resident-a".to_string()));
13134            assert!(keys.contains(&"resident-b".to_string()));
13135            assert!(!keys.contains(&"unseen".to_string()));
13136        }
13137    }
13138
13139    #[test]
13140    fn saturated_retry_after_rounds_fractional_windows_up() {
13141        let store = InMemoryRateLimitStore::with_max_keys(0);
13142        let result = store.check(
13143            "unseen",
13144            RateLimitAlgorithm::FixedWindow,
13145            10,
13146            Duration::from_millis(1_500),
13147        );
13148        assert!(!result.allowed);
13149        assert_eq!(result.reset_after_secs, 2);
13150    }
13151
13152    #[test]
13153    fn rate_limit_saturation_returns_429_without_resetting_resident_counter() {
13154        for algorithm in [
13155            RateLimitAlgorithm::TokenBucket,
13156            RateLimitAlgorithm::FixedWindow,
13157            RateLimitAlgorithm::SlidingWindow,
13158        ] {
13159            let mw = RateLimitMiddleware::builder()
13160                .requests(2)
13161                .per(Duration::from_secs(60))
13162                .algorithm(algorithm)
13163                .key_extractor(IpKeyExtractor)
13164                .max_keys(1)
13165                .build();
13166
13167            for _ in 0..2 {
13168                let mut resident = request_with_ip("resident");
13169                assert!(run_rate_limit_before(&mw, &mut resident).is_continue());
13170            }
13171
13172            let mut unseen = request_with_ip("unseen");
13173            let ControlFlow::Break(response) = run_rate_limit_before(&mw, &mut unseen) else {
13174                panic!("{algorithm:?} unseen key must fail closed while saturated");
13175            };
13176            assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
13177            assert!(response.headers().iter().any(|(name, value)| {
13178                name.eq_ignore_ascii_case("retry-after") && value.as_slice() == b"60"
13179            }));
13180
13181            let mut resident = request_with_ip("resident");
13182            assert!(
13183                run_rate_limit_before(&mw, &mut resident).is_break(),
13184                "{algorithm:?} resident counter must remain exhausted"
13185            );
13186
13187            let keys = rate_limit_entry_keys(&mw.store, algorithm);
13188            assert_eq!(keys, vec!["resident".to_string()]);
13189        }
13190    }
13191
13192    #[test]
13193    fn rate_limit_store_reclaims_algorithm_specific_stale_entries() {
13194        let window = Duration::from_secs(3_600);
13195
13196        for algorithm in [
13197            RateLimitAlgorithm::TokenBucket,
13198            RateLimitAlgorithm::FixedWindow,
13199            RateLimitAlgorithm::SlidingWindow,
13200        ] {
13201            let store = InMemoryRateLimitStore::with_max_keys(1);
13202            let start = Instant::now();
13203            assert!(
13204                store
13205                    .check_at("stale", algorithm, 10, window, start)
13206                    .allowed
13207            );
13208
13209            let replacement_at = if algorithm == RateLimitAlgorithm::SlidingWindow {
13210                start + Duration::from_secs(7_201)
13211            } else {
13212                start + Duration::from_secs(3_601)
13213            };
13214
13215            assert!(
13216                store
13217                    .check_at("replacement", algorithm, 10, window, replacement_at)
13218                    .allowed,
13219                "{algorithm:?} should reclaim its stale entry"
13220            );
13221            assert_eq!(
13222                rate_limit_entry_keys(&store, algorithm),
13223                vec!["replacement".to_string()]
13224            );
13225        }
13226    }
13227
13228    #[test]
13229    fn sliding_window_retains_entries_until_two_windows_are_inactive() {
13230        let algorithm = RateLimitAlgorithm::SlidingWindow;
13231        let window = Duration::from_secs(3_600);
13232        let store = InMemoryRateLimitStore::with_max_keys(1);
13233        let start = Instant::now();
13234        assert!(
13235            store
13236                .check_at("resident", algorithm, 10, window, start)
13237                .allowed
13238        );
13239
13240        assert!(
13241            !store
13242                .check_at(
13243                    "too-early",
13244                    algorithm,
13245                    10,
13246                    window,
13247                    start + Duration::from_secs(3_601),
13248                )
13249                .allowed
13250        );
13251        assert_eq!(
13252            rate_limit_entry_keys(&store, algorithm),
13253            vec!["resident".to_string()]
13254        );
13255
13256        assert!(
13257            store
13258                .check_at(
13259                    "replacement",
13260                    algorithm,
13261                    10,
13262                    window,
13263                    start + Duration::from_secs(7_202),
13264                )
13265                .allowed
13266        );
13267        assert_eq!(
13268            rate_limit_entry_keys(&store, algorithm),
13269            vec!["replacement".to_string()]
13270        );
13271    }
13272
13273    #[test]
13274    fn sliding_window_preserves_fractional_progress_when_rotating() {
13275        let algorithm = RateLimitAlgorithm::SlidingWindow;
13276        let window = Duration::from_secs(100);
13277        let store = InMemoryRateLimitStore::with_max_keys(1);
13278        let start = Instant::now();
13279
13280        for _ in 0..10 {
13281            assert!(
13282                store
13283                    .check_at("resident", algorithm, 10, window, start)
13284                    .allowed
13285            );
13286        }
13287        assert!(
13288            !store
13289                .check_at("resident", algorithm, 10, window, start)
13290                .allowed
13291        );
13292
13293        let half_into_next_window = store.check_at(
13294            "resident",
13295            algorithm,
13296            10,
13297            window,
13298            start + Duration::from_secs(150),
13299        );
13300        assert!(half_into_next_window.allowed);
13301        assert_eq!(half_into_next_window.remaining, 4);
13302        assert_eq!(half_into_next_window.reset_after_secs, 50);
13303    }
13304
13305    #[test]
13306    fn cleanup_uses_each_resident_entry_retention_policy() {
13307        let short_window = Duration::from_secs(1);
13308        let long_window = Duration::from_secs(3_600);
13309
13310        for algorithm in [
13311            RateLimitAlgorithm::TokenBucket,
13312            RateLimitAlgorithm::FixedWindow,
13313            RateLimitAlgorithm::SlidingWindow,
13314        ] {
13315            let store = InMemoryRateLimitStore::with_max_keys(1);
13316            let start = Instant::now();
13317            assert!(
13318                store
13319                    .check_at("long-lived", algorithm, 10, long_window, start)
13320                    .allowed
13321            );
13322
13323            let result = store.check_at(
13324                "short-window-unseen",
13325                algorithm,
13326                10,
13327                short_window,
13328                start + Duration::from_secs(2),
13329            );
13330            assert!(
13331                !result.allowed,
13332                "{algorithm:?} must not evict a resident using the incoming short window"
13333            );
13334            assert_eq!(
13335                rate_limit_entry_keys(&store, algorithm),
13336                vec!["long-lived".to_string()]
13337            );
13338        }
13339    }
13340
13341    #[test]
13342    fn cleanup_reclaims_short_resident_despite_long_incoming_window() {
13343        let short_window = Duration::from_secs(1);
13344        let long_window = Duration::from_secs(3_600);
13345
13346        for algorithm in [
13347            RateLimitAlgorithm::TokenBucket,
13348            RateLimitAlgorithm::FixedWindow,
13349            RateLimitAlgorithm::SlidingWindow,
13350        ] {
13351            let store = InMemoryRateLimitStore::with_max_keys(1);
13352            let start = Instant::now();
13353            assert!(
13354                store
13355                    .check_at("short-lived", algorithm, 10, short_window, start)
13356                    .allowed
13357            );
13358
13359            let result = store.check_at(
13360                "long-window-replacement",
13361                algorithm,
13362                10,
13363                long_window,
13364                start + Duration::from_secs(3),
13365            );
13366            assert!(
13367                result.allowed,
13368                "{algorithm:?} must reclaim a stale resident independently of the incoming window"
13369            );
13370            assert_eq!(
13371                rate_limit_entry_keys(&store, algorithm),
13372                vec!["long-window-replacement".to_string()]
13373            );
13374        }
13375    }
13376
13377    #[test]
13378    fn rate_limit_cleanup_is_throttled_to_one_sweep_per_second() {
13379        let algorithm = RateLimitAlgorithm::SlidingWindow;
13380        let window = Duration::from_secs(100);
13381        let store = InMemoryRateLimitStore::with_max_keys(1);
13382        let start = Instant::now();
13383        assert!(
13384            store
13385                .check_at("stale", algorithm, 10, window, start)
13386                .allowed
13387        );
13388
13389        assert!(
13390            !store
13391                .check_at(
13392                    "initial-sweep",
13393                    algorithm,
13394                    10,
13395                    window,
13396                    start + Duration::from_millis(199_500),
13397                )
13398                .allowed
13399        );
13400        assert!(
13401            !store
13402                .check_at(
13403                    "first-unseen",
13404                    algorithm,
13405                    10,
13406                    window,
13407                    start + Duration::from_millis(200_100),
13408                )
13409                .allowed
13410        );
13411        assert!(
13412            !store
13413                .check_at(
13414                    "second-unseen",
13415                    algorithm,
13416                    10,
13417                    window,
13418                    start + Duration::from_millis(200_400),
13419                )
13420                .allowed
13421        );
13422        assert_eq!(
13423            rate_limit_entry_keys(&store, algorithm),
13424            vec!["stale".to_string()]
13425        );
13426
13427        assert!(
13428            store
13429                .check_at(
13430                    "replacement",
13431                    algorithm,
13432                    10,
13433                    window,
13434                    start + Duration::from_millis(200_500),
13435                )
13436                .allowed
13437        );
13438        assert_eq!(
13439            rate_limit_entry_keys(&store, algorithm),
13440            vec!["replacement".to_string()]
13441        );
13442    }
13443
13444    #[test]
13445    fn zero_window_fails_closed_without_retaining_keys() {
13446        let store = InMemoryRateLimitStore::with_max_keys(1);
13447
13448        for algorithm in [
13449            RateLimitAlgorithm::TokenBucket,
13450            RateLimitAlgorithm::FixedWindow,
13451            RateLimitAlgorithm::SlidingWindow,
13452        ] {
13453            let result = store.check("never-retained", algorithm, 10, Duration::ZERO);
13454            assert!(
13455                !result.allowed,
13456                "{algorithm:?} zero window must fail closed"
13457            );
13458            assert_eq!(result.reset_after_secs, 1);
13459            assert!(rate_limit_entry_keys(&store, algorithm).is_empty());
13460        }
13461    }
13462
13463    #[test]
13464    fn rate_limit_builder_per_minute() {
13465        let mw = RateLimitMiddleware::builder()
13466            .requests(50)
13467            .per_minute(2)
13468            .algorithm(RateLimitAlgorithm::SlidingWindow)
13469            .build();
13470        assert_eq!(mw.config.max_requests, 50);
13471        assert_eq!(mw.config.window, Duration::from_secs(120));
13472        assert_eq!(mw.config.algorithm, RateLimitAlgorithm::SlidingWindow);
13473    }
13474
13475    #[test]
13476    fn rate_limit_builder_per_hour() {
13477        let mw = RateLimitMiddleware::builder()
13478            .requests(1000)
13479            .per_hour(1)
13480            .build();
13481        assert_eq!(mw.config.window, Duration::from_secs(3600));
13482    }
13483
13484    #[test]
13485    fn rate_limit_middleware_name() {
13486        let mw = RateLimitMiddleware::new();
13487        assert_eq!(mw.name(), "RateLimit");
13488    }
13489
13490    #[test]
13491    fn rate_limit_default_via_default_trait() {
13492        let mw = RateLimitMiddleware::default();
13493        assert_eq!(mw.config.max_requests, 100);
13494    }
13495
13496    // ========================================================================
13497    // ETag Middleware Tests
13498    // ========================================================================
13499
13500    #[test]
13501    fn etag_middleware_generates_etag_for_get() {
13502        let mw = ETagMiddleware::new();
13503        let ctx = test_context();
13504        let req = Request::new(crate::request::Method::Get, "/resource");
13505
13506        // Create response with body
13507        let response = Response::ok()
13508            .header("content-type", b"application/json".to_vec())
13509            .body(ResponseBody::Bytes(br#"{"status":"ok"}"#.to_vec()));
13510
13511        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13512
13513        // Should have ETag header
13514        let etag = response
13515            .headers()
13516            .iter()
13517            .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13518        assert!(etag.is_some(), "Response should have ETag header");
13519
13520        // ETag should be a quoted hex string
13521        let etag_value = std::str::from_utf8(&etag.unwrap().1).unwrap();
13522        assert!(etag_value.starts_with('"'), "ETag should start with quote");
13523        assert!(etag_value.ends_with('"'), "ETag should end with quote");
13524    }
13525
13526    #[test]
13527    fn etag_middleware_returns_304_on_match() {
13528        let mw = ETagMiddleware::new();
13529        let ctx = test_context();
13530
13531        // First request to get the ETag
13532        let req1 = Request::new(crate::request::Method::Get, "/resource");
13533        let body = br#"{"status":"ok"}"#.to_vec();
13534        let response1 = Response::ok().body(ResponseBody::Bytes(body.clone()));
13535        let response1 = futures_executor::block_on(mw.after(&ctx, &req1, response1));
13536
13537        let etag = response1
13538            .headers()
13539            .iter()
13540            .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13541            .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13542            .unwrap();
13543
13544        // Second request with If-None-Match header
13545        let mut req2 = Request::new(crate::request::Method::Get, "/resource");
13546        req2.headers_mut()
13547            .insert("if-none-match", etag.as_bytes().to_vec());
13548
13549        let response2 = Response::ok().body(ResponseBody::Bytes(body));
13550        let response2 = futures_executor::block_on(mw.after(&ctx, &req2, response2));
13551
13552        // Should return 304 Not Modified
13553        assert_eq!(response2.status().as_u16(), 304);
13554        assert!(response2.body_ref().is_empty());
13555    }
13556
13557    #[test]
13558    fn etag_middleware_returns_full_response_on_mismatch() {
13559        let mw = ETagMiddleware::new();
13560        let ctx = test_context();
13561
13562        let mut req = Request::new(crate::request::Method::Get, "/resource");
13563        req.headers_mut()
13564            .insert("if-none-match", b"\"old-etag\"".to_vec());
13565
13566        let body = br#"{"status":"updated"}"#.to_vec();
13567        let response = Response::ok().body(ResponseBody::Bytes(body.clone()));
13568        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13569
13570        // Should return 200 OK with body
13571        assert_eq!(response.status().as_u16(), 200);
13572        assert!(!response.body_ref().is_empty());
13573    }
13574
13575    #[test]
13576    fn etag_middleware_weak_etag_generation() {
13577        let config = ETagConfig::new().weak(true);
13578        let mw = ETagMiddleware::with_config(config);
13579        let ctx = test_context();
13580        let req = Request::new(crate::request::Method::Get, "/resource");
13581
13582        let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13583        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13584
13585        let etag = response
13586            .headers()
13587            .iter()
13588            .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13589            .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13590            .unwrap();
13591
13592        assert!(etag.starts_with("W/"), "Weak ETag should start with W/");
13593    }
13594
13595    #[test]
13596    fn etag_middleware_skips_post_requests() {
13597        let mw = ETagMiddleware::new();
13598        let ctx = test_context();
13599        let req = Request::new(crate::request::Method::Post, "/resource");
13600
13601        let response = Response::ok().body(ResponseBody::Bytes(b"created".to_vec()));
13602        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13603
13604        // POST should not get ETag
13605        let etag = response
13606            .headers()
13607            .iter()
13608            .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13609        assert!(etag.is_none(), "POST should not have ETag");
13610    }
13611
13612    #[test]
13613    fn etag_middleware_handles_head_requests() {
13614        let mw = ETagMiddleware::new();
13615        let ctx = test_context();
13616        let req = Request::new(crate::request::Method::Head, "/resource");
13617
13618        let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13619        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13620
13621        // HEAD should get ETag
13622        let etag = response
13623            .headers()
13624            .iter()
13625            .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13626        assert!(etag.is_some(), "HEAD should have ETag");
13627    }
13628
13629    #[test]
13630    fn etag_middleware_disabled_mode() {
13631        let config = ETagConfig::new().mode(ETagMode::Disabled);
13632        let mw = ETagMiddleware::with_config(config);
13633        let ctx = test_context();
13634        let req = Request::new(crate::request::Method::Get, "/resource");
13635
13636        let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13637        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13638
13639        // Should not have ETag when disabled
13640        let etag = response
13641            .headers()
13642            .iter()
13643            .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13644        assert!(etag.is_none(), "Disabled mode should not add ETag");
13645    }
13646
13647    #[test]
13648    fn etag_middleware_min_size_filter() {
13649        let config = ETagConfig::new().min_size(1000);
13650        let mw = ETagMiddleware::with_config(config);
13651        let ctx = test_context();
13652        let req = Request::new(crate::request::Method::Get, "/resource");
13653
13654        // Small body below min_size
13655        let response = Response::ok().body(ResponseBody::Bytes(b"small".to_vec()));
13656        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13657
13658        // Should not have ETag for small body
13659        let etag = response
13660            .headers()
13661            .iter()
13662            .find(|(name, _)| name.eq_ignore_ascii_case("etag"));
13663        assert!(etag.is_none(), "Small body should not get ETag");
13664    }
13665
13666    #[test]
13667    fn etag_middleware_preserves_existing_etag() {
13668        let config = ETagConfig::new().mode(ETagMode::Manual);
13669        let mw = ETagMiddleware::with_config(config);
13670        let ctx = test_context();
13671
13672        // First request to set up cached ETag
13673        let mut req = Request::new(crate::request::Method::Get, "/resource");
13674        req.headers_mut()
13675            .insert("if-none-match", b"\"custom-etag\"".to_vec());
13676
13677        // Response with pre-set ETag matching the request
13678        let response = Response::ok()
13679            .header("etag", b"\"custom-etag\"".to_vec())
13680            .body(ResponseBody::Bytes(b"data".to_vec()));
13681        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13682
13683        // Should return 304 since custom ETag matches
13684        assert_eq!(response.status().as_u16(), 304);
13685    }
13686
13687    #[test]
13688    fn etag_middleware_wildcard_if_none_match() {
13689        let mw = ETagMiddleware::new();
13690        let ctx = test_context();
13691        let mut req = Request::new(crate::request::Method::Get, "/resource");
13692        req.headers_mut().insert("if-none-match", b"*".to_vec());
13693
13694        let response = Response::ok().body(ResponseBody::Bytes(b"data".to_vec()));
13695        let response = futures_executor::block_on(mw.after(&ctx, &req, response));
13696
13697        // Wildcard should match any ETag
13698        assert_eq!(response.status().as_u16(), 304);
13699    }
13700
13701    #[test]
13702    fn etag_middleware_weak_comparison_matches() {
13703        let mw = ETagMiddleware::new();
13704        let ctx = test_context();
13705
13706        // Get the strong ETag
13707        let req1 = Request::new(crate::request::Method::Get, "/resource");
13708        let body = b"test data".to_vec();
13709        let response1 = Response::ok().body(ResponseBody::Bytes(body.clone()));
13710        let response1 = futures_executor::block_on(mw.after(&ctx, &req1, response1));
13711
13712        let etag = response1
13713            .headers()
13714            .iter()
13715            .find(|(name, _)| name.eq_ignore_ascii_case("etag"))
13716            .map(|(_, v)| std::str::from_utf8(v).unwrap().to_string())
13717            .unwrap();
13718
13719        // Send request with weak version of the same ETag
13720        let mut req2 = Request::new(crate::request::Method::Get, "/resource");
13721        let weak_etag = format!("W/{}", etag);
13722        req2.headers_mut()
13723            .insert("if-none-match", weak_etag.as_bytes().to_vec());
13724
13725        let response2 = Response::ok().body(ResponseBody::Bytes(body));
13726        let response2 = futures_executor::block_on(mw.after(&ctx, &req2, response2));
13727
13728        // Weak comparison should match
13729        assert_eq!(response2.status().as_u16(), 304);
13730    }
13731
13732    #[test]
13733    fn etag_middleware_name() {
13734        let mw = ETagMiddleware::new();
13735        assert_eq!(mw.name(), "ETagMiddleware");
13736    }
13737
13738    #[test]
13739    fn etag_config_builder() {
13740        let config = ETagConfig::new()
13741            .mode(ETagMode::Auto)
13742            .weak(true)
13743            .min_size(512);
13744
13745        assert_eq!(config.mode, ETagMode::Auto);
13746        assert!(config.weak);
13747        assert_eq!(config.min_size, 512);
13748    }
13749
13750    #[test]
13751    fn etag_generates_consistent_hash() {
13752        // Same data should produce same ETag
13753        let etag1 = ETagMiddleware::generate_etag(b"hello world", false);
13754        let etag2 = ETagMiddleware::generate_etag(b"hello world", false);
13755        assert_eq!(etag1, etag2);
13756
13757        // Different data should produce different ETag
13758        let etag3 = ETagMiddleware::generate_etag(b"hello world!", false);
13759        assert_ne!(etag1, etag3);
13760    }
13761}