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.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +2 -2
- data/ext/spikard_rb/extconf.rb +4 -3
- data/lib/spikard/config.rb +88 -12
- data/lib/spikard/testing.rb +3 -1
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +11 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
- data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
- data/vendor/crates/spikard-core/Cargo.toml +3 -3
- data/vendor/crates/spikard-core/src/di/container.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
- data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
- data/vendor/crates/spikard-core/src/di/value.rs +1 -1
- data/vendor/crates/spikard-core/src/http.rs +75 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
- data/vendor/crates/spikard-core/src/parameters.rs +14 -19
- data/vendor/crates/spikard-core/src/problem.rs +1 -1
- data/vendor/crates/spikard-core/src/request_data.rs +7 -16
- data/vendor/crates/spikard-core/src/router.rs +6 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
- data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
- data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
- data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
- data/vendor/crates/spikard-http/Cargo.toml +4 -2
- data/vendor/crates/spikard-http/src/cors.rs +32 -11
- data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
- data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
- data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
- data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
- data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
- data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
- data/vendor/crates/spikard-http/src/lib.rs +1 -1
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
- data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
- data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
- data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
- data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
- data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
- data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
- data/vendor/crates/spikard-rb/Cargo.toml +3 -1
- data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
- data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
- data/vendor/crates/spikard-rb/src/handler.rs +169 -91
- data/vendor/crates/spikard-rb/src/lib.rs +444 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
- data/vendor/crates/spikard-rb/src/request.rs +117 -20
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
- data/vendor/crates/spikard-rb/src/server.rs +23 -14
- data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
- data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
- data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
- data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
- metadata +14 -4
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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()
|
|
66
|
-
let raw_bytes = request_data.raw_body.as_ref()
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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());
|