spikard 0.13.0 → 0.15.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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/Steepfile +6 -0
  3. data/ext/spikard_rb/extconf.rb +1 -2
  4. data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +819 -424
  5. data/ext/spikard_rb/native/Cargo.toml +24 -0
  6. data/ext/spikard_rb/src/lib.rs +5366 -3
  7. data/lib/spikard/native.rb +86 -0
  8. data/lib/spikard/version.rb +6 -1
  9. data/lib/spikard.rb +8 -52
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -243
  13. data/LICENSE +0 -1
  14. data/README.md +0 -285
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -458
  17. data/lib/spikard/background.rb +0 -58
  18. data/lib/spikard/config.rb +0 -506
  19. data/lib/spikard/converters.rb +0 -13
  20. data/lib/spikard/grpc.rb +0 -232
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -315
  23. data/lib/spikard/response.rb +0 -198
  24. data/lib/spikard/schema.rb +0 -243
  25. data/lib/spikard/sse.rb +0 -111
  26. data/lib/spikard/streaming_response.rb +0 -44
  27. data/lib/spikard/testing.rb +0 -474
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -739
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -75
  32. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
  33. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
  34. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
  35. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
  36. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
  37. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
  38. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
  39. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
  40. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
  41. data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
  42. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
  43. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
  44. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
  45. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
  46. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
  47. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
  48. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
  49. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
  50. data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
  51. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
  52. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
  53. data/vendor/crates/spikard-core/Cargo.toml +0 -55
  54. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  55. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
  56. data/vendor/crates/spikard-core/src/debug.rs +0 -127
  57. data/vendor/crates/spikard-core/src/di/container.rs +0 -711
  58. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  59. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  60. data/vendor/crates/spikard-core/src/di/factory.rs +0 -548
  61. data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
  62. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  63. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
  64. data/vendor/crates/spikard-core/src/di/value.rs +0 -282
  65. data/vendor/crates/spikard-core/src/errors.rs +0 -72
  66. data/vendor/crates/spikard-core/src/http.rs +0 -492
  67. data/vendor/crates/spikard-core/src/lib.rs +0 -29
  68. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
  69. data/vendor/crates/spikard-core/src/metadata.rs +0 -378
  70. data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
  71. data/vendor/crates/spikard-core/src/problem.rs +0 -358
  72. data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
  73. data/vendor/crates/spikard-core/src/router.rs +0 -530
  74. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
  75. data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
  76. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
  77. data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
  78. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
  79. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
  80. data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
  81. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
  82. data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
  83. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
  84. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
  85. data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
  86. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
  87. data/vendor/crates/spikard-http/Cargo.toml +0 -82
  88. data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
  89. data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
  90. data/vendor/crates/spikard-http/src/auth.rs +0 -301
  91. data/vendor/crates/spikard-http/src/background.rs +0 -1859
  92. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  93. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  94. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  95. data/vendor/crates/spikard-http/src/cors.rs +0 -1026
  96. data/vendor/crates/spikard-http/src/debug.rs +0 -128
  97. data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
  98. data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -653
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1211
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -556
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -706
  102. data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
  103. data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
  104. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
  105. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
  106. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
  107. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
  108. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -60
  109. data/vendor/crates/spikard-http/src/jsonrpc/openrpc.rs +0 -325
  110. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  111. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  112. data/vendor/crates/spikard-http/src/lib.rs +0 -566
  113. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  114. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  115. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  116. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  117. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  118. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  119. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  120. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  121. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  122. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  123. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  124. data/vendor/crates/spikard-http/src/response.rs +0 -720
  125. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  126. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -1243
  127. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  128. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  129. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1717
  130. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  131. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  132. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  133. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  134. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  135. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -825
  136. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  137. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  138. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  139. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  140. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  141. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  142. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  143. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  144. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  145. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  146. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  147. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  148. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  149. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  150. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  151. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  152. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  153. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -975
  154. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  155. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  156. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  157. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  158. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  159. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -335
  160. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -374
  161. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  162. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  163. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  164. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  165. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  166. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  167. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -427
  168. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  169. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  170. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  171. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  172. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  173. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  174. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  175. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  176. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  177. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  178. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  179. data/vendor/crates/spikard-rb/Cargo.toml +0 -63
  180. data/vendor/crates/spikard-rb/build.rs +0 -200
  181. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  182. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  183. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  184. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  185. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  186. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -410
  187. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -875
  188. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  189. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  190. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  191. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  192. data/vendor/crates/spikard-rb/src/lib.rs +0 -2268
  193. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -334
  194. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  195. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  196. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  197. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  198. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -368
  199. data/vendor/crates/spikard-rb/src/server.rs +0 -304
  200. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  201. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  202. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  203. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  204. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  205. data/vendor/crates/spikard-rb/src/websocket.rs +0 -521
  206. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -20
  207. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
