spikard 0.8.3 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -10
  3. data/ext/spikard_rb/Cargo.lock +234 -162
  4. data/ext/spikard_rb/Cargo.toml +2 -2
  5. data/ext/spikard_rb/extconf.rb +4 -3
  6. data/lib/spikard/config.rb +88 -12
  7. data/lib/spikard/testing.rb +3 -1
  8. data/lib/spikard/version.rb +1 -1
  9. data/lib/spikard.rb +11 -0
  10. data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
  11. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
  12. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
  14. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
  15. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
  16. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
  17. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  18. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
  22. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
  23. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  24. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
  25. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  26. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  27. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  28. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  29. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  30. data/vendor/crates/spikard-core/Cargo.toml +3 -3
  31. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  32. data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
  34. data/vendor/crates/spikard-core/src/di/value.rs +1 -1
  35. data/vendor/crates/spikard-core/src/http.rs +75 -0
  36. data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
  37. data/vendor/crates/spikard-core/src/parameters.rs +14 -19
  38. data/vendor/crates/spikard-core/src/problem.rs +1 -1
  39. data/vendor/crates/spikard-core/src/request_data.rs +7 -16
  40. data/vendor/crates/spikard-core/src/router.rs +6 -0
  41. data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
  42. data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
  43. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
  44. data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
  45. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  46. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  47. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  48. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  49. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  50. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  51. data/vendor/crates/spikard-http/Cargo.toml +4 -2
  52. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  53. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  54. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  55. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  56. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  57. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  58. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  59. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  60. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  61. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  62. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  63. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  64. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  65. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  66. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  67. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  68. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  69. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  70. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  71. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  72. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  73. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  74. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  75. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  76. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  77. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  78. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  79. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  80. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  81. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  82. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  83. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  84. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  85. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  86. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  87. data/vendor/crates/spikard-rb/Cargo.toml +3 -1
  88. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  89. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  90. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  91. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  92. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  93. data/vendor/crates/spikard-rb/src/lib.rs +444 -62
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
  95. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  96. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  97. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  98. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  99. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  100. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  101. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  102. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  103. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  104. metadata +14 -4
  105. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  106. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -43,9 +43,12 @@ impl Handler for ValidatingHandler {
43
43
  req: axum::http::Request<Body>,
44
44
  mut request_data: RequestData,
45
45
  ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
46
- let inner = self.inner.clone();
47
- let request_validator = self.request_validator.clone();
48
- let parameter_validator = self.parameter_validator.clone();
46
+ // Performance: Use references where possible to avoid Arc clones on every request.
47
+ // The Arc clones here are cheap (atomic increment), but we store references
48
+ // to Option<Arc<...>> to avoid cloning when validators are None (common case).
49
+ let inner = &self.inner;
50
+ let request_validator = &self.request_validator;
51
+ let parameter_validator = &self.parameter_validator;
49
52
 
50
53
  Box::pin(async move {
51
54
  let is_json_body = request_data.body.is_null()
@@ -55,17 +58,25 @@ impl Handler for ValidatingHandler {
55
58
  .get("content-type")
56
59
  .is_some_and(|ct| crate::middleware::validation::is_json_like_str(ct));
57
60
 
58
- if is_json_body && request_validator.is_none() && !inner.prefers_raw_json_body() {
59
- let raw_bytes = request_data.raw_body.as_ref().unwrap();
60
- request_data.body = serde_json::from_slice::<Value>(raw_bytes)
61
- .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?;
61
+ if is_json_body
62
+ && request_validator.is_none()
63
+ && !inner.prefers_raw_json_body()
64
+ && let Some(raw_bytes) = request_data.raw_body.as_ref()
65
+ {
66
+ request_data.body = Arc::new(
67
+ serde_json::from_slice::<Value>(raw_bytes)
68
+ .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?,
69
+ );
62
70
  }
63
71
 
64
72
  if let Some(validator) = request_validator {
65
- if request_data.body.is_null() && request_data.raw_body.is_some() {
66
- let raw_bytes = request_data.raw_body.as_ref().unwrap();
67
- request_data.body = serde_json::from_slice::<Value>(raw_bytes)
68
- .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?;
73
+ if request_data.body.is_null()
74
+ && let Some(raw_bytes) = request_data.raw_body.as_ref()
75
+ {
76
+ request_data.body = Arc::new(
77
+ serde_json::from_slice::<Value>(raw_bytes)
78
+ .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, format!("Invalid JSON: {}", e)))?,
79
+ );
69
80
  }
70
81
 
71
82
  if let Err(errors) = validator.validate(&request_data.body) {
@@ -86,7 +97,7 @@ impl Handler for ValidatingHandler {
86
97
  &request_data.cookies,
87
98
  ) {
88
99
  Ok(validated) => {
89
- request_data.validated_params = Some(validated);
100
+ request_data.validated_params = Some(Arc::new(validated));
90
101
  }
91
102
  Err(errors) => {
92
103
  let problem = ProblemDetails::from_validation_error(&errors);
@@ -124,10 +135,10 @@ mod tests {
124
135
  fn create_request_data(body: Value) -> RequestData {
125
136
  RequestData {
126
137
  path_params: Arc::new(HashMap::new()),
127
- query_params: json!({}),
138
+ query_params: Arc::new(json!({})),
128
139
  validated_params: None,
129
140
  raw_query_params: Arc::new(HashMap::new()),
130
- body,
141
+ body: Arc::new(body),
131
142
  raw_body: None,
132
143
  headers: Arc::new(HashMap::new()),
133
144
  cookies: Arc::new(HashMap::new()),
@@ -142,10 +153,10 @@ mod tests {
142
153
  fn create_request_data_with_raw_body(raw_body: Vec<u8>) -> RequestData {
143
154
  RequestData {
144
155
  path_params: Arc::new(HashMap::new()),
145
- query_params: json!({}),
156
+ query_params: Arc::new(json!({})),
146
157
  validated_params: None,
147
158
  raw_query_params: Arc::new(HashMap::new()),
148
- body: Value::Null,
159
+ body: Arc::new(Value::Null),
149
160
  raw_body: Some(bytes::Bytes::from(raw_body)),
150
161
  headers: Arc::new(HashMap::new()),
151
162
  cookies: Arc::new(HashMap::new()),
@@ -261,10 +272,10 @@ mod tests {
261
272
  headers.insert("content-type".to_string(), "application/json".to_string());
262
273
  let request_data = RequestData {
263
274
  path_params: Arc::new(HashMap::new()),
264
- query_params: json!({}),
275
+ query_params: Arc::new(json!({})),
265
276
  validated_params: None,
266
277
  raw_query_params: Arc::new(HashMap::new()),
267
- body: Value::Null,
278
+ body: Arc::new(Value::Null),
268
279
  raw_body: Some(bytes::Bytes::from(br#"{"name":"Alice"}"#.to_vec())),
269
280
  headers: Arc::new(headers),
270
281
  cookies: Arc::new(HashMap::new()),
@@ -762,10 +773,10 @@ mod tests {
762
773
 
763
774
  let request_data = RequestData {
764
775
  path_params: Arc::new(HashMap::new()),
765
- query_params: json!({}),
776
+ query_params: Arc::new(json!({})),
766
777
  validated_params: None,
767
778
  raw_query_params: Arc::new(HashMap::new()),
768
- body: Value::Null,
779
+ body: Arc::new(Value::Null),
769
780
  raw_body: None,
770
781
  headers: Arc::new(HashMap::new()),
771
782
  cookies: Arc::new(HashMap::new()),
@@ -900,10 +911,10 @@ mod tests {
900
911
 
901
912
  let request_data = RequestData {
902
913
  path_params: Arc::new(HashMap::new()),
903
- query_params: json!({}),
914
+ query_params: Arc::new(json!({})),
904
915
  validated_params: None,
905
916
  raw_query_params: Arc::new(HashMap::new()),
906
- body: Value::Null,
917
+ body: Arc::new(Value::Null),
907
918
  raw_body: Some(bytes::Bytes::from(br#"{"id":42}"#.to_vec())),
908
919
  headers: Arc::new(HashMap::new()),
909
920
  cookies: Arc::new(HashMap::new()),
@@ -29,6 +29,25 @@ pub async fn execute_with_lifecycle_hooks(
29
29
  return handler.call(req, request_data).await;
30
30
  }
31
31
 
32
+ let req = match hooks.execute_on_request(req).await {
33
+ Ok(HookResult::Continue(r)) => r,
34
+ Ok(HookResult::ShortCircuit(response)) => return Ok(response),
35
+ Err(e) => {
36
+ let error_response = axum::http::Response::builder()
37
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
38
+ .body(Body::from(format!("{{\"error\":\"onRequest hook failed: {}\"}}", e)))
39
+ .unwrap();
40
+
41
+ return match hooks.execute_on_error(error_response).await {
42
+ Ok(resp) => Ok(resp),
43
+ Err(_) => Ok(axum::http::Response::builder()
44
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
45
+ .body(Body::from("{\"error\":\"Hook execution failed\"}"))
46
+ .unwrap()),
47
+ };
48
+ }
49
+ };
50
+
32
51
  let req = match hooks.execute_pre_validation(req).await {
33
52
  Ok(HookResult::Continue(r)) => r,
34
53
  Ok(HookResult::ShortCircuit(response)) => return Ok(response),
@@ -140,10 +159,10 @@ mod tests {
140
159
  fn empty_request_data() -> crate::handler_trait::RequestData {
141
160
  crate::handler_trait::RequestData {
142
161
  path_params: std::sync::Arc::new(HashMap::new()),
143
- query_params: json!({}),
162
+ query_params: std::sync::Arc::new(json!({})),
144
163
  validated_params: None,
145
164
  raw_query_params: std::sync::Arc::new(HashMap::new()),
146
- body: json!(null),
165
+ body: std::sync::Arc::new(json!(null)),
147
166
  raw_body: None,
148
167
  headers: std::sync::Arc::new(HashMap::new()),
149
168
  cookies: std::sync::Arc::new(HashMap::new()),
@@ -3,6 +3,7 @@
3
3
  //! This module provides the main server builder and routing infrastructure, with
4
4
  //! focused submodules for handler validation, request extraction, and lifecycle execution.
5
5
 
6
+ pub mod fast_router;
6
7
  pub mod grpc_routing;
7
8
  pub mod handler;
8
9
  pub mod lifecycle_execution;
@@ -483,6 +484,7 @@ fn build_router_with_handlers_inner(
483
484
  enable_http_trace: bool,
484
485
  ) -> Result<AxumRouter, String> {
485
486
  let mut app = AxumRouter::new();
487
+ let mut fast_router = fast_router::FastRouter::new();
486
488
 
487
489
  let mut routes_by_path: HashMap<String, Vec<RouteHandlerPair>> = HashMap::new();
488
490
  for (route, handler) in routes {
@@ -500,7 +502,8 @@ fn build_router_with_handlers_inner(
500
502
  .remove(&path)
501
503
  .ok_or_else(|| format!("Missing handlers for path '{}'", path))?;
502
504
 
503
- let mut handlers_by_method: HashMap<crate::Method, (crate::Route, Arc<dyn Handler>)> = HashMap::new();
505
+ type RouteEntry = (crate::Route, Arc<dyn Handler>, Option<crate::StaticResponse>);
506
+ let mut handlers_by_method: HashMap<crate::Method, RouteEntry> = HashMap::new();
504
507
  for (route, handler) in route_handlers {
505
508
  #[cfg(feature = "di")]
506
509
  let handler = if let Some(ref container) = di_container {
@@ -522,13 +525,16 @@ fn build_router_with_handlers_inner(
522
525
  handler
523
526
  };
524
527
 
528
+ // Check for static_response before wrapping in ValidatingHandler,
529
+ // since ValidatingHandler doesn't delegate static_response().
530
+ let static_resp = handler.static_response();
525
531
  let validating_handler = Arc::new(handler::ValidatingHandler::new(handler, &route));
526
- handlers_by_method.insert(route.method.clone(), (route, validating_handler));
532
+ handlers_by_method.insert(route.method.clone(), (route, validating_handler, static_resp));
527
533
  }
528
534
 
529
535
  let cors_config: Option<CorsConfig> = handlers_by_method
530
536
  .values()
531
- .find_map(|(route, _)| route.cors.as_ref())
537
+ .find_map(|(route, _, _)| route.cors.as_ref())
532
538
  .cloned();
533
539
 
534
540
  let has_options_handler = handlers_by_method.keys().any(|m| m.as_str() == "OPTIONS");
@@ -536,8 +542,68 @@ fn build_router_with_handlers_inner(
536
542
  let mut combined_router: Option<MethodRouter> = None;
537
543
  let has_path_params = path.contains('{');
538
544
 
539
- for (_method, (route, handler)) in handlers_by_method {
545
+ for (_method, (route, handler, static_resp_opt)) in handlers_by_method {
540
546
  let method = route.method.clone();
547
+
548
+ // Fast-path: if the handler declares a static response, bypass the
549
+ // entire middleware pipeline (validation, hooks, request extraction).
550
+ //
551
+ // NOTE: static routes also bypass CORS handling, content-type
552
+ // validation, and HTTP tracing. If CORS headers are needed they
553
+ // must be included in `StaticResponse.headers` explicitly.
554
+ //
555
+ // Non-parameterized paths are also inserted into the FastRouter for
556
+ // O(1) HashMap-based lookup as the outermost middleware. The Axum
557
+ // route below serves as fallback — it handles the same request if
558
+ // the FastRouter layer is somehow bypassed and also covers
559
+ // parameterized static routes that cannot go into the FastRouter.
560
+ if let Some(static_resp) = static_resp_opt {
561
+ let resp_status = static_resp.status;
562
+
563
+ if !has_path_params {
564
+ let axum_path_for_fast = spikard_core::type_hints::strip_type_hints(&path);
565
+ let http_method: axum::http::Method = route.method.as_str().parse().map_err(|_| {
566
+ format!(
567
+ "Invalid HTTP method '{}' for static route {}",
568
+ route.method.as_str(),
569
+ path
570
+ )
571
+ })?;
572
+ fast_router.insert(http_method, &axum_path_for_fast, &static_resp);
573
+ }
574
+
575
+ // Axum fallback handler — uses the same `to_response()` as the
576
+ // FastRouter and `StaticResponseHandler::call`.
577
+ let static_handler = move || {
578
+ let resp = static_resp.to_response();
579
+ async move { resp }
580
+ };
581
+
582
+ let method_router: MethodRouter = match method {
583
+ crate::Method::Get => axum::routing::get(static_handler),
584
+ crate::Method::Post => axum::routing::post(static_handler),
585
+ crate::Method::Put => axum::routing::put(static_handler),
586
+ crate::Method::Patch => axum::routing::patch(static_handler),
587
+ crate::Method::Delete => axum::routing::delete(static_handler),
588
+ crate::Method::Head => axum::routing::head(static_handler),
589
+ crate::Method::Options => axum::routing::options(static_handler),
590
+ crate::Method::Trace => axum::routing::trace(static_handler),
591
+ };
592
+
593
+ combined_router = Some(match combined_router {
594
+ None => method_router,
595
+ Some(existing) => existing.merge(method_router),
596
+ });
597
+
598
+ tracing::info!(
599
+ "Registered static route: {} {} (status {})",
600
+ route.method.as_str(),
601
+ path,
602
+ resp_status,
603
+ );
604
+ continue;
605
+ }
606
+
541
607
  let method_router: MethodRouter = match method {
542
608
  crate::Method::Options => {
543
609
  if let Some(ref cors_cfg) = route.cors {
@@ -572,12 +638,24 @@ fn build_router_with_handlers_inner(
572
638
  }
573
639
  };
574
640
 
575
- let method_router = method_router.layer(axum::middleware::from_fn_with_state(
576
- crate::middleware::RouteInfo {
577
- expects_json_body: route.expects_json_body,
578
- },
579
- crate::middleware::validate_content_type_middleware,
580
- ));
641
+ // Only apply content-type validation middleware for methods that
642
+ // carry a request body. GET/DELETE/HEAD/OPTIONS/TRACE never have
643
+ // meaningful content-type headers, so the middleware just adds
644
+ // into_parts/from_parts overhead for those methods.
645
+ let method_router = if matches!(
646
+ route.method,
647
+ crate::Method::Post | crate::Method::Put | crate::Method::Patch
648
+ ) && (route.expects_json_body || route.file_params.is_some())
649
+ {
650
+ method_router.layer(axum::middleware::from_fn_with_state(
651
+ crate::middleware::RouteInfo {
652
+ expects_json_body: route.expects_json_body,
653
+ },
654
+ crate::middleware::validate_content_type_middleware,
655
+ ))
656
+ } else {
657
+ method_router
658
+ };
581
659
 
582
660
  combined_router = Some(match combined_router {
583
661
  None => method_router,
@@ -616,6 +694,23 @@ fn build_router_with_handlers_inner(
616
694
  app = app.layer(TraceLayer::new_for_http());
617
695
  }
618
696
 
697
+ // Install the fast-router as the outermost middleware so that static-response
698
+ // routes are served without entering the Axum routing tree at all.
699
+ if fast_router.has_routes() {
700
+ let fast_router = Arc::new(fast_router);
701
+ app = app.layer(axum::middleware::from_fn(
702
+ move |req: axum::extract::Request, next: axum::middleware::Next| {
703
+ let fast_router = Arc::clone(&fast_router);
704
+ async move {
705
+ if let Some(resp) = fast_router.lookup(req.method(), req.uri().path()) {
706
+ return resp;
707
+ }
708
+ next.run(req).await
709
+ }
710
+ },
711
+ ));
712
+ }
713
+
619
714
  Ok(app)
620
715
  }
621
716
 
@@ -626,13 +721,13 @@ pub fn build_router_with_handlers_and_config(
626
721
  route_metadata: Vec<crate::RouteMetadata>,
627
722
  ) -> Result<AxumRouter, String> {
628
723
  #[cfg(feature = "di")]
629
- if config.di_container.is_none() {
630
- eprintln!("[spikard-di] build_router: di_container is None");
631
- } else {
724
+ if let Some(di_container) = config.di_container.as_ref() {
632
725
  eprintln!(
633
726
  "[spikard-di] build_router: di_container has keys: {:?}",
634
- config.di_container.as_ref().unwrap().keys()
727
+ di_container.keys()
635
728
  );
729
+ } else {
730
+ eprintln!("[spikard-di] build_router: di_container is None");
636
731
  }
637
732
  let hooks = config.lifecycle_hooks.clone();
638
733
 
@@ -698,10 +793,15 @@ pub fn build_router_with_handlers_and_config(
698
793
  #[cfg(not(feature = "di"))]
699
794
  let mut app = build_router_with_handlers_inner(routes, hooks, None, config.enable_http_trace)?;
700
795
 
701
- app = app.layer(SetSensitiveRequestHeadersLayer::new([
702
- axum::http::header::AUTHORIZATION,
703
- axum::http::header::COOKIE,
704
- ]));
796
+ // Only add the sensitive-header redaction layer when auth middleware is
797
+ // configured — without auth there is nothing to redact, and the layer
798
+ // otherwise adds per-request overhead.
799
+ if config.jwt_auth.is_some() || config.api_key_auth.is_some() {
800
+ app = app.layer(SetSensitiveRequestHeadersLayer::new([
801
+ axum::http::header::AUTHORIZATION,
802
+ axum::http::header::COOKIE,
803
+ ]));
804
+ }
705
805
 
706
806
  if let Some(ref compression) = config.compression {
707
807
  let mut compression_layer = CompressionLayer::new();
@@ -772,10 +872,11 @@ pub fn build_router_with_handlers_and_config(
772
872
  .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid));
773
873
  }
774
874
 
875
+ // Only add the body-limit layer when a limit is explicitly configured.
876
+ // Omitting the layer entirely (instead of `disable()`) avoids a no-op
877
+ // middleware dispatch on every request.
775
878
  if let Some(max_size) = config.max_body_size {
776
879
  app = app.layer(DefaultBodyLimit::max(max_size));
777
- } else {
778
- app = app.layer(DefaultBodyLimit::disable());
779
880
  }
780
881
 
781
882
  for static_config in &config.static_files {
@@ -932,6 +1033,7 @@ impl Server {
932
1033
  .jsonrpc_method
933
1034
  .as_ref()
934
1035
  .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
1036
+ static_response: None,
935
1037
  }
936
1038
  }
937
1039
  #[cfg(not(feature = "di"))]
@@ -951,6 +1053,7 @@ impl Server {
951
1053
  .jsonrpc_method
952
1054
  .as_ref()
953
1055
  .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
1056
+ static_response: None,
954
1057
  }
955
1058
  }
956
1059
  })
@@ -1448,6 +1551,7 @@ mod tests {
1448
1551
  expose_headers: None,
1449
1552
  max_age: Some(3600),
1450
1553
  allow_credentials: Some(true),
1554
+ ..Default::default()
1451
1555
  };
1452
1556
 
1453
1557
  let route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config);
@@ -1468,6 +1572,7 @@ mod tests {
1468
1572
  expose_headers: None,
1469
1573
  max_age: Some(3600),
1470
1574
  allow_credentials: Some(true),
1575
+ ..Default::default()
1471
1576
  };
1472
1577
 
1473
1578
  let get_route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config.clone());