Skip to main content

spikard_http/server/
mod.rs

1//! HTTP server implementation using Tokio and Axum
2//!
3//! This module provides the main server builder and routing infrastructure, with
4//! focused submodules for handler validation, request extraction, and lifecycle execution.
5
6pub(crate) mod fast_router;
7pub mod grpc_routing;
8pub(crate) mod handler;
9pub(crate) mod lifecycle_execution;
10pub(crate) mod request_extraction;
11
12use crate::handler_trait::{Handler, HandlerResult, RequestData};
13use crate::{CorsConfig, ServerConfig};
14use axum::Router as AxumRouter;
15use axum::body::Body;
16use axum::extract::{DefaultBodyLimit, Path};
17use axum::http::StatusCode;
18use axum::routing::{MethodRouter, get, post};
19use spikard_core::type_hints;
20use std::collections::HashMap;
21use std::net::SocketAddr;
22use std::sync::Arc;
23use std::time::Duration;
24use tokio::net::TcpListener;
25use tower_governor::governor::GovernorConfigBuilder;
26use tower_governor::key_extractor::GlobalKeyExtractor;
27use tower_http::compression::CompressionLayer;
28use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
29use tower_http::request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer};
30use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer;
31use tower_http::services::ServeDir;
32use tower_http::set_header::SetResponseHeaderLayer;
33use tower_http::timeout::TimeoutLayer;
34use tower_http::trace::TraceLayer;
35use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
36use uuid::Uuid;
37
38/// Type alias for route handler pairs
39type RouteHandlerPair = (crate::Route, Arc<dyn Handler>);
40
41#[derive(Clone)]
42struct GrpcMiddlewareState {
43    registry: Arc<crate::grpc::GrpcRegistry>,
44    config: crate::grpc::GrpcConfig,
45}
46
47/// Extract required dependencies from route metadata
48///
49/// Placeholder implementation until routes can declare dependencies via metadata.
50#[cfg(feature = "di")]
51fn extract_handler_dependencies(route: &crate::Route) -> Vec<String> {
52    route.handler_dependencies.clone()
53}
54
55/// Determines if a method typically has a request body
56fn method_expects_body(method: &crate::Method) -> bool {
57    matches!(method, crate::Method::Post | crate::Method::Put | crate::Method::Patch)
58}
59
60fn looks_like_json(body: &str) -> bool {
61    let trimmed = body.trim_start();
62    trimmed.starts_with('{') || trimmed.starts_with('[')
63}
64
65fn route_to_metadata(route: &crate::Route) -> crate::RouteMetadata {
66    #[cfg(feature = "di")]
67    {
68        crate::RouteMetadata {
69            method: route.method.to_string(),
70            path: route.path.clone(),
71            handler_name: route.handler_name.clone(),
72            request_schema: route
73                .request_validator
74                .as_ref()
75                .map(|validator| validator.schema().clone()),
76            response_schema: route
77                .response_validator
78                .as_ref()
79                .map(|validator| validator.schema().clone()),
80            parameter_schema: route
81                .parameter_validator
82                .as_ref()
83                .map(|validator| validator.schema().clone()),
84            file_params: route.file_params.clone(),
85            is_async: route.is_async,
86            cors: route.cors.clone(),
87            body_param_name: route.expects_json_body.then(|| "body".to_string()),
88            handler_dependencies: Some(route.handler_dependencies.clone()),
89            jsonrpc_method: route
90                .jsonrpc_method
91                .as_ref()
92                .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
93            static_response: None,
94            compression: route.compression.clone(),
95        }
96    }
97    #[cfg(not(feature = "di"))]
98    {
99        crate::RouteMetadata {
100            method: route.method.to_string(),
101            path: route.path.clone(),
102            handler_name: route.handler_name.clone(),
103            request_schema: route
104                .request_validator
105                .as_ref()
106                .map(|validator| validator.schema().clone()),
107            response_schema: route
108                .response_validator
109                .as_ref()
110                .map(|validator| validator.schema().clone()),
111            parameter_schema: route
112                .parameter_validator
113                .as_ref()
114                .map(|validator| validator.schema().clone()),
115            file_params: route.file_params.clone(),
116            is_async: route.is_async,
117            cors: route.cors.clone(),
118            body_param_name: route.expects_json_body.then(|| "body".to_string()),
119            jsonrpc_method: route
120                .jsonrpc_method
121                .as_ref()
122                .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
123            static_response: None,
124            compression: route.compression.clone(),
125        }
126    }
127}
128
129fn error_to_response(status: StatusCode, body: String) -> axum::response::Response {
130    let content_type = if looks_like_json(&body) {
131        "application/json"
132    } else {
133        "text/plain; charset=utf-8"
134    };
135
136    axum::response::Response::builder()
137        .status(status)
138        .header(axum::http::header::CONTENT_TYPE, content_type)
139        .body(Body::from(body))
140        .unwrap_or_else(|_| {
141            axum::response::Response::builder()
142                .status(StatusCode::INTERNAL_SERVER_ERROR)
143                .header(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")
144                .body(Body::from("Failed to build error response"))
145                .unwrap()
146        })
147}
148
149fn handler_result_to_response(result: HandlerResult) -> axum::response::Response {
150    match result {
151        Ok(response) => response,
152        Err((status, body)) => error_to_response(status, body),
153    }
154}
155
156async fn grpc_routing_middleware(
157    axum::extract::State(state): axum::extract::State<GrpcMiddlewareState>,
158    request: axum::extract::Request,
159    next: axum::middleware::Next,
160) -> axum::response::Response {
161    if grpc_routing::is_grpc_request(&request) {
162        return match grpc_routing::route_grpc_request(Arc::clone(&state.registry), &state.config, request).await {
163            Ok(response) => response,
164            Err((status, body)) => error_to_response(status, body),
165        };
166    }
167
168    next.run(request).await
169}
170
171#[inline]
172async fn call_with_optional_hooks(
173    req: axum::http::Request<Body>,
174    request_data: RequestData,
175    handler: Arc<dyn Handler>,
176    hooks: Option<Arc<crate::LifecycleHooks>>,
177) -> HandlerResult {
178    let request_data = if let Some(claims) = req.extensions().get::<crate::auth::Claims>() {
179        let mut request_data = request_data;
180        if let Ok(serialized_claims) = serde_json::to_string(claims) {
181            let mut headers = (*request_data.headers).clone();
182            headers.insert(crate::auth::INTERNAL_JWT_CLAIMS_HEADER.to_string(), serialized_claims);
183            request_data.headers = Arc::new(headers);
184        }
185        request_data
186    } else {
187        request_data
188    };
189
190    if hooks.as_ref().is_some_and(|h| !h.is_empty()) {
191        lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler, hooks).await
192    } else {
193        handler.call(req, request_data).await
194    }
195}
196
197/// Creates a method router for the given HTTP method.
198/// Handles both path parameters and non-path variants.
199fn create_method_router(
200    method: crate::Method,
201    has_path_params: bool,
202    handler: Arc<dyn Handler>,
203    hooks: Option<Arc<crate::LifecycleHooks>>,
204    include_raw_query_params: bool,
205    include_query_params_json: bool,
206) -> axum::routing::MethodRouter {
207    let expects_body = method_expects_body(&method);
208    let include_headers = handler.wants_headers();
209    let include_cookies = handler.wants_cookies();
210    let without_body_options = request_extraction::WithoutBodyExtractionOptions {
211        include_raw_query_params,
212        include_query_params_json,
213        include_headers,
214        include_cookies,
215    };
216
217    if expects_body {
218        if has_path_params {
219            let handler_clone = handler.clone();
220            let hooks_clone = hooks.clone();
221            match method {
222                crate::Method::Post => axum::routing::post(
223                    move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
224                        let (parts, body) = req.into_parts();
225                        let request_data = match request_extraction::create_request_data_with_body(
226                            &parts,
227                            path_params.0,
228                            body,
229                            include_raw_query_params,
230                            include_query_params_json,
231                            include_headers,
232                            include_cookies,
233                        )
234                        .await
235                        {
236                            Ok(data) => data,
237                            Err((status, body)) => return error_to_response(status, body),
238                        };
239                        let req = axum::extract::Request::from_parts(parts, Body::empty());
240                        handler_result_to_response(
241                            call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
242                        )
243                    },
244                ),
245                crate::Method::Put => axum::routing::put(
246                    move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
247                        let (parts, body) = req.into_parts();
248                        let request_data = match request_extraction::create_request_data_with_body(
249                            &parts,
250                            path_params.0,
251                            body,
252                            include_raw_query_params,
253                            include_query_params_json,
254                            include_headers,
255                            include_cookies,
256                        )
257                        .await
258                        {
259                            Ok(data) => data,
260                            Err((status, body)) => return error_to_response(status, body),
261                        };
262                        let req = axum::extract::Request::from_parts(parts, Body::empty());
263                        handler_result_to_response(
264                            call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
265                        )
266                    },
267                ),
268                crate::Method::Patch => axum::routing::patch(
269                    move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
270                        let (parts, body) = req.into_parts();
271                        let request_data = match request_extraction::create_request_data_with_body(
272                            &parts,
273                            path_params.0,
274                            body,
275                            include_raw_query_params,
276                            include_query_params_json,
277                            include_headers,
278                            include_cookies,
279                        )
280                        .await
281                        {
282                            Ok(data) => data,
283                            Err((status, body)) => return error_to_response(status, body),
284                        };
285                        let req = axum::extract::Request::from_parts(parts, Body::empty());
286                        handler_result_to_response(
287                            call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
288                        )
289                    },
290                ),
291                crate::Method::Get
292                | crate::Method::Delete
293                | crate::Method::Head
294                | crate::Method::Options
295                | crate::Method::Connect
296                | crate::Method::Trace => MethodRouter::new(),
297            }
298        } else {
299            let handler_clone = handler.clone();
300            let hooks_clone = hooks.clone();
301            match method {
302                crate::Method::Post => axum::routing::post(move |req: axum::extract::Request| async move {
303                    let (parts, body) = req.into_parts();
304                    let request_data = match request_extraction::create_request_data_with_body(
305                        &parts,
306                        HashMap::new(),
307                        body,
308                        include_raw_query_params,
309                        include_query_params_json,
310                        include_headers,
311                        include_cookies,
312                    )
313                    .await
314                    {
315                        Ok(data) => data,
316                        Err((status, body)) => return error_to_response(status, body),
317                    };
318                    let req = axum::extract::Request::from_parts(parts, Body::empty());
319                    handler_result_to_response(
320                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
321                    )
322                }),
323                crate::Method::Put => axum::routing::put(move |req: axum::extract::Request| async move {
324                    let (parts, body) = req.into_parts();
325                    let request_data = match request_extraction::create_request_data_with_body(
326                        &parts,
327                        HashMap::new(),
328                        body,
329                        include_raw_query_params,
330                        include_query_params_json,
331                        include_headers,
332                        include_cookies,
333                    )
334                    .await
335                    {
336                        Ok(data) => data,
337                        Err((status, body)) => return error_to_response(status, body),
338                    };
339                    let req = axum::extract::Request::from_parts(parts, Body::empty());
340                    handler_result_to_response(
341                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
342                    )
343                }),
344                crate::Method::Patch => axum::routing::patch(move |req: axum::extract::Request| async move {
345                    let (parts, body) = req.into_parts();
346                    let request_data = match request_extraction::create_request_data_with_body(
347                        &parts,
348                        HashMap::new(),
349                        body,
350                        include_raw_query_params,
351                        include_query_params_json,
352                        include_headers,
353                        include_cookies,
354                    )
355                    .await
356                    {
357                        Ok(data) => data,
358                        Err((status, body)) => return error_to_response(status, body),
359                    };
360                    let req = axum::extract::Request::from_parts(parts, Body::empty());
361                    handler_result_to_response(
362                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
363                    )
364                }),
365                crate::Method::Get
366                | crate::Method::Delete
367                | crate::Method::Head
368                | crate::Method::Options
369                | crate::Method::Connect
370                | crate::Method::Trace => MethodRouter::new(),
371            }
372        }
373    } else if has_path_params {
374        let handler_clone = handler.clone();
375        let hooks_clone = hooks.clone();
376        match method {
377            crate::Method::Get => axum::routing::get(
378                move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
379                    let request_data = request_extraction::create_request_data_without_body(
380                        req.uri(),
381                        req.method(),
382                        req.headers(),
383                        path_params.0,
384                        without_body_options,
385                    );
386                    handler_result_to_response(
387                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
388                    )
389                },
390            ),
391            crate::Method::Delete => axum::routing::delete(
392                move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
393                    let request_data = request_extraction::create_request_data_without_body(
394                        req.uri(),
395                        req.method(),
396                        req.headers(),
397                        path_params.0,
398                        without_body_options,
399                    );
400                    handler_result_to_response(
401                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
402                    )
403                },
404            ),
405            crate::Method::Head => axum::routing::head(
406                move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
407                    let request_data = request_extraction::create_request_data_without_body(
408                        req.uri(),
409                        req.method(),
410                        req.headers(),
411                        path_params.0,
412                        without_body_options,
413                    );
414                    handler_result_to_response(
415                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
416                    )
417                },
418            ),
419            crate::Method::Trace => axum::routing::trace(
420                move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
421                    let request_data = request_extraction::create_request_data_without_body(
422                        req.uri(),
423                        req.method(),
424                        req.headers(),
425                        path_params.0,
426                        without_body_options,
427                    );
428                    handler_result_to_response(
429                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
430                    )
431                },
432            ),
433            crate::Method::Options => axum::routing::options(
434                move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
435                    let request_data = request_extraction::create_request_data_without_body(
436                        req.uri(),
437                        req.method(),
438                        req.headers(),
439                        path_params.0,
440                        without_body_options,
441                    );
442                    handler_result_to_response(
443                        call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
444                    )
445                },
446            ),
447            crate::Method::Post | crate::Method::Put | crate::Method::Patch | crate::Method::Connect => {
448                MethodRouter::new()
449            }
450        }
451    } else {
452        let handler_clone = handler.clone();
453        let hooks_clone = hooks.clone();
454        match method {
455            crate::Method::Get => axum::routing::get(move |req: axum::extract::Request| async move {
456                let request_data = request_extraction::create_request_data_without_body(
457                    req.uri(),
458                    req.method(),
459                    req.headers(),
460                    HashMap::new(),
461                    without_body_options,
462                );
463                handler_result_to_response(
464                    call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
465                )
466            }),
467            crate::Method::Delete => axum::routing::delete(move |req: axum::extract::Request| async move {
468                let request_data = request_extraction::create_request_data_without_body(
469                    req.uri(),
470                    req.method(),
471                    req.headers(),
472                    HashMap::new(),
473                    without_body_options,
474                );
475                handler_result_to_response(
476                    call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
477                )
478            }),
479            crate::Method::Head => axum::routing::head(move |req: axum::extract::Request| async move {
480                let request_data = request_extraction::create_request_data_without_body(
481                    req.uri(),
482                    req.method(),
483                    req.headers(),
484                    HashMap::new(),
485                    without_body_options,
486                );
487                handler_result_to_response(
488                    call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
489                )
490            }),
491            crate::Method::Trace => axum::routing::trace(move |req: axum::extract::Request| async move {
492                let request_data = request_extraction::create_request_data_without_body(
493                    req.uri(),
494                    req.method(),
495                    req.headers(),
496                    HashMap::new(),
497                    without_body_options,
498                );
499                handler_result_to_response(
500                    call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
501                )
502            }),
503            crate::Method::Options => axum::routing::options(move |req: axum::extract::Request| async move {
504                let request_data = request_extraction::create_request_data_without_body(
505                    req.uri(),
506                    req.method(),
507                    req.headers(),
508                    HashMap::new(),
509                    without_body_options,
510                );
511                handler_result_to_response(
512                    call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
513                )
514            }),
515            crate::Method::Post | crate::Method::Put | crate::Method::Patch | crate::Method::Connect => {
516                MethodRouter::new()
517            }
518        }
519    }
520}
521
522/// Request ID generator using UUIDs
523#[derive(Clone, Default)]
524struct MakeRequestUuid;
525
526impl MakeRequestId for MakeRequestUuid {
527    fn make_request_id<B>(&mut self, _request: &axum::http::Request<B>) -> Option<RequestId> {
528        let id = Uuid::new_v4().to_string().parse().ok()?;
529        Some(RequestId::new(id))
530    }
531}
532
533/// Graceful shutdown signal handler
534///
535/// Coverage: Tested via integration tests (Unix signal handling not easily unit testable)
536#[cfg(not(tarpaulin_include))]
537async fn shutdown_signal() {
538    let ctrl_c = async {
539        tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
540    };
541
542    #[cfg(unix)]
543    let terminate = async {
544        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
545            .expect("failed to install signal handler")
546            .recv()
547            .await;
548    };
549
550    #[cfg(not(unix))]
551    let terminate = std::future::pending::<()>();
552
553    tokio::select! {
554        _ = ctrl_c => {
555            tracing::info!("Received SIGINT (Ctrl+C), starting graceful shutdown");
556        },
557        _ = terminate => {
558            tracing::info!("Received SIGTERM, starting graceful shutdown");
559        },
560    }
561}
562
563/// Build an Axum router from routes and foreign handlers
564#[cfg(not(feature = "di"))]
565pub fn build_router_with_handlers(
566    routes: Vec<(crate::Route, Arc<dyn Handler>)>,
567    hooks: Option<Arc<crate::LifecycleHooks>>,
568) -> Result<AxumRouter, String> {
569    build_router_with_handlers_inner(routes, hooks, None, true)
570}
571
572/// Build an Axum router from routes and foreign handlers with optional DI container
573#[cfg(feature = "di")]
574pub fn build_router_with_handlers(
575    routes: Vec<(crate::Route, Arc<dyn Handler>)>,
576    hooks: Option<Arc<crate::LifecycleHooks>>,
577    di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
578) -> Result<AxumRouter, String> {
579    build_router_with_handlers_inner(routes, hooks, di_container, true)
580}
581
582fn build_router_with_handlers_inner(
583    routes: Vec<(crate::Route, Arc<dyn Handler>)>,
584    hooks: Option<Arc<crate::LifecycleHooks>>,
585    #[cfg(feature = "di")] di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
586    #[cfg(not(feature = "di"))] _di_container: Option<()>,
587    enable_http_trace: bool,
588) -> Result<AxumRouter, String> {
589    let mut app = AxumRouter::new();
590    let mut fast_router = fast_router::FastRouter::new();
591
592    let mut routes_by_path: HashMap<String, Vec<RouteHandlerPair>> = HashMap::new();
593    for (route, handler) in routes {
594        routes_by_path
595            .entry(route.path.clone())
596            .or_default()
597            .push((route, handler));
598    }
599
600    let mut sorted_paths: Vec<String> = routes_by_path.keys().cloned().collect();
601    sorted_paths.sort();
602
603    for path in sorted_paths {
604        let route_handlers = routes_by_path
605            .remove(&path)
606            .ok_or_else(|| format!("Missing handlers for path '{}'", path))?;
607
608        type RouteEntry = (crate::Route, Arc<dyn Handler>, Option<crate::StaticResponse>);
609        let mut handlers_by_method: HashMap<crate::Method, RouteEntry> = HashMap::new();
610        for (route, handler) in route_handlers {
611            #[cfg(feature = "di")]
612            let handler = if let Some(ref container) = di_container {
613                let required_deps = extract_handler_dependencies(&route);
614                if !required_deps.is_empty() {
615                    Arc::new(crate::di_handler::DependencyInjectingHandler::new(
616                        handler,
617                        Arc::clone(container),
618                        required_deps,
619                    )) as Arc<dyn Handler>
620                } else {
621                    handler
622                }
623            } else {
624                handler
625            };
626
627            // Check for static_response before wrapping in ValidatingHandler,
628            // since ValidatingHandler doesn't delegate static_response().
629            let static_resp = handler.static_response();
630            let validating_handler = Arc::new(handler::ValidatingHandler::new(handler, &route));
631            handlers_by_method.insert(route.method.clone(), (route, validating_handler, static_resp));
632        }
633
634        let cors_config: Option<CorsConfig> = handlers_by_method
635            .values()
636            .find_map(|(route, _, _)| route.cors.as_ref())
637            .cloned();
638
639        let has_options_handler = handlers_by_method.keys().any(|m| m.as_str() == "OPTIONS");
640
641        let mut combined_router: Option<MethodRouter> = None;
642        let has_path_params = path.contains('{');
643
644        for (_method, (route, handler, static_resp_opt)) in handlers_by_method {
645            let method = route.method.clone();
646
647            // Fast-path: if the handler declares a static response, bypass the
648            // entire middleware pipeline (validation, hooks, request extraction).
649            //
650            // NOTE: static routes also bypass CORS handling, content-type
651            // validation, and HTTP tracing. If CORS headers are needed they
652            // must be included in `StaticResponse.headers` explicitly.
653            //
654            // Non-parameterized paths are also inserted into the FastRouter for
655            // O(1) HashMap-based lookup as the outermost middleware. The Axum
656            // route below serves as fallback — it handles the same request if
657            // the FastRouter layer is somehow bypassed and also covers
658            // parameterized static routes that cannot go into the FastRouter.
659            if let Some(static_resp) = static_resp_opt {
660                let resp_status = static_resp.status;
661
662                if !has_path_params {
663                    let axum_path_for_fast = spikard_core::type_hints::strip_type_hints(&path);
664                    let http_method: axum::http::Method = route.method.as_str().parse().map_err(|_| {
665                        format!(
666                            "Invalid HTTP method '{}' for static route {}",
667                            route.method.as_str(),
668                            path
669                        )
670                    })?;
671                    fast_router.insert(http_method, &axum_path_for_fast, &static_resp);
672                }
673
674                // Axum fallback handler — uses the same `to_response()` as the
675                // FastRouter and `StaticResponseHandler::call`.
676                let static_handler = move || {
677                    let resp = static_resp.to_response();
678                    async move { resp }
679                };
680
681                let method_router: MethodRouter = match method {
682                    crate::Method::Get => axum::routing::get(static_handler),
683                    crate::Method::Post => axum::routing::post(static_handler),
684                    crate::Method::Put => axum::routing::put(static_handler),
685                    crate::Method::Patch => axum::routing::patch(static_handler),
686                    crate::Method::Delete => axum::routing::delete(static_handler),
687                    crate::Method::Head => axum::routing::head(static_handler),
688                    crate::Method::Options => axum::routing::options(static_handler),
689                    crate::Method::Connect => MethodRouter::new(),
690                    crate::Method::Trace => axum::routing::trace(static_handler),
691                };
692
693                combined_router = Some(match combined_router {
694                    None => method_router,
695                    Some(existing) => existing.merge(method_router),
696                });
697
698                tracing::info!(
699                    "Registered static route: {} {} (status {})",
700                    route.method.as_str(),
701                    path,
702                    resp_status,
703                );
704                continue;
705            }
706
707            let method_router: MethodRouter = match method {
708                crate::Method::Options => {
709                    if let Some(ref cors_cfg) = route.cors {
710                        let cors_config = cors_cfg.clone();
711                        axum::routing::options(move |req: axum::extract::Request| async move {
712                            crate::cors::handle_preflight(req.headers(), &cors_config).map_err(|e| *e)
713                        })
714                    } else {
715                        let include_raw_query_params = route.parameter_validator.is_some();
716                        let include_query_params_json = !handler.prefers_parameter_extraction();
717                        create_method_router(
718                            method,
719                            has_path_params,
720                            handler,
721                            hooks.clone(),
722                            include_raw_query_params,
723                            include_query_params_json,
724                        )
725                    }
726                }
727                method => {
728                    let include_raw_query_params = route.parameter_validator.is_some();
729                    let include_query_params_json = !handler.prefers_parameter_extraction();
730                    create_method_router(
731                        method,
732                        has_path_params,
733                        handler,
734                        hooks.clone(),
735                        include_raw_query_params,
736                        include_query_params_json,
737                    )
738                }
739            };
740
741            // Only apply content-type validation middleware for methods that
742            // carry a request body. GET/DELETE/HEAD/OPTIONS/TRACE never have
743            // meaningful content-type headers, so the middleware just adds
744            // into_parts/from_parts overhead for those methods.
745            let method_router = if matches!(
746                route.method,
747                crate::Method::Post | crate::Method::Put | crate::Method::Patch
748            ) && (route.expects_json_body || route.file_params.is_some())
749            {
750                method_router.layer(axum::middleware::from_fn_with_state(
751                    crate::middleware::RouteInfo {
752                        expects_json_body: route.expects_json_body,
753                    },
754                    crate::middleware::validate_content_type_middleware,
755                ))
756            } else {
757                method_router
758            };
759
760            combined_router = Some(match combined_router {
761                None => method_router,
762                Some(existing) => existing.merge(method_router),
763            });
764
765            tracing::info!("Registered route: {} {}", route.method.as_str(), path);
766        }
767
768        if let Some(ref cors_cfg) = cors_config
769            && !has_options_handler
770        {
771            let cors_config_clone: CorsConfig = cors_cfg.clone();
772            let options_router = axum::routing::options(move |req: axum::extract::Request| async move {
773                crate::cors::handle_preflight(req.headers(), &cors_config_clone).map_err(|e| *e)
774            });
775
776            combined_router = Some(match combined_router {
777                None => options_router,
778                Some(existing) => existing.merge(options_router),
779            });
780
781            tracing::info!("Auto-generated OPTIONS handler for CORS preflight: {}", path);
782        }
783
784        if let Some(router) = combined_router {
785            let mut axum_path = type_hints::strip_type_hints(&path);
786            if !axum_path.starts_with('/') {
787                axum_path = format!("/{}", axum_path);
788            }
789            // Also register the path with a trailing slash so that requests
790            // to e.g. GET /app/ are handled by the same route as GET /app.
791            // Axum does not perform trailing-slash normalization automatically.
792            // Skip catch-all paths (containing `{*`) — appending `/` to those
793            // creates an invalid route because catch-all parameters must be at
794            // the very end of a pattern.
795            if axum_path != "/" && !axum_path.ends_with('/') && !axum_path.contains("{*") {
796                app = app.route(&format!("{}/", axum_path), router.clone());
797            }
798            app = app.route(&axum_path, router);
799        }
800    }
801
802    if enable_http_trace {
803        app = app.layer(TraceLayer::new_for_http());
804    }
805
806    // Install the fast-router as the outermost middleware so that static-response
807    // routes are served without entering the Axum routing tree at all.
808    if fast_router.has_routes() {
809        let fast_router = Arc::new(fast_router);
810        app = app.layer(axum::middleware::from_fn(
811            move |req: axum::extract::Request, next: axum::middleware::Next| {
812                let fast_router = Arc::clone(&fast_router);
813                async move {
814                    if let Some(resp) = fast_router.lookup(req.method(), req.uri().path()) {
815                        return resp;
816                    }
817                    next.run(req).await
818                }
819            },
820        ));
821    }
822
823    Ok(app)
824}
825
826/// Build router with handlers and apply middleware based on config
827pub fn build_router_with_handlers_and_config(
828    routes: Vec<RouteHandlerPair>,
829    config: ServerConfig,
830    route_metadata: Vec<crate::RouteMetadata>,
831) -> Result<AxumRouter, String> {
832    build_router_with_handlers_and_config_and_grpc(routes, config, route_metadata, None)
833}
834
835/// Build router with handlers, config, and an optional gRPC service registry.
836pub(crate) fn build_router_with_handlers_and_config_and_grpc(
837    routes: Vec<RouteHandlerPair>,
838    config: ServerConfig,
839    route_metadata: Vec<crate::RouteMetadata>,
840    grpc_registry: Option<Arc<crate::grpc::GrpcRegistry>>,
841) -> Result<AxumRouter, String> {
842    #[cfg(all(feature = "di", debug_assertions))]
843    if let Some(di_container) = config.di_container.as_ref() {
844        eprintln!(
845            "[spikard-di] build_router: di_container has keys: {:?}",
846            di_container.keys()
847        );
848    } else {
849        eprintln!("[spikard-di] build_router: di_container is None");
850    }
851    let hooks = config.lifecycle_hooks.clone();
852
853    let jsonrpc_registry = if let Some(ref jsonrpc_config) = config.jsonrpc {
854        if jsonrpc_config.enabled {
855            let registry = Arc::new(crate::jsonrpc::JsonRpcMethodRegistry::new());
856
857            for (route, handler) in &routes {
858                if let Some(ref jsonrpc_info) = route.jsonrpc_method {
859                    let method_name = jsonrpc_info.method_name.clone();
860
861                    let metadata = crate::jsonrpc::MethodMetadata::new(&method_name)
862                        .with_params_schema(jsonrpc_info.params_schema.clone().unwrap_or(serde_json::json!({})))
863                        .with_result_schema(jsonrpc_info.result_schema.clone().unwrap_or(serde_json::json!({})));
864
865                    let metadata = if let Some(ref description) = jsonrpc_info.description {
866                        metadata.with_description(description.clone())
867                    } else {
868                        metadata
869                    };
870
871                    let metadata = if jsonrpc_info.deprecated {
872                        metadata.mark_deprecated()
873                    } else {
874                        metadata
875                    };
876
877                    let mut metadata = metadata;
878                    for tag in &jsonrpc_info.tags {
879                        metadata = metadata.with_tag(tag.clone());
880                    }
881
882                    if let Err(e) = registry.register(&method_name, Arc::clone(handler), metadata) {
883                        tracing::warn!(
884                            "Failed to register JSON-RPC method '{}' for route {}: {}",
885                            method_name,
886                            route.path,
887                            e
888                        );
889                    } else {
890                        tracing::debug!(
891                            "Registered JSON-RPC method '{}' for route {} {} (handler: {})",
892                            method_name,
893                            route.method,
894                            route.path,
895                            route.handler_name
896                        );
897                    }
898                }
899            }
900
901            Some(registry)
902        } else {
903            None
904        }
905    } else {
906        None
907    };
908
909    #[cfg(feature = "di")]
910    let mut app =
911        build_router_with_handlers_inner(routes, hooks, config.di_container.clone(), config.enable_http_trace)?;
912    #[cfg(not(feature = "di"))]
913    let mut app = build_router_with_handlers_inner(routes, hooks, None, config.enable_http_trace)?;
914
915    if let (Some(grpc_config), Some(registry)) = (config.grpc.clone(), grpc_registry)
916        && !registry.is_empty()
917    {
918        let state = GrpcMiddlewareState {
919            registry,
920            config: grpc_config,
921        };
922        app = app.layer(axum::middleware::from_fn_with_state(state, grpc_routing_middleware));
923    }
924
925    // Only add the sensitive-header redaction layer when auth middleware is
926    // configured — without auth there is nothing to redact, and the layer
927    // otherwise adds per-request overhead.
928    if config.jwt_auth.is_some() || config.api_key_auth.is_some() {
929        app = app.layer(SetSensitiveRequestHeadersLayer::new([
930            axum::http::header::AUTHORIZATION,
931            axum::http::header::COOKIE,
932        ]));
933    }
934
935    if let Some(ref compression) = config.compression {
936        let mut compression_layer = CompressionLayer::new();
937        if !compression.gzip {
938            compression_layer = compression_layer.gzip(false);
939        }
940        if !compression.brotli {
941            compression_layer = compression_layer.br(false);
942        }
943
944        let min_threshold = compression.min_size.min(u64::MAX as usize) as u64;
945        let predicate = SizeAbove::new(min_threshold)
946            .and(NotForContentType::GRPC)
947            .and(NotForContentType::IMAGES)
948            .and(NotForContentType::SSE);
949        let compression_layer = compression_layer.compress_when(predicate);
950
951        app = app.layer(compression_layer);
952    }
953
954    if let Some(ref rate_limit) = config.rate_limit {
955        if rate_limit.ip_based {
956            let governor_conf = Arc::new(
957                GovernorConfigBuilder::default()
958                    .per_second(rate_limit.per_second)
959                    .burst_size(rate_limit.burst)
960                    .finish()
961                    .ok_or_else(|| "Failed to create rate limiter".to_string())?,
962            );
963            app = app.layer(tower_governor::GovernorLayer::new(governor_conf));
964        } else {
965            let governor_conf = Arc::new(
966                GovernorConfigBuilder::default()
967                    .per_second(rate_limit.per_second)
968                    .burst_size(rate_limit.burst)
969                    .key_extractor(GlobalKeyExtractor)
970                    .finish()
971                    .ok_or_else(|| "Failed to create rate limiter".to_string())?,
972            );
973            app = app.layer(tower_governor::GovernorLayer::new(governor_conf));
974        }
975    }
976
977    if let Some(ref jwt_config) = config.jwt_auth {
978        let jwt_config_clone = jwt_config.clone();
979        app = app.layer(axum::middleware::from_fn(move |headers, req, next| {
980            crate::auth::jwt_auth_middleware(jwt_config_clone.clone(), headers, req, next)
981        }));
982    }
983
984    if let Some(ref api_key_config) = config.api_key_auth {
985        let api_key_config_clone = api_key_config.clone();
986        app = app.layer(axum::middleware::from_fn(move |headers, req, next| {
987            crate::auth::api_key_auth_middleware(api_key_config_clone.clone(), headers, req, next)
988        }));
989    }
990
991    if let Some(timeout_secs) = config.request_timeout {
992        app = app.layer(TimeoutLayer::with_status_code(
993            StatusCode::REQUEST_TIMEOUT,
994            Duration::from_secs(timeout_secs),
995        ));
996    }
997
998    if config.enable_request_id {
999        app = app
1000            .layer(PropagateRequestIdLayer::x_request_id())
1001            .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid));
1002    }
1003
1004    // Only add the body-limit layer when a limit is explicitly configured.
1005    // Omitting the layer entirely (instead of `disable()`) avoids a no-op
1006    // middleware dispatch on every request.
1007    if let Some(max_size) = config.max_body_size {
1008        app = app.layer(DefaultBodyLimit::max(max_size));
1009    }
1010
1011    for static_config in &config.static_files {
1012        let mut serve_dir = ServeDir::new(&static_config.directory);
1013        if static_config.index_file {
1014            serve_dir = serve_dir.append_index_html_on_directories(true);
1015        }
1016
1017        let mut static_router = AxumRouter::new().fallback_service(serve_dir);
1018        if let Some(ref cache_control) = static_config.cache_control {
1019            let header_value = axum::http::HeaderValue::from_str(cache_control)
1020                .map_err(|e| format!("Invalid cache-control header: {}", e))?;
1021            static_router = static_router.layer(SetResponseHeaderLayer::overriding(
1022                axum::http::header::CACHE_CONTROL,
1023                header_value,
1024            ));
1025        }
1026
1027        app = app.nest_service(&static_config.route_prefix, static_router);
1028
1029        tracing::info!(
1030            "Serving static files from '{}' at '{}'",
1031            static_config.directory,
1032            static_config.route_prefix
1033        );
1034    }
1035
1036    if let Some(ref openapi_config) = config.openapi
1037        && openapi_config.enabled
1038    {
1039        use axum::response::{Html, Json};
1040
1041        let schema_registry = crate::SchemaRegistry::new();
1042        let openapi_spec =
1043            crate::openapi::generate_openapi_spec(&route_metadata, openapi_config, &schema_registry, Some(&config))
1044                .map_err(|e| format!("Failed to generate OpenAPI spec: {}", e))?;
1045
1046        let spec_json =
1047            serde_json::to_string(&openapi_spec).map_err(|e| format!("Failed to serialize OpenAPI spec: {}", e))?;
1048        let spec_value = serde_json::from_str::<serde_json::Value>(&spec_json)
1049            .map_err(|e| format!("Failed to parse OpenAPI spec: {}", e))?;
1050
1051        let openapi_json_path = openapi_config.openapi_json_path.clone();
1052        app = app.route(&openapi_json_path, get(move || async move { Json(spec_value) }));
1053
1054        let swagger_html = format!(
1055            r#"<!DOCTYPE html>
1056<html>
1057<head>
1058    <title>Swagger UI</title>
1059    <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
1060</head>
1061<body>
1062    <div id="swagger-ui"></div>
1063    <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
1064    <script>
1065        SwaggerUIBundle({{
1066            url: '{}',
1067            dom_id: '#swagger-ui',
1068        }});
1069    </script>
1070</body>
1071</html>"#,
1072            openapi_json_path
1073        );
1074        let swagger_ui_path = openapi_config.swagger_ui_path.clone();
1075        app = app.route(&swagger_ui_path, get(move || async move { Html(swagger_html) }));
1076
1077        let redoc_html = format!(
1078            r#"<!DOCTYPE html>
1079<html>
1080<head>
1081    <title>Redoc</title>
1082</head>
1083<body>
1084    <redoc spec-url='{}'></redoc>
1085    <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
1086</body>
1087</html>"#,
1088            openapi_json_path
1089        );
1090        let redoc_path = openapi_config.redoc_path.clone();
1091        app = app.route(&redoc_path, get(move || async move { Html(redoc_html) }));
1092
1093        tracing::info!("OpenAPI documentation enabled at {}", openapi_json_path);
1094    }
1095
1096    if let Some(ref asyncapi_config) = config.asyncapi
1097        && asyncapi_config.enabled
1098    {
1099        use crate::asyncapi::{AsyncApiState, handle_asyncapi_json, handle_asyncapi_parse, handle_asyncapi_validate};
1100
1101        let registered_spec = asyncapi_config.spec.as_ref().map(|s| Arc::new(s.clone()));
1102        let state = AsyncApiState { registered_spec };
1103
1104        app = app
1105            .route("/asyncapi/parse", post(handle_asyncapi_parse))
1106            .route("/asyncapi/validate", post(handle_asyncapi_validate))
1107            .route("/asyncapi.json", get(handle_asyncapi_json).with_state(state));
1108
1109        tracing::info!("AsyncAPI endpoints enabled: POST /asyncapi/parse, POST /asyncapi/validate, GET /asyncapi.json");
1110    }
1111
1112    if let Some(ref jsonrpc_config) = config.jsonrpc
1113        && jsonrpc_config.enabled
1114        && let Some(registry) = jsonrpc_registry
1115    {
1116        use axum::response::Json;
1117
1118        let jsonrpc_router = Arc::new(crate::jsonrpc::JsonRpcRouter::new(
1119            Arc::clone(&registry),
1120            jsonrpc_config.enable_batch,
1121            jsonrpc_config.max_batch_size,
1122        ));
1123
1124        let state = Arc::new(crate::jsonrpc::JsonRpcState { router: jsonrpc_router });
1125
1126        let endpoint_path = jsonrpc_config.endpoint_path.clone();
1127        app = app.route(&endpoint_path, post(crate::jsonrpc::handle_jsonrpc).with_state(state));
1128        let openrpc_spec = crate::jsonrpc::generate_openrpc_spec(&registry, &config)?;
1129        app = app.route("/openrpc.json", get(move || async move { Json(openrpc_spec) }));
1130
1131        tracing::info!("JSON-RPC endpoint enabled at {}", endpoint_path);
1132        tracing::info!("OpenRPC documentation enabled at /openrpc.json");
1133    }
1134
1135    Ok(app)
1136}
1137
1138/// HTTP Server
1139pub struct Server;
1140
1141impl Server {
1142    /// Build a server router with runtime handlers.
1143    ///
1144    /// Build router with trait-based handlers
1145    /// Routes are grouped by path before registration to support multiple HTTP methods
1146    /// for the same path (e.g., GET /data and POST /data). Axum requires that all methods
1147    /// for a path be merged into a single MethodRouter before calling `.route()`.
1148    pub fn with_handlers(
1149        config: ServerConfig,
1150        routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1151    ) -> Result<AxumRouter, String> {
1152        let metadata: Vec<crate::RouteMetadata> = routes.iter().map(|(route, _)| route_to_metadata(route)).collect();
1153        build_router_with_handlers_and_config(routes, config, metadata)
1154    }
1155
1156    /// Build a server router with runtime handlers and explicit metadata for OpenAPI.
1157    pub fn with_handlers_and_metadata(
1158        config: ServerConfig,
1159        routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1160        metadata: Vec<crate::RouteMetadata>,
1161    ) -> Result<AxumRouter, String> {
1162        build_router_with_handlers_and_config(routes, config, metadata)
1163    }
1164
1165    /// Run the server with the Axum router and config
1166    ///
1167    /// Coverage: Production-only, tested via integration tests
1168    #[cfg(not(tarpaulin_include))]
1169    pub async fn run_with_config(app: AxumRouter, config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
1170        let addr = format!("{}:{}", config.host, config.port);
1171        let socket_addr: SocketAddr = addr.parse()?;
1172        let listener = TcpListener::bind(socket_addr).await?;
1173
1174        tracing::info!("Listening on http://{}", socket_addr);
1175
1176        if config.graceful_shutdown {
1177            axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
1178                .with_graceful_shutdown(shutdown_signal())
1179                .await?;
1180        } else {
1181            axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
1182        }
1183
1184        Ok(())
1185    }
1186
1187    /// Initialize logging
1188    ///
1189    /// This function is idempotent - calling it multiple times is safe.
1190    /// It uses `try_init()` instead of `init()` to avoid panics when the
1191    /// global subscriber has already been set (e.g., by a language runtime
1192    /// or a previous call).
1193    pub fn init_logging() {
1194        let _ = tracing_subscriber::registry()
1195            .with(
1196                tracing_subscriber::EnvFilter::try_from_default_env()
1197                    .unwrap_or_else(|_| "spikard=info,tower_http=info".into()),
1198            )
1199            .with(tracing_subscriber::fmt::layer())
1200            .try_init();
1201    }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207    use std::pin::Pin;
1208    use std::sync::Arc;
1209
1210    struct TestHandler;
1211
1212    impl Handler for TestHandler {
1213        fn call(
1214            &self,
1215            _request: axum::http::Request<Body>,
1216            _request_data: crate::handler_trait::RequestData,
1217        ) -> Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>> {
1218            Box::pin(async { Ok(axum::http::Response::builder().status(200).body(Body::empty()).unwrap()) })
1219        }
1220    }
1221
1222    fn build_test_route(path: &str, method: &str, handler_name: &str, expects_json_body: bool) -> crate::Route {
1223        use std::str::FromStr;
1224        crate::Route {
1225            path: path.to_string(),
1226            method: spikard_core::Method::from_str(method).expect("valid method"),
1227            handler_name: handler_name.to_string(),
1228            expects_json_body,
1229            cors: None,
1230            is_async: true,
1231            file_params: None,
1232            request_validator: None,
1233            response_validator: None,
1234            parameter_validator: None,
1235            jsonrpc_method: None,
1236            compression: None,
1237            #[cfg(feature = "di")]
1238            handler_dependencies: vec![],
1239        }
1240    }
1241
1242    fn build_test_route_with_cors(
1243        path: &str,
1244        method: &str,
1245        handler_name: &str,
1246        expects_json_body: bool,
1247        cors: crate::CorsConfig,
1248    ) -> crate::Route {
1249        use std::str::FromStr;
1250        crate::Route {
1251            path: path.to_string(),
1252            method: spikard_core::Method::from_str(method).expect("valid method"),
1253            handler_name: handler_name.to_string(),
1254            expects_json_body,
1255            cors: Some(cors),
1256            is_async: true,
1257            file_params: None,
1258            request_validator: None,
1259            response_validator: None,
1260            parameter_validator: None,
1261            jsonrpc_method: None,
1262            compression: None,
1263            #[cfg(feature = "di")]
1264            handler_dependencies: vec![],
1265        }
1266    }
1267
1268    #[test]
1269    fn test_method_expects_body_post() {
1270        assert!(method_expects_body(&crate::Method::Post));
1271    }
1272
1273    #[test]
1274    fn test_method_expects_body_put() {
1275        assert!(method_expects_body(&crate::Method::Put));
1276    }
1277
1278    #[test]
1279    fn test_method_expects_body_patch() {
1280        assert!(method_expects_body(&crate::Method::Patch));
1281    }
1282
1283    #[test]
1284    fn test_method_expects_body_get() {
1285        assert!(!method_expects_body(&crate::Method::Get));
1286    }
1287
1288    #[test]
1289    fn test_method_expects_body_delete() {
1290        assert!(!method_expects_body(&crate::Method::Delete));
1291    }
1292
1293    #[test]
1294    fn test_method_expects_body_head() {
1295        assert!(!method_expects_body(&crate::Method::Head));
1296    }
1297
1298    #[test]
1299    fn test_method_expects_body_options() {
1300        assert!(!method_expects_body(&crate::Method::Options));
1301    }
1302
1303    #[test]
1304    fn test_method_expects_body_trace() {
1305        assert!(!method_expects_body(&crate::Method::Trace));
1306    }
1307
1308    #[test]
1309    fn test_make_request_uuid_generates_valid_uuid() {
1310        let mut maker = MakeRequestUuid;
1311        let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1312
1313        let id = maker.make_request_id(&request);
1314
1315        assert!(id.is_some());
1316        let id_val = id.unwrap();
1317        let id_str = id_val.header_value().to_str().expect("valid utf8");
1318        assert!(!id_str.is_empty());
1319        assert!(Uuid::parse_str(id_str).is_ok());
1320    }
1321
1322    #[test]
1323    fn test_make_request_uuid_unique_per_call() {
1324        let mut maker = MakeRequestUuid;
1325        let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1326
1327        let id1 = maker.make_request_id(&request).unwrap();
1328        let id2 = maker.make_request_id(&request).unwrap();
1329
1330        let id1_str = id1.header_value().to_str().expect("valid utf8");
1331        let id2_str = id2.header_value().to_str().expect("valid utf8");
1332        assert_ne!(id1_str, id2_str);
1333    }
1334
1335    #[test]
1336    fn test_make_request_uuid_v4_format() {
1337        let mut maker = MakeRequestUuid;
1338        let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1339
1340        let id = maker.make_request_id(&request).unwrap();
1341        let id_str = id.header_value().to_str().expect("valid utf8");
1342
1343        let uuid = Uuid::parse_str(id_str).expect("valid UUID");
1344        assert_eq!(uuid.get_version(), Some(uuid::Version::Random));
1345    }
1346
1347    #[test]
1348    fn test_make_request_uuid_multiple_independent_makers() {
1349        let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1350
1351        let id1 = {
1352            let mut maker1 = MakeRequestUuid;
1353            maker1.make_request_id(&request).unwrap()
1354        };
1355        let id2 = {
1356            let mut maker2 = MakeRequestUuid;
1357            maker2.make_request_id(&request).unwrap()
1358        };
1359
1360        let id1_str = id1.header_value().to_str().expect("valid utf8");
1361        let id2_str = id2.header_value().to_str().expect("valid utf8");
1362        assert_ne!(id1_str, id2_str);
1363    }
1364
1365    #[test]
1366    fn test_make_request_uuid_clone_independence() {
1367        let mut maker1 = MakeRequestUuid;
1368        let mut maker2 = maker1.clone();
1369        let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1370
1371        let id1 = maker1.make_request_id(&request).unwrap();
1372        let id2 = maker2.make_request_id(&request).unwrap();
1373
1374        let id1_str = id1.header_value().to_str().expect("valid utf8");
1375        let id2_str = id2.header_value().to_str().expect("valid utf8");
1376        assert_ne!(id1_str, id2_str);
1377    }
1378
1379    #[test]
1380    fn test_server_config_default_values() {
1381        let config = ServerConfig::default();
1382
1383        assert_eq!(config.host, "127.0.0.1");
1384        assert_eq!(config.port, 8000);
1385        assert_eq!(config.workers, 1);
1386        assert!(!config.enable_request_id);
1387        assert!(config.max_body_size.is_some());
1388        assert!(config.request_timeout.is_none());
1389        assert!(config.graceful_shutdown);
1390    }
1391
1392    #[test]
1393    fn test_server_config_builder_pattern() {
1394        let config = ServerConfig::builder().port(9000).host("0.0.0.0".to_string()).build();
1395
1396        assert_eq!(config.port, 9000);
1397        assert_eq!(config.host, "0.0.0.0");
1398    }
1399
1400    #[cfg(feature = "di")]
1401    fn build_router_for_tests(
1402        routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1403        hooks: Option<Arc<crate::LifecycleHooks>>,
1404    ) -> Result<AxumRouter, String> {
1405        build_router_with_handlers(routes, hooks, None)
1406    }
1407
1408    #[cfg(not(feature = "di"))]
1409    fn build_router_for_tests(
1410        routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1411        hooks: Option<Arc<crate::LifecycleHooks>>,
1412    ) -> Result<AxumRouter, String> {
1413        build_router_with_handlers(routes, hooks)
1414    }
1415
1416    #[test]
1417    fn test_route_registry_empty_routes() {
1418        let routes: Vec<(crate::Route, Arc<dyn Handler>)> = vec![];
1419        let _result = build_router_for_tests(routes, None);
1420    }
1421
1422    #[test]
1423    fn test_route_registry_single_route() {
1424        let route = build_test_route("/test", "GET", "test_handler", false);
1425
1426        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1427        let routes = vec![(route, handler)];
1428
1429        let result = build_router_for_tests(routes, None);
1430        assert!(result.is_ok());
1431    }
1432
1433    #[test]
1434    fn test_route_path_normalization_without_leading_slash() {
1435        let route = build_test_route("api/users", "GET", "list_users", false);
1436
1437        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1438        let routes = vec![(route, handler)];
1439
1440        let result = build_router_for_tests(routes, None);
1441        assert!(result.is_ok());
1442    }
1443
1444    #[test]
1445    fn test_route_path_normalization_with_leading_slash() {
1446        let route = build_test_route("/api/users", "GET", "list_users", false);
1447
1448        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1449        let routes = vec![(route, handler)];
1450
1451        let result = build_router_for_tests(routes, None);
1452        assert!(result.is_ok());
1453    }
1454
1455    #[test]
1456    fn test_multiple_routes_same_path_different_methods() {
1457        let get_route = build_test_route("/users", "GET", "list_users", false);
1458        let post_route = build_test_route("/users", "POST", "create_user", true);
1459
1460        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1461        let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1462
1463        let result = build_router_for_tests(routes, None);
1464        assert!(result.is_ok());
1465    }
1466
1467    #[test]
1468    fn test_multiple_different_routes() {
1469        let users_route = build_test_route("/users", "GET", "list_users", false);
1470        let posts_route = build_test_route("/posts", "GET", "list_posts", false);
1471
1472        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1473        let routes = vec![(users_route, handler.clone()), (posts_route, handler)];
1474
1475        let result = build_router_for_tests(routes, None);
1476        assert!(result.is_ok());
1477    }
1478
1479    #[test]
1480    fn test_route_with_single_path_parameter() {
1481        let route = build_test_route("/users/{id}", "GET", "get_user", false);
1482
1483        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1484        let routes = vec![(route, handler)];
1485
1486        let result = build_router_for_tests(routes, None);
1487        assert!(result.is_ok());
1488    }
1489
1490    #[test]
1491    fn test_route_with_multiple_path_parameters() {
1492        let route = build_test_route("/users/{user_id}/posts/{post_id}", "GET", "get_user_post", false);
1493
1494        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1495        let routes = vec![(route, handler)];
1496
1497        let result = build_router_for_tests(routes, None);
1498        assert!(result.is_ok());
1499    }
1500
1501    #[test]
1502    fn test_route_with_path_parameter_post_with_body() {
1503        let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1504
1505        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1506        let routes = vec![(route, handler)];
1507
1508        let result = build_router_for_tests(routes, None);
1509        assert!(result.is_ok());
1510    }
1511
1512    #[test]
1513    fn test_route_with_path_parameter_delete() {
1514        let route = build_test_route("/users/{id}", "DELETE", "delete_user", false);
1515
1516        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1517        let routes = vec![(route, handler)];
1518
1519        let result = build_router_for_tests(routes, None);
1520        assert!(result.is_ok());
1521    }
1522
1523    #[test]
1524    fn test_route_post_method_with_body() {
1525        let route = build_test_route("/users", "POST", "create_user", true);
1526
1527        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1528        let routes = vec![(route, handler)];
1529
1530        let result = build_router_for_tests(routes, None);
1531        assert!(result.is_ok());
1532    }
1533
1534    #[test]
1535    fn test_route_put_method_with_body() {
1536        let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1537
1538        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1539        let routes = vec![(route, handler)];
1540
1541        let result = build_router_for_tests(routes, None);
1542        assert!(result.is_ok());
1543    }
1544
1545    #[test]
1546    fn test_route_patch_method_with_body() {
1547        let route = build_test_route("/users/{id}", "PATCH", "patch_user", true);
1548
1549        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1550        let routes = vec![(route, handler)];
1551
1552        let result = build_router_for_tests(routes, None);
1553        assert!(result.is_ok());
1554    }
1555
1556    #[test]
1557    fn test_route_head_method() {
1558        let route = build_test_route("/users", "HEAD", "head_users", false);
1559
1560        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1561        let routes = vec![(route, handler)];
1562
1563        let result = build_router_for_tests(routes, None);
1564        assert!(result.is_ok());
1565    }
1566
1567    #[test]
1568    fn test_route_options_method() {
1569        let route = build_test_route("/users", "OPTIONS", "options_users", false);
1570
1571        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1572        let routes = vec![(route, handler)];
1573
1574        let result = build_router_for_tests(routes, None);
1575        assert!(result.is_ok());
1576    }
1577
1578    #[test]
1579    fn test_route_trace_method() {
1580        let route = build_test_route("/users", "TRACE", "trace_users", false);
1581
1582        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1583        let routes = vec![(route, handler)];
1584
1585        let result = build_router_for_tests(routes, None);
1586        assert!(result.is_ok());
1587    }
1588
1589    #[test]
1590    fn test_route_with_cors_config() {
1591        let cors_config = crate::CorsConfig {
1592            allowed_origins: vec!["https://example.com".to_string()],
1593            allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1594            allowed_headers: vec!["Content-Type".to_string()],
1595            expose_headers: None,
1596            max_age: Some(3600),
1597            allow_credentials: Some(true),
1598            ..Default::default()
1599        };
1600
1601        let route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config);
1602
1603        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1604        let routes = vec![(route, handler)];
1605
1606        let result = build_router_for_tests(routes, None);
1607        assert!(result.is_ok());
1608    }
1609
1610    #[test]
1611    fn test_multiple_routes_with_cors_same_path() {
1612        let cors_config = crate::CorsConfig {
1613            allowed_origins: vec!["https://example.com".to_string()],
1614            allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1615            allowed_headers: vec!["Content-Type".to_string()],
1616            expose_headers: None,
1617            max_age: Some(3600),
1618            allow_credentials: Some(true),
1619            ..Default::default()
1620        };
1621
1622        let get_route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config.clone());
1623        let post_route = build_test_route_with_cors("/users", "POST", "create_user", true, cors_config);
1624
1625        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1626        let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1627
1628        let result = build_router_for_tests(routes, None);
1629        assert!(result.is_ok());
1630    }
1631
1632    #[test]
1633    fn test_routes_sorted_by_path() {
1634        let zebra_route = build_test_route("/zebra", "GET", "get_zebra", false);
1635        let alpha_route = build_test_route("/alpha", "GET", "get_alpha", false);
1636
1637        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1638        let routes = vec![(zebra_route, handler.clone()), (alpha_route, handler)];
1639
1640        let result = build_router_for_tests(routes, None);
1641        assert!(result.is_ok());
1642    }
1643
1644    #[test]
1645    fn test_routes_with_nested_paths() {
1646        let parent_route = build_test_route("/api", "GET", "get_api", false);
1647        let child_route = build_test_route("/api/users", "GET", "get_users", false);
1648
1649        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1650        let routes = vec![(parent_route, handler.clone()), (child_route, handler)];
1651
1652        let result = build_router_for_tests(routes, None);
1653        assert!(result.is_ok());
1654    }
1655
1656    #[test]
1657    fn test_routes_with_lifecycle_hooks() {
1658        let hooks = crate::LifecycleHooks::new();
1659        let hooks = Arc::new(hooks);
1660
1661        let route = build_test_route("/users", "GET", "list_users", false);
1662
1663        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1664        let routes = vec![(route, handler)];
1665
1666        let result = build_router_for_tests(routes, Some(hooks));
1667        assert!(result.is_ok());
1668    }
1669
1670    #[test]
1671    fn test_routes_without_lifecycle_hooks() {
1672        let route = build_test_route("/users", "GET", "list_users", false);
1673
1674        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1675        let routes = vec![(route, handler)];
1676
1677        let result = build_router_for_tests(routes, None);
1678        assert!(result.is_ok());
1679    }
1680
1681    #[test]
1682    fn test_route_with_trailing_slash() {
1683        let route = build_test_route("/users/", "GET", "list_users", false);
1684
1685        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1686        let routes = vec![(route, handler)];
1687
1688        let result = build_router_for_tests(routes, None);
1689        assert!(result.is_ok());
1690    }
1691
1692    #[test]
1693    fn test_route_with_root_path() {
1694        let route = build_test_route("/", "GET", "root_handler", false);
1695
1696        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1697        let routes = vec![(route, handler)];
1698
1699        let result = build_router_for_tests(routes, None);
1700        assert!(result.is_ok());
1701    }
1702
1703    #[test]
1704    fn test_large_number_of_routes() {
1705        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1706        let mut routes = vec![];
1707
1708        for i in 0..50 {
1709            let route = build_test_route(&format!("/route{}", i), "GET", &format!("handler_{}", i), false);
1710            routes.push((route, handler.clone()));
1711        }
1712
1713        let result = build_router_for_tests(routes, None);
1714        assert!(result.is_ok());
1715    }
1716
1717    #[test]
1718    fn test_route_with_query_params_in_path_definition() {
1719        let route = build_test_route("/search", "GET", "search", false);
1720
1721        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1722        let routes = vec![(route, handler)];
1723
1724        let result = build_router_for_tests(routes, None);
1725        assert!(result.is_ok());
1726    }
1727
1728    #[test]
1729    fn test_all_http_methods_on_same_path() {
1730        let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1731        let methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1732
1733        let mut routes = vec![];
1734        for method in methods {
1735            let expects_body = matches!(method, "POST" | "PUT" | "PATCH");
1736            let route = build_test_route("/resource", method, &format!("handler_{}", method), expects_body);
1737            routes.push((route, handler.clone()));
1738        }
1739
1740        let result = build_router_for_tests(routes, None);
1741        assert!(result.is_ok());
1742    }
1743}