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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//! Fast-path HashMap router for static responses.
|
|
2
|
+
//!
|
|
3
|
+
//! Routes registered with `StaticResponse` and without path parameters are
|
|
4
|
+
//! placed into a two-level `AHashMap` keyed by `Method` then `path`. An axum
|
|
5
|
+
//! middleware layer checks this map first — on a hit the pre-built response is
|
|
6
|
+
//! returned immediately, avoiding the full Axum routing + middleware pipeline.
|
|
7
|
+
|
|
8
|
+
use ahash::AHashMap;
|
|
9
|
+
use axum::body::Body;
|
|
10
|
+
use axum::http::Method;
|
|
11
|
+
|
|
12
|
+
use crate::handler_trait::StaticResponse;
|
|
13
|
+
|
|
14
|
+
/// HashMap-based router for static-response routes without path parameters.
|
|
15
|
+
///
|
|
16
|
+
/// Uses a two-level map (`Method` → `path` → `StaticResponse`) so that
|
|
17
|
+
/// lookups only require a `&str` borrow — no heap allocation per request.
|
|
18
|
+
///
|
|
19
|
+
/// Inserted as the outermost middleware layer so that matching requests
|
|
20
|
+
/// never reach the Axum router at all.
|
|
21
|
+
#[derive(Clone)]
|
|
22
|
+
pub struct FastRouter {
|
|
23
|
+
routes: AHashMap<Method, AHashMap<String, StaticResponse>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl FastRouter {
|
|
27
|
+
/// Create an empty fast router.
|
|
28
|
+
pub fn new() -> Self {
|
|
29
|
+
Self {
|
|
30
|
+
routes: AHashMap::new(),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Register a static response for an exact method + path pair.
|
|
35
|
+
pub fn insert(&mut self, method: Method, path: &str, resp: &StaticResponse) {
|
|
36
|
+
self.routes
|
|
37
|
+
.entry(method)
|
|
38
|
+
.or_default()
|
|
39
|
+
.insert(path.to_owned(), resp.clone());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Returns `true` when at least one route has been registered.
|
|
43
|
+
pub fn has_routes(&self) -> bool {
|
|
44
|
+
!self.routes.is_empty()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Try to serve a request from the fast router.
|
|
48
|
+
/// Returns `None` if the method + path pair is not registered.
|
|
49
|
+
///
|
|
50
|
+
/// `Bytes::clone()` inside `to_response()` is reference-counted (not a
|
|
51
|
+
/// deep copy), so this is cheap even for large response bodies.
|
|
52
|
+
pub fn lookup(&self, method: &Method, path: &str) -> Option<axum::response::Response<Body>> {
|
|
53
|
+
let by_path = self.routes.get(method)?;
|
|
54
|
+
let resp = by_path.get(path)?;
|
|
55
|
+
Some(resp.to_response())
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl Default for FastRouter {
|
|
60
|
+
fn default() -> Self {
|
|
61
|
+
Self::new()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[cfg(test)]
|
|
66
|
+
mod tests {
|
|
67
|
+
use super::*;
|
|
68
|
+
use axum::http::{HeaderValue, StatusCode};
|
|
69
|
+
use bytes::Bytes;
|
|
70
|
+
use http_body_util::BodyExt;
|
|
71
|
+
|
|
72
|
+
fn make_static_response(status: u16, body: &str) -> StaticResponse {
|
|
73
|
+
StaticResponse {
|
|
74
|
+
status,
|
|
75
|
+
headers: vec![],
|
|
76
|
+
body: Bytes::from(body.to_owned()),
|
|
77
|
+
content_type: HeaderValue::from_static("text/plain"),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn test_fast_router_miss_returns_none() {
|
|
83
|
+
let router = FastRouter::new();
|
|
84
|
+
assert!(router.lookup(&Method::GET, "/health").is_none());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn test_fast_router_hit_returns_response() {
|
|
89
|
+
let mut router = FastRouter::new();
|
|
90
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
91
|
+
|
|
92
|
+
let resp = router.lookup(&Method::GET, "/health");
|
|
93
|
+
assert!(resp.is_some());
|
|
94
|
+
let resp = resp.unwrap();
|
|
95
|
+
assert_eq!(resp.status(), StatusCode::OK);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn test_fast_router_method_mismatch() {
|
|
100
|
+
let mut router = FastRouter::new();
|
|
101
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
102
|
+
|
|
103
|
+
assert!(router.lookup(&Method::POST, "/health").is_none());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn test_fast_router_path_mismatch() {
|
|
108
|
+
let mut router = FastRouter::new();
|
|
109
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
110
|
+
|
|
111
|
+
assert!(router.lookup(&Method::GET, "/ready").is_none());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn test_fast_router_has_routes() {
|
|
116
|
+
let mut router = FastRouter::new();
|
|
117
|
+
assert!(!router.has_routes());
|
|
118
|
+
|
|
119
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
120
|
+
assert!(router.has_routes());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn test_fast_router_multiple_routes() {
|
|
125
|
+
let mut router = FastRouter::new();
|
|
126
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
127
|
+
router.insert(Method::GET, "/ready", &make_static_response(200, "ready"));
|
|
128
|
+
router.insert(Method::POST, "/health", &make_static_response(201, "created"));
|
|
129
|
+
|
|
130
|
+
assert!(router.lookup(&Method::GET, "/health").is_some());
|
|
131
|
+
assert!(router.lookup(&Method::GET, "/ready").is_some());
|
|
132
|
+
assert!(router.lookup(&Method::POST, "/health").is_some());
|
|
133
|
+
assert!(router.lookup(&Method::DELETE, "/health").is_none());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn test_fast_router_custom_headers() {
|
|
138
|
+
use axum::http::header::HeaderName;
|
|
139
|
+
|
|
140
|
+
let resp = StaticResponse {
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: vec![(HeaderName::from_static("x-custom"), HeaderValue::from_static("value"))],
|
|
143
|
+
body: Bytes::from_static(b"OK"),
|
|
144
|
+
content_type: HeaderValue::from_static("application/json"),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let mut router = FastRouter::new();
|
|
148
|
+
router.insert(Method::GET, "/test", &resp);
|
|
149
|
+
|
|
150
|
+
let response = router.lookup(&Method::GET, "/test").unwrap();
|
|
151
|
+
assert_eq!(response.headers().get("x-custom").unwrap(), "value");
|
|
152
|
+
assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[tokio::test]
|
|
156
|
+
async fn test_fast_router_response_body_content() {
|
|
157
|
+
let mut router = FastRouter::new();
|
|
158
|
+
router.insert(Method::GET, "/health", &make_static_response(200, "OK"));
|
|
159
|
+
router.insert(Method::GET, "/ready", &make_static_response(200, "ready"));
|
|
160
|
+
|
|
161
|
+
let resp = router.lookup(&Method::GET, "/health").unwrap();
|
|
162
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
163
|
+
assert_eq!(body.as_ref(), b"OK");
|
|
164
|
+
|
|
165
|
+
let resp = router.lookup(&Method::GET, "/ready").unwrap();
|
|
166
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
167
|
+
assert_eq!(body.as_ref(), b"ready");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[tokio::test]
|
|
171
|
+
async fn test_fast_router_custom_status_code() {
|
|
172
|
+
let mut router = FastRouter::new();
|
|
173
|
+
router.insert(Method::POST, "/items", &make_static_response(201, "created"));
|
|
174
|
+
|
|
175
|
+
let resp = router.lookup(&Method::POST, "/items").unwrap();
|
|
176
|
+
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
177
|
+
let body = resp.into_body().collect().await.unwrap().to_bytes();
|
|
178
|
+
assert_eq!(body.as_ref(), b"created");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn test_fast_router_default() {
|
|
183
|
+
let router = FastRouter::default();
|
|
184
|
+
assert!(!router.has_routes());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! This module handles routing gRPC requests to the appropriate handlers
|
|
4
4
|
//! and multiplexing between HTTP/1.1 REST and HTTP/2 gRPC traffic.
|
|
5
5
|
|
|
6
|
-
use crate::grpc::{GrpcRegistry, parse_grpc_path};
|
|
6
|
+
use crate::grpc::{GrpcConfig, GrpcRegistry, RpcMode, parse_grpc_path};
|
|
7
7
|
use axum::body::Body;
|
|
8
8
|
use axum::http::{Request, Response, StatusCode};
|
|
9
9
|
use std::sync::Arc;
|
|
@@ -42,6 +42,7 @@ fn grpc_status_to_http(code: tonic::Code) -> StatusCode {
|
|
|
42
42
|
/// # Arguments
|
|
43
43
|
///
|
|
44
44
|
/// * `registry` - gRPC handler registry
|
|
45
|
+
/// * `config` - gRPC configuration with size limits
|
|
45
46
|
/// * `request` - The incoming gRPC request
|
|
46
47
|
///
|
|
47
48
|
/// # Returns
|
|
@@ -49,6 +50,7 @@ fn grpc_status_to_http(code: tonic::Code) -> StatusCode {
|
|
|
49
50
|
/// A future that resolves to a gRPC response or error
|
|
50
51
|
pub async fn route_grpc_request(
|
|
51
52
|
registry: Arc<GrpcRegistry>,
|
|
53
|
+
config: &GrpcConfig,
|
|
52
54
|
request: Request<Body>,
|
|
53
55
|
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
54
56
|
// Extract the request path
|
|
@@ -66,18 +68,55 @@ pub async fn route_grpc_request(
|
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
// Look up the handler for this service
|
|
69
|
-
let handler = match registry.get(&service_name) {
|
|
70
|
-
Some(h) => h,
|
|
71
|
+
let (handler, rpc_mode) = match registry.get(&service_name) {
|
|
72
|
+
Some((h, mode)) => (h, mode),
|
|
71
73
|
None => {
|
|
72
74
|
return Err((StatusCode::NOT_FOUND, format!("Service not found: {}", service_name)));
|
|
73
75
|
}
|
|
74
76
|
};
|
|
75
77
|
|
|
76
|
-
//
|
|
78
|
+
// Create the service bridge
|
|
79
|
+
let service = crate::grpc::GenericGrpcService::new(handler);
|
|
80
|
+
|
|
81
|
+
// Dispatch based on RPC mode
|
|
82
|
+
match rpc_mode {
|
|
83
|
+
RpcMode::Unary => {
|
|
84
|
+
handle_unary_request(service, service_name, method_name, config.max_message_size, request).await
|
|
85
|
+
}
|
|
86
|
+
RpcMode::ServerStreaming => {
|
|
87
|
+
handle_server_streaming_request(service, service_name, method_name, config.max_message_size, request).await
|
|
88
|
+
}
|
|
89
|
+
RpcMode::ClientStreaming => {
|
|
90
|
+
handle_client_streaming_request(service, service_name, method_name, config.max_message_size, request).await
|
|
91
|
+
}
|
|
92
|
+
RpcMode::BidirectionalStreaming => {
|
|
93
|
+
handle_bidirectional_streaming_request(service, service_name, method_name, config.max_message_size, request)
|
|
94
|
+
.await
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Handle a unary RPC request
|
|
100
|
+
async fn handle_unary_request(
|
|
101
|
+
service: crate::grpc::GenericGrpcService,
|
|
102
|
+
service_name: String,
|
|
103
|
+
method_name: String,
|
|
104
|
+
max_message_size: usize,
|
|
105
|
+
request: Request<Body>,
|
|
106
|
+
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
107
|
+
// Convert the Axum request to bytes with the configured size limit
|
|
77
108
|
let (parts, body) = request.into_parts();
|
|
78
|
-
let body_bytes = match axum::body::to_bytes(body,
|
|
109
|
+
let body_bytes = match axum::body::to_bytes(body, max_message_size).await {
|
|
79
110
|
Ok(bytes) => bytes,
|
|
80
111
|
Err(e) => {
|
|
112
|
+
// Check if error is due to size limit (axum returns "body size exceeded" or similar)
|
|
113
|
+
let error_msg = e.to_string();
|
|
114
|
+
if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
|
|
115
|
+
return Err((
|
|
116
|
+
StatusCode::PAYLOAD_TOO_LARGE,
|
|
117
|
+
format!("Message exceeds maximum size of {} bytes", max_message_size),
|
|
118
|
+
));
|
|
119
|
+
}
|
|
81
120
|
return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
|
|
82
121
|
}
|
|
83
122
|
};
|
|
@@ -87,22 +126,17 @@ pub async fn route_grpc_request(
|
|
|
87
126
|
|
|
88
127
|
// Copy headers to Tonic metadata
|
|
89
128
|
for (key, value) in parts.headers.iter() {
|
|
90
|
-
if let Ok(value_str) = value.to_str()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
129
|
+
if let Ok(value_str) = value.to_str()
|
|
130
|
+
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
131
|
+
&& let Ok(metadata_key) = key
|
|
132
|
+
.as_str()
|
|
133
|
+
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
134
|
+
{
|
|
135
|
+
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
101
136
|
}
|
|
102
137
|
}
|
|
103
138
|
|
|
104
139
|
// Use the service bridge to handle the request
|
|
105
|
-
let service = crate::grpc::GenericGrpcService::new(handler);
|
|
106
140
|
let tonic_response = match service.handle_unary(service_name, method_name, tonic_request).await {
|
|
107
141
|
Ok(resp) => resp,
|
|
108
142
|
Err(status) => {
|
|
@@ -142,6 +176,225 @@ pub async fn route_grpc_request(
|
|
|
142
176
|
Ok(response)
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
/// Handle a server streaming RPC request
|
|
180
|
+
async fn handle_server_streaming_request(
|
|
181
|
+
service: crate::grpc::GenericGrpcService,
|
|
182
|
+
service_name: String,
|
|
183
|
+
method_name: String,
|
|
184
|
+
max_message_size: usize,
|
|
185
|
+
request: Request<Body>,
|
|
186
|
+
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
187
|
+
// Convert the Axum request to bytes with the configured size limit
|
|
188
|
+
let (parts, body) = request.into_parts();
|
|
189
|
+
let body_bytes = match axum::body::to_bytes(body, max_message_size).await {
|
|
190
|
+
Ok(bytes) => bytes,
|
|
191
|
+
Err(e) => {
|
|
192
|
+
// Check if error is due to size limit (axum returns "body size exceeded" or similar)
|
|
193
|
+
let error_msg = e.to_string();
|
|
194
|
+
if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
|
|
195
|
+
return Err((
|
|
196
|
+
StatusCode::PAYLOAD_TOO_LARGE,
|
|
197
|
+
format!("Message exceeds maximum size of {} bytes", max_message_size),
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Create a Tonic request
|
|
205
|
+
let mut tonic_request = tonic::Request::new(body_bytes);
|
|
206
|
+
|
|
207
|
+
// Copy headers to Tonic metadata
|
|
208
|
+
for (key, value) in parts.headers.iter() {
|
|
209
|
+
if let Ok(value_str) = value.to_str()
|
|
210
|
+
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
211
|
+
&& let Ok(metadata_key) = key
|
|
212
|
+
.as_str()
|
|
213
|
+
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
214
|
+
{
|
|
215
|
+
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Use the service bridge to handle the streaming request
|
|
220
|
+
let tonic_response = match service
|
|
221
|
+
.handle_server_stream(service_name, method_name, tonic_request)
|
|
222
|
+
.await
|
|
223
|
+
{
|
|
224
|
+
Ok(resp) => resp,
|
|
225
|
+
Err(status) => {
|
|
226
|
+
let status_code = grpc_status_to_http(status.code());
|
|
227
|
+
return Err((status_code, status.message().to_string()));
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Convert Tonic response to Axum response
|
|
232
|
+
let body = tonic_response.into_inner();
|
|
233
|
+
let mut response = Response::builder()
|
|
234
|
+
.status(StatusCode::OK)
|
|
235
|
+
.header("content-type", "application/grpc+proto");
|
|
236
|
+
|
|
237
|
+
// Add gRPC status trailer (success)
|
|
238
|
+
response = response.header("grpc-status", "0");
|
|
239
|
+
|
|
240
|
+
let response = response.body(body).map_err(|e| {
|
|
241
|
+
(
|
|
242
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
243
|
+
format!("Failed to build response: {}", e),
|
|
244
|
+
)
|
|
245
|
+
})?;
|
|
246
|
+
|
|
247
|
+
Ok(response)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// Handle a client streaming RPC request
|
|
251
|
+
async fn handle_client_streaming_request(
|
|
252
|
+
service: crate::grpc::GenericGrpcService,
|
|
253
|
+
service_name: String,
|
|
254
|
+
method_name: String,
|
|
255
|
+
max_message_size: usize,
|
|
256
|
+
request: Request<Body>,
|
|
257
|
+
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
258
|
+
// Extract request parts - keep body as stream for frame parsing
|
|
259
|
+
let (parts, body) = request.into_parts();
|
|
260
|
+
|
|
261
|
+
// Create a Tonic request with streaming body
|
|
262
|
+
// Body will be parsed by service.handle_client_stream using frame parser
|
|
263
|
+
let mut tonic_request = tonic::Request::new(body);
|
|
264
|
+
|
|
265
|
+
// Copy headers to Tonic metadata
|
|
266
|
+
for (key, value) in parts.headers.iter() {
|
|
267
|
+
if let Ok(value_str) = value.to_str()
|
|
268
|
+
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
269
|
+
&& let Ok(metadata_key) = key
|
|
270
|
+
.as_str()
|
|
271
|
+
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
272
|
+
{
|
|
273
|
+
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Use the service bridge to handle the client streaming request
|
|
278
|
+
// Frame parsing and size validation happens in handle_client_stream
|
|
279
|
+
let tonic_response = match service
|
|
280
|
+
.handle_client_stream(service_name, method_name, tonic_request, max_message_size)
|
|
281
|
+
.await
|
|
282
|
+
{
|
|
283
|
+
Ok(resp) => resp,
|
|
284
|
+
Err(status) => {
|
|
285
|
+
let status_code = grpc_status_to_http(status.code());
|
|
286
|
+
return Err((status_code, status.message().to_string()));
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Convert Tonic response to Axum response
|
|
291
|
+
let payload = tonic_response.get_ref().clone();
|
|
292
|
+
let metadata = tonic_response.metadata();
|
|
293
|
+
|
|
294
|
+
let mut response = Response::builder()
|
|
295
|
+
.status(StatusCode::OK)
|
|
296
|
+
.header("content-type", "application/grpc+proto");
|
|
297
|
+
|
|
298
|
+
// Copy metadata to response headers
|
|
299
|
+
for key_value in metadata.iter() {
|
|
300
|
+
if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
|
|
301
|
+
&& let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
|
|
302
|
+
{
|
|
303
|
+
response = response.header(key.as_str(), header_value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Add gRPC status trailer (success)
|
|
308
|
+
response = response.header("grpc-status", "0");
|
|
309
|
+
|
|
310
|
+
// Convert bytes::Bytes to Body
|
|
311
|
+
let response = response.body(Body::from(payload)).map_err(|e| {
|
|
312
|
+
(
|
|
313
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
314
|
+
format!("Failed to build response: {}", e),
|
|
315
|
+
)
|
|
316
|
+
})?;
|
|
317
|
+
|
|
318
|
+
Ok(response)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Handle a bidirectional streaming RPC request
|
|
322
|
+
///
|
|
323
|
+
/// Bidirectional streaming allows both client and server to send multiple messages.
|
|
324
|
+
/// This function:
|
|
325
|
+
/// 1. Keeps the request body as a stream (not converting to bytes)
|
|
326
|
+
/// 2. Copies HTTP headers to gRPC metadata
|
|
327
|
+
/// 3. Passes the streaming body to the service for frame parsing
|
|
328
|
+
/// 4. Returns the response stream from the handler
|
|
329
|
+
///
|
|
330
|
+
/// # Arguments
|
|
331
|
+
///
|
|
332
|
+
/// * `service` - The GenericGrpcService containing the handler
|
|
333
|
+
/// * `service_name` - Fully qualified service name (e.g., "mypackage.ChatService")
|
|
334
|
+
/// * `method_name` - Method name (e.g., "Chat")
|
|
335
|
+
/// * `max_message_size` - Maximum size per message in bytes
|
|
336
|
+
/// * `request` - Axum HTTP request with streaming body
|
|
337
|
+
///
|
|
338
|
+
/// # Returns
|
|
339
|
+
///
|
|
340
|
+
/// Response with streaming body containing response messages, or error with status code
|
|
341
|
+
async fn handle_bidirectional_streaming_request(
|
|
342
|
+
service: crate::grpc::GenericGrpcService,
|
|
343
|
+
service_name: String,
|
|
344
|
+
method_name: String,
|
|
345
|
+
max_message_size: usize,
|
|
346
|
+
request: Request<Body>,
|
|
347
|
+
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
348
|
+
// Extract request parts - keep body as stream for frame parsing
|
|
349
|
+
let (parts, body) = request.into_parts();
|
|
350
|
+
|
|
351
|
+
// Create a Tonic request with streaming body
|
|
352
|
+
// Body will be parsed by service.handle_bidi_stream using frame parser
|
|
353
|
+
let mut tonic_request = tonic::Request::new(body);
|
|
354
|
+
|
|
355
|
+
// Copy HTTP headers to gRPC metadata
|
|
356
|
+
for (key, value) in parts.headers.iter() {
|
|
357
|
+
if let Ok(value_str) = value.to_str()
|
|
358
|
+
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
359
|
+
&& let Ok(metadata_key) = key
|
|
360
|
+
.as_str()
|
|
361
|
+
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
362
|
+
{
|
|
363
|
+
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Call service handler - frame parsing and size validation happens inside
|
|
368
|
+
let tonic_response = match service
|
|
369
|
+
.handle_bidi_stream(service_name, method_name, tonic_request, max_message_size)
|
|
370
|
+
.await
|
|
371
|
+
{
|
|
372
|
+
Ok(response) => response,
|
|
373
|
+
Err(status) => {
|
|
374
|
+
let status_code = grpc_status_to_http(status.code());
|
|
375
|
+
return Err((status_code, status.message().to_string()));
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Convert Tonic response to Axum response with streaming body
|
|
380
|
+
let body = tonic_response.into_inner();
|
|
381
|
+
let mut response = Response::builder()
|
|
382
|
+
.status(StatusCode::OK)
|
|
383
|
+
.header("content-type", "application/grpc+proto");
|
|
384
|
+
|
|
385
|
+
// Add gRPC status trailer (success)
|
|
386
|
+
response = response.header("grpc-status", "0");
|
|
387
|
+
|
|
388
|
+
let response = response.body(body).map_err(|e| {
|
|
389
|
+
(
|
|
390
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
391
|
+
format!("Failed to build response: {}", e),
|
|
392
|
+
)
|
|
393
|
+
})?;
|
|
394
|
+
|
|
395
|
+
Ok(response)
|
|
396
|
+
}
|
|
397
|
+
|
|
145
398
|
/// Check if an incoming request is a gRPC request
|
|
146
399
|
///
|
|
147
400
|
/// Returns true if the request has a content-type starting with "application/grpc"
|
|
@@ -157,7 +410,7 @@ pub fn is_grpc_request(request: &Request<Body>) -> bool {
|
|
|
157
410
|
#[cfg(test)]
|
|
158
411
|
mod tests {
|
|
159
412
|
use super::*;
|
|
160
|
-
use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
|
|
413
|
+
use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData, RpcMode};
|
|
161
414
|
use bytes::Bytes;
|
|
162
415
|
use std::future::Future;
|
|
163
416
|
use std::pin::Pin;
|
|
@@ -174,7 +427,7 @@ mod tests {
|
|
|
174
427
|
})
|
|
175
428
|
}
|
|
176
429
|
|
|
177
|
-
fn service_name(&self) -> &
|
|
430
|
+
fn service_name(&self) -> &str {
|
|
178
431
|
"test.EchoService"
|
|
179
432
|
}
|
|
180
433
|
}
|
|
@@ -182,8 +435,9 @@ mod tests {
|
|
|
182
435
|
#[tokio::test]
|
|
183
436
|
async fn test_route_grpc_request_success() {
|
|
184
437
|
let mut registry = GrpcRegistry::new();
|
|
185
|
-
registry.register("test.EchoService", Arc::new(EchoHandler));
|
|
438
|
+
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
186
439
|
let registry = Arc::new(registry);
|
|
440
|
+
let config = GrpcConfig::default();
|
|
187
441
|
|
|
188
442
|
let request = Request::builder()
|
|
189
443
|
.uri("/test.EchoService/Echo")
|
|
@@ -191,13 +445,14 @@ mod tests {
|
|
|
191
445
|
.body(Body::from(Bytes::from("test payload")))
|
|
192
446
|
.unwrap();
|
|
193
447
|
|
|
194
|
-
let result = route_grpc_request(registry, request).await;
|
|
448
|
+
let result = route_grpc_request(registry, &config, request).await;
|
|
195
449
|
assert!(result.is_ok());
|
|
196
450
|
}
|
|
197
451
|
|
|
198
452
|
#[tokio::test]
|
|
199
453
|
async fn test_route_grpc_request_service_not_found() {
|
|
200
454
|
let registry = Arc::new(GrpcRegistry::new());
|
|
455
|
+
let config = GrpcConfig::default();
|
|
201
456
|
|
|
202
457
|
let request = Request::builder()
|
|
203
458
|
.uri("/nonexistent.Service/Method")
|
|
@@ -205,7 +460,7 @@ mod tests {
|
|
|
205
460
|
.body(Body::from(Bytes::new()))
|
|
206
461
|
.unwrap();
|
|
207
462
|
|
|
208
|
-
let result = route_grpc_request(registry, request).await;
|
|
463
|
+
let result = route_grpc_request(registry, &config, request).await;
|
|
209
464
|
assert!(result.is_err());
|
|
210
465
|
|
|
211
466
|
let (status, message) = result.unwrap_err();
|
|
@@ -216,6 +471,7 @@ mod tests {
|
|
|
216
471
|
#[tokio::test]
|
|
217
472
|
async fn test_route_grpc_request_invalid_path() {
|
|
218
473
|
let registry = Arc::new(GrpcRegistry::new());
|
|
474
|
+
let config = GrpcConfig::default();
|
|
219
475
|
|
|
220
476
|
let request = Request::builder()
|
|
221
477
|
.uri("/invalid")
|
|
@@ -223,7 +479,7 @@ mod tests {
|
|
|
223
479
|
.body(Body::from(Bytes::new()))
|
|
224
480
|
.unwrap();
|
|
225
481
|
|
|
226
|
-
let result = route_grpc_request(registry, request).await;
|
|
482
|
+
let result = route_grpc_request(registry, &config, request).await;
|
|
227
483
|
assert!(result.is_err());
|
|
228
484
|
|
|
229
485
|
let (status, _message) = result.unwrap_err();
|
|
@@ -324,4 +580,49 @@ mod tests {
|
|
|
324
580
|
StatusCode::UNAUTHORIZED
|
|
325
581
|
);
|
|
326
582
|
}
|
|
583
|
+
|
|
584
|
+
#[tokio::test]
|
|
585
|
+
async fn test_route_grpc_request_with_custom_max_message_size() {
|
|
586
|
+
let mut registry = GrpcRegistry::new();
|
|
587
|
+
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
588
|
+
let registry = Arc::new(registry);
|
|
589
|
+
|
|
590
|
+
let mut config = GrpcConfig::default();
|
|
591
|
+
config.max_message_size = 100; // Set small limit
|
|
592
|
+
|
|
593
|
+
let request = Request::builder()
|
|
594
|
+
.uri("/test.EchoService/Echo")
|
|
595
|
+
.header("content-type", "application/grpc")
|
|
596
|
+
.body(Body::from(Bytes::from("test payload")))
|
|
597
|
+
.unwrap();
|
|
598
|
+
|
|
599
|
+
// This should succeed since payload is small
|
|
600
|
+
let result = route_grpc_request(registry.clone(), &config, request).await;
|
|
601
|
+
assert!(result.is_ok());
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[tokio::test]
|
|
605
|
+
async fn test_route_grpc_request_exceeds_max_message_size() {
|
|
606
|
+
let mut registry = GrpcRegistry::new();
|
|
607
|
+
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
608
|
+
let registry = Arc::new(registry);
|
|
609
|
+
|
|
610
|
+
let mut config = GrpcConfig::default();
|
|
611
|
+
config.max_message_size = 10; // Set very small limit
|
|
612
|
+
|
|
613
|
+
// Create a large payload that exceeds the limit
|
|
614
|
+
let large_payload = vec![b'x'; 1000];
|
|
615
|
+
let request = Request::builder()
|
|
616
|
+
.uri("/test.EchoService/Echo")
|
|
617
|
+
.header("content-type", "application/grpc")
|
|
618
|
+
.body(Body::from(Bytes::from(large_payload)))
|
|
619
|
+
.unwrap();
|
|
620
|
+
|
|
621
|
+
let result = route_grpc_request(registry, &config, request).await;
|
|
622
|
+
assert!(result.is_err());
|
|
623
|
+
|
|
624
|
+
let (status, message) = result.unwrap_err();
|
|
625
|
+
assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
|
|
626
|
+
assert!(message.contains("Message exceeds maximum size"));
|
|
627
|
+
}
|
|
327
628
|
}
|