spikard 0.12.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.
- checksums.yaml +4 -4
- data/Steepfile +6 -0
- data/ext/spikard_rb/extconf.rb +1 -2
- data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +897 -451
- data/ext/spikard_rb/native/Cargo.toml +24 -0
- data/ext/spikard_rb/src/lib.rs +5366 -3
- data/lib/spikard/native.rb +86 -0
- data/lib/spikard/version.rb +6 -1
- data/lib/spikard.rb +8 -45
- data/lib/spikard_rb.so +0 -0
- data/sig/types.rbs +427 -0
- metadata +14 -242
- data/LICENSE +0 -1
- data/README.md +0 -267
- data/ext/spikard_rb/Cargo.toml +0 -17
- data/lib/spikard/app.rb +0 -428
- data/lib/spikard/background.rb +0 -58
- data/lib/spikard/config.rb +0 -506
- data/lib/spikard/converters.rb +0 -13
- data/lib/spikard/grpc.rb +0 -182
- data/lib/spikard/handler_wrapper.rb +0 -113
- data/lib/spikard/provide.rb +0 -214
- data/lib/spikard/response.rb +0 -173
- data/lib/spikard/schema.rb +0 -243
- data/lib/spikard/sse.rb +0 -111
- data/lib/spikard/streaming_response.rb +0 -44
- data/lib/spikard/testing.rb +0 -432
- data/lib/spikard/upload_file.rb +0 -131
- data/lib/spikard/websocket.rb +0 -59
- data/sig/spikard.rbs +0 -719
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -80
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
- data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
- data/vendor/crates/spikard-core/Cargo.toml +0 -60
- data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
- data/vendor/crates/spikard-core/src/debug.rs +0 -127
- data/vendor/crates/spikard-core/src/di/container.rs +0 -702
- data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
- data/vendor/crates/spikard-core/src/di/error.rs +0 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +0 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
- data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
- data/vendor/crates/spikard-core/src/di/value.rs +0 -282
- data/vendor/crates/spikard-core/src/errors.rs +0 -72
- data/vendor/crates/spikard-core/src/http.rs +0 -492
- data/vendor/crates/spikard-core/src/lib.rs +0 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
- data/vendor/crates/spikard-core/src/metadata.rs +0 -378
- data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
- data/vendor/crates/spikard-core/src/problem.rs +0 -358
- data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
- data/vendor/crates/spikard-core/src/router.rs +0 -530
- data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
- data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
- data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
- data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
- data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
- data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
- data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
- data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
- data/vendor/crates/spikard-http/Cargo.toml +0 -87
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
- data/vendor/crates/spikard-http/src/auth.rs +0 -301
- data/vendor/crates/spikard-http/src/background.rs +0 -1860
- data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
- data/vendor/crates/spikard-http/src/cors.rs +0 -1026
- data/vendor/crates/spikard-http/src/debug.rs +0 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
- data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -469
- data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1122
- data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -434
- data/vendor/crates/spikard-http/src/grpc/service.rs +0 -622
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
- data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
- data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
- data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -58
- data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
- data/vendor/crates/spikard-http/src/lib.rs +0 -548
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
- data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
- data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
- data/vendor/crates/spikard-http/src/response.rs +0 -720
- data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -858
- data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
- data/vendor/crates/spikard-http/src/server/mod.rs +0 -1649
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
- data/vendor/crates/spikard-http/src/sse.rs +0 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
- data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -787
- data/vendor/crates/spikard-http/src/testing.rs +0 -617
- data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
- data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
- data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
- data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
- data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
- data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
- data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
- data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
- data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -974
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
- data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
- data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -314
- data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -200
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
- data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
- data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
- data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
- data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
- data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -421
- data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
- data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
- data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
- data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
- data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
- data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
- data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
- data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
- data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
- data/vendor/crates/spikard-rb/Cargo.toml +0 -68
- data/vendor/crates/spikard-rb/build.rs +0 -200
- data/vendor/crates/spikard-rb/src/background.rs +0 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
- data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
- data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +0 -375
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -834
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
- data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
- data/vendor/crates/spikard-rb/src/handler.rs +0 -699
- data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
- data/vendor/crates/spikard-rb/src/lib.rs +0 -2264
- data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -303
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
- data/vendor/crates/spikard-rb/src/request.rs +0 -439
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -344
- data/vendor/crates/spikard-rb/src/server.rs +0 -307
- data/vendor/crates/spikard-rb/src/sse.rs +0 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
- data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
- data/vendor/crates/spikard-rb/src/websocket.rs +0 -475
- data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -25
- data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
|
@@ -1,858 +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::{GrpcConfig, GrpcRegistry, RpcMode, parse_grpc_path};
|
|
7
|
-
use axum::body::Body;
|
|
8
|
-
use axum::http::{Request, Response, StatusCode};
|
|
9
|
-
use std::sync::Arc;
|
|
10
|
-
|
|
11
|
-
/// Convert gRPC status code to HTTP status code
|
|
12
|
-
///
|
|
13
|
-
/// Maps all gRPC status codes to appropriate HTTP status codes
|
|
14
|
-
/// following the gRPC-HTTP status code mapping specification.
|
|
15
|
-
fn grpc_status_to_http(code: tonic::Code) -> StatusCode {
|
|
16
|
-
match code {
|
|
17
|
-
tonic::Code::Ok => StatusCode::OK,
|
|
18
|
-
tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap(), // Client Closed Request
|
|
19
|
-
tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
|
|
20
|
-
tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
|
|
21
|
-
tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
|
|
22
|
-
tonic::Code::NotFound => StatusCode::NOT_FOUND,
|
|
23
|
-
tonic::Code::AlreadyExists => StatusCode::CONFLICT,
|
|
24
|
-
tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
|
|
25
|
-
tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
|
|
26
|
-
tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
|
|
27
|
-
tonic::Code::Aborted => StatusCode::CONFLICT,
|
|
28
|
-
tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
|
|
29
|
-
tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
|
|
30
|
-
tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
|
31
|
-
tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
|
|
32
|
-
tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
|
|
33
|
-
tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/// Route a gRPC request to the appropriate handler
|
|
38
|
-
///
|
|
39
|
-
/// Parses the request path to extract service and method names,
|
|
40
|
-
/// looks up the handler in the registry, and invokes it.
|
|
41
|
-
///
|
|
42
|
-
/// # Arguments
|
|
43
|
-
///
|
|
44
|
-
/// * `registry` - gRPC handler registry
|
|
45
|
-
/// * `config` - gRPC configuration with size limits
|
|
46
|
-
/// * `request` - The incoming gRPC request
|
|
47
|
-
///
|
|
48
|
-
/// # Returns
|
|
49
|
-
///
|
|
50
|
-
/// A future that resolves to a gRPC response or error
|
|
51
|
-
pub async fn route_grpc_request(
|
|
52
|
-
registry: Arc<GrpcRegistry>,
|
|
53
|
-
config: &GrpcConfig,
|
|
54
|
-
request: Request<Body>,
|
|
55
|
-
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
56
|
-
// Extract the request path
|
|
57
|
-
let path = request.uri().path();
|
|
58
|
-
|
|
59
|
-
// Parse service and method names from the path
|
|
60
|
-
let (service_name, method_name) = match parse_grpc_path(path) {
|
|
61
|
-
Ok(names) => names,
|
|
62
|
-
Err(status) => {
|
|
63
|
-
return Err((
|
|
64
|
-
StatusCode::BAD_REQUEST,
|
|
65
|
-
format!("Invalid gRPC path: {}", status.message()),
|
|
66
|
-
));
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Look up the handler for this service
|
|
71
|
-
let (handler, rpc_mode) = match registry.get(&service_name) {
|
|
72
|
-
Some((h, mode)) => (h, mode),
|
|
73
|
-
None => {
|
|
74
|
-
return Err((StatusCode::NOT_FOUND, format!("Service not found: {}", service_name)));
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Create the service bridge
|
|
79
|
-
let service = crate::grpc::GenericGrpcService::new(handler);
|
|
80
|
-
|
|
81
|
-
// Dispatch based on RPC mode
|
|
82
|
-
match rpc_mode {
|
|
83
|
-
RpcMode::Unary => {
|
|
84
|
-
handle_unary_request(service, service_name, method_name, config.max_message_size, request).await
|
|
85
|
-
}
|
|
86
|
-
RpcMode::ServerStreaming => {
|
|
87
|
-
handle_server_streaming_request(service, service_name, method_name, config.max_message_size, request).await
|
|
88
|
-
}
|
|
89
|
-
RpcMode::ClientStreaming => {
|
|
90
|
-
handle_client_streaming_request(service, service_name, method_name, config.max_message_size, request).await
|
|
91
|
-
}
|
|
92
|
-
RpcMode::BidirectionalStreaming => {
|
|
93
|
-
handle_bidirectional_streaming_request(service, service_name, method_name, config.max_message_size, request)
|
|
94
|
-
.await
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Handle a unary RPC request
|
|
100
|
-
async fn handle_unary_request(
|
|
101
|
-
service: crate::grpc::GenericGrpcService,
|
|
102
|
-
service_name: String,
|
|
103
|
-
method_name: String,
|
|
104
|
-
max_message_size: usize,
|
|
105
|
-
request: Request<Body>,
|
|
106
|
-
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
107
|
-
// Convert the Axum request to bytes with the configured size limit
|
|
108
|
-
let (parts, body) = request.into_parts();
|
|
109
|
-
let body_bytes = match axum::body::to_bytes(body, max_message_size).await {
|
|
110
|
-
Ok(bytes) => bytes,
|
|
111
|
-
Err(e) => {
|
|
112
|
-
// Check if error is due to size limit (axum returns "body size exceeded" or similar)
|
|
113
|
-
let error_msg = e.to_string();
|
|
114
|
-
if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
|
|
115
|
-
return Err((
|
|
116
|
-
StatusCode::PAYLOAD_TOO_LARGE,
|
|
117
|
-
format!("Message exceeds maximum size of {} bytes", max_message_size),
|
|
118
|
-
));
|
|
119
|
-
}
|
|
120
|
-
return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Create a Tonic request
|
|
125
|
-
let mut tonic_request = tonic::Request::new(body_bytes);
|
|
126
|
-
|
|
127
|
-
// Copy headers to Tonic metadata
|
|
128
|
-
for (key, value) in parts.headers.iter() {
|
|
129
|
-
if let Ok(value_str) = value.to_str()
|
|
130
|
-
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
131
|
-
&& let Ok(metadata_key) = key
|
|
132
|
-
.as_str()
|
|
133
|
-
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
134
|
-
{
|
|
135
|
-
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Use the service bridge to handle the request
|
|
140
|
-
let tonic_response = match service.handle_unary(service_name, method_name, tonic_request).await {
|
|
141
|
-
Ok(resp) => resp,
|
|
142
|
-
Err(status) => {
|
|
143
|
-
let status_code = grpc_status_to_http(status.code());
|
|
144
|
-
return Err((status_code, status.message().to_string()));
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Convert Tonic response to Axum response
|
|
149
|
-
let payload = tonic_response.get_ref().clone();
|
|
150
|
-
let metadata = tonic_response.metadata();
|
|
151
|
-
|
|
152
|
-
let mut response = Response::builder()
|
|
153
|
-
.status(StatusCode::OK)
|
|
154
|
-
.header("content-type", "application/grpc+proto");
|
|
155
|
-
|
|
156
|
-
// Copy metadata to response headers
|
|
157
|
-
for key_value in metadata.iter() {
|
|
158
|
-
if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
|
|
159
|
-
&& let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
|
|
160
|
-
{
|
|
161
|
-
response = response.header(key.as_str(), header_value);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Add gRPC status trailer (success)
|
|
166
|
-
response = response.header("grpc-status", "0");
|
|
167
|
-
|
|
168
|
-
// Convert bytes::Bytes to Body
|
|
169
|
-
let response = response.body(Body::from(payload)).map_err(|e| {
|
|
170
|
-
(
|
|
171
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
172
|
-
format!("Failed to build response: {}", e),
|
|
173
|
-
)
|
|
174
|
-
})?;
|
|
175
|
-
|
|
176
|
-
Ok(response)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// Handle a server streaming RPC request
|
|
180
|
-
async fn handle_server_streaming_request(
|
|
181
|
-
service: crate::grpc::GenericGrpcService,
|
|
182
|
-
service_name: String,
|
|
183
|
-
method_name: String,
|
|
184
|
-
max_message_size: usize,
|
|
185
|
-
request: Request<Body>,
|
|
186
|
-
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
187
|
-
// Convert the Axum request to bytes with the configured size limit
|
|
188
|
-
let (parts, body) = request.into_parts();
|
|
189
|
-
let body_bytes = match axum::body::to_bytes(body, max_message_size).await {
|
|
190
|
-
Ok(bytes) => bytes,
|
|
191
|
-
Err(e) => {
|
|
192
|
-
// Check if error is due to size limit (axum returns "body size exceeded" or similar)
|
|
193
|
-
let error_msg = e.to_string();
|
|
194
|
-
if error_msg.contains("body") || error_msg.contains("size") || error_msg.contains("exceeded") {
|
|
195
|
-
return Err((
|
|
196
|
-
StatusCode::PAYLOAD_TOO_LARGE,
|
|
197
|
-
format!("Message exceeds maximum size of {} bytes", max_message_size),
|
|
198
|
-
));
|
|
199
|
-
}
|
|
200
|
-
return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// Create a Tonic request
|
|
205
|
-
let mut tonic_request = tonic::Request::new(body_bytes);
|
|
206
|
-
|
|
207
|
-
// Copy headers to Tonic metadata
|
|
208
|
-
for (key, value) in parts.headers.iter() {
|
|
209
|
-
if let Ok(value_str) = value.to_str()
|
|
210
|
-
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
211
|
-
&& let Ok(metadata_key) = key
|
|
212
|
-
.as_str()
|
|
213
|
-
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
214
|
-
{
|
|
215
|
-
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Use the service bridge to handle the streaming request
|
|
220
|
-
let tonic_response = match service
|
|
221
|
-
.handle_server_stream(service_name, method_name, tonic_request)
|
|
222
|
-
.await
|
|
223
|
-
{
|
|
224
|
-
Ok(resp) => resp,
|
|
225
|
-
Err(status) => {
|
|
226
|
-
let status_code = grpc_status_to_http(status.code());
|
|
227
|
-
return Err((status_code, status.message().to_string()));
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Convert Tonic response to Axum response
|
|
232
|
-
let body = tonic_response.into_inner();
|
|
233
|
-
let response = Response::builder()
|
|
234
|
-
.status(StatusCode::OK)
|
|
235
|
-
.header("content-type", "application/grpc+proto");
|
|
236
|
-
|
|
237
|
-
let response = response.body(body).map_err(|e| {
|
|
238
|
-
(
|
|
239
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
240
|
-
format!("Failed to build response: {}", e),
|
|
241
|
-
)
|
|
242
|
-
})?;
|
|
243
|
-
|
|
244
|
-
Ok(response)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/// Handle a client streaming RPC request
|
|
248
|
-
async fn handle_client_streaming_request(
|
|
249
|
-
service: crate::grpc::GenericGrpcService,
|
|
250
|
-
service_name: String,
|
|
251
|
-
method_name: String,
|
|
252
|
-
max_message_size: usize,
|
|
253
|
-
request: Request<Body>,
|
|
254
|
-
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
255
|
-
// Extract request parts - keep body as stream for frame parsing
|
|
256
|
-
let (parts, body) = request.into_parts();
|
|
257
|
-
|
|
258
|
-
// Create a Tonic request with streaming body
|
|
259
|
-
// Body will be parsed by service.handle_client_stream using frame parser
|
|
260
|
-
let mut tonic_request = tonic::Request::new(body);
|
|
261
|
-
|
|
262
|
-
// Copy headers to Tonic metadata
|
|
263
|
-
for (key, value) in parts.headers.iter() {
|
|
264
|
-
if let Ok(value_str) = value.to_str()
|
|
265
|
-
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
266
|
-
&& let Ok(metadata_key) = key
|
|
267
|
-
.as_str()
|
|
268
|
-
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
269
|
-
{
|
|
270
|
-
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Use the service bridge to handle the client streaming request
|
|
275
|
-
// Frame parsing and size validation happens in handle_client_stream
|
|
276
|
-
let tonic_response = match service
|
|
277
|
-
.handle_client_stream(service_name, method_name, tonic_request, max_message_size)
|
|
278
|
-
.await
|
|
279
|
-
{
|
|
280
|
-
Ok(resp) => resp,
|
|
281
|
-
Err(status) => {
|
|
282
|
-
let status_code = grpc_status_to_http(status.code());
|
|
283
|
-
return Err((status_code, status.message().to_string()));
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// Convert Tonic response to Axum response
|
|
288
|
-
let payload = tonic_response.get_ref().clone();
|
|
289
|
-
let metadata = tonic_response.metadata();
|
|
290
|
-
|
|
291
|
-
let mut response = Response::builder()
|
|
292
|
-
.status(StatusCode::OK)
|
|
293
|
-
.header("content-type", "application/grpc+proto");
|
|
294
|
-
|
|
295
|
-
// Copy metadata to response headers
|
|
296
|
-
for key_value in metadata.iter() {
|
|
297
|
-
if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
|
|
298
|
-
&& let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
|
|
299
|
-
{
|
|
300
|
-
response = response.header(key.as_str(), header_value);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Add gRPC status trailer (success)
|
|
305
|
-
response = response.header("grpc-status", "0");
|
|
306
|
-
|
|
307
|
-
// Convert bytes::Bytes to Body
|
|
308
|
-
let response = response.body(Body::from(payload)).map_err(|e| {
|
|
309
|
-
(
|
|
310
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
311
|
-
format!("Failed to build response: {}", e),
|
|
312
|
-
)
|
|
313
|
-
})?;
|
|
314
|
-
|
|
315
|
-
Ok(response)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/// Handle a bidirectional streaming RPC request
|
|
319
|
-
///
|
|
320
|
-
/// Bidirectional streaming allows both client and server to send multiple messages.
|
|
321
|
-
/// This function:
|
|
322
|
-
/// 1. Keeps the request body as a stream (not converting to bytes)
|
|
323
|
-
/// 2. Copies HTTP headers to gRPC metadata
|
|
324
|
-
/// 3. Passes the streaming body to the service for frame parsing
|
|
325
|
-
/// 4. Returns the response stream from the handler
|
|
326
|
-
///
|
|
327
|
-
/// # Arguments
|
|
328
|
-
///
|
|
329
|
-
/// * `service` - The GenericGrpcService containing the handler
|
|
330
|
-
/// * `service_name` - Fully qualified service name (e.g., "mypackage.ChatService")
|
|
331
|
-
/// * `method_name` - Method name (e.g., "Chat")
|
|
332
|
-
/// * `max_message_size` - Maximum size per message in bytes
|
|
333
|
-
/// * `request` - Axum HTTP request with streaming body
|
|
334
|
-
///
|
|
335
|
-
/// # Returns
|
|
336
|
-
///
|
|
337
|
-
/// Response with streaming body containing response messages, or error with status code
|
|
338
|
-
async fn handle_bidirectional_streaming_request(
|
|
339
|
-
service: crate::grpc::GenericGrpcService,
|
|
340
|
-
service_name: String,
|
|
341
|
-
method_name: String,
|
|
342
|
-
max_message_size: usize,
|
|
343
|
-
request: Request<Body>,
|
|
344
|
-
) -> Result<Response<Body>, (StatusCode, String)> {
|
|
345
|
-
// Extract request parts - keep body as stream for frame parsing
|
|
346
|
-
let (parts, body) = request.into_parts();
|
|
347
|
-
|
|
348
|
-
// Create a Tonic request with streaming body
|
|
349
|
-
// Body will be parsed by service.handle_bidi_stream using frame parser
|
|
350
|
-
let mut tonic_request = tonic::Request::new(body);
|
|
351
|
-
|
|
352
|
-
// Copy HTTP headers to gRPC metadata
|
|
353
|
-
for (key, value) in parts.headers.iter() {
|
|
354
|
-
if let Ok(value_str) = value.to_str()
|
|
355
|
-
&& let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
|
356
|
-
&& let Ok(metadata_key) = key
|
|
357
|
-
.as_str()
|
|
358
|
-
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
|
359
|
-
{
|
|
360
|
-
tonic_request.metadata_mut().insert(metadata_key, metadata_value);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Call service handler - frame parsing and size validation happens inside
|
|
365
|
-
let tonic_response = match service
|
|
366
|
-
.handle_bidi_stream(service_name, method_name, tonic_request, max_message_size)
|
|
367
|
-
.await
|
|
368
|
-
{
|
|
369
|
-
Ok(response) => response,
|
|
370
|
-
Err(status) => {
|
|
371
|
-
let status_code = grpc_status_to_http(status.code());
|
|
372
|
-
return Err((status_code, status.message().to_string()));
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// Convert Tonic response to Axum response with streaming body
|
|
377
|
-
let body = tonic_response.into_inner();
|
|
378
|
-
let response = Response::builder()
|
|
379
|
-
.status(StatusCode::OK)
|
|
380
|
-
.header("content-type", "application/grpc+proto");
|
|
381
|
-
|
|
382
|
-
let response = response.body(body).map_err(|e| {
|
|
383
|
-
(
|
|
384
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
385
|
-
format!("Failed to build response: {}", e),
|
|
386
|
-
)
|
|
387
|
-
})?;
|
|
388
|
-
|
|
389
|
-
Ok(response)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/// Check if an incoming request is a gRPC request
|
|
393
|
-
///
|
|
394
|
-
/// Returns true if the request has a content-type starting with "application/grpc"
|
|
395
|
-
pub fn is_grpc_request(request: &Request<Body>) -> bool {
|
|
396
|
-
request
|
|
397
|
-
.headers()
|
|
398
|
-
.get(axum::http::header::CONTENT_TYPE)
|
|
399
|
-
.and_then(|v| v.to_str().ok())
|
|
400
|
-
.map(|v| v.starts_with("application/grpc"))
|
|
401
|
-
.unwrap_or(false)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
#[cfg(test)]
|
|
405
|
-
mod tests {
|
|
406
|
-
use super::*;
|
|
407
|
-
use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData, RpcMode};
|
|
408
|
-
use crate::grpc::streaming::{MessageStream, StreamingRequest, message_stream_from_vec};
|
|
409
|
-
use bytes::Bytes;
|
|
410
|
-
use futures_util::StreamExt;
|
|
411
|
-
use std::future::Future;
|
|
412
|
-
use std::pin::Pin;
|
|
413
|
-
use tonic::metadata::MetadataMap;
|
|
414
|
-
|
|
415
|
-
struct EchoHandler;
|
|
416
|
-
|
|
417
|
-
impl GrpcHandler for EchoHandler {
|
|
418
|
-
fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
419
|
-
Box::pin(async move {
|
|
420
|
-
Ok(GrpcResponseData {
|
|
421
|
-
payload: request.payload,
|
|
422
|
-
metadata: tonic::metadata::MetadataMap::new(),
|
|
423
|
-
})
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
fn service_name(&self) -> &str {
|
|
428
|
-
"test.EchoService"
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
#[tokio::test]
|
|
433
|
-
async fn test_route_grpc_request_success() {
|
|
434
|
-
let mut registry = GrpcRegistry::new();
|
|
435
|
-
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
436
|
-
let registry = Arc::new(registry);
|
|
437
|
-
let config = GrpcConfig::default();
|
|
438
|
-
|
|
439
|
-
let request = Request::builder()
|
|
440
|
-
.uri("/test.EchoService/Echo")
|
|
441
|
-
.header("content-type", "application/grpc")
|
|
442
|
-
.body(Body::from(Bytes::from("test payload")))
|
|
443
|
-
.unwrap();
|
|
444
|
-
|
|
445
|
-
let result = route_grpc_request(registry, &config, request).await;
|
|
446
|
-
assert!(result.is_ok());
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
#[tokio::test]
|
|
450
|
-
async fn test_route_grpc_request_service_not_found() {
|
|
451
|
-
let registry = Arc::new(GrpcRegistry::new());
|
|
452
|
-
let config = GrpcConfig::default();
|
|
453
|
-
|
|
454
|
-
let request = Request::builder()
|
|
455
|
-
.uri("/nonexistent.Service/Method")
|
|
456
|
-
.header("content-type", "application/grpc")
|
|
457
|
-
.body(Body::from(Bytes::new()))
|
|
458
|
-
.unwrap();
|
|
459
|
-
|
|
460
|
-
let result = route_grpc_request(registry, &config, request).await;
|
|
461
|
-
assert!(result.is_err());
|
|
462
|
-
|
|
463
|
-
let (status, message) = result.unwrap_err();
|
|
464
|
-
assert_eq!(status, StatusCode::NOT_FOUND);
|
|
465
|
-
assert!(message.contains("Service not found"));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
#[tokio::test]
|
|
469
|
-
async fn test_route_grpc_request_invalid_path() {
|
|
470
|
-
let registry = Arc::new(GrpcRegistry::new());
|
|
471
|
-
let config = GrpcConfig::default();
|
|
472
|
-
|
|
473
|
-
let request = Request::builder()
|
|
474
|
-
.uri("/invalid")
|
|
475
|
-
.header("content-type", "application/grpc")
|
|
476
|
-
.body(Body::from(Bytes::new()))
|
|
477
|
-
.unwrap();
|
|
478
|
-
|
|
479
|
-
let result = route_grpc_request(registry, &config, request).await;
|
|
480
|
-
assert!(result.is_err());
|
|
481
|
-
|
|
482
|
-
let (status, _message) = result.unwrap_err();
|
|
483
|
-
assert_eq!(status, StatusCode::BAD_REQUEST);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
#[test]
|
|
487
|
-
fn test_is_grpc_request_true() {
|
|
488
|
-
let request = Request::builder()
|
|
489
|
-
.header("content-type", "application/grpc")
|
|
490
|
-
.body(Body::empty())
|
|
491
|
-
.unwrap();
|
|
492
|
-
|
|
493
|
-
assert!(is_grpc_request(&request));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
#[test]
|
|
497
|
-
fn test_is_grpc_request_with_subtype() {
|
|
498
|
-
let request = Request::builder()
|
|
499
|
-
.header("content-type", "application/grpc+proto")
|
|
500
|
-
.body(Body::empty())
|
|
501
|
-
.unwrap();
|
|
502
|
-
|
|
503
|
-
assert!(is_grpc_request(&request));
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
#[test]
|
|
507
|
-
fn test_is_grpc_request_false() {
|
|
508
|
-
let request = Request::builder()
|
|
509
|
-
.header("content-type", "application/json")
|
|
510
|
-
.body(Body::empty())
|
|
511
|
-
.unwrap();
|
|
512
|
-
|
|
513
|
-
assert!(!is_grpc_request(&request));
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
#[test]
|
|
517
|
-
fn test_is_grpc_request_no_content_type() {
|
|
518
|
-
let request = Request::builder().body(Body::empty()).unwrap();
|
|
519
|
-
|
|
520
|
-
assert!(!is_grpc_request(&request));
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
#[test]
|
|
524
|
-
fn test_grpc_status_to_http_mappings() {
|
|
525
|
-
// Test all gRPC status codes map correctly
|
|
526
|
-
assert_eq!(grpc_status_to_http(tonic::Code::Ok), StatusCode::OK);
|
|
527
|
-
assert_eq!(
|
|
528
|
-
grpc_status_to_http(tonic::Code::Cancelled),
|
|
529
|
-
StatusCode::from_u16(499).unwrap()
|
|
530
|
-
);
|
|
531
|
-
assert_eq!(
|
|
532
|
-
grpc_status_to_http(tonic::Code::Unknown),
|
|
533
|
-
StatusCode::INTERNAL_SERVER_ERROR
|
|
534
|
-
);
|
|
535
|
-
assert_eq!(
|
|
536
|
-
grpc_status_to_http(tonic::Code::InvalidArgument),
|
|
537
|
-
StatusCode::BAD_REQUEST
|
|
538
|
-
);
|
|
539
|
-
assert_eq!(
|
|
540
|
-
grpc_status_to_http(tonic::Code::DeadlineExceeded),
|
|
541
|
-
StatusCode::GATEWAY_TIMEOUT
|
|
542
|
-
);
|
|
543
|
-
assert_eq!(grpc_status_to_http(tonic::Code::NotFound), StatusCode::NOT_FOUND);
|
|
544
|
-
assert_eq!(grpc_status_to_http(tonic::Code::AlreadyExists), StatusCode::CONFLICT);
|
|
545
|
-
assert_eq!(
|
|
546
|
-
grpc_status_to_http(tonic::Code::PermissionDenied),
|
|
547
|
-
StatusCode::FORBIDDEN
|
|
548
|
-
);
|
|
549
|
-
assert_eq!(
|
|
550
|
-
grpc_status_to_http(tonic::Code::ResourceExhausted),
|
|
551
|
-
StatusCode::TOO_MANY_REQUESTS
|
|
552
|
-
);
|
|
553
|
-
assert_eq!(
|
|
554
|
-
grpc_status_to_http(tonic::Code::FailedPrecondition),
|
|
555
|
-
StatusCode::BAD_REQUEST
|
|
556
|
-
);
|
|
557
|
-
assert_eq!(grpc_status_to_http(tonic::Code::Aborted), StatusCode::CONFLICT);
|
|
558
|
-
assert_eq!(grpc_status_to_http(tonic::Code::OutOfRange), StatusCode::BAD_REQUEST);
|
|
559
|
-
assert_eq!(
|
|
560
|
-
grpc_status_to_http(tonic::Code::Unimplemented),
|
|
561
|
-
StatusCode::NOT_IMPLEMENTED
|
|
562
|
-
);
|
|
563
|
-
assert_eq!(
|
|
564
|
-
grpc_status_to_http(tonic::Code::Internal),
|
|
565
|
-
StatusCode::INTERNAL_SERVER_ERROR
|
|
566
|
-
);
|
|
567
|
-
assert_eq!(
|
|
568
|
-
grpc_status_to_http(tonic::Code::Unavailable),
|
|
569
|
-
StatusCode::SERVICE_UNAVAILABLE
|
|
570
|
-
);
|
|
571
|
-
assert_eq!(
|
|
572
|
-
grpc_status_to_http(tonic::Code::DataLoss),
|
|
573
|
-
StatusCode::INTERNAL_SERVER_ERROR
|
|
574
|
-
);
|
|
575
|
-
assert_eq!(
|
|
576
|
-
grpc_status_to_http(tonic::Code::Unauthenticated),
|
|
577
|
-
StatusCode::UNAUTHORIZED
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
#[tokio::test]
|
|
582
|
-
async fn test_route_grpc_request_with_custom_max_message_size() {
|
|
583
|
-
let mut registry = GrpcRegistry::new();
|
|
584
|
-
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
585
|
-
let registry = Arc::new(registry);
|
|
586
|
-
|
|
587
|
-
let mut config = GrpcConfig::default();
|
|
588
|
-
config.max_message_size = 100; // Set small limit
|
|
589
|
-
|
|
590
|
-
let request = Request::builder()
|
|
591
|
-
.uri("/test.EchoService/Echo")
|
|
592
|
-
.header("content-type", "application/grpc")
|
|
593
|
-
.body(Body::from(Bytes::from("test payload")))
|
|
594
|
-
.unwrap();
|
|
595
|
-
|
|
596
|
-
// This should succeed since payload is small
|
|
597
|
-
let result = route_grpc_request(registry.clone(), &config, request).await;
|
|
598
|
-
assert!(result.is_ok());
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
#[tokio::test]
|
|
602
|
-
async fn test_route_grpc_request_exceeds_max_message_size() {
|
|
603
|
-
let mut registry = GrpcRegistry::new();
|
|
604
|
-
registry.register("test.EchoService", Arc::new(EchoHandler), RpcMode::Unary);
|
|
605
|
-
let registry = Arc::new(registry);
|
|
606
|
-
|
|
607
|
-
let mut config = GrpcConfig::default();
|
|
608
|
-
config.max_message_size = 10; // Set very small limit
|
|
609
|
-
|
|
610
|
-
// Create a large payload that exceeds the limit
|
|
611
|
-
let large_payload = vec![b'x'; 1000];
|
|
612
|
-
let request = Request::builder()
|
|
613
|
-
.uri("/test.EchoService/Echo")
|
|
614
|
-
.header("content-type", "application/grpc")
|
|
615
|
-
.body(Body::from(Bytes::from(large_payload)))
|
|
616
|
-
.unwrap();
|
|
617
|
-
|
|
618
|
-
let result = route_grpc_request(registry, &config, request).await;
|
|
619
|
-
assert!(result.is_err());
|
|
620
|
-
|
|
621
|
-
let (status, message) = result.unwrap_err();
|
|
622
|
-
assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
|
|
623
|
-
assert!(message.contains("Message exceeds maximum size"));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
#[tokio::test]
|
|
627
|
-
async fn test_route_grpc_request_server_streaming_success() {
|
|
628
|
-
struct StreamHandler;
|
|
629
|
-
|
|
630
|
-
impl GrpcHandler for StreamHandler {
|
|
631
|
-
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
632
|
-
Box::pin(async { Err(tonic::Status::unimplemented("Use server streaming")) })
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
fn service_name(&self) -> &str {
|
|
636
|
-
"test.StreamService"
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
fn call_server_stream(
|
|
640
|
-
&self,
|
|
641
|
-
_request: GrpcRequestData,
|
|
642
|
-
) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
|
|
643
|
-
Box::pin(async {
|
|
644
|
-
let messages = vec![Bytes::from("m1"), Bytes::from("m2")];
|
|
645
|
-
Ok(message_stream_from_vec(messages))
|
|
646
|
-
})
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
let mut registry = GrpcRegistry::new();
|
|
651
|
-
registry.register("test.StreamService", Arc::new(StreamHandler), RpcMode::ServerStreaming);
|
|
652
|
-
let registry = Arc::new(registry);
|
|
653
|
-
let config = GrpcConfig::default();
|
|
654
|
-
|
|
655
|
-
let request = Request::builder()
|
|
656
|
-
.uri("/test.StreamService/Stream")
|
|
657
|
-
.header("content-type", "application/grpc")
|
|
658
|
-
.body(Body::from(Bytes::from("ignored")))
|
|
659
|
-
.unwrap();
|
|
660
|
-
|
|
661
|
-
let response = route_grpc_request(registry, &config, request).await.unwrap();
|
|
662
|
-
assert_eq!(response.status(), StatusCode::OK);
|
|
663
|
-
assert_eq!(
|
|
664
|
-
response.headers().get("content-type").unwrap(),
|
|
665
|
-
"application/grpc+proto"
|
|
666
|
-
);
|
|
667
|
-
assert!(
|
|
668
|
-
response.headers().get("grpc-status").is_none(),
|
|
669
|
-
"server-streaming responses must not predeclare grpc-status"
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
// The body is a stream. For test purposes, just ensure we can read it.
|
|
673
|
-
let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
|
|
674
|
-
assert!(!body.is_empty());
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
#[tokio::test]
|
|
678
|
-
async fn test_route_grpc_request_client_streaming_success_and_response_metadata() {
|
|
679
|
-
struct ClientStreamHandler;
|
|
680
|
-
|
|
681
|
-
impl GrpcHandler for ClientStreamHandler {
|
|
682
|
-
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
683
|
-
Box::pin(async { Err(tonic::Status::unimplemented("Use client streaming")) })
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
fn service_name(&self) -> &str {
|
|
687
|
-
"test.ClientStreamService"
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
fn call_client_stream(
|
|
691
|
-
&self,
|
|
692
|
-
mut request: StreamingRequest,
|
|
693
|
-
) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
|
|
694
|
-
Box::pin(async move {
|
|
695
|
-
// Make sure routing copied request headers into metadata.
|
|
696
|
-
assert!(request.metadata.get("x-request-id").is_some());
|
|
697
|
-
|
|
698
|
-
let mut first = None;
|
|
699
|
-
while let Some(item) = request.message_stream.next().await {
|
|
700
|
-
first = Some(item?);
|
|
701
|
-
break;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
let payload = first.unwrap_or_else(Bytes::new);
|
|
705
|
-
let mut metadata = MetadataMap::new();
|
|
706
|
-
metadata.insert("x-response-id", "resp-123".parse().unwrap());
|
|
707
|
-
|
|
708
|
-
Ok(GrpcResponseData { payload, metadata })
|
|
709
|
-
})
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
let mut registry = GrpcRegistry::new();
|
|
714
|
-
registry.register(
|
|
715
|
-
"test.ClientStreamService",
|
|
716
|
-
Arc::new(ClientStreamHandler),
|
|
717
|
-
RpcMode::ClientStreaming,
|
|
718
|
-
);
|
|
719
|
-
let registry = Arc::new(registry);
|
|
720
|
-
let config = GrpcConfig::default();
|
|
721
|
-
|
|
722
|
-
// Single gRPC frame: compression=0, length=5, message="hello"
|
|
723
|
-
let frame = vec![
|
|
724
|
-
0x00, // compression: no
|
|
725
|
-
0x00, 0x00, 0x00, 0x05, // length: 5 bytes
|
|
726
|
-
b'h', b'e', b'l', b'l', b'o',
|
|
727
|
-
];
|
|
728
|
-
|
|
729
|
-
let request = Request::builder()
|
|
730
|
-
.uri("/test.ClientStreamService/ClientStream")
|
|
731
|
-
.header("content-type", "application/grpc")
|
|
732
|
-
.header("x-request-id", "req-123")
|
|
733
|
-
.body(Body::from(frame))
|
|
734
|
-
.unwrap();
|
|
735
|
-
|
|
736
|
-
let response = route_grpc_request(registry, &config, request).await.unwrap();
|
|
737
|
-
assert_eq!(response.status(), StatusCode::OK);
|
|
738
|
-
assert!(
|
|
739
|
-
response.headers().get("grpc-status").is_none(),
|
|
740
|
-
"bidi-streaming responses must not predeclare grpc-status"
|
|
741
|
-
);
|
|
742
|
-
assert_eq!(response.headers().get("x-response-id").unwrap(), "resp-123");
|
|
743
|
-
|
|
744
|
-
let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
|
|
745
|
-
assert_eq!(body, Bytes::from_static(b"hello"));
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
#[tokio::test]
|
|
749
|
-
async fn test_route_grpc_request_bidi_streaming_success() {
|
|
750
|
-
struct BidiHandler;
|
|
751
|
-
|
|
752
|
-
impl GrpcHandler for BidiHandler {
|
|
753
|
-
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
754
|
-
Box::pin(async { Err(tonic::Status::unimplemented("Use bidi streaming")) })
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
fn service_name(&self) -> &str {
|
|
758
|
-
"test.BidiService"
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
fn call_bidi_stream(
|
|
762
|
-
&self,
|
|
763
|
-
_request: StreamingRequest,
|
|
764
|
-
) -> Pin<Box<dyn Future<Output = Result<MessageStream, tonic::Status>> + Send>> {
|
|
765
|
-
Box::pin(async {
|
|
766
|
-
let messages = vec![Bytes::from("r1")];
|
|
767
|
-
Ok(message_stream_from_vec(messages))
|
|
768
|
-
})
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
let mut registry = GrpcRegistry::new();
|
|
773
|
-
registry.register(
|
|
774
|
-
"test.BidiService",
|
|
775
|
-
Arc::new(BidiHandler),
|
|
776
|
-
RpcMode::BidirectionalStreaming,
|
|
777
|
-
);
|
|
778
|
-
let registry = Arc::new(registry);
|
|
779
|
-
let config = GrpcConfig::default();
|
|
780
|
-
|
|
781
|
-
// Provide a well-formed request frame so the frame parser succeeds.
|
|
782
|
-
let frame = vec![
|
|
783
|
-
0x00, // compression: no
|
|
784
|
-
0x00, 0x00, 0x00, 0x01, // length: 1 byte
|
|
785
|
-
b'x',
|
|
786
|
-
];
|
|
787
|
-
|
|
788
|
-
let request = Request::builder()
|
|
789
|
-
.uri("/test.BidiService/Chat")
|
|
790
|
-
.header("content-type", "application/grpc")
|
|
791
|
-
.body(Body::from(frame))
|
|
792
|
-
.unwrap();
|
|
793
|
-
|
|
794
|
-
let response = route_grpc_request(registry, &config, request).await.unwrap();
|
|
795
|
-
assert_eq!(response.status(), StatusCode::OK);
|
|
796
|
-
assert!(
|
|
797
|
-
response.headers().get("grpc-status").is_none(),
|
|
798
|
-
"bidi-streaming responses must not predeclare grpc-status"
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
let body = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
|
|
802
|
-
assert!(!body.is_empty());
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
#[tokio::test]
|
|
806
|
-
async fn test_route_grpc_request_client_streaming_invalid_compression_flag_maps_to_501() {
|
|
807
|
-
// Use a handler that would succeed, but the request is invalid before handler gets called.
|
|
808
|
-
struct NeverCalledHandler;
|
|
809
|
-
|
|
810
|
-
impl GrpcHandler for NeverCalledHandler {
|
|
811
|
-
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
812
|
-
Box::pin(async { Err(tonic::Status::unimplemented("not used")) })
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
fn service_name(&self) -> &str {
|
|
816
|
-
"test.BadClientStreamService"
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
fn call_client_stream(
|
|
820
|
-
&self,
|
|
821
|
-
_request: StreamingRequest,
|
|
822
|
-
) -> Pin<Box<dyn Future<Output = Result<GrpcResponseData, tonic::Status>> + Send>> {
|
|
823
|
-
Box::pin(async {
|
|
824
|
-
Ok(GrpcResponseData {
|
|
825
|
-
payload: Bytes::from_static(b"ok"),
|
|
826
|
-
metadata: MetadataMap::new(),
|
|
827
|
-
})
|
|
828
|
-
})
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
let mut registry = GrpcRegistry::new();
|
|
833
|
-
registry.register(
|
|
834
|
-
"test.BadClientStreamService",
|
|
835
|
-
Arc::new(NeverCalledHandler),
|
|
836
|
-
RpcMode::ClientStreaming,
|
|
837
|
-
);
|
|
838
|
-
let registry = Arc::new(registry);
|
|
839
|
-
let config = GrpcConfig::default();
|
|
840
|
-
|
|
841
|
-
// Compression flag = 1 => UNIMPLEMENTED per parser.
|
|
842
|
-
let frame = vec![
|
|
843
|
-
0x01, // compression: yes (unsupported)
|
|
844
|
-
0x00, 0x00, 0x00, 0x01, // length: 1 byte
|
|
845
|
-
b'x',
|
|
846
|
-
];
|
|
847
|
-
|
|
848
|
-
let request = Request::builder()
|
|
849
|
-
.uri("/test.BadClientStreamService/ClientStream")
|
|
850
|
-
.header("content-type", "application/grpc")
|
|
851
|
-
.body(Body::from(frame))
|
|
852
|
-
.unwrap();
|
|
853
|
-
|
|
854
|
-
let err = route_grpc_request(registry, &config, request).await.unwrap_err();
|
|
855
|
-
assert_eq!(err.0, StatusCode::NOT_IMPLEMENTED);
|
|
856
|
-
assert!(err.1.contains("compression") || err.1.contains("supported"));
|
|
857
|
-
}
|
|
858
|
-
}
|