tonic-rails 0.1.0
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 +7 -0
- data/CHANGELOG.md +52 -0
- data/Cargo.lock +1242 -0
- data/Cargo.toml +11 -0
- data/LICENSE +21 -0
- data/README.md +391 -0
- data/ext/tonic_rails_native/Cargo.toml +44 -0
- data/ext/tonic_rails_native/extconf.rb +9 -0
- data/ext/tonic_rails_native/src/body.rs +289 -0
- data/ext/tonic_rails_native/src/dispatch.rs +250 -0
- data/ext/tonic_rails_native/src/framing.rs +349 -0
- data/ext/tonic_rails_native/src/gvl.rs +473 -0
- data/ext/tonic_rails_native/src/lib.rs +90 -0
- data/ext/tonic_rails_native/src/net.rs +146 -0
- data/ext/tonic_rails_native/src/server.rs +507 -0
- data/lib/tonic/rails/base.rb +215 -0
- data/lib/tonic/rails/configuration.rb +149 -0
- data/lib/tonic/rails/railtie.rb +56 -0
- data/lib/tonic/rails/reflection.rb +50 -0
- data/lib/tonic/rails/router.rb +71 -0
- data/lib/tonic/rails/rpc_error.rb +65 -0
- data/lib/tonic/rails/server.rb +224 -0
- data/lib/tonic/rails/tasks/tonic-rails.rake +36 -0
- data/lib/tonic/rails/testing.rb +85 -0
- data/lib/tonic/rails/version.rb +7 -0
- data/lib/tonic-rails.rb +91 -0
- metadata +121 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
//! gRPC response bodies and request accounting.
|
|
2
|
+
//!
|
|
3
|
+
//! gRPC carries `grpc-status` in trailing HEADERS, so responses use
|
|
4
|
+
//! custom `http_body::Body` implementations that yield data frames
|
|
5
|
+
//! followed by a trailers frame.
|
|
6
|
+
|
|
7
|
+
use std::future::Future;
|
|
8
|
+
use std::pin::Pin;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
11
|
+
use std::task::{Context, Poll};
|
|
12
|
+
use std::time::Instant;
|
|
13
|
+
|
|
14
|
+
use bytes::Bytes;
|
|
15
|
+
use http::HeaderMap;
|
|
16
|
+
use http_body::{Body, Frame};
|
|
17
|
+
use tokio::sync::mpsc;
|
|
18
|
+
use tonic::{Code, Status};
|
|
19
|
+
|
|
20
|
+
use crate::framing::{COMPRESSION_THRESHOLD, frame, sanitize_for_log, status_trailers};
|
|
21
|
+
use crate::gvl::StreamMessage;
|
|
22
|
+
|
|
23
|
+
/// Request counters, exposed to Ruby via `Server#stats`.
|
|
24
|
+
#[derive(Default)]
|
|
25
|
+
pub struct ServerStats {
|
|
26
|
+
pub total: AtomicUsize,
|
|
27
|
+
pub successful: AtomicUsize,
|
|
28
|
+
pub errors: AtomicUsize,
|
|
29
|
+
pub cancelled: AtomicUsize,
|
|
30
|
+
pub deadline_exceeded: AtomicUsize,
|
|
31
|
+
pub resource_exhausted: AtomicUsize,
|
|
32
|
+
pub unimplemented: AtomicUsize,
|
|
33
|
+
/// Log every request (including OK) to stderr. Errors are always logged.
|
|
34
|
+
pub log_requests: AtomicBool,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl ServerStats {
|
|
38
|
+
pub fn record(&self, path: &str, code: u32, message: &str, start: Instant) {
|
|
39
|
+
let counter = match Code::from_i32(code as i32) {
|
|
40
|
+
Code::Ok => &self.successful,
|
|
41
|
+
Code::Cancelled => &self.cancelled,
|
|
42
|
+
_ => &self.errors,
|
|
43
|
+
};
|
|
44
|
+
counter.fetch_add(1, Ordering::Relaxed);
|
|
45
|
+
match Code::from_i32(code as i32) {
|
|
46
|
+
Code::DeadlineExceeded => {
|
|
47
|
+
self.deadline_exceeded.fetch_add(1, Ordering::Relaxed);
|
|
48
|
+
}
|
|
49
|
+
Code::ResourceExhausted => {
|
|
50
|
+
self.resource_exhausted.fetch_add(1, Ordering::Relaxed);
|
|
51
|
+
}
|
|
52
|
+
Code::Unimplemented => {
|
|
53
|
+
self.unimplemented.fetch_add(1, Ordering::Relaxed);
|
|
54
|
+
}
|
|
55
|
+
_ => {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let is_error = !matches!(Code::from_i32(code as i32), Code::Ok | Code::Cancelled);
|
|
59
|
+
if is_error || self.log_requests.load(Ordering::Relaxed) {
|
|
60
|
+
let path = sanitize_for_log(path);
|
|
61
|
+
let dur_us = start.elapsed().as_micros();
|
|
62
|
+
if message.is_empty() {
|
|
63
|
+
eprintln!("tonic-rails: path={path} code={code} duration_us={dur_us}");
|
|
64
|
+
} else {
|
|
65
|
+
let message = sanitize_for_log(message);
|
|
66
|
+
eprintln!("tonic-rails: path={path} code={code} duration_us={dur_us} message={message}");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Trailers-only error response for requests rejected before dispatch.
|
|
73
|
+
pub struct ErrorBody {
|
|
74
|
+
trailers: Option<HeaderMap>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl ErrorBody {
|
|
78
|
+
pub fn new(code: Code, message: &str) -> Self {
|
|
79
|
+
Self {
|
|
80
|
+
trailers: Some(status_trailers(code as u32, message)),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
impl Body for ErrorBody {
|
|
86
|
+
type Data = Bytes;
|
|
87
|
+
type Error = Status;
|
|
88
|
+
|
|
89
|
+
fn poll_frame(
|
|
90
|
+
mut self: Pin<&mut Self>,
|
|
91
|
+
_cx: &mut Context<'_>,
|
|
92
|
+
) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
|
|
93
|
+
Poll::Ready(self.trailers.take().map(|t| Ok(Frame::trailers(t))))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn is_end_stream(&self) -> bool {
|
|
97
|
+
self.trailers.is_none()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Streaming response body fed by the Ruby handler through an mpsc
|
|
102
|
+
/// channel. Each `Data` message becomes a gRPC frame; `End` (or the
|
|
103
|
+
/// deadline) produces the trailers.
|
|
104
|
+
pub struct ResponseBody {
|
|
105
|
+
pub receiver: Option<mpsc::Receiver<StreamMessage>>,
|
|
106
|
+
pub stats: Arc<ServerStats>,
|
|
107
|
+
pub path: String,
|
|
108
|
+
pub start: Instant,
|
|
109
|
+
pub compress: bool,
|
|
110
|
+
pub deadline: Option<Pin<Box<tokio::time::Sleep>>>,
|
|
111
|
+
pub max_message_size: usize,
|
|
112
|
+
pub finished: bool,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
impl ResponseBody {
|
|
116
|
+
fn finish(&mut self, code: Code, message: &str) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
|
|
117
|
+
self.receiver = None;
|
|
118
|
+
self.finished = true;
|
|
119
|
+
self.stats.record(&self.path, code as u32, message, self.start);
|
|
120
|
+
Poll::Ready(Some(Ok(Frame::trailers(status_trailers(code as u32, message)))))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl Body for ResponseBody {
|
|
125
|
+
type Data = Bytes;
|
|
126
|
+
type Error = Status;
|
|
127
|
+
|
|
128
|
+
fn poll_frame(
|
|
129
|
+
mut self: Pin<&mut Self>,
|
|
130
|
+
cx: &mut Context<'_>,
|
|
131
|
+
) -> Poll<Option<Result<Frame<Bytes>, Status>>> {
|
|
132
|
+
let Some(receiver) = self.receiver.as_mut() else {
|
|
133
|
+
return Poll::Ready(None);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Drain ready messages before checking the deadline so a response
|
|
137
|
+
// that made it in time is never discarded.
|
|
138
|
+
match receiver.poll_recv(cx) {
|
|
139
|
+
Poll::Ready(Some(StreamMessage::Data(bytes))) => {
|
|
140
|
+
if bytes.len() > self.max_message_size {
|
|
141
|
+
return self.finish(
|
|
142
|
+
Code::ResourceExhausted,
|
|
143
|
+
"response message exceeds max_message_size",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
let compress = self.compress && bytes.len() > COMPRESSION_THRESHOLD;
|
|
147
|
+
return Poll::Ready(Some(Ok(Frame::data(frame(&bytes, compress)))));
|
|
148
|
+
}
|
|
149
|
+
Poll::Ready(Some(StreamMessage::End(None))) => return self.finish(Code::Ok, ""),
|
|
150
|
+
Poll::Ready(Some(StreamMessage::End(Some((code, message))))) => {
|
|
151
|
+
return self.finish(Code::from_i32(code as i32), &message);
|
|
152
|
+
}
|
|
153
|
+
Poll::Ready(None) => return self.finish(Code::Internal, "handler ended without a response"),
|
|
154
|
+
Poll::Pending => {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if let Some(deadline) = self.deadline.as_mut()
|
|
158
|
+
&& deadline.as_mut().poll(cx).is_ready()
|
|
159
|
+
{
|
|
160
|
+
return self.finish(Code::DeadlineExceeded, "deadline exceeded");
|
|
161
|
+
}
|
|
162
|
+
Poll::Pending
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn is_end_stream(&self) -> bool {
|
|
166
|
+
self.receiver.is_none()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
impl Drop for ResponseBody {
|
|
171
|
+
fn drop(&mut self) {
|
|
172
|
+
// Dropped before trailers were produced: the client went away
|
|
173
|
+
// (RST_STREAM / disconnect) — count it as cancelled.
|
|
174
|
+
if !self.finished {
|
|
175
|
+
self.stats.cancelled.fetch_add(1, Ordering::Relaxed);
|
|
176
|
+
if self.stats.log_requests.load(Ordering::Relaxed) {
|
|
177
|
+
eprintln!(
|
|
178
|
+
"tonic-rails: path={} code=1 message=cancelled by client",
|
|
179
|
+
sanitize_for_log(&self.path)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[cfg(test)]
|
|
187
|
+
mod tests {
|
|
188
|
+
use super::*;
|
|
189
|
+
use std::task::Waker;
|
|
190
|
+
|
|
191
|
+
type PolledFrame = Poll<Option<Result<Frame<Bytes>, Status>>>;
|
|
192
|
+
|
|
193
|
+
fn poll<B: Body<Data = Bytes, Error = Status> + Unpin>(body: &mut B) -> PolledFrame {
|
|
194
|
+
let mut cx = Context::from_waker(Waker::noop());
|
|
195
|
+
Pin::new(body).poll_frame(&mut cx)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn response_body(rx: mpsc::Receiver<StreamMessage>, stats: Arc<ServerStats>) -> ResponseBody {
|
|
199
|
+
ResponseBody {
|
|
200
|
+
receiver: Some(rx),
|
|
201
|
+
stats,
|
|
202
|
+
path: "/test.Svc/Method".into(),
|
|
203
|
+
start: Instant::now(),
|
|
204
|
+
compress: false,
|
|
205
|
+
deadline: None,
|
|
206
|
+
max_message_size: 16,
|
|
207
|
+
finished: false,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn trailers(p: PolledFrame) -> HeaderMap {
|
|
212
|
+
match p {
|
|
213
|
+
Poll::Ready(Some(Ok(frame))) => frame.into_trailers().expect("trailers frame"),
|
|
214
|
+
_ => panic!("expected trailers"),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#[test]
|
|
219
|
+
fn error_body_yields_trailers_once() {
|
|
220
|
+
let mut body = ErrorBody::new(Code::NotFound, "missing");
|
|
221
|
+
let t = trailers(poll(&mut body));
|
|
222
|
+
assert_eq!(t["grpc-status"], "5");
|
|
223
|
+
assert_eq!(t["grpc-message"], "missing");
|
|
224
|
+
assert!(matches!(poll(&mut body), Poll::Ready(None)));
|
|
225
|
+
assert!(body.is_end_stream());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn response_body_streams_data_then_ok() {
|
|
230
|
+
let (tx, rx) = mpsc::channel(4);
|
|
231
|
+
let stats = Arc::new(ServerStats::default());
|
|
232
|
+
let mut body = response_body(rx, stats.clone());
|
|
233
|
+
tx.try_send(StreamMessage::Data(b"abc".to_vec())).ok();
|
|
234
|
+
tx.try_send(StreamMessage::End(None)).ok();
|
|
235
|
+
|
|
236
|
+
match poll(&mut body) {
|
|
237
|
+
Poll::Ready(Some(Ok(frame))) => {
|
|
238
|
+
assert_eq!(
|
|
239
|
+
frame.into_data().unwrap().as_ref(),
|
|
240
|
+
&[0, 0, 0, 0, 3, b'a', b'b', b'c']
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
_ => panic!("expected data"),
|
|
244
|
+
}
|
|
245
|
+
assert_eq!(trailers(poll(&mut body))["grpc-status"], "0");
|
|
246
|
+
assert!(matches!(poll(&mut body), Poll::Ready(None)));
|
|
247
|
+
drop(body);
|
|
248
|
+
assert_eq!(stats.successful.load(Ordering::Relaxed), 1);
|
|
249
|
+
assert_eq!(stats.cancelled.load(Ordering::Relaxed), 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn response_body_rejects_oversized_message() {
|
|
254
|
+
let (tx, rx) = mpsc::channel(4);
|
|
255
|
+
let stats = Arc::new(ServerStats::default());
|
|
256
|
+
let mut body = response_body(rx, stats.clone());
|
|
257
|
+
tx.try_send(StreamMessage::Data(vec![0; 17])).ok();
|
|
258
|
+
assert_eq!(trailers(poll(&mut body))["grpc-status"], "8");
|
|
259
|
+
assert_eq!(stats.resource_exhausted.load(Ordering::Relaxed), 1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#[test]
|
|
263
|
+
fn response_body_reports_handler_error() {
|
|
264
|
+
let (tx, rx) = mpsc::channel(4);
|
|
265
|
+
let mut body = response_body(rx, Arc::new(ServerStats::default()));
|
|
266
|
+
tx.try_send(StreamMessage::End(Some((7, "denied".into())))).ok();
|
|
267
|
+
let t = trailers(poll(&mut body));
|
|
268
|
+
assert_eq!(t["grpc-status"], "7");
|
|
269
|
+
assert_eq!(t["grpc-message"], "denied");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn response_body_closed_channel_is_internal() {
|
|
274
|
+
let (tx, rx) = mpsc::channel(4);
|
|
275
|
+
let mut body = response_body(rx, Arc::new(ServerStats::default()));
|
|
276
|
+
drop(tx);
|
|
277
|
+
assert_eq!(trailers(poll(&mut body))["grpc-status"], "13");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[test]
|
|
281
|
+
fn dropped_body_counts_as_cancelled() {
|
|
282
|
+
let (_tx, rx) = mpsc::channel(4);
|
|
283
|
+
let stats = Arc::new(ServerStats::default());
|
|
284
|
+
let mut body = response_body(rx, stats.clone());
|
|
285
|
+
assert!(poll(&mut body).is_pending());
|
|
286
|
+
drop(body);
|
|
287
|
+
assert_eq!(stats.cancelled.load(Ordering::Relaxed), 1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
//! Raw-byte gRPC dispatch service.
|
|
2
|
+
//!
|
|
3
|
+
//! Every request that is not for the built-in health or reflection
|
|
4
|
+
//! services lands here. Requests are validated, the handler is queued
|
|
5
|
+
//! for a Ruby dispatch thread *before* the body is read (so bidi RPCs can
|
|
6
|
+
//! interleave), and a spawned task feeds request messages to the handler
|
|
7
|
+
//! through a bounded channel — giving HTTP/2 flow-control backpressure.
|
|
8
|
+
|
|
9
|
+
use std::collections::HashSet;
|
|
10
|
+
use std::convert::Infallible;
|
|
11
|
+
use std::future::Future;
|
|
12
|
+
use std::pin::Pin;
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
15
|
+
use std::task::{Context, Poll};
|
|
16
|
+
use std::time::Instant;
|
|
17
|
+
|
|
18
|
+
use http::{HeaderValue, Request, Response};
|
|
19
|
+
use http_body_util::BodyExt;
|
|
20
|
+
use magnus::{Value, value::Opaque};
|
|
21
|
+
use tokio::sync::mpsc;
|
|
22
|
+
use tonic::Code;
|
|
23
|
+
use tonic::body::Body as TonicBody;
|
|
24
|
+
|
|
25
|
+
use crate::body::{ErrorBody, ResponseBody, ServerStats};
|
|
26
|
+
use crate::framing::{MessageReader, parse_grpc_timeout};
|
|
27
|
+
use crate::gvl::{DispatchContext, RequestError, StreamMessage};
|
|
28
|
+
|
|
29
|
+
/// Messages buffered per direction before backpressure applies.
|
|
30
|
+
const STREAM_BUFFER: usize = 32;
|
|
31
|
+
|
|
32
|
+
/// Request metadata larger than this (sum of names + values) is rejected.
|
|
33
|
+
const MAX_METADATA_BYTES: usize = 8 * 1024;
|
|
34
|
+
|
|
35
|
+
/// Headers consumed by the transport and not exposed to handlers.
|
|
36
|
+
fn is_reserved_header(name: &str) -> bool {
|
|
37
|
+
matches!(
|
|
38
|
+
name,
|
|
39
|
+
"content-type" | "te" | "grpc-timeout" | "grpc-encoding" | "grpc-accept-encoding"
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub struct DispatchInner {
|
|
44
|
+
pub callback: Opaque<Value>,
|
|
45
|
+
pub routes: HashSet<String>,
|
|
46
|
+
pub request_tx: crossbeam_channel::Sender<Box<DispatchContext>>,
|
|
47
|
+
pub stats: Arc<ServerStats>,
|
|
48
|
+
pub max_message_size: usize,
|
|
49
|
+
pub compression: Arc<AtomicBool>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Clone)]
|
|
53
|
+
pub struct DispatchService(pub Arc<DispatchInner>);
|
|
54
|
+
|
|
55
|
+
impl tower::Service<Request<axum::body::Body>> for DispatchService {
|
|
56
|
+
type Response = Response<TonicBody>;
|
|
57
|
+
type Error = Infallible;
|
|
58
|
+
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Infallible>> + Send>>;
|
|
59
|
+
|
|
60
|
+
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
|
|
61
|
+
Poll::Ready(Ok(()))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn call(&mut self, req: Request<axum::body::Body>) -> Self::Future {
|
|
65
|
+
let response = self.0.handle(req);
|
|
66
|
+
Box::pin(async move { Ok(response) })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn grpc_response(body: TonicBody, compressed: bool) -> Response<TonicBody> {
|
|
71
|
+
let mut resp = Response::new(body);
|
|
72
|
+
let headers = resp.headers_mut();
|
|
73
|
+
headers.insert(
|
|
74
|
+
http::header::CONTENT_TYPE,
|
|
75
|
+
HeaderValue::from_static("application/grpc"),
|
|
76
|
+
);
|
|
77
|
+
headers.insert("grpc-accept-encoding", HeaderValue::from_static("gzip,identity"));
|
|
78
|
+
if compressed {
|
|
79
|
+
headers.insert("grpc-encoding", HeaderValue::from_static("gzip"));
|
|
80
|
+
}
|
|
81
|
+
resp
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl DispatchInner {
|
|
85
|
+
fn reject(&self, path: &str, start: Instant, code: Code, message: &str) -> Response<TonicBody> {
|
|
86
|
+
self.stats.record(path, code as u32, message, start);
|
|
87
|
+
grpc_response(TonicBody::new(ErrorBody::new(code, message)), false)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn handle(&self, req: Request<axum::body::Body>) -> Response<TonicBody> {
|
|
91
|
+
let start = Instant::now();
|
|
92
|
+
self.stats.total.fetch_add(1, Ordering::Relaxed);
|
|
93
|
+
let path = req.uri().path().to_owned();
|
|
94
|
+
let headers = req.headers();
|
|
95
|
+
|
|
96
|
+
if req.method() != http::Method::POST {
|
|
97
|
+
return self.reject(&path, start, Code::Unimplemented, "gRPC requires HTTP POST");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// application/grpc, optionally with a +proto / ;params suffix.
|
|
101
|
+
// gRPC-Web and other framings are not supported.
|
|
102
|
+
let content_type = headers
|
|
103
|
+
.get(http::header::CONTENT_TYPE)
|
|
104
|
+
.and_then(|v| v.to_str().ok())
|
|
105
|
+
.unwrap_or("");
|
|
106
|
+
let base = content_type.split(';').next().unwrap_or("").trim();
|
|
107
|
+
if base != "application/grpc" && base != "application/grpc+proto" {
|
|
108
|
+
return self.reject(&path, start, Code::Unimplemented, "unsupported content-type");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if !self.routes.contains(&path) {
|
|
112
|
+
return self.reject(&path, start, Code::Unimplemented, "unknown method");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let gzip_request = match headers.get("grpc-encoding").map(|v| v.to_str().unwrap_or("")) {
|
|
116
|
+
None | Some("identity") => false,
|
|
117
|
+
Some("gzip") => true,
|
|
118
|
+
Some(_) => return self.reject(&path, start, Code::Unimplemented, "unsupported grpc-encoding"),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let accepts_gzip = headers
|
|
122
|
+
.get("grpc-accept-encoding")
|
|
123
|
+
.and_then(|v| v.to_str().ok())
|
|
124
|
+
.is_some_and(|v| v.split(',').any(|e| e.trim() == "gzip"));
|
|
125
|
+
let compress = accepts_gzip && self.compression.load(Ordering::Relaxed);
|
|
126
|
+
|
|
127
|
+
let timeout = headers
|
|
128
|
+
.get("grpc-timeout")
|
|
129
|
+
.and_then(|v| v.to_str().ok())
|
|
130
|
+
.and_then(parse_grpc_timeout);
|
|
131
|
+
|
|
132
|
+
let mut metadata_bytes = 0;
|
|
133
|
+
let mut metadata = Vec::with_capacity(headers.len());
|
|
134
|
+
for (name, value) in headers {
|
|
135
|
+
let name = name.as_str();
|
|
136
|
+
if is_reserved_header(name) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
metadata_bytes += name.len() + value.len();
|
|
140
|
+
// Non-UTF-8 values cannot be represented as Ruby Strings
|
|
141
|
+
// faithfully; binary metadata uses `-bin` + base64 per spec.
|
|
142
|
+
if let Ok(value) = value.to_str() {
|
|
143
|
+
metadata.push((name.to_owned(), value.to_owned()));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if metadata_bytes > MAX_METADATA_BYTES {
|
|
147
|
+
return self.reject(
|
|
148
|
+
&path,
|
|
149
|
+
start,
|
|
150
|
+
Code::ResourceExhausted,
|
|
151
|
+
"request metadata too large",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let (request_tx, request_rx) = mpsc::channel::<Vec<u8>>(STREAM_BUFFER);
|
|
156
|
+
let (response_tx, response_rx) = mpsc::channel::<StreamMessage>(STREAM_BUFFER);
|
|
157
|
+
let request_error = Arc::new(RequestError::default());
|
|
158
|
+
|
|
159
|
+
let ctx = Box::new(DispatchContext {
|
|
160
|
+
callback: self.callback,
|
|
161
|
+
path: path.clone(),
|
|
162
|
+
metadata,
|
|
163
|
+
timeout_ms: timeout.map(|t| t.as_millis()),
|
|
164
|
+
response_tx,
|
|
165
|
+
request_rx,
|
|
166
|
+
request_error: request_error.clone(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
match self.request_tx.try_send(ctx) {
|
|
170
|
+
Ok(()) => {}
|
|
171
|
+
Err(crossbeam_channel::TrySendError::Full(_)) => {
|
|
172
|
+
return self.reject(
|
|
173
|
+
&path,
|
|
174
|
+
start,
|
|
175
|
+
Code::ResourceExhausted,
|
|
176
|
+
"server busy: dispatch queue full",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
|
|
180
|
+
return self.reject(&path, start, Code::Unavailable, "server shutting down");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let max_message_size = self.max_message_size;
|
|
185
|
+
tokio::spawn(feed_request(
|
|
186
|
+
req.into_body(),
|
|
187
|
+
request_tx,
|
|
188
|
+
request_error,
|
|
189
|
+
max_message_size,
|
|
190
|
+
gzip_request,
|
|
191
|
+
));
|
|
192
|
+
|
|
193
|
+
let body = ResponseBody {
|
|
194
|
+
receiver: Some(response_rx),
|
|
195
|
+
stats: self.stats.clone(),
|
|
196
|
+
path,
|
|
197
|
+
start,
|
|
198
|
+
compress,
|
|
199
|
+
deadline: timeout.map(|t| Box::pin(tokio::time::sleep(t))),
|
|
200
|
+
max_message_size,
|
|
201
|
+
finished: false,
|
|
202
|
+
};
|
|
203
|
+
grpc_response(TonicBody::new(body), compress)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Read the request body and forward each unframed message to the
|
|
208
|
+
/// handler. Dropping `tx` signals end of stream; errors are recorded
|
|
209
|
+
/// in `error` first so the reader can distinguish them.
|
|
210
|
+
async fn feed_request(
|
|
211
|
+
body: axum::body::Body,
|
|
212
|
+
tx: mpsc::Sender<Vec<u8>>,
|
|
213
|
+
error: Arc<RequestError>,
|
|
214
|
+
max_message_size: usize,
|
|
215
|
+
gzip: bool,
|
|
216
|
+
) {
|
|
217
|
+
let mut reader = MessageReader::new(max_message_size, gzip);
|
|
218
|
+
let mut body = body;
|
|
219
|
+
loop {
|
|
220
|
+
let frame = match body.frame().await {
|
|
221
|
+
None => break,
|
|
222
|
+
Some(Ok(frame)) => frame,
|
|
223
|
+
Some(Err(_)) => {
|
|
224
|
+
error.set(Code::Cancelled, "request stream reset by client");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
let Ok(data) = frame.into_data() else {
|
|
229
|
+
continue; // request trailers carry nothing for us
|
|
230
|
+
};
|
|
231
|
+
reader.push(&data);
|
|
232
|
+
loop {
|
|
233
|
+
match reader.try_next() {
|
|
234
|
+
Ok(Some(message)) => {
|
|
235
|
+
if tx.send(message).await.is_err() {
|
|
236
|
+
return; // handler finished without reading everything
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
Ok(None) => break,
|
|
240
|
+
Err(e) => {
|
|
241
|
+
error.set(e.code(), e.message());
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if let Err(e) = reader.finish() {
|
|
248
|
+
error.set(e.code(), e.message());
|
|
249
|
+
}
|
|
250
|
+
}
|