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
@@ -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
- // Convert the Axum request to bytes
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, usize::MAX).await {
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
- // Try to parse as ASCII metadata
92
- if let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>() {
93
- // Use key.as_str() directly instead of creating String
94
- if let Ok(metadata_key) = key
95
- .as_str()
96
- .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
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) -> &'static str {
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
  }