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