Skip to main content

shared_framework/controller/
mod.rs

1//! HTTP routing and controllers built on Hyper.
2//!
3//! Key types: [`Router`] dispatches requests, [`RouteController`] groups routes
4//! under a base path and version, [`RouteDescription`] declares documentation,
5//! validation, and rate-limit metadata, and [`Middleware`] runs before handlers.
6//!
7//! Request lifecycle: Hyper accepts a connection, the body, query parameters, and
8//! headers are attached to a [`CorrelationContext`](CorrelationContext),
9//! global middlewares run, the matching route's middlewares run, then the handler
10//! receives only the context and returns `Result<ServiceResult<T>, ErrorResult>`.
11//!
12//! Handlers read input off the context (`ctx.body::<T>()`, `ctx.header()`,
13//! `ctx.query_param()`) and return [`ServiceResult`](ServiceResult)
14//! or a boxed [`TypedServiceResult`](TypedServiceResult).
15//! ```ignore
16//! // ctx: CorrelationContext
17//! // let dto = ctx.body::<CreateUser>()?;
18//! // Ok(ServiceResult::ok("Created", dto))
19//! ```
20
21pub mod errors;
22
23use futures::FutureExt;
24use std::collections::HashMap;
25use std::net::SocketAddr;
26use std::sync::{
27    Arc, OnceLock,
28    atomic::{AtomicBool, AtomicUsize, Ordering},
29};
30
31use crate::doc::{DocumentableDTO, DocumentationRegistrant};
32use crate::logging::CorrelationContext;
33use crate::middleware::rate::{RateLimiter, create_from_env};
34use crate::response::{ErrorResult, ResponseType, ServiceResult, TypedServiceResult};
35use crate::validation::Validate;
36use http::{HeaderMap, Method, Request, Response};
37use http_body_util::BodyExt;
38use hyper::body::Incoming;
39use hyper::server::conn::http1;
40use hyper::service::service_fn;
41use hyper_util::rt::TokioIo;
42use schemars::generate::SchemaSettings;
43use serde_json::Value;
44use tokio::net::TcpListener;
45use tracing::{info, warn};
46
47#[derive(Clone)]
48struct Holder {
49    path: String,
50    limit: u32,
51}
52
53/// Documented DTO payload: its JSON Schema, example value, and Rust type name.
54/// Used for request bodies and response examples in generated documentation.
55#[derive(Clone, Debug)]
56pub struct DtoEntity {
57    pub(crate) schema: Value,
58    pub(crate) example: Value,
59    pub(crate) name: &'static str,
60}
61
62/// Declared file-upload kind for a multipart file parameter.
63/// Determines the example extension list reported in the documentation.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum FileParameterType {
66    /// Image files (e.g. `.png, .jpg, .jpeg, .gif, .webp`).
67    Image,
68    /// Document files (e.g. `.pdf, .doc, .docx, .txt`).
69    Document,
70    /// Spreadsheet or CSV files (e.g. `.xls, .xlsx, .csv`).
71    Spreadsheet,
72    /// Video files (e.g. `.mp4, .mov, .avi`).
73    Video,
74    /// Audio files (e.g. `.mp3, .wav, .aac`).
75    Audio,
76    /// Archive files (e.g. `.zip, .tar, .gz`).
77    Archive,
78    /// Any file extension is accepted.
79    Any,
80}
81
82impl FileParameterType {
83    /// Returns the example extension list shown in documentation for this file kind.
84    pub fn allowed_extensions_example(&self) -> &'static str {
85        match self {
86            Self::Image => ".png, .jpg, .jpeg, .gif, .webp",
87            Self::Document => ".pdf, .doc, .docx, .txt",
88            Self::Spreadsheet => ".xls, .xlsx, .csv",
89            Self::Video => ".mp4, .mov, .avi",
90            Self::Audio => ".mp3, .wav, .aac",
91            Self::Archive => ".zip, .tar, .gz",
92            Self::Any => "Any extension",
93        }
94    }
95}
96
97/// File-upload declaration for a multipart route: the part of the name, a description,
98/// the accepted file kind, and optional send caps enforced before the handler runs.
99#[derive(Clone, Debug)]
100pub struct FileParameter {
101    /// Multipart part name this declaration applies to.
102    pub name: String,
103    /// Human-readable description shown in documentation.
104    pub description: String,
105    /// Accepted file kind, used for the documented extension hint.
106    pub file_type: FileParameterType,
107    /// Maximum times this part may be sent; enforced as a 400 rejection when exceeded.
108    pub count: Option<u32>,
109    /// Optional size or count limit recorded for documentation.
110    pub limit: Option<u32>,
111}
112
113/// Declarative metadata for one route: documentation, validation, and policy.
114/// A description is registered for docs generation, declares the expected request
115/// body (required when `has_body` is set on mount), response examples, params,
116/// file parts, rate limit, and auth notes. Build with the `new` constructor and
117/// the `group` / `with_body` / `response` / `header` / `query_param` builders.
118#[derive(Debug, Clone)]
119pub struct RouteDescription {
120    /// Documentation group this route belongs to; also seeds the default tag.
121    pub group: String,
122    /// Route name; kept in sync with `summary` by [`RouteDescription::name`].
123    pub name: String,
124    /// Long-form description shown in documentation.
125    pub description: String,
126    /// Declared request-body DTO schema and example, if the route takes a body.
127    pub request_body: Option<DtoEntity>,
128    /// Response examples keyed by HTTP status code.
129    pub response_examples: HashMap<u16, DtoEntity>,
130    /// Documented request headers as name-to-description pairs.
131    pub headers: HashMap<String, String>,
132    /// Documented path parameters as name-to-description pairs.
133    pub path_parameters: HashMap<String, String>,
134    /// Default values for documented path parameters.
135    pub path_parameter_defaults: HashMap<String, String>,
136    /// Documented query parameters as name-to-description pairs.
137    pub query_parameters: HashMap<String, String>,
138    /// Default values for documented query parameters.
139    pub query_parameter_defaults: HashMap<String, String>,
140    /// Declared multipart file parameters keyed by part name.
141    pub file_parameters: HashMap<String, FileParameter>,
142    /// Per-route request cap; `None` means no route-level limit is installed.
143    pub rate_limit: Option<u32>,
144    /// Whether callers must authenticate; enforced by middleware, not by this struct.
145    pub authentication_required: bool,
146    /// Optional note explaining the authentication requirement.
147    pub authentication_comment: Option<String>,
148    /// Short summary shown in documentation; defaults to the name.
149    pub summary: String,
150    /// Documentation tags used for grouping; defaults to the group name.
151    pub tags: Vec<String>,
152    /// Marks the route as deprecated in documentation.
153    pub is_deprecated: bool,
154}
155
156impl RouteDescription {
157    /// Creates a description with the given summary; group and tags default to `"Default"`.
158    pub fn new(summary: impl Into<String>) -> Self {
159        let s = summary.into();
160        Self {
161            group: "Default".to_string(),
162            name: s.clone(),
163            description: String::new(),
164            request_body: None,
165            response_examples: Default::default(),
166            headers: Default::default(),
167            path_parameters: Default::default(),
168            path_parameter_defaults: Default::default(),
169            query_parameters: Default::default(),
170            query_parameter_defaults: Default::default(),
171            file_parameters: Default::default(),
172            rate_limit: None,
173            authentication_required: false,
174            authentication_comment: None,
175            summary: s.clone(),
176            tags: vec!["Default".to_string()],
177            is_deprecated: false,
178        }
179    }
180    /// Sets the documentation group and resets the tag list to that group.
181    pub fn group(mut self, group: impl Into<String>) -> Self {
182        let g = group.into();
183        self.group = g.clone();
184        self.tags = vec![g];
185        self
186    }
187    /// Sets the route name and copies it into the summary.
188    pub fn name(mut self, name: impl Into<String>) -> Self {
189        let n = name.into();
190        self.name = n.clone();
191        self.summary = n;
192        self
193    }
194    /// Sets the long-form description shown in documentation.
195    pub fn description(mut self, desc: impl Into<String>) -> Self {
196        self.description = desc.into();
197        self
198    }
199    /// Sets the per-route request cap (alias for [`RouteDescription::with_rate_limit`]).
200    pub fn rate_limit(mut self, limit: u32) -> Self {
201        self.rate_limit = Some(limit);
202        self
203    }
204    /// Declares the request-body DTO type `T`: records its JSON Schema and example.
205    /// Required when the route is mounted with `has_body`; `T` must also be validatable.
206    pub fn with_body<T>(mut self) -> Self
207    where
208        T: DocumentableDTO + Validate,
209    {
210        let settings = SchemaSettings::openapi3();
211        let generator = settings.into_generator();
212        let schema = generator.into_root_schema_for::<T>();
213        let example = T::make_example();
214        self.request_body = Some(DtoEntity {
215            schema: schema.to_value(),
216            example: example.unwrap(),
217            name: std::any::type_name::<T>(),
218        });
219        self
220    }
221    /// Records whether callers must authenticate (informational; enforced by middleware).
222    pub fn authentication(mut self, required: bool) -> Self {
223        self.authentication_required = required;
224        self
225    }
226
227    /// Records a response example for `code` using DTO type `T`'s schema and example.
228    pub fn response<T>(mut self, code: u16) -> Self
229    where
230        T: DocumentableDTO,
231    {
232        let settings = SchemaSettings::openapi3();
233        let generator = settings.into_generator();
234        let schema = generator.into_root_schema_for::<T>();
235        let example = T::make_example();
236        let response = DtoEntity {
237            schema: schema.to_value(),
238            example: example.unwrap(),
239            name: std::any::type_name::<T>(),
240        };
241        self.response_examples.insert(code, response);
242        self
243    }
244    /// Attaches an explanatory note about the authentication requirement.
245    pub fn authentication_comment(mut self, comment: impl Into<String>) -> Self {
246        self.authentication_comment = Some(comment.into());
247        self
248    }
249    /// Documents one expected request header as a name-to-description entry.
250    pub fn header(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
251        self.headers.insert(k.into(), v.into());
252        self
253    }
254    /// Documents one path parameter as a name-to-description entry.
255    pub fn path_param(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
256        self.path_parameters.insert(k.into(), v.into());
257        self
258    }
259    /// Documents one path parameter together with its default value.
260    pub fn path_param_with_default(
261        mut self,
262        k: impl Into<String>,
263        v: impl Into<String>,
264        default: impl Into<String>,
265    ) -> Self {
266        let k = k.into();
267        self.path_parameters.insert(k.clone(), v.into());
268        self.path_parameter_defaults.insert(k, default.into());
269        self
270    }
271    /// Documents one query parameter as a name-to-description entry.
272    pub fn query_param(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
273        self.query_parameters.insert(k.into(), v.into());
274        self
275    }
276    /// Documents one query parameter together with its default value.
277    pub fn query_param_with_default(
278        mut self,
279        k: impl Into<String>,
280        v: impl Into<String>,
281        default: impl Into<String>,
282    ) -> Self {
283        let k = k.into();
284        self.query_parameters.insert(k.clone(), v.into());
285        self.query_parameter_defaults.insert(k, default.into());
286        self
287    }
288    /// Declares an accepted multipart file part without total send caps.
289    pub fn file_param(
290        mut self,
291        name: impl Into<String>,
292        description: impl Into<String>,
293        file_type: FileParameterType,
294    ) -> Self {
295        let name = name.into();
296        self.file_parameters.insert(
297            name.clone(),
298            FileParameter {
299                name,
300                description: description.into(),
301                file_type,
302                count: None,
303                limit: None,
304            },
305        );
306        self
307    }
308    /// Declares an accepted multipart file part with sending limits.
309    /// Panics if `limit` or `count` is zero; a `count` overrun rejects the request with a 400.
310    pub fn file_param_with_limit(
311        mut self,
312        name: impl Into<String>,
313        description: impl Into<String>,
314        file_type: FileParameterType,
315        limit: u32,
316        count: u32,
317    ) -> Self {
318        let name = name.into();
319        assert!(limit >= 1, "File parameter limit must be greater than 0");
320        assert!(count >= 1, "File parameter count must be greater than 0");
321        self.file_parameters.insert(
322            name.clone(),
323            FileParameter {
324                name,
325                description: description.into(),
326                file_type,
327                count: Some(count),
328                limit: Some(limit),
329            },
330        );
331        self
332    }
333    /// Marks the route as deprecated (`true`) or not (`false`) in documentation.
334    pub fn deprecated(mut self, deprecated: bool) -> Self {
335        self.is_deprecated = deprecated;
336        self
337    }
338    /// Adds a documentation tag unless it is already present.
339    pub fn tag(mut self, tag: impl Into<String>) -> Self {
340        let t = tag.into();
341        if !self.tags.contains(&t) {
342            self.tags.push(t);
343        }
344        self
345    }
346    /// Returns the per-route limit when set, otherwise the given global limit.
347    pub fn effective_rate_limit(&self, global: Option<u32>) -> Option<u32> {
348        self.rate_limit.or(global)
349    }
350}
351
352impl Default for RouteDescription {
353    fn default() -> Self {
354        Self::new("No description")
355    }
356}
357
358// ── Global state ───────────────────────────────────────────────────────────
359
360static TOTAL_COUNT: AtomicUsize = AtomicUsize::new(0);
361static LIMITER: OnceLock<Arc<dyn RateLimiter>> = OnceLock::new();
362static MOUNTED_HANDLERS: AtomicBool = AtomicBool::new(false);
363
364// ── Middleware / Handler types ───────────────────────────────────────────────
365/// Pre-handler hook: inspects or mutates the context and headers, returning `Ok(())`
366/// to continue or `Err(ErrorResult)` to short-circuit with an error response.
367pub type Middleware = Arc<
368    dyn for<'a> Fn(
369            &'a mut CorrelationContext,
370        ) -> futures::future::BoxFuture<'a, Result<(), ErrorResult>>
371        + Send
372        + Sync,
373>;
374
375/// Low-level route handler used by the dispatcher: receives the prepared context
376/// plus raw headers, method, path, and body bytes, and returns a complete HTTP response.
377pub type BoxHandler = Arc<
378    dyn Fn(
379            CorrelationContext,
380            HeaderMap,
381            Method,
382            String,
383            Vec<u8>,
384        ) -> futures::future::BoxFuture<'static, Response<String>>
385        + Send
386        + Sync,
387>;
388
389// ── RouteEntry / Router ──────────────────────────────────────────────────────
390pub(crate) struct RouteEntry {
391    method: Method,
392    path: String,
393    handler: BoxHandler,
394    middlewares: Vec<Middleware>,
395}
396
397#[derive(Default)]
398struct TrieNode {
399    static_children: HashMap<String, TrieNode>,
400    param_child: Option<(String, Box<TrieNode>)>, // (param name, subtree)
401    wildcard_routes: HashMap<Method, RouteEntry>, // method -> route, for a "/*" mounted here
402    exact_routes: HashMap<Method, RouteEntry>,    // method -> route, for a path ending here
403}
404
405/// Hyper-backed route registry: stores routes with per-route middlewares,
406/// runs global middlewares for every request, and dispatches by method and path.
407/// A trailing `/*` on a route path acts as a prefix wildcard when matching.
408pub struct Router {
409    root: TrieNode,
410    global_middlewares: Vec<Middleware>,
411
412    /// Reserved test-execution hook for documentation-driven test clients.
413    /// When installed and the request carries the test-client header, this handler
414    /// runs instead of the matched route.
415    test_client_handler: Option<BoxHandler>,
416}
417
418impl Router {
419    /// Creates an empty router with no routes and no global middlewares.
420    pub fn new() -> Self {
421        Self {
422            root: TrieNode::default(),
423            global_middlewares: Vec::new(),
424            test_client_handler: None,
425        }
426    }
427
428    /// Registers a route in the trie, indexed by its path segments and method.
429    ///
430    /// The path is split on `/` (leading/trailing slashes and empty segments from
431    /// repeated `/` are ignored). Each segment is classified as one of:
432    ///
433    /// - **Static** (`users`, `settings`) — matched literally.
434    /// - **Param** (`:id`, `:slug`) — matches any single non-empty segment; the
435    ///   name keys the captured value after the colon.
436    /// - **Wildcard** (`*`) — must be the final segment; matches the remainder of
437    ///   the path, including zero remaining segments.
438    ///
439    /// Method keys routes at the node where their path terminates, so
440    /// registering the same path with different methods (e.g. `GET /users/:id`
441    /// and `POST /users/:id`) is expected and does not conflict.
442    ///
443    /// # Precedence
444    ///
445    /// Insertion order does not affect resolution order. At lookup time, static
446    /// segments are always preferred over a param segment at the same position,
447    /// which is preferred over a wildcard — this is enforced by the trie
448    /// structure itself, not by the order routes are inserted here.
449    ///
450    /// # Panics
451    ///
452    /// In debug builds, panics if a param segment is inserted at a trie position
453    /// that already holds a param child with a *different* name (e.g., inserting
454    /// both `/users/:id` and `/users/:userId`). This check is compiled out in
455    /// release builds — conflicting names will silently take the first
456    /// registered name instead.
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use http::Method;
462    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/users/:id".into(), .. });
463    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/users/active".into(), .. });
464    /// router.insert(crate::shared_framework::controller::RouteEntry { method: Method::GET, path: "/admin/*".into(), .. });
465    /// ```
466    pub(crate) fn insert(&mut self, route: RouteEntry) {
467        let segments: Vec<String> = route
468            .path
469            .trim_matches('/')
470            .split('/')
471            .filter(|s| !s.is_empty())
472            .map(String::from)
473            .collect();
474        Self::insert_at(&mut self.root, &segments, route);
475    }
476
477    fn insert_at(node: &mut TrieNode, segments: &[String], route: RouteEntry) {
478        match segments.split_first() {
479            None => {
480                node.exact_routes.insert(route.method.clone(), route);
481            }
482            Some((seg, _rest)) if seg == "*" => {
483                node.wildcard_routes.insert(route.method.clone(), route);
484            }
485            Some((seg, rest)) if seg.starts_with(':') => {
486                let name = seg[1..].to_string();
487                let (existing_name, child) = node
488                    .param_child
489                    .get_or_insert_with(|| (name.clone(), Box::new(TrieNode::default())));
490                debug_assert_eq!(
491                    existing_name, &name,
492                    "conflicting param names at same position: {existing_name} vs {name}"
493                );
494                Self::insert_at(child, rest, route);
495            }
496            Some((seg, rest)) => {
497                let child = node.static_children.entry(seg.clone()).or_default();
498                Self::insert_at(child, rest, route);
499            }
500        }
501    }
502
503    /// Resolves an incoming request to a registered route, extracting any path
504    /// parameters along the way.
505    ///
506    /// The path is normalized (see [`normalize_path`]) and split into segments
507    /// before matching. Matching descends the trie one segment at a time,
508    /// preferring branches in this order at each level:
509    ///
510    /// 1. **Static** children — exact segment match.
511    /// 2. **Param** child — matches any segment, capturing its value.
512    /// 3. **Wildcard** — matches all remaining segments (including none).
513    ///
514    /// If a static branch matches part of the path but fails to produce a full
515    /// match further down, resolution backtracks and retries via the param
516    /// branch at that level before falling back to the wildcard. This ensures a
517    /// route like `/users/:id/settings` is still reachable even when a sibling
518    /// static route `/users/active` exists but doesn't match past the first
519    /// segment.
520    ///
521    /// The method is checked only at the point a path fully matches (an exact
522    /// route or a wildcard boundary) — a path that matches some route's shape
523    /// but not under the requested method resolves to `None`, indistinguishable
524    /// from no matching path at all.
525    ///
526    /// # Returns
527    ///
528    /// `Some((route, params))` on a match, where `params` maps each `:name`
529    /// segment in the matched route's path to the corresponding literal value
530    /// from the request path. Returns `None` if no route matches the given
531    /// method and path.
532    ///
533    /// # Examples
534    ///
535    /// ```
536    /// let (route, params) = router.resolve("GET", "/users/42").unwrap();
537    /// assert_eq!(params["id"], "42");
538    /// ```
539    ///
540    /// # Warnings
541    /// - Path segments are not percent-decoded; encoded characters (e.g. `%20`)
542    ///   are returned as-is in extracted params.
543    /// - Does not distinguish a path that exists under a different method from a
544    ///   path that doesn't exist at all — both return `None`.
545    pub(crate) fn resolve(
546        &self,
547        method: &Method,
548        path: &str,
549    ) -> Option<(&RouteEntry, HashMap<String, String>)> {
550        let normal_path = normalize_path(path);
551        let segments: Vec<&str> = normal_path
552            .trim_matches('/')
553            .split('/')
554            .filter(|s| !s.is_empty())
555            .collect();
556
557        let mut params = Vec::new();
558        let route = Self::find(&self.root, &segments, method, &mut params)?;
559        Some((route, params.into_iter().collect()))
560    }
561
562    /// Installs the reserved test-client interceptor used for documentation-driven test runs.
563    pub fn set_test_client_handler(&mut self, handler: BoxHandler) {
564        self.test_client_handler = Some(handler);
565    }
566
567    fn ensure_global_middleware(&mut self) {
568        if MOUNTED_HANDLERS
569            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
570            .is_ok()
571        {
572            self.global_middlewares.push(Arc::new(|ctx| {
573                async move {
574                    if ctx.request_id().is_empty() {
575                        let rid = hex::encode(rand::random::<[u8; 8]>());
576                        ctx.set_request_id(&rid);
577                    }
578                    Ok(())
579                }
580                .boxed()
581            }));
582            self.global_middlewares.push(Arc::new(|ctx| {
583                async move {
584                    let headers = ctx.headers();
585                    if let Some(v) = headers
586                        .get("x-correlation-id")
587                        .and_then(|h| h.to_str().ok())
588                    {
589                        if !v.is_empty() {
590                            ctx.set_correlation_id(v);
591                        }
592                    }
593                    if let Some(v) = headers
594                        .get("x-correlation-flow")
595                        .and_then(|h| h.to_str().ok())
596                    {
597                        if let Ok(flow) = v.parse::<crate::logging::CorrelationFlow>() {
598                            ctx.set_flow(flow);
599                        }
600                    }
601                    Ok(())
602                }
603                .boxed()
604            }));
605        }
606    }
607
608    /// Registers a typed handler at `base_path` + version + `path` for `method`.
609    ///
610    /// Type parameter `T` is the `ServiceResult` payload; `F`/`Fut` describes the
611    /// `Fn(CorrelationContext)` async handler returning `Result<ServiceResult<T>, ErrorResult>`.
612    /// The handler receives only the context: body, headers, and query parameters are
613    /// attached at dispatch, so use `ctx.body::<T>()`, `ctx.header()`, or `ctx.query_param()`.
614    /// Registers the description for documentation, installs per-route rate-limit
615    /// middleware when `description.rate_limit` is set, and enforces declared file-part
616    /// `count` caps before the handler runs.
617    ///
618    /// Panics if `has_body` is set without a declared request body or file parameter.
619    /// ```ignore
620    /// router.mount("/users", 1, "/:id", false, Method::GET, desc, handler, vec![]);
621    /// ```
622    pub fn mount<T, F, Fut>(
623        &mut self,
624        base_path: &str,
625        version: u32,
626        path: &str,
627        has_body: bool,
628        method: Method,
629        description: RouteDescription,
630        handler: F,
631        middlewares: Vec<Middleware>,
632    ) where
633        T: serde::Serialize + Send + Sync + 'static,
634        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
635        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
636    {
637        self.ensure_global_middleware();
638        let full_path = calculate_full_path(base_path, version, path);
639        let meta_path = Holder {
640            path: full_path.to_string(),
641            limit: description.rate_limit.unwrap_or(0),
642        };
643
644        if let Ok(mut reg) = DocumentationRegistrant::global().write() {
645            reg.register_route(
646                &full_path,
647                method.as_str(),
648                "Controller",
649                description.clone(),
650            );
651        }
652
653        if has_body {
654            let expected = description.request_body.clone();
655            // File-only routes satisfy the body contract through file_parameters.
656            if expected.is_none() && description.file_parameters.is_empty() {
657                panic!(
658                    "Request body class must be specified in the route description for {} {}",
659                    method.as_str(),
660                    full_path
661                );
662            }
663        }
664
665        let mut all_middlewares = Vec::new();
666        if let Some(limit) = description.rate_limit {
667            if limit > 0 {
668                let path_clone = meta_path.clone();
669
670                let m: Middleware = Arc::new(move |ctx: &mut CorrelationContext| {
671                    let headers = ctx.headers();
672                    let path_inner = path_clone.clone();
673                    let fut = async move {
674                        let limiter = LIMITER.get_or_init(|| create_from_env()).clone();
675                        let key = ctx.user_id().unwrap_or_else(|| {
676                            headers
677                                .get("x-forwarded-for")
678                                .and_then(|v| v.to_str().ok())
679                                .unwrap_or("anonymous")
680                                .to_string()
681                        }) + ":"
682                            + &path_inner.path;
683                        if limiter.is_allowed(&key, path_inner.limit).await {
684                            return Ok(());
685                        }
686                        Err(ErrorResult::from_error(
687                            "Too many requests. Please try again later.",
688                            429,
689                        ))
690                    };
691
692                    // Explicitly force the BoxFuture type to drop any inferred lifetime relationships
693                    // linked to the arguments ctx or headers
694                    FutureExt::boxed(fut)
695                });
696                all_middlewares.push(m);
697            }
698        }
699        all_middlewares.extend(middlewares);
700
701        // Declared per-parameter send caps (`FileParameter.count`), enforced in
702        // place before the handler runs just like body validation.
703        let file_counts: Vec<(String, u32)> = description
704            .file_parameters
705            .iter()
706            .filter_map(|(name, p)| p.count.map(|c| (name.clone(), c)))
707            .collect();
708        let handler = Arc::new(handler);
709
710        let boxed: BoxHandler = Arc::new(move |ctx, _headers, _method, _path, _body| {
711            let file_counts = file_counts.clone();
712            let handler = handler.clone();
713            Box::pin(async move {
714                let ctx = Arc::new(ctx);
715                if let Some(err) = file_count_violation(ctx.clone(), &file_counts) {
716                    return crate::response::error_response(&err, ctx.clone());
717                }
718                match handler((*ctx).clone()).await {
719                    Ok(typed) => {
720                        let body = match typed.serialize() {
721                            Ok(b) => b,
722                            Err(e) => {
723                                let err =
724                                    ErrorResult::from_error(&anyhow::anyhow!(e.to_string()), 500);
725                                return crate::response::error_response(&err, ctx.clone());
726                            }
727                        };
728                        if body.is_empty() {
729                            let err = ErrorResult::new("Response body is null", None, 503);
730                            return crate::response::error_response(&err, ctx.clone());
731                        }
732                        let mut builder = Response::builder()
733                            .status(typed.code())
734                            .header("X-Request-ID", ctx.request_id())
735                            .header("X-Correlation-ID", ctx.correlation_id())
736                            .header("X-Correlation-Flow", ctx.flow().to_string());
737                        let ct = match typed.response_type() {
738                            ResponseType::Json => "application/json",
739                            ResponseType::File => {
740                                let filename = body.rsplit('/').next().unwrap_or("file");
741                                builder = builder.header(
742                                    "Content-Disposition",
743                                    format!("attachment; filename=\"{}\"", filename),
744                                );
745                                "application/octet-stream"
746                            }
747                            ResponseType::Xml => "application/xml",
748                            ResponseType::Javascript => "application/javascript",
749                            ResponseType::Html => "text/html",
750                            ResponseType::Text => "text/plain",
751                        };
752                        builder = builder.header("Content-Type", ct);
753                        builder.body(body).unwrap()
754                    }
755                    Err(e) => crate::response::error_response(&e, ctx.clone()),
756                }
757            })
758        });
759
760        self.insert(RouteEntry {
761            method: method.clone(),
762            path: full_path.clone(),
763            handler: boxed,
764            middlewares: all_middlewares,
765        });
766        info!(
767            target: "routing",
768            handler = "Controller",
769            method = method.as_str(),
770            path = %full_path,
771            "Mounted a '{}' handler which listens on  '{}'",
772            method.as_str(),
773            full_path
774        );
775        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
776    }
777
778    /// Registers a handler returning heterogeneous payloads as `Box<dyn TypedServiceResult>`.
779    /// Use this when one handler returns different `ServiceResult<T>` shapes; prefer
780    /// [`Router::mount`] when the result type is statically known.
781    /// Like [`Router::mount`], the handler takes only the [`CorrelationContext`].
782    pub fn mount_typed<F, Fut>(
783        &mut self,
784        base_path: &str,
785        version: u32,
786        path: &str,
787        method: Method,
788        description: RouteDescription,
789        handler: F,
790        middlewares: Vec<Middleware>,
791    ) where
792        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
793        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
794    {
795        self.ensure_global_middleware();
796        let full_path = calculate_full_path(base_path, version, path);
797        let meta_path = Holder {
798            path: full_path.to_string(),
799            limit: description.rate_limit.unwrap_or(0),
800        };
801
802        if let Ok(mut reg) = DocumentationRegistrant::global().write() {
803            reg.register_route(
804                &full_path,
805                method.as_str(),
806                "Controller",
807                description.clone(),
808            );
809        }
810
811        let mut all_middlewares = Vec::new();
812        if let Some(limit) = description.rate_limit {
813            if limit > 0 {
814                let path_clone = meta_path.clone();
815                let m: Middleware = Arc::new(move |ctx: &mut CorrelationContext| {
816                    let headers = ctx.headers();
817                    let path_inner = path_clone.clone();
818                    let fut = async move {
819                        let limiter = LIMITER.get_or_init(|| create_from_env()).clone();
820                        let key = ctx.user_id().unwrap_or_else(|| {
821                            headers
822                                .get("x-forwarded-for")
823                                .and_then(|v| v.to_str().ok())
824                                .unwrap_or("anonymous")
825                                .to_string()
826                        }) + ":"
827                            + &path_inner.path;
828                        if limiter.is_allowed(&key, path_inner.limit).await {
829                            return Ok(());
830                        }
831                        Err(ErrorResult::from_error(
832                            "Too many requests. Please try again later.",
833                            429,
834                        ))
835                    };
836                    FutureExt::boxed(fut)
837                });
838                all_middlewares.push(m);
839            }
840        }
841        all_middlewares.extend(middlewares);
842
843        // Declared per-parameter send caps (`FileParameter.count`), enforced in
844        // place before the handler runs just like body validation.
845        let file_counts: Vec<(String, u32)> = description
846            .file_parameters
847            .iter()
848            .filter_map(|(name, p)| p.count.map(|c| (name.clone(), c)))
849            .collect();
850        let handler = Arc::new(handler);
851
852        let boxed: BoxHandler = Arc::new(move |ctx, _headers, _method, _path, _body| {
853            let file_counts = file_counts.clone();
854            let handler = handler.clone();
855            Box::pin(async move {
856                let ctx = Arc::new(ctx);
857                if let Some(err) = file_count_violation(ctx.clone(), &file_counts) {
858                    return crate::response::error_response(&err, ctx.clone());
859                }
860                match handler((*ctx).clone()).await {
861                    Ok(typed) => crate::response::build_response(typed.as_ref(), ctx.clone()),
862                    Err(e) => crate::response::error_response(&e, ctx.clone()),
863                }
864            })
865        });
866
867        self.insert(RouteEntry {
868            method: method.clone(),
869            path: full_path.clone(),
870            handler: boxed,
871            middlewares: all_middlewares,
872        });
873        info!(
874            handler = "Controller",
875            method = method.as_str(),
876            path = %full_path,
877            "Mounted a '{}' handler which listens on  '{}'",
878            method.as_str(),
879            full_path
880        );
881        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
882    }
883
884    /// Registers a pre-built [`BoxHandler`] at the resolved path with the given middlewares.
885    /// A trailing `/*` on the resolved path acts as a prefix wildcard when matching.
886    /// Unlike [`Router::mount`], this performs no docs registration and imposes no
887    /// body contract, making it suitable for static-asset serving and custom handlers.
888    pub fn mount_raw(
889        &mut self,
890        base_path: &str,
891        version: u32,
892        path: &str,
893        method: Method,
894        handler: BoxHandler,
895        middlewares: Vec<Middleware>,
896    ) {
897        self.ensure_global_middleware();
898        let full_path = calculate_full_path(base_path, version, path);
899        self.insert(RouteEntry {
900            method: method.clone(),
901            path: full_path.clone(),
902            handler,
903            middlewares,
904        });
905        info!(
906            handler = "raw",
907            method = method.as_str(),
908            path = %full_path,
909            "Mounted a '{}' handler which listens on  '{}'",
910            method.as_str(),
911            full_path
912        );
913        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
914    }
915
916    /// Serves files under `fs_path` at `base_path` + version + `path` with a `/*` wildcard.
917    /// Requests for `/` resolve to `index.html` when present; missing files fall back
918    /// to a plain-text placeholder response instead of a 404.
919    pub fn mount_static(
920        &mut self,
921        base_path: &str,
922        version: u32,
923        path: &str,
924        fs_path: String,
925        middlewares: Vec<Middleware>,
926    ) {
927        let full_path = if version == 0 {
928            no_trailing_slash(&format!(
929                "/{}/{}/*",
930                base_path.trim_matches('/'),
931                path.trim_matches('/')
932            ))
933        } else {
934            no_trailing_slash(&format!(
935                "/v{}/{}/{}/*",
936                version,
937                base_path.trim_matches('/'),
938                path.trim_matches('/')
939            ))
940        };
941        let fs_path_clone = fs_path.clone();
942        let full_path_no_wildcard = full_path.trim_end_matches("/*").to_string();
943        let handler: BoxHandler = Arc::new(move |ctx, _headers, _method, req_path, _body| {
944            let fs_path = fs_path_clone.clone();
945            let req_path = req_path.clone();
946            let ctx = ctx.clone();
947            let prefix = full_path_no_wildcard.clone();
948            Box::pin(async move {
949                let rel = req_path.trim_start_matches(&prefix).trim_start_matches('/');
950                // Resolve to a real file under fs_path when present (Swagger UI `doc/` dir).
951                let candidates = if rel.is_empty() {
952                    vec![
953                        format!("{}/index.html", fs_path.trim_end_matches('/')),
954                        fs_path.clone(),
955                    ]
956                } else {
957                    vec![format!("{}/{}", fs_path.trim_end_matches('/'), rel)]
958                };
959                for candidate in &candidates {
960                    if let Ok(bytes) = std::fs::read(candidate) {
961                        let ct = guess_content_type(candidate);
962                        let body = String::from_utf8_lossy(&bytes).into_owned();
963                        return Response::builder()
964                            .status(200)
965                            .header("X-Request-ID", ctx.request_id())
966                            .header("Content-Type", ct)
967                            .body(body)
968                            .unwrap();
969                    }
970                }
971                // Fallback placeholder (keeps offline/dev behavior useful)
972                let file_path = if rel.is_empty() {
973                    fs_path.clone()
974                } else {
975                    format!("{}/{}", fs_path.trim_end_matches('/'), rel)
976                };
977                let typed = ServiceResult::new(
978                    "success",
979                    "OK",
980                    Some(format!("Serving static file: {}", file_path)),
981                    200,
982                )
983                .with_response_type(ResponseType::Text);
984                let body = TypedServiceResult::serialize(&typed).unwrap();
985                Response::builder()
986                    .status(TypedServiceResult::code(&typed))
987                    .header("X-Request-ID", ctx.request_id())
988                    .header("Content-Type", "text/plain")
989                    .body(body)
990                    .unwrap()
991            })
992        });
993        self.insert(RouteEntry {
994            method: Method::GET,
995            path: full_path.clone(),
996            handler,
997            middlewares,
998        });
999        info!(
1000            handler = "static",
1001            path = %full_path,
1002            "Mounted a '{}' handler which listens on  '{}'",
1003            "static file",
1004            full_path
1005        );
1006        TOTAL_COUNT.fetch_add(1, Ordering::SeqCst);
1007    }
1008
1009    fn find<'a>(
1010        node: &'a TrieNode,
1011        segments: &[&str],
1012        method: &Method,
1013        params: &mut Vec<(String, String)>,
1014    ) -> Option<&'a RouteEntry> {
1015        match segments.split_first() {
1016            None => node
1017                .exact_routes
1018                .get(method)
1019                .or_else(|| node.wildcard_routes.get(method)),
1020            Some((seg, rest)) => {
1021                // static beats param beats wildcard, tried in that order with backtracking
1022                if let Some(child) = node.static_children.get(*seg) {
1023                    if let Some(r) = Self::find(child, rest, method, params) {
1024                        return Some(r);
1025                    }
1026                }
1027
1028                if let Some((name, child)) = &node.param_child {
1029                    params.push((name.clone(), (*seg).to_string()));
1030                    if let Some(r) = Self::find(child, rest, method, params) {
1031                        return Some(r);
1032                    }
1033                    params.pop();
1034                }
1035
1036                node.wildcard_routes.get(method)
1037            }
1038        }
1039    }
1040}
1041
1042impl Default for Router {
1043    fn default() -> Self {
1044        Self::new()
1045    }
1046}
1047
1048fn guess_content_type(path: &str) -> &'static str {
1049    let lower = path.to_ascii_lowercase();
1050    if lower.ends_with(".html") || lower.ends_with(".htm") {
1051        "text/html"
1052    } else if lower.ends_with(".js") {
1053        "application/javascript"
1054    } else if lower.ends_with(".css") {
1055        "text/css"
1056    } else if lower.ends_with(".json") {
1057        "application/json"
1058    } else if lower.ends_with(".png") {
1059        "image/png"
1060    } else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
1061        "image/jpeg"
1062    } else if lower.ends_with(".svg") {
1063        "image/svg+xml"
1064    } else if lower.ends_with(".yaml") || lower.ends_with(".yml") {
1065        "application/yaml"
1066    } else {
1067        "text/plain"
1068    }
1069}
1070fn normalize_path(p: &str) -> String {
1071    let p = p.trim().replace("//", "/");
1072    if p.ends_with('/') && p.len() > 1 {
1073        p[..p.len() - 1].to_string()
1074    } else if p.is_empty() {
1075        "/".to_string()
1076    } else {
1077        p
1078    }
1079}
1080fn no_trailing_slash(path: &str) -> String {
1081    let p = path.trim().replace("//", "/");
1082    if p.is_empty() || p == "/" {
1083        return "/".to_string();
1084    }
1085    if p.ends_with('/') {
1086        p[..p.rfind('/').unwrap()].to_string()
1087    } else {
1088        p
1089    }
1090}
1091/// Enforce declared `FileParameter.count` caps against a dispatched context.
1092/// Counts every part bearing the parameter name (files plus text fields) —
1093/// `count` is the maximum number of times the parameter may be sent.
1094/// Non-multipart requests and undeclared parameters are never rejected.
1095fn file_count_violation(
1096    ctx: Arc<CorrelationContext>,
1097    counts: &[(String, u32)],
1098) -> Option<ErrorResult> {
1099    if counts.is_empty() {
1100        return None;
1101    }
1102    let mp = ctx.multipart()?;
1103    for (name, max) in counts {
1104        let occurrences = mp.files.iter().filter(|f| &f.field_name == name).count()
1105            + mp.fields.get(name).map_or(0, |v| v.len());
1106        if occurrences as u32 > *max {
1107            return Some(ErrorResult::bad_request(format!(
1108                "Too many '{}' parts: got {}, maximum is {}",
1109                name, occurrences, max
1110            )));
1111        }
1112    }
1113    None
1114}
1115
1116fn calculate_full_path(base_path: &str, version: u32, path: &str) -> String {
1117    let decoded = if version == 0 {
1118        let dddd = no_trailing_slash(&format!(
1119            "/{}/{}",
1120            base_path.trim_matches('/'),
1121            path.trim_start_matches('/')
1122        ));
1123        urlencoding::decode(dddd.leak())
1124    } else {
1125        let dddd = no_trailing_slash(&format!(
1126            "/v{}/{}/{}",
1127            version,
1128            base_path.trim_matches('/'),
1129            path.trim_start_matches('/')
1130        ));
1131        urlencoding::decode(dddd.leak())
1132    };
1133
1134    if decoded.is_err() {
1135        return base_path.to_string();
1136    }
1137
1138    decoded.unwrap().to_string()
1139}
1140
1141// ── RouteController trait ──────────────────────────────────────────────────
1142
1143#[async_trait::async_trait]
1144/// Groups related routes under a shared base path and version.
1145/// Implement [`RouteController::base_path`] and [`RouteController::register_routes`];
1146/// override [`RouteController::version`] to prefix paths with `/v{version}`.
1147pub trait RouteController: Send + Sync {
1148    /// Base path prefix for every route registered by this controller.
1149    fn base_path(&self) -> &str;
1150    /// API version prefix; `0` means no `/v{version}` segment is added.
1151    fn version(&self) -> u32 {
1152        0
1153    }
1154    /// Resolves a relative `path` to its full mounted path including base path and version.
1155    fn full_path(&self, path: &str) -> String {
1156        calculate_full_path(self.base_path(), self.version(), path)
1157    }
1158    /// Registers this controller's routes on `router`, typically via [`RouteControllerExt`].
1159    async fn register_routes(&self, router: &mut Router);
1160    /// Default registration entry point; delegates to [`RouteController::register_routes`].
1161    async fn register(&self, router: &mut Router) {
1162        self.register_routes(router).await;
1163    }
1164
1165    ///
1166    fn type_name(&self) -> &'static str {
1167        std::any::type_name::<Self>()
1168    }
1169}
1170
1171/// Convenience mounts that fill in this controller's base path and version.
1172/// Each `mount_*` takes an `Fn(CorrelationContext)` handler returning
1173/// `Result<ServiceResult<T>, ErrorResult>`; the `*_typed` variants instead return
1174/// `Result<Box<dyn TypedServiceResult>, ErrorResult>` for heterogeneous payloads.
1175/// ```ignore
1176/// self.mount_get(&mut router, "/list", desc, handler, vec![]);
1177/// ```
1178pub trait RouteControllerExt: RouteController {
1179    /// Mounts a GET handler with no required body at `path`.
1180    fn mount_get<T, F, Fut>(
1181        &self,
1182        router: &mut Router,
1183        path: &str,
1184        description: RouteDescription,
1185        handler: F,
1186        middlewares: Vec<Middleware>,
1187    ) where
1188        T: serde::Serialize + Send + Sync + 'static,
1189        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1190        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1191    {
1192        router.mount(
1193            self.base_path(),
1194            self.version(),
1195            path,
1196            false,
1197            Method::GET,
1198            description,
1199            handler,
1200            middlewares,
1201        );
1202    }
1203    /// Mounts a POST handler; requires a declared request body or file parameter.
1204    fn mount_post<T, F, Fut>(
1205        &self,
1206        router: &mut Router,
1207        path: &str,
1208        description: RouteDescription,
1209        handler: F,
1210        middlewares: Vec<Middleware>,
1211    ) where
1212        T: serde::Serialize + Send + Sync + 'static,
1213        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1214        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1215    {
1216        router.mount(
1217            self.base_path(),
1218            self.version(),
1219            path,
1220            true,
1221            Method::POST,
1222            description,
1223            handler,
1224            middlewares,
1225        );
1226    }
1227    /// Mounts a PUT handler; requires a declared request body or file parameter.
1228    fn mount_put<T, F, Fut>(
1229        &self,
1230        router: &mut Router,
1231        path: &str,
1232        description: RouteDescription,
1233        handler: F,
1234        middlewares: Vec<Middleware>,
1235    ) where
1236        T: serde::Serialize + Send + Sync + 'static,
1237        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1238        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1239    {
1240        router.mount(
1241            self.base_path(),
1242            self.version(),
1243            path,
1244            true,
1245            Method::PUT,
1246            description,
1247            handler,
1248            middlewares,
1249        );
1250    }
1251    /// Mounts a PATCH handler; `has_body` selects whether a request body is required.
1252    fn mount_patch<T, F, Fut>(
1253        &self,
1254        router: &mut Router,
1255        path: &str,
1256        has_body: bool,
1257        description: RouteDescription,
1258        handler: F,
1259        middlewares: Vec<Middleware>,
1260    ) where
1261        T: serde::Serialize + Send + Sync + 'static,
1262        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1263        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1264    {
1265        router.mount(
1266            self.base_path(),
1267            self.version(),
1268            path,
1269            has_body,
1270            Method::PATCH,
1271            description,
1272            handler,
1273            middlewares,
1274        );
1275    }
1276    /// Mounts a PATCH handler that requires a request body.
1277    fn mount_patch_with_body<T, F, Fut>(
1278        &self,
1279        router: &mut Router,
1280        path: &str,
1281        description: RouteDescription,
1282        handler: F,
1283        middlewares: Vec<Middleware>,
1284    ) where
1285        T: serde::Serialize + Send + Sync + 'static,
1286        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1287        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1288    {
1289        self.mount_patch(router, path, true, description, handler, middlewares);
1290    }
1291    /// Mounts a PATCH handler that requires no request body.
1292    fn mount_patch_without_body<T, F, Fut>(
1293        &self,
1294        router: &mut Router,
1295        path: &str,
1296        description: RouteDescription,
1297        handler: F,
1298        middlewares: Vec<Middleware>,
1299    ) where
1300        T: serde::Serialize + Send + Sync + 'static,
1301        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1302        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1303    {
1304        self.mount_patch(router, path, false, description, handler, middlewares);
1305    }
1306    /// Mounts a DELETE handler with no required body at `path`.
1307    fn mount_delete<T, F, Fut>(
1308        &self,
1309        router: &mut Router,
1310        path: &str,
1311        description: RouteDescription,
1312        handler: F,
1313        middlewares: Vec<Middleware>,
1314    ) where
1315        T: serde::Serialize + Send + Sync + 'static,
1316        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1317        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1318    {
1319        router.mount(
1320            self.base_path(),
1321            self.version(),
1322            path,
1323            false,
1324            Method::DELETE,
1325            description,
1326            handler,
1327            middlewares,
1328        );
1329    }
1330    /// Mounts an OPTIONS handler with no required body at `path`.
1331    fn mount_options<T, F, Fut>(
1332        &self,
1333        router: &mut Router,
1334        path: &str,
1335        description: RouteDescription,
1336        handler: F,
1337        middlewares: Vec<Middleware>,
1338    ) where
1339        T: serde::Serialize + Send + Sync + 'static,
1340        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1341        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1342    {
1343        router.mount(
1344            self.base_path(),
1345            self.version(),
1346            path,
1347            false,
1348            Method::OPTIONS,
1349            description,
1350            handler,
1351            middlewares,
1352        );
1353    }
1354    /// Mounts a HEAD handler with no required body at `path`.
1355    fn mount_head<T, F, Fut>(
1356        &self,
1357        router: &mut Router,
1358        path: &str,
1359        description: RouteDescription,
1360        handler: F,
1361        middlewares: Vec<Middleware>,
1362    ) where
1363        T: serde::Serialize + Send + Sync + 'static,
1364        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1365        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1366    {
1367        router.mount(
1368            self.base_path(),
1369            self.version(),
1370            path,
1371            false,
1372            Method::HEAD,
1373            description,
1374            handler,
1375            middlewares,
1376        );
1377    }
1378    /// Mounts a TRACE handler with no required body at `path`.
1379    fn mount_trace<T, F, Fut>(
1380        &self,
1381        router: &mut Router,
1382        path: &str,
1383        description: RouteDescription,
1384        handler: F,
1385        middlewares: Vec<Middleware>,
1386    ) where
1387        T: serde::Serialize + Send + Sync + 'static,
1388        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1389        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1390    {
1391        router.mount(
1392            self.base_path(),
1393            self.version(),
1394            path,
1395            false,
1396            Method::TRACE,
1397            description,
1398            handler,
1399            middlewares,
1400        );
1401    }
1402    /// Mounts a CONNECT handler with no required body at `path`.
1403    fn mount_connect<T, F, Fut>(
1404        &self,
1405        router: &mut Router,
1406        path: &str,
1407        description: RouteDescription,
1408        handler: F,
1409        middlewares: Vec<Middleware>,
1410    ) where
1411        T: serde::Serialize + Send + Sync + 'static,
1412        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1413        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1414    {
1415        router.mount(
1416            self.base_path(),
1417            self.version(),
1418            path,
1419            false,
1420            Method::CONNECT,
1421            description,
1422            handler,
1423            middlewares,
1424        );
1425    }
1426    /// Mounts a WebDAV COPY handler with no required body at `path`.
1427    fn mount_copy<T, F, Fut>(
1428        &self,
1429        router: &mut Router,
1430        path: &str,
1431        description: RouteDescription,
1432        handler: F,
1433        middlewares: Vec<Middleware>,
1434    ) where
1435        T: serde::Serialize + Send + Sync + 'static,
1436        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1437        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1438    {
1439        router.mount(
1440            self.base_path(),
1441            self.version(),
1442            path,
1443            false,
1444            Method::from_bytes(b"COPY").unwrap(),
1445            description,
1446            handler,
1447            middlewares,
1448        );
1449    }
1450    /// Mounts a WebDAV MOVE handler with no required body at `path`.
1451    fn mount_move<T, F, Fut>(
1452        &self,
1453        router: &mut Router,
1454        path: &str,
1455        description: RouteDescription,
1456        handler: F,
1457        middlewares: Vec<Middleware>,
1458    ) where
1459        T: serde::Serialize + Send + Sync + 'static,
1460        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1461        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1462    {
1463        router.mount(
1464            self.base_path(),
1465            self.version(),
1466            path,
1467            false,
1468            Method::from_bytes(b"MOVE").unwrap(),
1469            description,
1470            handler,
1471            middlewares,
1472        );
1473    }
1474    /// Mounts a WebDAV LOCK handler; requires a declared request body or file parameter.
1475    fn mount_lock<T, F, Fut>(
1476        &self,
1477        router: &mut Router,
1478        path: &str,
1479        description: RouteDescription,
1480        handler: F,
1481        middlewares: Vec<Middleware>,
1482    ) where
1483        T: serde::Serialize + Send + Sync + 'static,
1484        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1485        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1486    {
1487        router.mount(
1488            self.base_path(),
1489            self.version(),
1490            path,
1491            true,
1492            Method::from_bytes(b"LOCK").unwrap(),
1493            description,
1494            handler,
1495            middlewares,
1496        );
1497    }
1498    /// Mounts a WebDAV UNLOCK handler with no required body at `path`.
1499    fn mount_unlock<T, F, Fut>(
1500        &self,
1501        router: &mut Router,
1502        path: &str,
1503        description: RouteDescription,
1504        handler: F,
1505        middlewares: Vec<Middleware>,
1506    ) where
1507        T: serde::Serialize + Send + Sync + 'static,
1508        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1509        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1510    {
1511        router.mount(
1512            self.base_path(),
1513            self.version(),
1514            path,
1515            false,
1516            Method::from_bytes(b"UNLOCK").unwrap(),
1517            description,
1518            handler,
1519            middlewares,
1520        );
1521    }
1522    /// Mounts a WebDAV PROPFIND handler; requires a declared request body or file parameter.
1523    fn mount_propfind<T, F, Fut>(
1524        &self,
1525        router: &mut Router,
1526        path: &str,
1527        description: RouteDescription,
1528        handler: F,
1529        middlewares: Vec<Middleware>,
1530    ) where
1531        T: serde::Serialize + Send + Sync + 'static,
1532        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1533        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1534    {
1535        router.mount(
1536            self.base_path(),
1537            self.version(),
1538            path,
1539            true,
1540            Method::from_bytes(b"PROPFIND").unwrap(),
1541            description,
1542            handler,
1543            middlewares,
1544        );
1545    }
1546    /// Mounts a WebDAV MKCOL handler with no required body at `path`.
1547    fn mount_mkcol<T, F, Fut>(
1548        &self,
1549        router: &mut Router,
1550        path: &str,
1551        description: RouteDescription,
1552        handler: F,
1553        middlewares: Vec<Middleware>,
1554    ) where
1555        T: serde::Serialize + Send + Sync + 'static,
1556        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1557        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1558    {
1559        router.mount(
1560            self.base_path(),
1561            self.version(),
1562            path,
1563            false,
1564            Method::from_bytes(b"MKCOL").unwrap(),
1565            description,
1566            handler,
1567            middlewares,
1568        );
1569    }
1570    /// Mounts a WebDAV SEARCH handler; requires a declared request body or file parameter.
1571    fn mount_search<T, F, Fut>(
1572        &self,
1573        router: &mut Router,
1574        path: &str,
1575        description: RouteDescription,
1576        handler: F,
1577        middlewares: Vec<Middleware>,
1578    ) where
1579        T: serde::Serialize + Send + Sync + 'static,
1580        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1581        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1582    {
1583        router.mount(
1584            self.base_path(),
1585            self.version(),
1586            path,
1587            true,
1588            Method::from_bytes(b"SEARCH").unwrap(),
1589            description,
1590            handler,
1591            middlewares,
1592        );
1593    }
1594    /// Mounts a WebDAV REPORT handler; requires a declared request body or file parameter.
1595    fn mount_report<T, F, Fut>(
1596        &self,
1597        router: &mut Router,
1598        path: &str,
1599        description: RouteDescription,
1600        handler: F,
1601        middlewares: Vec<Middleware>,
1602    ) where
1603        T: serde::Serialize + Send + Sync + 'static,
1604        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1605        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1606    {
1607        router.mount(
1608            self.base_path(),
1609            self.version(),
1610            path,
1611            true,
1612            Method::from_bytes(b"REPORT").unwrap(),
1613            description,
1614            handler,
1615            middlewares,
1616        );
1617    }
1618    /// Mounts a versioning CHECKIN handler with no required body at `path`.
1619    fn mount_checkin<T, F, Fut>(
1620        &self,
1621        router: &mut Router,
1622        path: &str,
1623        description: RouteDescription,
1624        handler: F,
1625        middlewares: Vec<Middleware>,
1626    ) where
1627        T: serde::Serialize + Send + Sync + 'static,
1628        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1629        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1630    {
1631        router.mount(
1632            self.base_path(),
1633            self.version(),
1634            path,
1635            false,
1636            Method::from_bytes(b"CHECKIN").unwrap(),
1637            description,
1638            handler,
1639            middlewares,
1640        );
1641    }
1642    /// Mounts a versioning CHECKOUT handler with no required body at `path`.
1643    fn mount_checkout<T, F, Fut>(
1644        &self,
1645        router: &mut Router,
1646        path: &str,
1647        description: RouteDescription,
1648        handler: F,
1649        middlewares: Vec<Middleware>,
1650    ) where
1651        T: serde::Serialize + Send + Sync + 'static,
1652        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1653        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1654    {
1655        router.mount(
1656            self.base_path(),
1657            self.version(),
1658            path,
1659            false,
1660            Method::from_bytes(b"CHECKOUT").unwrap(),
1661            description,
1662            handler,
1663            middlewares,
1664        );
1665    }
1666    /// Mounts a versioning UNCHECKOUT handler with no required body at `path`.
1667    fn mount_uncheckout<T, F, Fut>(
1668        &self,
1669        router: &mut Router,
1670        path: &str,
1671        description: RouteDescription,
1672        handler: F,
1673        middlewares: Vec<Middleware>,
1674    ) where
1675        T: serde::Serialize + Send + Sync + 'static,
1676        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1677        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1678    {
1679        router.mount(
1680            self.base_path(),
1681            self.version(),
1682            path,
1683            false,
1684            Method::from_bytes(b"UNCHECKOUT").unwrap(),
1685            description,
1686            handler,
1687            middlewares,
1688        );
1689    }
1690    /// Mounts a WebDAV MERGE handler; requires a declared request body or file parameter.
1691    fn mount_merge<T, F, Fut>(
1692        &self,
1693        router: &mut Router,
1694        path: &str,
1695        description: RouteDescription,
1696        handler: F,
1697        middlewares: Vec<Middleware>,
1698    ) where
1699        T: serde::Serialize + Send + Sync + 'static,
1700        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1701        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1702    {
1703        router.mount(
1704            self.base_path(),
1705            self.version(),
1706            path,
1707            true,
1708            Method::from_bytes(b"MERGE").unwrap(),
1709            description,
1710            handler,
1711            middlewares,
1712        );
1713    }
1714    /// Mounts a WebDAV ACL handler; requires a declared request body or file parameter.
1715    fn mount_acl<T, F, Fut>(
1716        &self,
1717        router: &mut Router,
1718        path: &str,
1719        description: RouteDescription,
1720        handler: F,
1721        middlewares: Vec<Middleware>,
1722    ) where
1723        T: serde::Serialize + Send + Sync + 'static,
1724        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1725        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1726    {
1727        router.mount(
1728            self.base_path(),
1729            self.version(),
1730            path,
1731            true,
1732            Method::from_bytes(b"ACL").unwrap(),
1733            description,
1734            handler,
1735            middlewares,
1736        );
1737    }
1738    /// Mounts a handler for an arbitrary `method` with no required body at `path`.
1739    fn mount_custom<T, F, Fut>(
1740        &self,
1741        router: &mut Router,
1742        path: &str,
1743        method: Method,
1744        description: RouteDescription,
1745        handler: F,
1746        middlewares: Vec<Middleware>,
1747    ) where
1748        T: serde::Serialize + Send + Sync + 'static,
1749        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1750        Fut: Future<Output = Result<ServiceResult<T>, ErrorResult>> + Send + 'static,
1751    {
1752        router.mount(
1753            self.base_path(),
1754            self.version(),
1755            path,
1756            false,
1757            method,
1758            description,
1759            handler,
1760            middlewares,
1761        );
1762    }
1763    /// Serves files under `fs_path` at this controller's base path plus `path`.
1764    fn mount_static(
1765        &self,
1766        router: &mut Router,
1767        path: &str,
1768        fs_path: String,
1769        middlewares: Vec<Middleware>,
1770    ) {
1771        router.mount_static(self.base_path(), self.version(), path, fs_path, middlewares);
1772    }
1773    /// Mounts a handler returning `Box<dyn TypedServiceResult>` for heterogeneous payloads.
1774    fn mount_typed<F, Fut>(
1775        &self,
1776        router: &mut Router,
1777        path: &str,
1778        method: Method,
1779        description: RouteDescription,
1780        handler: F,
1781        middlewares: Vec<Middleware>,
1782    ) where
1783        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1784        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1785    {
1786        router.mount_typed(
1787            self.base_path(),
1788            self.version(),
1789            path,
1790            method,
1791            description,
1792            handler,
1793            middlewares,
1794        );
1795    }
1796    /// Mounts a boxed-result GET handler returning heterogeneous payloads.
1797    fn mount_get_typed<F, Fut>(
1798        &self,
1799        router: &mut Router,
1800        path: &str,
1801        description: RouteDescription,
1802        handler: F,
1803        middlewares: Vec<Middleware>,
1804    ) where
1805        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1806        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1807    {
1808        self.mount_typed(router, path, Method::GET, description, handler, middlewares);
1809    }
1810    /// Mounts a boxed-result POST handler returning heterogeneous payloads.
1811    fn mount_post_typed<F, Fut>(
1812        &self,
1813        router: &mut Router,
1814        path: &str,
1815        description: RouteDescription,
1816        handler: F,
1817        middlewares: Vec<Middleware>,
1818    ) where
1819        F: Fn(CorrelationContext) -> Fut + Send + Sync + 'static,
1820        Fut: Future<Output = Result<Box<dyn TypedServiceResult>, ErrorResult>> + Send + 'static,
1821    {
1822        self.mount_typed(
1823            router,
1824            path,
1825            Method::POST,
1826            description,
1827            handler,
1828            middlewares,
1829        );
1830    }
1831}
1832impl<T: RouteController> RouteControllerExt for T {}
1833
1834// ── ConfigurationRegistrant — Hyper server bootstrap ───────────────────────
1835
1836/// Hyper server bootstrap: owns the shared [`Router`] and the bind address.
1837/// Mount controllers and global middlewares, then call [`ConfigurationRegistrant::serve`].
1838/// ```ignore
1839/// let server = std::sync::Arc::new(ConfigurationRegistrant::new(addr));
1840/// server.mount_controller(MyController).await;
1841/// let bound = server.serve().await?;
1842/// ```
1843pub struct ConfigurationRegistrant {
1844    router: Arc<tokio::sync::RwLock<Router>>,
1845    addr: SocketAddr,
1846}
1847
1848impl ConfigurationRegistrant {
1849    /// Creates a bootstrap with a fresh router bound to `addr` on [`ConfigurationRegistrant::serve`].
1850    pub fn new(addr: SocketAddr) -> Self {
1851        Self {
1852            router: Arc::new(tokio::sync::RwLock::new(Router::new())),
1853            addr,
1854        }
1855    }
1856
1857    /// Registers every route of `controller` on the shared router.
1858    pub async fn mount_controller<C: RouteController + 'static>(&self, controller: C) {
1859        let mut r = self.router.write().await;
1860        info!(
1861            target: "routing",
1862            handler = std::any::type_name::<C>(),
1863            path = %controller.base_path(),
1864            "Mounted controller '{}' at '{}'",
1865            std::any::type_name::<C>(),
1866            controller.base_path()
1867        );
1868        controller.register_routes(&mut r).await;
1869    }
1870
1871    /// Pushes a middleware that runs for every request before route matching.
1872    pub async fn mount_middleware(&self, _mw: Middleware) {
1873        let mut r = self.router.write().await;
1874        r.global_middlewares.push(_mw);
1875    }
1876
1877    /// Binds the socket, spawns the Hyper accept loop in the background, and returns the bound address.
1878    /// Returns an error if the socket cannot be bound.
1879    pub async fn serve(self: Arc<Self>) -> anyhow::Result<SocketAddr> {
1880        let listener = TcpListener::bind(self.addr).await?;
1881        let addr = listener.local_addr()?;
1882        info!(address = %addr, "HTTP server listening");
1883        let router = self.router.clone();
1884        tokio::spawn(async move {
1885            loop {
1886                let (stream, remote) = match listener.accept().await {
1887                    Ok(v) => v,
1888                    Err(e) => {
1889                        warn!(error = %e, "Failed to accept HTTP connection");
1890                        continue;
1891                    }
1892                };
1893                let io = TokioIo::new(stream);
1894                let router = router.clone();
1895                tokio::spawn(async move {
1896                    let svc = service_fn(move |req: Request<Incoming>| {
1897                        let router = router.clone();
1898                        let remote = remote;
1899                        async move { handle_request(router, req, remote).await }
1900                    });
1901                    if let Err(e) = http1::Builder::new().serve_connection(io, svc).await {
1902                        tracing::debug!(remote_addr = %remote, error = %e, "HTTP connection ended with an error");
1903                    }
1904                });
1905            }
1906        });
1907        Ok(addr)
1908    }
1909
1910    /// Returns a clone of the shared router handle for direct route registration.
1911    pub fn router_handle(&self) -> Arc<tokio::sync::RwLock<Router>> {
1912        self.router.clone()
1913    }
1914}
1915
1916async fn handle_request(
1917    router: Arc<tokio::sync::RwLock<Router>>,
1918    req: Request<Incoming>,
1919    remote_addr: SocketAddr,
1920) -> Result<Response<String>, ErrorResult> {
1921    let request_started = std::time::Instant::now();
1922    let method = req.method().clone();
1923    let path = req.uri().path();
1924    let old_path = path;
1925    let path = urlencoding::decode(path);
1926    if path.is_err() {
1927        tracing::error!(
1928            "This should not be possible. Encountered an error while processing the URL {}",
1929            old_path
1930        );
1931        return Err(ErrorResult::bad_request("Invalid path parameter"));
1932    }
1933    let path = path.ok().unwrap().to_string();
1934    let query = req.uri().query().unwrap_or("").to_string();
1935    let headers = req.headers().clone();
1936    let (_parts, body) = req.into_parts();
1937    let body_bytes = match body.collect().await {
1938        Ok(collected) => collected.to_bytes().to_vec(),
1939        Err(e) => {
1940            warn!(remote_addr = %remote_addr, error = %e, "Failed to read request body");
1941            vec![]
1942        }
1943    };
1944
1945    let params: HashMap<String, String> = serde_urlencoded::from_str(&query).unwrap_or_default();
1946    let mut ctx = build_correlation_context(&headers, &params, &body_bytes);
1947
1948    // Decode multipart eagerly so handlers and middlewares read fields/files
1949    // off the context via `form_field`/`files` and `ctx.body::<T>()`
1950    // instead of parsing raw bytes themselves.
1951    if let Some(ct) = headers
1952        .get(http::header::CONTENT_TYPE)
1953        .and_then(|v| v.to_str().ok())
1954    {
1955        if ct.to_lowercase().starts_with("multipart/form-data") {
1956            if let Some(boundary) =
1957                crate::utils::request_parser::RequestParser::multipart_boundary(ct)
1958            {
1959                let mp = crate::utils::request_parser::RequestParser::parse_multipart(
1960                    &body_bytes,
1961                    &boundary,
1962                );
1963                ctx.set_multipart(mp);
1964            }
1965        }
1966    }
1967    {
1968        let guard = router.read().await;
1969        for mw in &guard.global_middlewares {
1970            if let Err(e) = mw(&mut ctx).await {
1971                let resp = crate::response::error_response(&e, Arc::new(ctx.clone()));
1972                return Ok(finish_request(
1973                    &method,
1974                    &path,
1975                    request_started,
1976                    adapt_response(resp),
1977                ));
1978            }
1979        }
1980    }
1981
1982    let limit = ctx
1983        .query_param("limit")
1984        .and_then(|v| v.parse().ok())
1985        .unwrap_or(15usize);
1986    let cursor = ctx.query_param("cursor");
1987    ctx.set_pagination(cursor, limit);
1988
1989    // Reserved test-client interception — catch-all guard on
1990    // `X-Moovable-Test-Client`. Only intercepts when the header is present and
1991    // DocumentationController installed the hook.
1992    let test_hook = {
1993        let guard = router.read().await;
1994        let has_header = headers.contains_key("x-moovable-test-client")
1995            || headers.contains_key("x-tm30-test-client");
1996        if has_header {
1997            guard.test_client_handler.clone()
1998        } else {
1999            None
2000        }
2001    };
2002    if let Some(hook) = test_hook {
2003        let resp = hook(
2004            ctx.clone(),
2005            headers.clone(),
2006            method.clone(),
2007            path.clone(),
2008            body_bytes,
2009        )
2010        .await;
2011        return Ok(finish_request(
2012            &method,
2013            &path,
2014            request_started,
2015            adapt_string_response(resp, Arc::new(ctx.clone())),
2016        ));
2017    }
2018
2019    let guard = router.read().await;
2020    if let Some((entry, params)) = guard.resolve(&method, &path) {
2021        for mw in &entry.middlewares {
2022            if let Err(e) = mw(&mut ctx).await {
2023                let resp = crate::response::error_response(&e, Arc::new(ctx.clone()));
2024                return Ok(finish_request(
2025                    &method,
2026                    &path,
2027                    request_started,
2028                    adapt_response(resp),
2029                ));
2030            }
2031        }
2032        ctx.set_params(params);
2033        let handler = entry.handler.clone();
2034        let path_clone = path.clone();
2035        let headers_clone = headers.clone();
2036        let method_clone = method.clone();
2037        drop(guard);
2038        let resp = handler(
2039            ctx.clone(),
2040            headers_clone,
2041            method_clone,
2042            path_clone,
2043            body_bytes,
2044        )
2045        .await;
2046        Ok(finish_request(
2047            &method,
2048            &path,
2049            request_started,
2050            adapt_string_response(resp, Arc::new(ctx.clone())),
2051        ))
2052    } else {
2053        drop(guard);
2054        let err = ErrorResult::not_found(format!("The requested resource was not found: {path}"));
2055        let resp = crate::response::error_response(&err, Arc::new(ctx.clone()));
2056        Ok(finish_request(
2057            &method,
2058            &path,
2059            request_started,
2060            adapt_response(resp),
2061        ))
2062    }
2063}
2064
2065fn finish_request(
2066    method: &Method,
2067    path: &str,
2068    started: std::time::Instant,
2069    response: Response<String>,
2070) -> Response<String> {
2071    crate::middleware::monitoring::RequestLogger::log(
2072        path,
2073        method.as_str(),
2074        response.status().as_u16(),
2075        started.elapsed(),
2076    );
2077    response
2078}
2079
2080fn build_correlation_context(
2081    headers: &HeaderMap,
2082    query: &HashMap<String, String>,
2083    body: &[u8],
2084) -> CorrelationContext {
2085    let corr_id = headers
2086        .get("x-correlation-id")
2087        .and_then(|v| v.to_str().ok())
2088        .unwrap_or("");
2089    let flow_str = headers
2090        .get("x-correlation-flow")
2091        .and_then(|v| v.to_str().ok())
2092        .unwrap_or("ONCE");
2093    let flow = flow_str
2094        .parse()
2095        .unwrap_or(crate::logging::CorrelationFlow::Once);
2096    let ctx = if corr_id.is_empty() {
2097        CorrelationContext::new()
2098    } else {
2099        CorrelationContext::with_ids(corr_id, &hex::encode(rand::random::<[u8; 8]>()))
2100    };
2101    ctx.set_flow(flow);
2102    // Attach request-scoped data in place — handlers read body, headers, and
2103    // query parameters off the context, so handler signatures stay context-only.
2104    ctx.set_headers(headers.clone());
2105    ctx.set_query_params(query.clone());
2106    ctx.set_body(body.to_vec());
2107    let req_id = headers.get("x-request-id").and_then(|v| v.to_str().ok());
2108    if let Some(rid) = req_id {
2109        ctx.set_request_id(rid);
2110    } else {
2111        let rid = hex::encode(rand::random::<[u8; 8]>());
2112        ctx.set_request_id(&rid);
2113    }
2114    ctx
2115}
2116
2117fn adapt_response(r: Response<String>) -> Response<String> {
2118    r
2119}
2120fn adapt_string_response(
2121    mut r: Response<String>,
2122    ctx: Arc<CorrelationContext>,
2123) -> Response<String> {
2124    let headers = r.headers_mut();
2125    headers
2126        .entry("x-request-id")
2127        .or_insert(ctx.request_id().parse().unwrap());
2128    headers
2129        .entry("x-correlation-id")
2130        .or_insert(ctx.correlation_id().parse().unwrap());
2131    headers
2132        .entry("x-correlation-flow")
2133        .or_insert(ctx.flow().to_string().parse().unwrap());
2134    r
2135}
2136
2137/// Builds a complete HTTP response from a `ServiceResult<T>`, setting correlation
2138/// headers and the content type from the result's response type.
2139pub fn json_response<T: serde::Serialize + Send + Sync>(
2140    result: ServiceResult<T>,
2141    ctx: Arc<CorrelationContext>,
2142) -> Response<String> {
2143    crate::response::build_response(&result, ctx)
2144}
2145
2146/// Builds a complete HTTP response from any `TypedServiceResult`, setting correlation
2147/// headers and the content type. Serialization failures fall back to a 500 error message body.
2148pub fn typed_response(
2149    result: &dyn TypedServiceResult,
2150    ctx: Arc<CorrelationContext>,
2151) -> Response<String> {
2152    let body = result.serialize().unwrap_or_else(|e| {
2153        ErrorResult::from_error(&anyhow::anyhow!(e.to_string()), 500)
2154            .message
2155            .clone()
2156    });
2157    let mut builder = Response::builder()
2158        .status(result.code())
2159        .header("X-Request-ID", ctx.request_id())
2160        .header("X-Correlation-ID", ctx.correlation_id())
2161        .header("X-Correlation-Flow", ctx.flow().to_string());
2162    let ct = match result.response_type() {
2163        ResponseType::Json => "application/json",
2164        ResponseType::File => {
2165            let filename = body.rsplit('/').next().unwrap_or("file");
2166            builder = builder.header(
2167                "Content-Disposition",
2168                format!("attachment; filename=\"{}\"", filename),
2169            );
2170            "application/octet-stream"
2171        }
2172        ResponseType::Xml => "application/xml",
2173        ResponseType::Javascript => "application/javascript",
2174        ResponseType::Html => "text/html",
2175        ResponseType::Text => "text/plain",
2176    };
2177    builder = builder.header("Content-Type", ct);
2178    builder.body(body).unwrap()
2179}