@@ -1,1243 +0,0 @@
1
- //! gRPC request routing and multiplexing
2
- //!
3
- //! This module handles routing gRPC requests to the appropriate handlers
4
- //! and multiplexing between HTTP/1.1 REST and HTTP/2 gRPC traffic.
5
-
6
- use crate::grpc::framing::{GRPC_MESSAGE_HEADER_LEN, encode_grpc_message, parse_unary_grpc_message};
7
- use crate::grpc::{GrpcConfig, GrpcRegistry, RpcMode, parse_grpc_path};
8
- use axum::body::Body;
9
- use axum::http::{Request, Response, StatusCode};
10
- use std::sync::Arc;
11
-
12
- /// Convert gRPC status code to HTTP status code
13
- ///
14
- /// Maps all gRPC status codes to appropriate HTTP status codes
15
- /// following the gRPC-HTTP status code mapping specification.
16
- fn grpc_status_to_http(code: tonic::Code) -> StatusCode {
17
- match code {
18
- tonic::Code::Ok => StatusCode::OK,
19
- tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap(), // Client Closed Request
20
- tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
21
- tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
22
- tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
23
- tonic::Code::NotFound => StatusCode::NOT_FOUND,
24
- tonic::Code::AlreadyExists => StatusCode::CONFLICT,
25
- tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
26
- tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
27
- tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
28
- tonic::Code::Aborted => StatusCode::CONFLICT,
29
- tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
30
- tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
31
- tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
32
- tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
33
- tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
34
- tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
35
- }
36
- }
37
-
38
- /// Route a gRPC request to the appropriate handler
39
- ///
40
- /// Parses the request path to extract service and method names,
41
- /// looks up the handler in the registry, and invokes it.
42
- ///
43
- /// # Arguments
44
- ///
45
- /// * `registry` - gRPC handler registry
46
- /// * `config` - gRPC configuration with size limits
47
- /// * `request` - The incoming gRPC request
48
- ///
49
- /// # Returns
50
- ///
51
- /// A future that resolves to a gRPC response or error
52
- pub async fn route_grpc_request(
53
- registry: Arc<GrpcRegistry>,
54
- config: &GrpcConfig,
55
- request: Request<Body>,
56
- ) -> Result<Response<Body>, (StatusCode, String)> {
57
- // Extract the request path
58
- let path = request.uri().path();
59
-
60
- // Parse service and method names from the path
61
- let (service_name, method_name) = match parse_grpc_path(path) {
62
- Ok(names) => names,
63
- Err(status) => {
64
- return Err((
65
- StatusCode::BAD_REQUEST,
66
- format!("Invalid gRPC path: {}", status.message()),
67
- ));
68
- }
69
- };
70
-
71
- // Look up the handler for this service and method
72
- let (handler, rpc_mode) = match registry.get(&service_name, &method_name) {
73
- Some((h, mode)) => (h, mode),
74
- None => {
75
- let message = if registry.contains_service(&service_name) {
76
- format!("Method not found: {service_name}/{method_name}")
77
- } else {
78
- format!("Service not found: {service_name}")
79
- };
80
- return Err((StatusCode::NOT_FOUND, message));
81
- }
82
- };
83
-
84
- // Create the service bridge
85
- let service = crate::grpc::GenericGrpcService::new(handler);
86
-
87
- // Dispatch based on RPC mode
88
- match rpc_mode {
89
- RpcMode::Unary => {
90
- handle_unary_request(
91
- service,
92
- service_name,
93
- method_name,
94
- config.max_message_size,
95
- config.enable_compression,
96
- request,
97
- )
98
- .await
99
- }
100
- RpcMode::ServerStreaming => {
101
- handle_server_streaming_request(
102
- service,
103
- service_name,
104
- method_name,
105
- config.max_message_size,
106
- config.enable_compression,
107
- request,
108
- )
109
- .await
110
- }
111
- RpcMode::ClientStreaming => {
112
- handle_client_streaming_request(
113
- service,
114
- service_name,
115
- method_name,
116
- config.max_message_size,
117
- config.enable_compression,
118
- request,
119
- )
120
- .await
121
- }
122
- RpcMode::BidirectionalStreaming => {
123
- handle_bidirectional_streaming_request(
124
- service,
125
- service_name,
126
- method_name,
127
- config.max_message_size,
128
- config.enable_compression,
129
- request,
130
- )
131
- .await
132
- }
133
- }
134
- }
135
-
136
- fn unary_message_read_limit(max_message_size: usize) -> usize {
137
- max_message_size.saturating_add(GRPC_MESSAGE_HEADER_LEN)
138
- }
139
-
140
- /// Handle a unary RPC request
141
- async fn handle_unary_request(
142
- service: crate::grpc::GenericGrpcService,
143
- service_name: String,
144
- method_name: String,
145
- max_message_size: usize,
146
- compression_enabled: bool,
147
- request: Request<Body>,
148
- ) -> Result<Response<Body>, (StatusCode, String)> {
149
- // Convert the Axum request to bytes with the configured size limit
150
- let (parts, body) = request.into_parts();
151
- let request_encoding = parts.headers.get("grpc-encoding").and_then(|value| value.to_str().ok());
152
- let body_bytes = match axum::body::to_bytes(body, unary_message_read_limit(max_message_size)).await {
153
- Ok(bytes) => bytes,
154
- Err(e) => {
155
- // Check if error is due to size limit (axum returns "body size exceeded" or similar)
156
- let error_msg = e.to_string();
157
- if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
158
- return Err((
159
- StatusCode::PAYLOAD_TOO_LARGE,
160
- format!("Message exceeds maximum size of {} bytes", max_message_size),
161
- ));
162
- }
163
- return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
164
- }
165
- };
166
-
167
- let payload = match parse_unary_grpc_message(&body_bytes, max_message_size, request_encoding, compression_enabled) {
168
- Ok(message) => message,
169
- Err(status) => {
170
- let status_code = grpc_status_to_http(status.code());
171
- return Err((status_code, status.message().to_string()));
172
- }
173
- };
174
-
175
- // Create a Tonic request
176
- let mut tonic_request = tonic::Request::new(payload);
177
-
178
- // Copy headers to Tonic metadata
179
- for (key, value) in parts.headers.iter() {
180
- if let Ok(value_str) = value.to_str()
181
- && let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
182
- && let Ok(metadata_key) = key
183
- .as_str()
184
- .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
185
- {
186
- tonic_request.metadata_mut().insert(metadata_key, metadata_value);
187
- }
188
- }
189
-
190
- // Use the service bridge to handle the request
191
- let tonic_response = match service.handle_unary(service_name, method_name, tonic_request).await {
192
- Ok(resp) => resp,
193
- Err(status) => {
194
- let status_code = grpc_status_to_http(status.code());
195
- return Err((status_code, status.message().to_string()));
196
- }
197
- };
198
-
199
- // Convert Tonic response to Axum response
200
- let payload = tonic_response.get_ref().clone();
201
- let framed_payload = match encode_grpc_message(payload) {
202
- Ok(framed) => framed,
203
- Err(status) => {
204
- let status_code = grpc_status_to_http(status.code());
205
- return Err((status_code, status.message().to_string()));
206
- }
207
- };
208
- let metadata = tonic_response.metadata();
209
-
210
- let mut response = Response::builder()
211
- .status(StatusCode::OK)
212
- .header("content-type", "application/grpc+proto");
213
-
214
- // Copy metadata to response headers
215
- for key_value in metadata.iter() {
216
- if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
217
- && let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
218
- {
219
- response = response.header(key.as_str(), header_value);
220
- }
221
- }
222
-
223
- // Add gRPC status trailer (success)
224
- response = response.header("grpc-status", "0");
225
-
226
- // Convert bytes::Bytes to Body
227
- let response = response.body(Body::from(framed_payload)).map_err(|e| {
228
- (
229
- StatusCode::INTERNAL_SERVER_ERROR,
230
- format!("Failed to build response: {}", e),
231
- )
232
- })?;
233
-
234
- Ok(response)
235
- }
236
-
237
- /// Handle a server streaming RPC request
238
- async fn handle_server_streaming_request(
239
- service: crate::grpc::GenericGrpcService,
240
- service_name: String,
241
- method_name: String,
242
- max_message_size: usize,
243
- compression_enabled: bool,
244
- request: Request<Body>,
245
- ) -> Result<Response<Body>, (StatusCode, String)> {
246
- // Convert the Axum request to bytes with the configured size limit
247
- let (parts, body) = request.into_parts();
248
- let request_encoding = parts.headers.get("grpc-encoding").and_then(|value| value.to_str().ok());
249
- let body_bytes = match axum::body::to_bytes(body, unary_message_read_limit(max_message_size)).await {
250
- Ok(bytes) => bytes,
251
- Err(e) => {
252
- // Check if error is due to size limit (axum returns "body size exceeded" or similar)
253
- let error_msg = e.to_string();
254
- if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
255
- return Err((
256
- StatusCode::PAYLOAD_TOO_LARGE,
257
- format!("Message exceeds maximum size of {} bytes", max_message_size),
258
- ));
259
- }
260
- return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
261
- }
262
- };
263
-
264
- let payload = match parse_unary_grpc_message(&body_bytes, max_message_size, request_encoding, compression_enabled) {
265
- Ok(message) => message,
266
- Err(status) => {
267
- let status_code = grpc_status_to_http(status.code());
268
- return Err((status_code, status.message().to_string()));
269
- }
270
- };
271
-
272
- // Create a Tonic request
273
- let mut tonic_request = tonic::Request::new(payload);
274
-
275
- // Copy headers to Tonic metadata
276
- for (key, value) in parts.headers.iter() {
277
- if let Ok(value_str) = value.to_str()
278
- && let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
279
- && let Ok(metadata_key) = key
280
- .as_str()
281
- .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
282
- {
283
- tonic_request.metadata_mut().insert(metadata_key, metadata_value);
284
- }
285
- }
286
-
287
- // Use the service bridge to handle the streaming request
288
- let tonic_response = match service
289
- .handle_server_stream(service_name, method_name, tonic_request)
290
- .await
291
- {
292
- Ok(resp) => resp,
293
- Err(status) => {
294
- let status_code = grpc_status_to_http(status.code());
295
- return Err((status_code, status.message().to_string()));
296
- }
297
- };
298
-
299
- // Convert Tonic response to Axum response
300
- let body = tonic_response.into_inner();
301
- let response = Response::builder()
302
- .status(StatusCode::OK)
303
- .header("content-type", "application/grpc+proto");
304
-
305
- let response = response.body(body).map_err(|e| {
306
- (
307
- StatusCode::INTERNAL_SERVER_ERROR,
308
- format!("Failed to build response: {}", e),
309
- )
310
- })?;
311
-
312
- Ok(response)
313
- }
314
-
315
- /// Handle a client streaming RPC request
316
- async fn handle_client_streaming_request(
317
- service: crate::grpc::GenericGrpcService,
318
- service_name: String,
319
- method_name: String,
320
- max_message_size: usize,
321
- compression_enabled: bool,
322
- request: Request<Body>,
323
- ) -> Result<Response<Body>, (StatusCode, String)> {
324
- // Extract request parts - keep body as stream for frame parsing
325
- let (parts, body) = request.into_parts();
326
-
327
- // Create a Tonic request with streaming body
328
- // Body will be parsed by service.handle_client_stream using frame parser
329
- let mut tonic_request = tonic::Request::new(body);
330
-
331
- // Copy headers to Tonic metadata
332
- for (key, value) in parts.headers.iter() {
333
- if let Ok(value_str) = value.to_str()
334
- && let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
335
- && let Ok(metadata_key) = key
336
- .as_str()
337
- .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
338
- {
339
- tonic_request.metadata_mut().insert(metadata_key, metadata_value);
340
- }
341
- }
342
-
343
- // Use the service bridge to handle the client streaming request
344
- // Frame parsing and size validation happens in handle_client_stream
345
- let tonic_response = match service
346
- .handle_client_stream(
347
- service_name,
348
- method_name,
349
- tonic_request,
350
- max_message_size,
351
- compression_enabled,
352
- )
353
- .await
354
- {
355
- Ok(resp) => resp,
356
- Err(status) => {
357
- let status_code = grpc_status_to_http(status.code());
358
- return Err((status_code, status.message().to_string()));
359
- }
360
- };
361
-
362
- // Convert Tonic response to Axum response
363
- let payload = tonic_response.get_ref().clone();
364
- let framed_payload = match encode_grpc_message(payload) {
365
- Ok(framed) => framed,
366
- Err(status) => {
367
- let status_code = grpc_status_to_http(status.code());
368
- return Err((status_code, status.message().to_string()));
369
- }
370
- };
371
- let metadata = tonic_response.metadata();
372
-
373
- let mut response = Response::builder()
374
- .status(StatusCode::OK)
375
- .header("content-type", "application/grpc+proto");
376
-
377
- // Copy metadata to response headers
378
- for key_value in metadata.iter() {
379
- if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
380
- && let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
381
- {
382
- response = response.header(key.as_str(), header_value);
383
- }
384
- }
385
-
386
- // Add gRPC status trailer (success)
387
- response = response.header("grpc-status", "0");
388
-
389
- // Convert bytes::Bytes to Body
390
- let response = response.body(Body::from(framed_payload)).map_err(|e| {
391
- (
392
- StatusCode::INTERNAL_SERVER_ERROR,
393
- format!("Failed to build response: {}", e),
394
- )
395
- })?;
396
-
397
- Ok(response)
398
- }
399
-
400
- /// Handle a bidirectional streaming RPC request
401
- ///
402
- /// Bidirectional streaming allows both client and server to send multiple messages.
403
- /// This function:
404
- /// 1. Keeps the request body as a stream (not converting to bytes)
405
- /// 2. Copies HTTP headers to gRPC metadata
406
- /// 3. Passes the streaming body to the service for frame parsing
407
- /// 4. Returns the response stream from the handler
408
- ///
409
- /// # Arguments
410
- ///
411
- /// * `service` - The GenericGrpcService containing the handler
412
- /// * `service_name` - Fully qualified service name (e.g., "mypackage.ChatService")
413
- /// * `method_name` - Method name (e.g., "Chat")
414
- /// * `max_message_size` - Maximum size per message in bytes
415
- /// * `compression_enabled` - Whether compressed gRPC request frames are accepted
416
- /// * `request` - Axum HTTP request with streaming body
417
- ///
418
- /// # Returns
419
- ///
420
- /// Response with streaming body containing response messages, or error with status code
421
- async fn handle_bidirectional_streaming_request(
422
- service: crate::grpc::GenericGrpcService,
423
- service_name: String,
424
- method_name: String,
425
- max_message_size: usize,
426
- compression_enabled: bool,
427
- request: Request<Body>,
428
- ) -> Result<Response<Body>, (StatusCode, String)> {
429
- // Extract request parts - keep body as stream for frame parsing
430
- let (parts, body) = request.into_parts();
431
-
432
- // Create a Tonic request with streaming body
433
- // Body will be parsed by service.handle_bidi_stream using frame parser
434
- let mut tonic_request = tonic::Request::new(body);
435
-
436
- // Copy HTTP headers to gRPC metadata
437
- for (key, value) in parts.headers.iter() {
438
- if let Ok(value_str) = value.to_str()
439
- && let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
440
- && let Ok(metadata_key) = key
441
- .as_str()
442
- .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
443
- {
444
- tonic_request.metadata_mut().insert(metadata_key, metadata_value);
445
- }
446
- }
447
-
448
- // Call service handler - frame parsing and size validation happens inside
449
- let tonic_response = match service
450
- .handle_bidi_stream(
451
- service_name,
452
- method_name,
453
- tonic_request,
454
- max_message_size,
455
- compression_enabled,
456
- )
457
- .await
458
- {
459
- Ok(response) => response,
460
- Err(status) => {
461
- let status_code = grpc_status_to_http(status.code());
462
- return Err((status_code, status.message().to_string()));
463
- }
464
- };
465
-
466
- // Convert Tonic response to Axum response with streaming body
467
- let body = tonic_response.into_inner();
468
- let response = Response::builder()
469
- .status(StatusCode::OK)
470
- .header("content-type", "application/grpc+proto");
471
-
472
- let response = response.body(body).map_err(|e| {
473
- (
474
- StatusCode::INTERNAL_SERVER_ERROR,
475
- format!("Failed to build response: {}", e),
476
- )
477
- })?;
478
-
479
- Ok(response)
480
- }
481
-
482
- /// Check if an incoming request is a gRPC request
483
- ///
484
- /// Returns true if the request has a content-type starting with "application/grpc"
485
- pub fn is_grpc_request(request: &Request<Body>) -> bool {
486
- request
487
- .headers()
488
- .get(axum::http::header::CONTENT_TYPE)
489
- .and_then(|v| v.to_str().ok())
490
- .map(|v| v.starts_with("application/grpc"))
491
- .unwrap_or(false)
492
- }
493
-
494
- #[cfg(test)]
495
- mod tests {
496
- use super::*;
497
- use crate::grpc::framing::{encode_grpc_message, parse_unary_grpc_message};
498
- use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData, RpcMode};
499
- use crate::grpc::streaming::{MessageStream, StreamingRequest, message_stream_from_vec};
500
- use bytes::Bytes;
501
- use flate2::{Compression, write::GzEncoder};
502
- use futures_util::StreamExt;
503
- use http_body_util::BodyExt;
504
- use std::future::Future;
505
- use std::io::Write;
506
- use std::pin::Pin;
507
- use tonic::metadata::MetadataMap;
508
-
509
- fn framed_message(payload: &[u8]) -> Bytes {
510
- encode_grpc_message(Bytes::copy_from_slice(payload)).expect("test payload should frame")
511
- }
512
-
513
- struct EchoHandler;
514
-
515
- impl GrpcHandler for EchoHandler {
516
- fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
517
- Box::pin(async move {
518
- Ok(GrpcResponseData {
519
- payload: request.payload,
520
- metadata: tonic::metadata::MetadataMap::new(),
521
- })
522
- })
523
- }
524
-
525
- fn service_name(&self) -> &str {
526
- "test.EchoService"
527
- }
528
- }
529
-
530
- #[tokio::test]
531
- async fn test_route_grpc_request_success() {
532
- let mut registry = GrpcRegistry::new();
533
- registry.register_service("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
534
- let registry = Arc::new(registry);
535
- let config = GrpcConfig::default();
536
-
537
- let request = Request::builder()
538
- .uri("/test.EchoService/Echo")
539
- .header("content-type", "application/grpc")
540
- .body(Body::from(framed_message(b"test payload")))
541
- .unwrap();
542
-
543
- let result = route_grpc_request(registry, &config, request).await;
544
- assert!(result.is_ok());
545
- }
546
-
547
- #[tokio::test]
548
- async fn test_route_grpc_request_supports_mixed_rpc_modes_per_service() {
549
- struct MixedUnaryHandler;
550
-
551
- impl GrpcHandler for MixedUnaryHandler {
552
- fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
553
- Box::pin(async move {
554
- Ok(GrpcResponseData {
555
- payload: request.payload,
556
- metadata: tonic::metadata::MetadataMap::new(),
557
- })
558
- })
559
- }
560
-
561
- fn service_name(&self) -> &str {
562
- "test.MixedService"
563
- }
564
- }
565
-
566
- struct MixedStreamHandler;
567
-
568
- impl GrpcHandler for MixedStreamHandler {
569
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
570
- Box::pin(async { Err(tonic::Status::unimplemented("Use server streaming")) })
571
- }
572
-
573
- fn service_name(&self) -> &str {
574
- "test.MixedService"
575
- }
576
-
577
- fn call_server_stream(
578
- &self,
579
- _request: GrpcRequestData,
580
- ) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
581
- Box::pin(async { Ok(message_stream_from_vec(vec![Bytes::from_static(b"streamed")])) })
582
- }
583
- }
584
-
585
- let mut registry = GrpcRegistry::new();
586
- registry.register(
587
- "test.MixedService",
588
- "GetUser",
589
- Arc::new(MixedUnaryHandler),
590
- RpcMode::Unary,
591
- );
592
- registry.register(
593
- "test.MixedService",
594
- "ListUsers",
595
- Arc::new(MixedStreamHandler),
596
- RpcMode::ServerStreaming,
597
- );
598
- let registry = Arc::new(registry);
599
- let config = GrpcConfig::default();
600
-
601
- let unary_response = route_grpc_request(
602
- Arc::clone(&registry),
603
- &config,
604
- Request::builder()
605
- .uri("/test.MixedService/GetUser")
606
- .header("content-type", "application/grpc")
607
- .body(Body::from(framed_message(b"user")))
608
- .unwrap(),
609
- )
610
- .await
611
- .unwrap();
612
- assert_eq!(unary_response.headers().get("grpc-status").unwrap(), "0");
613
-
614
- let streaming_response = route_grpc_request(
615
- registry,
616
- &config,
617
- Request::builder()
618
- .uri("/test.MixedService/ListUsers")
619
- .header("content-type", "application/grpc")
620
- .body(Body::from(framed_message(b"ignored")))
621
- .unwrap(),
622
- )
623
- .await
624
- .unwrap();
625
- assert!(streaming_response.headers().get("grpc-status").is_none());
626
- }
627
-
628
- #[tokio::test]
629
- async fn test_route_grpc_request_service_not_found() {
630
- let registry = Arc::new(GrpcRegistry::new());
631
- let config = GrpcConfig::default();
632
-
633
- let request = Request::builder()
634
- .uri("/nonexistent.Service/Method")
635
- .header("content-type", "application/grpc")
636
- .body(Body::from(Bytes::new()))
637
- .unwrap();
638
-
639
- let result = route_grpc_request(registry, &config, request).await;
640
- assert!(result.is_err());
641
-
642
- let (status, message) = result.unwrap_err();
643
- assert_eq!(status, StatusCode::NOT_FOUND);
644
- assert!(message.contains("Service not found"));
645
- }
646
-
647
- #[tokio::test]
648
- async fn test_route_grpc_request_method_not_found() {
649
- let mut registry = GrpcRegistry::new();
650
- registry.register("test.EchoService", "Echo", Arc::new(EchoHandler), RpcMode::Unary);
651
- let registry = Arc::new(registry);
652
- let config = GrpcConfig::default();
653
-
654
- let request = Request::builder()
655
- .uri("/test.EchoService/Missing")
656
- .header("content-type", "application/grpc")
657
- .body(Body::from(Bytes::new()))
658
- .unwrap();
659
-
660
- let result = route_grpc_request(registry, &config, request).await;
661
- assert!(result.is_err());
662
-
663
- let (status, message) = result.unwrap_err();
664
- assert_eq!(status, StatusCode::NOT_FOUND);
665
- assert!(message.contains("Method not found"));
666
- }
667
-
668
- #[tokio::test]
669
- async fn test_route_grpc_request_invalid_path() {
670
- let registry = Arc::new(GrpcRegistry::new());
671
- let config = GrpcConfig::default();
672
-
673
- let request = Request::builder()
674
- .uri("/invalid")
675
- .header("content-type", "application/grpc")
676
- .body(Body::from(Bytes::new()))
677
- .unwrap();
678
-
679
- let result = route_grpc_request(registry, &config, request).await;
680
- assert!(result.is_err());
681
-
682
- let (status, _message) = result.unwrap_err();
683
- assert_eq!(status, StatusCode::BAD_REQUEST);
684
- }
685
-
686
- #[test]
687
- fn test_is_grpc_request_true() {
688
- let request = Request::builder()
689
- .header("content-type", "application/grpc")
690
- .body(Body::empty())
691
- .unwrap();
692
-
693
- assert!(is_grpc_request(&request));
694
- }
695
-
696
- #[test]
697
- fn test_is_grpc_request_with_subtype() {
698
- let request = Request::builder()
699
- .header("content-type", "application/grpc+proto")
700
- .body(Body::empty())
701
- .unwrap();
702
-
703
- assert!(is_grpc_request(&request));
704
- }
705
-
706
- #[test]
707
- fn test_is_grpc_request_false() {
708
- let request = Request::builder()
709
- .header("content-type", "application/json")
710
- .body(Body::empty())
711
- .unwrap();
712
-
713
- assert!(!is_grpc_request(&request));
714
- }
715
-
716
- #[test]
717
- fn test_is_grpc_request_no_content_type() {
718
- let request = Request::builder().body(Body::empty()).unwrap();
719
-
720
- assert!(!is_grpc_request(&request));
721
- }
722
-
723
- #[test]
724
- fn test_grpc_status_to_http_mappings() {
725
- // Test all gRPC status codes map correctly
726
- assert_eq!(grpc_status_to_http(tonic::Code::Ok), StatusCode::OK);
727
- assert_eq!(
728
- grpc_status_to_http(tonic::Code::Cancelled),
729
- StatusCode::from_u16(499).unwrap()
730
- );
731
- assert_eq!(
732
- grpc_status_to_http(tonic::Code::Unknown),
733
- StatusCode::INTERNAL_SERVER_ERROR
734
- );
735
- assert_eq!(
736
- grpc_status_to_http(tonic::Code::InvalidArgument),
737
- StatusCode::BAD_REQUEST
738
- );
739
- assert_eq!(
740
- grpc_status_to_http(tonic::Code::DeadlineExceeded),
741
- StatusCode::GATEWAY_TIMEOUT
742
- );
743
- assert_eq!(grpc_status_to_http(tonic::Code::NotFound), StatusCode::NOT_FOUND);
744
- assert_eq!(grpc_status_to_http(tonic::Code::AlreadyExists), StatusCode::CONFLICT);
745
- assert_eq!(
746
- grpc_status_to_http(tonic::Code::PermissionDenied),
747
- StatusCode::FORBIDDEN
748
- );
749
- assert_eq!(
750
- grpc_status_to_http(tonic::Code::ResourceExhausted),
751
- StatusCode::TOO_MANY_REQUESTS
752
- );
753
- assert_eq!(
754
- grpc_status_to_http(tonic::Code::FailedPrecondition),
755
- StatusCode::BAD_REQUEST
756
- );
757
- assert_eq!(grpc_status_to_http(tonic::Code::Aborted), StatusCode::CONFLICT);
758
- assert_eq!(grpc_status_to_http(tonic::Code::OutOfRange), StatusCode::BAD_REQUEST);
759
- assert_eq!(
760
- grpc_status_to_http(tonic::Code::Unimplemented),
761
- StatusCode::NOT_IMPLEMENTED
762
- );
763
- assert_eq!(
764
- grpc_status_to_http(tonic::Code::Internal),
765
- StatusCode::INTERNAL_SERVER_ERROR
766
- );
767
- assert_eq!(
768
- grpc_status_to_http(tonic::Code::Unavailable),
769
- StatusCode::SERVICE_UNAVAILABLE
770
- );
771
- assert_eq!(
772
- grpc_status_to_http(tonic::Code::DataLoss),
773
- StatusCode::INTERNAL_SERVER_ERROR
774
- );
775
- assert_eq!(
776
- grpc_status_to_http(tonic::Code::Unauthenticated),
777
- StatusCode::UNAUTHORIZED
778
- );
779
- }
780
-
781
- #[tokio::test]
782
- async fn test_route_grpc_request_with_custom_max_message_size() {
783
- let mut registry = GrpcRegistry::new();
784
- registry.register_service("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
785
- let registry = Arc::new(registry);
786
-
787
- let mut config = GrpcConfig::default();
788
- config.max_message_size = 100; // Set small limit
789
-
790
- let request = Request::builder()
791
- .uri("/test.EchoService/Echo")
792
- .header("content-type", "application/grpc")
793
- .body(Body::from(framed_message(b"test payload")))
794
- .unwrap();
795
-
796
- // This should succeed since payload is small
797
- let result = route_grpc_request(registry.clone(), &config, request).await;
798
- assert!(result.is_ok());
799
- }
800
-
801
- #[tokio::test]
802
- async fn test_route_grpc_request_exceeds_max_message_size() {
803
- let mut registry = GrpcRegistry::new();
804
- registry.register_service("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
805
- let registry = Arc::new(registry);
806
-
807
- let mut config = GrpcConfig::default();
808
- config.max_message_size = 10; // Set very small limit
809
-
810
- // Create a large payload that exceeds the limit
811
- let large_payload = vec![b'x'; 1000];
812
- let framed_payload = encode_grpc_message(Bytes::from(large_payload)).unwrap();
813
- let request = Request::builder()
814
- .uri("/test.EchoService/Echo")
815
- .header("content-type", "application/grpc")
816
- .body(Body::from(framed_payload))
817
- .unwrap();
818
-
819
- let result = route_grpc_request(registry, &config, request).await;
820
- assert!(result.is_err());
821
-
822
- let (status, message) = result.unwrap_err();
823
- assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
824
- assert!(message.contains("Message exceeds maximum size"));
825
- }
826
-
827
- #[tokio::test]
828
- async fn test_route_grpc_request_server_streaming_success() {
829
- struct StreamHandler;
830
-
831
- impl GrpcHandler for StreamHandler {
832
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
833
- Box::pin(async { Err(tonic::Status::unimplemented("Use server streaming")) })
834
- }
835
-
836
- fn service_name(&self) -> &str {
837
- "test.StreamService"
838
- }
839
-
840
- fn call_server_stream(
841
- &self,
842
- _request: GrpcRequestData,
843
- ) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
844
- Box::pin(async {
845
- let messages = vec![Bytes::from("m1"), Bytes::from("m2")];
846
- Ok(message_stream_from_vec(messages))
847
- })
848
- }
849
- }
850
-
851
- let mut registry = GrpcRegistry::new();
852
- registry.register_service("test.StreamService", Arc::new(StreamHandler), RpcMode::ServerStreaming);
853
- let registry = Arc::new(registry);
854
- let config = GrpcConfig::default();
855
-
856
- let request = Request::builder()
857
- .uri("/test.StreamService/Stream")
858
- .header("content-type", "application/grpc")
859
- .body(Body::from(framed_message(b"ignored")))
860
- .unwrap();
861
-
862
- let response = route_grpc_request(registry, &config, request).await.unwrap();
863
- assert_eq!(response.status(), StatusCode::OK);
864
- assert_eq!(
865
- response.headers().get("content-type").unwrap(),
866
- "application/grpc+proto"
867
- );
868
- assert!(
869
- response.headers().get("grpc-status").is_none(),
870
- "server-streaming responses must not predeclare grpc-status"
871
- );
872
-
873
- let collected = response.into_body().collect().await.unwrap();
874
- let trailers = collected.trailers().expect("grpc trailers");
875
- let grpc_status = trailers.get("grpc-status").unwrap().to_str().unwrap().to_string();
876
- let grpc_message = trailers.get("grpc-message").unwrap().to_str().unwrap().to_string();
877
- let body = collected.to_bytes();
878
- assert!(!body.is_empty());
879
- assert_eq!(grpc_status, "0");
880
- assert_eq!(grpc_message, "OK");
881
- }
882
-
883
- #[tokio::test]
884
- async fn test_route_grpc_request_client_streaming_success_and_response_metadata() {
885
- struct ClientStreamHandler;
886
-
887
- impl GrpcHandler for ClientStreamHandler {
888
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
889
- Box::pin(async { Err(tonic::Status::unimplemented("Use client streaming")) })
890
- }
891
-
892
- fn service_name(&self) -> &str {
893
- "test.ClientStreamService"
894
- }
895
-
896
- fn call_client_stream(
897
- &self,
898
- mut request: StreamingRequest,
899
- ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
900
- Box::pin(async move {
901
- // Make sure routing copied request headers into metadata.
902
- assert!(request.metadata.get("x-request-id").is_some());
903
-
904
- let mut first = None;
905
- while let Some(item) = request.message_stream.next().await {
906
- first = Some(item?);
907
- break;
908
- }
909
-
910
- let payload = first.unwrap_or_else(Bytes::new);
911
- let mut metadata = MetadataMap::new();
912
- metadata.insert("x-response-id", "resp-123".parse().unwrap());
913
-
914
- Ok(GrpcResponseData { payload, metadata })
915
- })
916
- }
917
- }
918
-
919
- let mut registry = GrpcRegistry::new();
920
- registry.register_service(
921
- "test.ClientStreamService",
922
- Arc::new(ClientStreamHandler),
923
- RpcMode::ClientStreaming,
924
- );
925
- let registry = Arc::new(registry);
926
- let config = GrpcConfig::default();
927
-
928
- // Single gRPC frame: compression=0, length=5, message="hello"
929
- let frame = vec![
930
- 0x00, // compression: no
931
- 0x00, 0x00, 0x00, 0x05, // length: 5 bytes
932
- b'h', b'e', b'l', b'l', b'o',
933
- ];
934
-
935
- let request = Request::builder()
936
- .uri("/test.ClientStreamService/ClientStream")
937
- .header("content-type", "application/grpc")
938
- .header("x-request-id", "req-123")
939
- .body(Body::from(frame))
940
- .unwrap();
941
-
942
- let response = route_grpc_request(registry, &config, request).await.unwrap();
943
- assert_eq!(response.status(), StatusCode::OK);
944
- assert_eq!(response.headers().get("grpc-status").unwrap(), "0");
945
- assert_eq!(response.headers().get("x-response-id").unwrap(), "resp-123");
946
-
947
- let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
948
- let payload = parse_unary_grpc_message(&body, 1024, None, true).unwrap();
949
- assert_eq!(payload, Bytes::from_static(b"hello"));
950
- }
951
-
952
- #[tokio::test]
953
- async fn test_route_grpc_request_bidi_streaming_success() {
954
- struct BidiHandler;
955
-
956
- impl GrpcHandler for BidiHandler {
957
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
958
- Box::pin(async { Err(tonic::Status::unimplemented("Use bidi streaming")) })
959
- }
960
-
961
- fn service_name(&self) -> &str {
962
- "test.BidiService"
963
- }
964
-
965
- fn call_bidi_stream(
966
- &self,
967
- _request: StreamingRequest,
968
- ) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
969
- Box::pin(async {
970
- let messages = vec![Bytes::from("r1")];
971
- Ok(message_stream_from_vec(messages))
972
- })
973
- }
974
- }
975
-
976
- let mut registry = GrpcRegistry::new();
977
- registry.register_service(
978
- "test.BidiService",
979
- Arc::new(BidiHandler),
980
- RpcMode::BidirectionalStreaming,
981
- );
982
- let registry = Arc::new(registry);
983
- let config = GrpcConfig::default();
984
-
985
- // Provide a well-formed request frame so the frame parser succeeds.
986
- let frame = vec![
987
- 0x00, // compression: no
988
- 0x00, 0x00, 0x00, 0x01, // length: 1 byte
989
- b'x',
990
- ];
991
-
992
- let request = Request::builder()
993
- .uri("/test.BidiService/Chat")
994
- .header("content-type", "application/grpc")
995
- .body(Body::from(frame))
996
- .unwrap();
997
-
998
- let response = route_grpc_request(registry, &config, request).await.unwrap();
999
- assert_eq!(response.status(), StatusCode::OK);
1000
- assert!(
1001
- response.headers().get("grpc-status").is_none(),
1002
- "bidi-streaming responses must not predeclare grpc-status"
1003
- );
1004
-
1005
- let collected = response.into_body().collect().await.unwrap();
1006
- let trailers = collected.trailers().expect("grpc trailers");
1007
- let grpc_status = trailers.get("grpc-status").unwrap().to_str().unwrap().to_string();
1008
- let grpc_message = trailers.get("grpc-message").unwrap().to_str().unwrap().to_string();
1009
- let body = collected.to_bytes();
1010
- assert!(!body.is_empty());
1011
- assert_eq!(grpc_status, "0");
1012
- assert_eq!(grpc_message, "OK");
1013
- }
1014
-
1015
- #[tokio::test]
1016
- async fn test_route_grpc_request_client_streaming_unsupported_encoding_maps_to_501() {
1017
- // Use a handler that would succeed, but the request is invalid before handler gets called.
1018
- struct NeverCalledHandler;
1019
-
1020
- impl GrpcHandler for NeverCalledHandler {
1021
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
1022
- Box::pin(async { Err(tonic::Status::unimplemented("not used")) })
1023
- }
1024
-
1025
- fn service_name(&self) -> &str {
1026
- "test.BadClientStreamService"
1027
- }
1028
-
1029
- fn call_client_stream(
1030
- &self,
1031
- _request: StreamingRequest,
1032
- ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
1033
- Box::pin(async {
1034
- Ok(GrpcResponseData {
1035
- payload: Bytes::from_static(b"ok"),
1036
- metadata: MetadataMap::new(),
1037
- })
1038
- })
1039
- }
1040
- }
1041
-
1042
- let mut registry = GrpcRegistry::new();
1043
- registry.register_service(
1044
- "test.BadClientStreamService",
1045
- Arc::new(NeverCalledHandler),
1046
- RpcMode::ClientStreaming,
1047
- );
1048
- let registry = Arc::new(registry);
1049
- let config = GrpcConfig::default();
1050
-
1051
- // Compression flag = 1 with unsupported grpc-encoding => UNIMPLEMENTED.
1052
- let frame = vec![
1053
- 0x01, // compression: yes
1054
- 0x00, 0x00, 0x00, 0x01, // length: 1 byte
1055
- b'x',
1056
- ];
1057
-
1058
- let request = Request::builder()
1059
- .uri("/test.BadClientStreamService/ClientStream")
1060
- .header("content-type", "application/grpc")
1061
- .header("grpc-encoding", "br")
1062
- .body(Body::from(frame))
1063
- .unwrap();
1064
-
1065
- let err = route_grpc_request(registry, &config, request).await.unwrap_err();
1066
- assert_eq!(err.0, StatusCode::NOT_IMPLEMENTED);
1067
- assert!(err.1.contains("Unsupported grpc-encoding"));
1068
- }
1069
-
1070
- #[tokio::test]
1071
- async fn test_route_grpc_request_client_streaming_missing_encoding_header_maps_to_400() {
1072
- struct NeverCalledHandler;
1073
-
1074
- impl GrpcHandler for NeverCalledHandler {
1075
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
1076
- Box::pin(async { Err(tonic::Status::unimplemented("not used")) })
1077
- }
1078
-
1079
- fn service_name(&self) -> &str {
1080
- "test.BadClientStreamService"
1081
- }
1082
-
1083
- fn call_client_stream(
1084
- &self,
1085
- _request: StreamingRequest,
1086
- ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
1087
- Box::pin(async {
1088
- Ok(GrpcResponseData {
1089
- payload: Bytes::from_static(b"ok"),
1090
- metadata: MetadataMap::new(),
1091
- })
1092
- })
1093
- }
1094
- }
1095
-
1096
- let mut registry = GrpcRegistry::new();
1097
- registry.register_service(
1098
- "test.BadClientStreamService",
1099
- Arc::new(NeverCalledHandler),
1100
- RpcMode::ClientStreaming,
1101
- );
1102
- let registry = Arc::new(registry);
1103
- let config = GrpcConfig::default();
1104
-
1105
- // Compression flag = 1 with no grpc-encoding => INVALID_ARGUMENT.
1106
- let frame = vec![
1107
- 0x01, // compression: yes
1108
- 0x00, 0x00, 0x00, 0x01, // length: 1 byte
1109
- b'x',
1110
- ];
1111
-
1112
- let request = Request::builder()
1113
- .uri("/test.BadClientStreamService/ClientStream")
1114
- .header("content-type", "application/grpc")
1115
- .body(Body::from(frame))
1116
- .unwrap();
1117
-
1118
- let err = route_grpc_request(registry, &config, request).await.unwrap_err();
1119
- assert_eq!(err.0, StatusCode::BAD_REQUEST);
1120
- assert!(err.1.contains("missing grpc-encoding"));
1121
- }
1122
-
1123
- #[tokio::test]
1124
- async fn test_route_grpc_request_client_streaming_gzip_success() {
1125
- struct ClientStreamHandler;
1126
-
1127
- impl GrpcHandler for ClientStreamHandler {
1128
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
1129
- Box::pin(async { Err(tonic::Status::unimplemented("Use client streaming")) })
1130
- }
1131
-
1132
- fn service_name(&self) -> &str {
1133
- "test.CompressedClientStreamService"
1134
- }
1135
-
1136
- fn call_client_stream(
1137
- &self,
1138
- mut request: StreamingRequest,
1139
- ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
1140
- Box::pin(async move {
1141
- let payload = request
1142
- .message_stream
1143
- .next()
1144
- .await
1145
- .transpose()?
1146
- .unwrap_or_else(Bytes::new);
1147
- Ok(GrpcResponseData {
1148
- payload,
1149
- metadata: MetadataMap::new(),
1150
- })
1151
- })
1152
- }
1153
- }
1154
-
1155
- let mut registry = GrpcRegistry::new();
1156
- registry.register_service(
1157
- "test.CompressedClientStreamService",
1158
- Arc::new(ClientStreamHandler),
1159
- RpcMode::ClientStreaming,
1160
- );
1161
- let registry = Arc::new(registry);
1162
- let config = GrpcConfig::default();
1163
-
1164
- let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1165
- encoder.write_all(b"hello").unwrap();
1166
- let compressed_payload = encoder.finish().unwrap();
1167
-
1168
- let mut frame = Vec::new();
1169
- frame.push(0x01); // compression: yes
1170
- frame.extend_from_slice(&(compressed_payload.len() as u32).to_be_bytes());
1171
- frame.extend_from_slice(&compressed_payload);
1172
-
1173
- let request = Request::builder()
1174
- .uri("/test.CompressedClientStreamService/ClientStream")
1175
- .header("content-type", "application/grpc")
1176
- .header("grpc-encoding", "gzip")
1177
- .body(Body::from(frame))
1178
- .unwrap();
1179
-
1180
- let response = route_grpc_request(registry, &config, request).await.unwrap();
1181
- assert_eq!(response.status(), StatusCode::OK);
1182
- let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
1183
- let payload = parse_unary_grpc_message(&body, 1024, None, true).unwrap();
1184
- assert_eq!(payload, Bytes::from_static(b"hello"));
1185
- }
1186
-
1187
- #[tokio::test]
1188
- async fn test_route_grpc_request_client_streaming_gzip_when_compression_disabled_maps_to_501() {
1189
- struct NeverCalledHandler;
1190
-
1191
- impl GrpcHandler for NeverCalledHandler {
1192
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
1193
- Box::pin(async { Err(tonic::Status::unimplemented("not used")) })
1194
- }
1195
-
1196
- fn service_name(&self) -> &str {
1197
- "test.DisabledCompressionService"
1198
- }
1199
-
1200
- fn call_client_stream(
1201
- &self,
1202
- _request: StreamingRequest,
1203
- ) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
1204
- Box::pin(async {
1205
- Ok(GrpcResponseData {
1206
- payload: Bytes::from_static(b"ok"),
1207
- metadata: MetadataMap::new(),
1208
- })
1209
- })
1210
- }
1211
- }
1212
-
1213
- let mut registry = GrpcRegistry::new();
1214
- registry.register_service(
1215
- "test.DisabledCompressionService",
1216
- Arc::new(NeverCalledHandler),
1217
- RpcMode::ClientStreaming,
1218
- );
1219
- let registry = Arc::new(registry);
1220
- let mut config = GrpcConfig::default();
1221
- config.enable_compression = false;
1222
-
1223
- let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
1224
- encoder.write_all(b"hello").unwrap();
1225
- let compressed_payload = encoder.finish().unwrap();
1226
-
1227
- let mut frame = Vec::new();
1228
- frame.push(0x01); // compression: yes
1229
- frame.extend_from_slice(&(compressed_payload.len() as u32).to_be_bytes());
1230
- frame.extend_from_slice(&compressed_payload);
1231
-
1232
- let request = Request::builder()
1233
- .uri("/test.DisabledCompressionService/ClientStream")
1234
- .header("content-type", "application/grpc")
1235
- .header("grpc-encoding", "gzip")
1236
- .body(Body::from(frame))
1237
- .unwrap();
1238
-
1239
- let err = route_grpc_request(registry, &config, request).await.unwrap_err();
1240
- assert_eq!(err.0, StatusCode::NOT_IMPLEMENTED);
1241
- assert!(err.1.contains("disabled"));
1242
- }
1243
- }