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,653 +0,0 @@
1
- //! HTTP/2 gRPC frame parsing for client streaming support
2
- //!
3
- //! This module provides parsing of gRPC messages from HTTP/2 request bodies.
4
- //! gRPC frames are structured according to RFC 9109 (gRPC over HTTP/2):
5
- //!
6
- //! ```text
7
- //! +----------+----------+-+-+-+-+-+-+-+-+
8
- //! |Compression| Length |
9
- //! | Flags (1) | (4 bytes) |
10
- //! +----------+----------+-+-+-+-+-+-+-+-+-+-+
11
- //! | |
12
- //! | Serialized Message (N bytes) |
13
- //! | |
14
- //! +-------------------------------------+
15
- //! ```
16
- //!
17
- //! The compression flag indicates whether the message is compressed (1 = compressed, 0 = uncompressed).
18
- //! The length is encoded as a big-endian u32, indicating the size of the message bytes.
19
- //!
20
- //! # Protocol Details
21
- //!
22
- //! - **Compression Flag**: 1 byte, value 0 or 1
23
- //! - **Message Length**: 4 bytes, big-endian u32, maximum 4GB
24
- //! - **Message Data**: N bytes, where N is the length from the header
25
- //!
26
- //! # Stream Processing
27
- //!
28
- //! The parser processes the HTTP/2 body stream by:
29
- //! 1. Reading the 5-byte frame header (compression flag + length)
30
- //! 2. Parsing the length as big-endian u32
31
- //! 3. Validating the length against `max_message_size`
32
- //! 4. Reading the message bytes
33
- //! 5. Yielding the message
34
- //! 6. Repeating until the body is exhausted
35
- //!
36
- //! # Error Handling
37
- //!
38
- //! The parser returns gRPC status codes according to RFC 9110:
39
- //! - `INTERNAL`: Protocol parsing errors (incomplete frames, read errors)
40
- //! - `RESOURCE_EXHAUSTED`: Message size exceeds limit
41
- //! - `UNIMPLEMENTED`: Unsupported compression algorithm or compression disabled
42
- //!
43
- //! # Example
44
- //!
45
- //! ```ignore
46
- //! use spikard_http::grpc::framing::parse_grpc_client_stream;
47
- //! use axum::body::Body;
48
- //! use bytes::Bytes;
49
- //! use futures_util::StreamExt;
50
- //!
51
- //! let body = Body::from("...");
52
- //! let max_size = 4 * 1024 * 1024; // 4MB
53
- //! let mut stream = parse_grpc_client_stream(body, max_size, None, true).await?;
54
- //!
55
- //! while let Some(result) = stream.next().await {
56
- //! match result {
57
- //! Ok(message) => println!("Message: {:?}", message),
58
- //! Err(status) => eprintln!("Error: {}", status),
59
- //! }
60
- //! }
61
- //! ```
62
-
63
- use bytes::{Buf, BufMut, Bytes, BytesMut};
64
- use flate2::read::GzDecoder;
65
- use futures_util::stream;
66
- use std::io::Read;
67
- use tonic::Status;
68
-
69
- use super::streaming::MessageStream;
70
-
71
- /// Size of the gRPC message header in bytes.
72
- ///
73
- /// The header consists of:
74
- /// - 1 byte compression flag
75
- /// - 4 bytes big-endian message length
76
- pub const GRPC_MESSAGE_HEADER_LEN: usize = 5;
77
-
78
- /// Parses a unary gRPC payload from framed HTTP/2 body bytes.
79
- ///
80
- /// Unary and server-streaming requests must carry exactly one framed message.
81
- pub fn parse_unary_grpc_message(
82
- framed_body: &[u8],
83
- max_message_size: usize,
84
- grpc_encoding: Option<&str>,
85
- compression_enabled: bool,
86
- ) -> Result<Bytes, Status> {
87
- let messages = parse_all_frames(
88
- BytesMut::from(framed_body),
89
- max_message_size,
90
- grpc_encoding,
91
- compression_enabled,
92
- )?;
93
-
94
- match messages.len() {
95
- 1 => Ok(messages.into_iter().next().expect("single message exists")),
96
- count => Err(Status::invalid_argument(format!(
97
- "Unary gRPC request must contain exactly one message frame, got {}",
98
- count
99
- ))),
100
- }
101
- }
102
-
103
- /// Encodes a protobuf payload into a single gRPC message frame.
104
- pub fn encode_grpc_message(payload: Bytes) -> Result<Bytes, Status> {
105
- let message_length = u32::try_from(payload.len())
106
- .map_err(|_| Status::resource_exhausted("gRPC message exceeds 4GB frame length limit"))?;
107
-
108
- let mut framed = BytesMut::with_capacity(GRPC_MESSAGE_HEADER_LEN + payload.len());
109
- framed.put_u8(0); // compression flag: uncompressed
110
- framed.put_u32(message_length);
111
- framed.extend_from_slice(&payload);
112
-
113
- Ok(framed.freeze())
114
- }
115
-
116
- /// Parses an HTTP/2 gRPC request body as a stream of messages
117
- ///
118
- /// Reads the gRPC frame format from the body stream, validating each frame
119
- /// and yielding individual message bytes.
120
- ///
121
- /// # Arguments
122
- ///
123
- /// * `body` - The HTTP/2 request body stream
124
- /// * `max_message_size` - Maximum allowed message size in bytes (validated per message)
125
- /// * `grpc_encoding` - Value of the request `grpc-encoding` header, if present
126
- /// * `compression_enabled` - Whether compressed gRPC payloads are allowed
127
- ///
128
- /// # Returns
129
- ///
130
- /// A `MessageStream` yielding:
131
- /// - `Ok(Bytes)`: A complete parsed message
132
- /// - `Err(Status)`: A gRPC protocol error
133
- ///
134
- /// # Errors
135
- ///
136
- /// Returns gRPC errors for:
137
- /// - Incomplete frame (EOF before 5-byte header): `INTERNAL`
138
- /// - Incomplete message (EOF before all message bytes): `INTERNAL`
139
- /// - Message size > max_message_size: `RESOURCE_EXHAUSTED`
140
- /// - Compression flag set without valid `grpc-encoding`: `INVALID_ARGUMENT`
141
- /// - Unsupported or disabled compression: `UNIMPLEMENTED`
142
- /// - Read errors from the body stream: `INTERNAL`
143
- ///
144
- /// # Example
145
- ///
146
- /// ```ignore
147
- /// let body = Body::from(vec![
148
- /// 0x00, // compression: no
149
- /// 0x00, 0x00, 0x00, 0x05, // length: 5 bytes
150
- /// b'h', b'e', b'l', b'l', b'o', // message
151
- /// ]);
152
- ///
153
- /// let stream = parse_grpc_client_stream(body, 1024, None, true).await?;
154
- /// ```
155
- pub async fn parse_grpc_client_stream(
156
- body: axum::body::Body,
157
- max_message_size: usize,
158
- grpc_encoding: Option<&str>,
159
- compression_enabled: bool,
160
- ) -> Result<MessageStream, Status> {
161
- // Convert body into bytes
162
- let body_bytes = axum::body::to_bytes(body, usize::MAX)
163
- .await
164
- .map_err(|e| Status::internal(format!("Failed to read body: {}", e)))?;
165
-
166
- // Create a buffered reader
167
- let buffer = BytesMut::from(&body_bytes[..]);
168
-
169
- // Parse frames from the buffer
170
- let messages = parse_all_frames(buffer, max_message_size, grpc_encoding, compression_enabled)?;
171
-
172
- // Convert to a MessageStream
173
- Ok(Box::pin(stream::iter(messages.into_iter().map(Ok))))
174
- }
175
-
176
- /// Internal: Parse all frames from a buffer
177
- fn parse_all_frames(
178
- mut buffer: BytesMut,
179
- max_message_size: usize,
180
- grpc_encoding: Option<&str>,
181
- compression_enabled: bool,
182
- ) -> Result<Vec<Bytes>, Status> {
183
- let mut messages = Vec::new();
184
-
185
- while !buffer.is_empty() {
186
- // Check if we have enough bytes for the frame header
187
- if buffer.len() < GRPC_MESSAGE_HEADER_LEN {
188
- return Err(Status::internal(
189
- "Incomplete gRPC frame header: expected 5 bytes, got less",
190
- ));
191
- }
192
-
193
- // Read the compression flag (1 byte)
194
- let compression_flag = buffer[0];
195
- if compression_flag > 1 {
196
- return Err(Status::invalid_argument(format!(
197
- "Invalid gRPC compression flag: {}",
198
- compression_flag
199
- )));
200
- }
201
-
202
- // Read the message length (4 bytes, big-endian)
203
- let length_bytes = &buffer[1..GRPC_MESSAGE_HEADER_LEN];
204
- let message_length =
205
- u32::from_be_bytes([length_bytes[0], length_bytes[1], length_bytes[2], length_bytes[3]]) as usize;
206
-
207
- // Validate message length against max size
208
- if message_length > max_message_size {
209
- return Err(Status::resource_exhausted(format!(
210
- "Message size {} exceeds maximum allowed size of {}",
211
- message_length, max_message_size
212
- )));
213
- }
214
-
215
- // Check if we have the complete message
216
- let total_frame_size = GRPC_MESSAGE_HEADER_LEN + message_length;
217
- if buffer.len() < total_frame_size {
218
- return Err(Status::internal(
219
- "Incomplete gRPC message: expected more bytes than available",
220
- ));
221
- }
222
-
223
- // Extract the message bytes and decompress if needed.
224
- let message_bytes = &buffer[GRPC_MESSAGE_HEADER_LEN..total_frame_size];
225
- let message = if compression_flag == 0 {
226
- Bytes::copy_from_slice(message_bytes)
227
- } else {
228
- decompress_message(message_bytes, grpc_encoding, compression_enabled, max_message_size)?
229
- };
230
- messages.push(message);
231
-
232
- // Advance the buffer
233
- buffer.advance(total_frame_size);
234
- }
235
-
236
- Ok(messages)
237
- }
238
-
239
- fn decompress_message(
240
- message_bytes: &[u8],
241
- grpc_encoding: Option<&str>,
242
- compression_enabled: bool,
243
- max_message_size: usize,
244
- ) -> Result<Bytes, Status> {
245
- if !compression_enabled {
246
- return Err(Status::unimplemented(
247
- "gRPC message compression is disabled by server configuration",
248
- ));
249
- }
250
-
251
- let encoding = grpc_encoding
252
- .map(|value| value.trim().to_ascii_lowercase())
253
- .ok_or_else(|| Status::invalid_argument("Compressed gRPC message missing grpc-encoding header"))?;
254
-
255
- let decompressed = match encoding.as_str() {
256
- "gzip" => {
257
- let mut decoder = GzDecoder::new(message_bytes);
258
- let mut out = Vec::new();
259
- decoder
260
- .read_to_end(&mut out)
261
- .map_err(|e| Status::internal(format!("Failed to decompress gzip gRPC frame: {}", e)))?;
262
- out
263
- }
264
- "identity" => {
265
- return Err(Status::invalid_argument(
266
- "Compressed gRPC frame cannot use grpc-encoding=identity",
267
- ));
268
- }
269
- other => {
270
- return Err(Status::unimplemented(format!("Unsupported grpc-encoding '{}'", other)));
271
- }
272
- };
273
-
274
- if decompressed.len() > max_message_size {
275
- return Err(Status::resource_exhausted(format!(
276
- "Decompressed message size {} exceeds maximum allowed size of {}",
277
- decompressed.len(),
278
- max_message_size
279
- )));
280
- }
281
-
282
- Ok(Bytes::from(decompressed))
283
- }
284
-
285
- #[cfg(test)]
286
- mod tests {
287
- use super::*;
288
- use flate2::{Compression, write::GzEncoder};
289
- use futures_util::StreamExt;
290
- use std::io::Write;
291
-
292
- #[tokio::test]
293
- async fn test_single_frame_parsing() {
294
- // Frame: compression=0, length=5, message="hello"
295
- let frame = vec![
296
- 0x00, // compression: no
297
- 0x00, 0x00, 0x00, 0x05, // length: 5 bytes (big-endian)
298
- b'h', b'e', b'l', b'l', b'o', // message
299
- ];
300
-
301
- let body = axum::body::Body::from(frame);
302
- let mut stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
303
- let msg = stream.next().await;
304
-
305
- assert!(msg.is_some());
306
- assert!(msg.unwrap().is_ok());
307
- let result = stream.next().await;
308
- assert!(result.is_none());
309
- }
310
-
311
- #[test]
312
- fn test_encode_grpc_message_adds_framing_header() {
313
- let framed = encode_grpc_message(Bytes::from_static(b"hello")).unwrap();
314
-
315
- assert_eq!(framed[0], 0x00);
316
- assert_eq!(&framed[1..5], &[0x00, 0x00, 0x00, 0x05]);
317
- assert_eq!(&framed[5..], b"hello");
318
- }
319
-
320
- #[test]
321
- fn test_parse_unary_grpc_message_requires_exactly_one_frame() {
322
- let mut body = Vec::new();
323
- body.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, b'a']);
324
- body.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, b'b']);
325
-
326
- let err = parse_unary_grpc_message(&body, 1024, None, true).unwrap_err();
327
- assert_eq!(err.code(), tonic::Code::InvalidArgument);
328
- }
329
-
330
- #[tokio::test]
331
- async fn test_multiple_frames() {
332
- // Two frames back-to-back
333
- let mut frame = Vec::new();
334
-
335
- // Frame 1: "hello"
336
- frame.push(0x00);
337
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x05]);
338
- frame.extend_from_slice(b"hello");
339
-
340
- // Frame 2: "world"
341
- frame.push(0x00);
342
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x05]);
343
- frame.extend_from_slice(b"world");
344
-
345
- let body = axum::body::Body::from(frame);
346
- let mut stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
347
-
348
- let msg1 = stream.next().await;
349
- assert!(msg1.is_some());
350
- assert_eq!(msg1.unwrap().unwrap(), b"hello"[..]);
351
-
352
- let msg2 = stream.next().await;
353
- assert!(msg2.is_some());
354
- assert_eq!(msg2.unwrap().unwrap(), b"world"[..]);
355
-
356
- let msg3 = stream.next().await;
357
- assert!(msg3.is_none());
358
- }
359
-
360
- #[tokio::test]
361
- async fn test_empty_body() {
362
- let body = axum::body::Body::from(Vec::<u8>::new());
363
- let mut stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
364
-
365
- let result = stream.next().await;
366
- assert!(result.is_none());
367
- }
368
-
369
- #[tokio::test]
370
- async fn test_frame_size_at_limit() {
371
- let max_size = 10;
372
- let message = b"0123456789"; // exactly 10 bytes
373
-
374
- let mut frame = Vec::new();
375
- frame.push(0x00);
376
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x0a]); // length: 10
377
- frame.extend_from_slice(message);
378
-
379
- let body = axum::body::Body::from(frame);
380
- let mut stream = parse_grpc_client_stream(body, max_size, None, true).await.unwrap();
381
-
382
- let msg = stream.next().await;
383
- assert!(msg.is_some());
384
- assert_eq!(msg.unwrap().unwrap(), message[..]);
385
- }
386
-
387
- #[tokio::test]
388
- async fn test_frame_exceeds_limit() {
389
- let max_size = 5;
390
- let message = b"toolong"; // 7 bytes, exceeds 5-byte limit
391
-
392
- let mut frame = Vec::new();
393
- frame.push(0x00);
394
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x07]); // length: 7
395
- frame.extend_from_slice(message);
396
-
397
- let body = axum::body::Body::from(frame);
398
- let result = parse_grpc_client_stream(body, max_size, None, true).await;
399
-
400
- assert!(result.is_err());
401
- if let Err(status) = result {
402
- assert_eq!(status.code(), tonic::Code::ResourceExhausted);
403
- }
404
- }
405
-
406
- #[tokio::test]
407
- async fn test_incomplete_frame_header() {
408
- // Only 3 bytes of 5-byte header
409
- let frame = vec![0x00, 0x00, 0x00];
410
-
411
- let body = axum::body::Body::from(frame);
412
- let result = parse_grpc_client_stream(body, 1024, None, true).await;
413
-
414
- assert!(result.is_err());
415
- if let Err(status) = result {
416
- assert_eq!(status.code(), tonic::Code::Internal);
417
- }
418
- }
419
-
420
- #[tokio::test]
421
- async fn test_incomplete_frame_body() {
422
- // Header says 10 bytes but only provide 5
423
- let mut frame = Vec::new();
424
- frame.push(0x00);
425
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x0a]); // length: 10
426
- frame.extend_from_slice(b"short"); // only 5 bytes
427
-
428
- let body = axum::body::Body::from(frame);
429
- let result = parse_grpc_client_stream(body, 1024, None, true).await;
430
-
431
- assert!(result.is_err());
432
- if let Err(status) = result {
433
- assert_eq!(status.code(), tonic::Code::Internal);
434
- }
435
- }
436
-
437
- #[tokio::test]
438
- async fn test_compression_flag_set_with_missing_encoding_header() {
439
- let mut frame = Vec::new();
440
- frame.push(0x01); // compression: yes
441
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x05]);
442
- frame.extend_from_slice(b"hello");
443
-
444
- let body = axum::body::Body::from(frame);
445
- let result = parse_grpc_client_stream(body, 1024, None, true).await;
446
-
447
- assert!(result.is_err());
448
- if let Err(status) = result {
449
- assert_eq!(status.code(), tonic::Code::InvalidArgument);
450
- }
451
- }
452
-
453
- #[tokio::test]
454
- async fn test_compression_flag_set_with_unsupported_encoding() {
455
- let mut frame = Vec::new();
456
- frame.push(0x01); // compression: yes
457
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x05]);
458
- frame.extend_from_slice(b"hello");
459
-
460
- let body = axum::body::Body::from(frame);
461
- let result = parse_grpc_client_stream(body, 1024, Some("br"), true).await;
462
-
463
- assert!(result.is_err());
464
- if let Err(status) = result {
465
- assert_eq!(status.code(), tonic::Code::Unimplemented);
466
- assert!(status.message().contains("Unsupported grpc-encoding"));
467
- }
468
- }
469
-
470
- #[tokio::test]
471
- async fn test_compression_flag_set_when_compression_disabled() {
472
- let mut frame = Vec::new();
473
- frame.push(0x01); // compression: yes
474
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x05]);
475
- frame.extend_from_slice(b"hello");
476
-
477
- let body = axum::body::Body::from(frame);
478
- let result = parse_grpc_client_stream(body, 1024, Some("gzip"), false).await;
479
-
480
- assert!(result.is_err());
481
- if let Err(status) = result {
482
- assert_eq!(status.code(), tonic::Code::Unimplemented);
483
- assert!(status.message().contains("disabled"));
484
- }
485
- }
486
-
487
- #[tokio::test]
488
- async fn test_compression_flag_set_with_gzip_encoding_decompresses_message() {
489
- let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
490
- encoder.write_all(b"hello").unwrap();
491
- let compressed = encoder.finish().unwrap();
492
-
493
- let mut frame = Vec::new();
494
- frame.push(0x01); // compression: yes
495
- frame.extend_from_slice(&(compressed.len() as u32).to_be_bytes());
496
- frame.extend_from_slice(&compressed);
497
-
498
- let body = axum::body::Body::from(frame);
499
- let mut stream = parse_grpc_client_stream(body, 1024, Some("gzip"), true).await.unwrap();
500
-
501
- let msg = stream.next().await.unwrap().unwrap();
502
- assert_eq!(msg, Bytes::from_static(b"hello"));
503
- assert!(stream.next().await.is_none());
504
- }
505
-
506
- #[tokio::test]
507
- async fn test_large_message_length() {
508
- // Test with large length value (but within max_message_size for this test)
509
- let message = b"x".repeat(1000);
510
- let mut frame = Vec::new();
511
- frame.push(0x00);
512
- frame.extend_from_slice(&[0x00, 0x00, 0x03, 0xe8]); // length: 1000 (big-endian)
513
- frame.extend_from_slice(&message);
514
-
515
- let body = axum::body::Body::from(frame);
516
- let mut stream = parse_grpc_client_stream(body, 2000, None, true).await.unwrap();
517
-
518
- let msg = stream.next().await;
519
- assert!(msg.is_some());
520
- assert_eq!(msg.unwrap().unwrap().len(), 1000);
521
- }
522
-
523
- #[tokio::test]
524
- async fn test_zero_length_message() {
525
- // Frame with 0-byte message (valid in gRPC)
526
- let mut frame = Vec::new();
527
- frame.push(0x00);
528
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // length: 0
529
-
530
- let body = axum::body::Body::from(frame);
531
- let mut stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
532
-
533
- let msg = stream.next().await;
534
- assert!(msg.is_some());
535
- assert_eq!(msg.unwrap().unwrap().len(), 0);
536
- }
537
-
538
- #[tokio::test]
539
- async fn test_multiple_frames_with_mixed_sizes() {
540
- let mut frame = Vec::new();
541
-
542
- // Frame 1: "abc" (3 bytes)
543
- frame.push(0x00);
544
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x03]);
545
- frame.extend_from_slice(b"abc");
546
-
547
- // Frame 2: "defghij" (7 bytes)
548
- frame.push(0x00);
549
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x07]);
550
- frame.extend_from_slice(b"defghij");
551
-
552
- // Frame 3: "" (0 bytes)
553
- frame.push(0x00);
554
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
555
-
556
- // Frame 4: "x" (1 byte)
557
- frame.push(0x00);
558
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
559
- frame.extend_from_slice(b"x");
560
-
561
- let body = axum::body::Body::from(frame);
562
- let mut stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
563
-
564
- let msg1 = stream.next().await.unwrap().unwrap();
565
- assert_eq!(msg1, b"abc"[..]);
566
-
567
- let msg2 = stream.next().await.unwrap().unwrap();
568
- assert_eq!(msg2, b"defghij"[..]);
569
-
570
- let msg3 = stream.next().await.unwrap().unwrap();
571
- assert_eq!(msg3.len(), 0);
572
-
573
- let msg4 = stream.next().await.unwrap().unwrap();
574
- assert_eq!(msg4, b"x"[..]);
575
-
576
- let msg5 = stream.next().await;
577
- assert!(msg5.is_none());
578
- }
579
-
580
- #[test]
581
- fn test_big_endian_length_parsing() {
582
- // Test that length is correctly parsed as big-endian
583
- // Big-endian u32(256) = bytes [0x00, 0x00, 0x01, 0x00]
584
- let buffer = BytesMut::from(
585
- &[
586
- 0x00, // compression flag
587
- 0x00, 0x00, 0x01, 0x00, // length: 256 in big-endian
588
- ][..],
589
- );
590
-
591
- // Extract the 4-byte length manually to verify
592
- let length_bytes = &buffer[1..5];
593
- let length = u32::from_be_bytes([length_bytes[0], length_bytes[1], length_bytes[2], length_bytes[3]]);
594
-
595
- assert_eq!(length, 256);
596
- }
597
-
598
- #[test]
599
- fn test_big_endian_max_value() {
600
- // Test maximum u32 value in big-endian
601
- let buffer = BytesMut::from(
602
- &[
603
- 0x00, 0xff, 0xff, 0xff, 0xff, // max u32
604
- ][..],
605
- );
606
-
607
- let length_bytes = &buffer[1..5];
608
- let length = u32::from_be_bytes([length_bytes[0], length_bytes[1], length_bytes[2], length_bytes[3]]);
609
-
610
- assert_eq!(length, u32::MAX);
611
- }
612
-
613
- #[tokio::test]
614
- async fn test_error_message_includes_size_info() {
615
- let max_size = 100;
616
- let message = b"x".repeat(150);
617
-
618
- let mut frame = Vec::new();
619
- frame.push(0x00);
620
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x96]); // length: 150
621
- frame.extend_from_slice(&message);
622
-
623
- let body = axum::body::Body::from(frame);
624
- let result = parse_grpc_client_stream(body, max_size, None, true).await;
625
-
626
- assert!(result.is_err());
627
- if let Err(status) = result {
628
- assert!(status.message().contains("150"));
629
- assert!(status.message().contains("100"));
630
- }
631
- }
632
-
633
- #[tokio::test]
634
- async fn test_stream_collects_all_messages() {
635
- // Ensure that the returned stream properly yields all messages
636
- let mut frame = Vec::new();
637
-
638
- for i in 0..10 {
639
- frame.push(0x00);
640
- frame.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
641
- frame.push(b'0' + i as u8);
642
- }
643
-
644
- let body = axum::body::Body::from(frame);
645
- let stream = parse_grpc_client_stream(body, 1024, None, true).await.unwrap();
646
- let messages: Vec<_> = futures_util::StreamExt::collect(stream).await;
647
-
648
- assert_eq!(messages.len(), 10);
649
- for (i, msg) in messages.iter().enumerate() {
650
- assert_eq!(msg.as_ref().unwrap()[0], b'0' + i as u8);
651
- }
652
- }
653
- }