tonic-rails 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +72 -2
- data/Cargo.lock +5 -5
- data/README.md +204 -13
- data/ext/tonic_rails_native/Cargo.toml +2 -2
- data/ext/tonic_rails_native/src/body.rs +113 -49
- data/ext/tonic_rails_native/src/dispatch.rs +67 -8
- data/ext/tonic_rails_native/src/framing.rs +50 -0
- data/ext/tonic_rails_native/src/gvl.rs +109 -21
- data/ext/tonic_rails_native/src/lib.rs +6 -1
- data/ext/tonic_rails_native/src/log.rs +131 -0
- data/ext/tonic_rails_native/src/net.rs +7 -1
- data/ext/tonic_rails_native/src/server.rs +67 -6
- data/lib/generators/tonic_rails/install/install_generator.rb +45 -0
- data/lib/generators/tonic_rails/install/templates/tonic_rails.rb.tt +21 -0
- data/lib/generators/tonic_rails/service/service_generator.rb +110 -0
- data/lib/generators/tonic_rails/service/templates/handler.rb.tt +38 -0
- data/lib/generators/tonic_rails/service/templates/service.proto.tt +16 -0
- data/lib/generators/tonic_rails/service/templates/service_spec.rb.tt +17 -0
- data/lib/generators/tonic_rails/service/templates/service_test.rb.tt +14 -0
- data/lib/puma/plugin/tonic_rails.rb +116 -0
- data/lib/tonic/rails/base.rb +169 -23
- data/lib/tonic/rails/configuration.rb +27 -4
- data/lib/tonic/rails/health.rb +105 -0
- data/lib/tonic/rails/instrumentation.rb +78 -0
- data/lib/tonic/rails/metadata.rb +50 -0
- data/lib/tonic/rails/peer.rb +60 -0
- data/lib/tonic/rails/railtie.rb +2 -2
- data/lib/tonic/rails/router.rb +1 -0
- data/lib/tonic/rails/rpc_error.rb +73 -6
- data/lib/tonic/rails/server.rb +82 -12
- data/lib/tonic/rails/tasks/tonic-rails.rake +13 -1
- data/lib/tonic/rails/testing/errors.rb +47 -0
- data/lib/tonic/rails/testing/minitest.rb +44 -0
- data/lib/tonic/rails/testing/rspec.rb +53 -0
- data/lib/tonic/rails/testing.rb +35 -4
- data/lib/tonic/rails/version.rb +1 -1
- data/lib/tonic-rails.rb +4 -0
- metadata +17 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
use std::future::Future;
|
|
8
8
|
use std::pin::Pin;
|
|
9
9
|
use std::sync::Arc;
|
|
10
|
-
use std::sync::atomic::{
|
|
10
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
11
11
|
use std::task::{Context, Poll};
|
|
12
12
|
use std::time::Instant;
|
|
13
13
|
|
|
@@ -17,8 +17,9 @@ use http_body::{Body, Frame};
|
|
|
17
17
|
use tokio::sync::mpsc;
|
|
18
18
|
use tonic::{Code, Status};
|
|
19
19
|
|
|
20
|
-
use crate::framing::{COMPRESSION_THRESHOLD,
|
|
20
|
+
use crate::framing::{COMPRESSION_THRESHOLD, append_metadata, frame, status_trailers};
|
|
21
21
|
use crate::gvl::StreamMessage;
|
|
22
|
+
use crate::log::{Level, LogSink, status_name};
|
|
22
23
|
|
|
23
24
|
/// Request counters, exposed to Ruby via `Server#stats`.
|
|
24
25
|
#[derive(Default)]
|
|
@@ -30,12 +31,14 @@ pub struct ServerStats {
|
|
|
30
31
|
pub deadline_exceeded: AtomicUsize,
|
|
31
32
|
pub resource_exhausted: AtomicUsize,
|
|
32
33
|
pub unimplemented: AtomicUsize,
|
|
33
|
-
|
|
34
|
-
pub log_requests: AtomicBool,
|
|
34
|
+
pub log: LogSink,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
impl ServerStats {
|
|
38
|
-
|
|
38
|
+
/// Count a finished call. `transport` marks outcomes decided here in
|
|
39
|
+
/// Rust (rejections, deadlines, limits) rather than by the Ruby handler;
|
|
40
|
+
/// only those are logged, since Ruby instruments and logs its own calls.
|
|
41
|
+
pub fn record(&self, path: &str, code: u32, message: &str, start: Instant, transport: bool) {
|
|
39
42
|
let counter = match Code::from_i32(code as i32) {
|
|
40
43
|
Code::Ok => &self.successful,
|
|
41
44
|
Code::Cancelled => &self.cancelled,
|
|
@@ -55,16 +58,15 @@ impl ServerStats {
|
|
|
55
58
|
_ => {}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
eprintln!("tonic-rails: path={path} code={code} duration_us={dur_us}");
|
|
61
|
+
if transport && code != Code::Ok as u32 {
|
|
62
|
+
let ms = start.elapsed().as_secs_f64() * 1000.0;
|
|
63
|
+
let status = status_name(code);
|
|
64
|
+
let line = if message.is_empty() {
|
|
65
|
+
format!("gRPC {path} {status} in {ms:.1}ms")
|
|
64
66
|
} else {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
format!("gRPC {path} {status} in {ms:.1}ms: {message}")
|
|
68
|
+
};
|
|
69
|
+
self.log.log(Level::for_code(code), &line);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -102,6 +104,8 @@ impl Body for ErrorBody {
|
|
|
102
104
|
/// channel. Each `Data` message becomes a gRPC frame; `End` (or the
|
|
103
105
|
/// deadline) produces the trailers.
|
|
104
106
|
pub struct ResponseBody {
|
|
107
|
+
/// A message already received while waiting for response headers.
|
|
108
|
+
pub pending: Option<StreamMessage>,
|
|
105
109
|
pub receiver: Option<mpsc::Receiver<StreamMessage>>,
|
|
106
110
|
pub stats: Arc<ServerStats>,
|
|
107
111
|
pub path: String,
|
|
@@ -112,12 +116,63 @@ pub struct ResponseBody {
|
|
|
112
116
|
pub finished: bool,
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
type Polled = Poll<Option<Result<Frame<Bytes>, Status>>>;
|
|
120
|
+
|
|
115
121
|
impl ResponseBody {
|
|
116
|
-
fn finish(&mut self, code: Code, message: &str) ->
|
|
122
|
+
fn finish(&mut self, code: Code, message: &str, transport: bool) -> Polled {
|
|
123
|
+
self.finish_with(code, message, transport, &[])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn finish_with(
|
|
127
|
+
&mut self,
|
|
128
|
+
code: Code,
|
|
129
|
+
message: &str,
|
|
130
|
+
transport: bool,
|
|
131
|
+
metadata: &[(String, String)],
|
|
132
|
+
) -> Polled {
|
|
117
133
|
self.receiver = None;
|
|
118
134
|
self.finished = true;
|
|
119
|
-
self.stats
|
|
120
|
-
|
|
135
|
+
self.stats
|
|
136
|
+
.record(&self.path, code as u32, message, self.start, transport);
|
|
137
|
+
let mut trailers = status_trailers(code as u32, message);
|
|
138
|
+
let skipped = append_metadata(&mut trailers, metadata);
|
|
139
|
+
if !skipped.is_empty() {
|
|
140
|
+
self.stats.log.log(
|
|
141
|
+
Level::Warn,
|
|
142
|
+
&format!(
|
|
143
|
+
"gRPC {} dropped invalid or reserved trailers: {}",
|
|
144
|
+
self.path,
|
|
145
|
+
skipped.join(", ")
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
Poll::Ready(Some(Ok(Frame::trailers(trailers))))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Turn one handler message into a frame (None: nothing to emit).
|
|
153
|
+
fn handle(&mut self, message: StreamMessage) -> Option<Polled> {
|
|
154
|
+
match message {
|
|
155
|
+
StreamMessage::Data(bytes) => {
|
|
156
|
+
if bytes.len() > self.max_message_size {
|
|
157
|
+
return Some(self.finish(
|
|
158
|
+
Code::ResourceExhausted,
|
|
159
|
+
"response message exceeds max_message_size",
|
|
160
|
+
true,
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
let compress = self.compress && bytes.len() > COMPRESSION_THRESHOLD;
|
|
164
|
+
Some(Poll::Ready(Some(Ok(Frame::data(frame(&bytes, compress))))))
|
|
165
|
+
}
|
|
166
|
+
StreamMessage::End { status, trailers } => Some(match status {
|
|
167
|
+
None => self.finish_with(Code::Ok, "", false, &trailers),
|
|
168
|
+
Some((code, message)) => {
|
|
169
|
+
self.finish_with(Code::from_i32(code as i32), &message, false, &trailers)
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
// Headers are only valid first and are consumed by the
|
|
173
|
+
// dispatcher; a late one has nowhere to go.
|
|
174
|
+
StreamMessage::Headers(_) => None,
|
|
175
|
+
}
|
|
121
176
|
}
|
|
122
177
|
}
|
|
123
178
|
|
|
@@ -125,39 +180,36 @@ impl Body for ResponseBody {
|
|
|
125
180
|
type Data = Bytes;
|
|
126
181
|
type Error = Status;
|
|
127
182
|
|
|
128
|
-
fn poll_frame(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
183
|
+
fn poll_frame(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Polled {
|
|
184
|
+
if let Some(message) = self.pending.take()
|
|
185
|
+
&& let Some(poll) = self.handle(message)
|
|
186
|
+
{
|
|
187
|
+
return poll;
|
|
188
|
+
}
|
|
135
189
|
|
|
136
190
|
// Drain ready messages before checking the deadline so a response
|
|
137
191
|
// that made it in time is never discarded.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
192
|
+
loop {
|
|
193
|
+
let Some(receiver) = self.receiver.as_mut() else {
|
|
194
|
+
return Poll::Ready(None);
|
|
195
|
+
};
|
|
196
|
+
match receiver.poll_recv(cx) {
|
|
197
|
+
Poll::Ready(Some(message)) => {
|
|
198
|
+
if let Some(poll) = self.handle(message) {
|
|
199
|
+
return poll;
|
|
200
|
+
}
|
|
145
201
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Poll::Ready(Some(StreamMessage::End(Some((code, message))))) => {
|
|
151
|
-
return self.finish(Code::from_i32(code as i32), &message);
|
|
202
|
+
Poll::Ready(None) => {
|
|
203
|
+
return self.finish(Code::Internal, "handler ended without a response", true);
|
|
204
|
+
}
|
|
205
|
+
Poll::Pending => break,
|
|
152
206
|
}
|
|
153
|
-
Poll::Ready(None) => return self.finish(Code::Internal, "handler ended without a response"),
|
|
154
|
-
Poll::Pending => {}
|
|
155
207
|
}
|
|
156
208
|
|
|
157
209
|
if let Some(deadline) = self.deadline.as_mut()
|
|
158
210
|
&& deadline.as_mut().poll(cx).is_ready()
|
|
159
211
|
{
|
|
160
|
-
return self.finish(Code::DeadlineExceeded, "deadline exceeded");
|
|
212
|
+
return self.finish(Code::DeadlineExceeded, "deadline exceeded", true);
|
|
161
213
|
}
|
|
162
214
|
Poll::Pending
|
|
163
215
|
}
|
|
@@ -173,12 +225,6 @@ impl Drop for ResponseBody {
|
|
|
173
225
|
// (RST_STREAM / disconnect) — count it as cancelled.
|
|
174
226
|
if !self.finished {
|
|
175
227
|
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
228
|
}
|
|
183
229
|
}
|
|
184
230
|
}
|
|
@@ -197,6 +243,7 @@ mod tests {
|
|
|
197
243
|
|
|
198
244
|
fn response_body(rx: mpsc::Receiver<StreamMessage>, stats: Arc<ServerStats>) -> ResponseBody {
|
|
199
245
|
ResponseBody {
|
|
246
|
+
pending: None,
|
|
200
247
|
receiver: Some(rx),
|
|
201
248
|
stats,
|
|
202
249
|
path: "/test.Svc/Method".into(),
|
|
@@ -231,7 +278,11 @@ mod tests {
|
|
|
231
278
|
let stats = Arc::new(ServerStats::default());
|
|
232
279
|
let mut body = response_body(rx, stats.clone());
|
|
233
280
|
tx.try_send(StreamMessage::Data(b"abc".to_vec())).ok();
|
|
234
|
-
tx.try_send(StreamMessage::End
|
|
281
|
+
tx.try_send(StreamMessage::End {
|
|
282
|
+
status: None,
|
|
283
|
+
trailers: vec![("x-done".into(), "yes".into())],
|
|
284
|
+
})
|
|
285
|
+
.ok();
|
|
235
286
|
|
|
236
287
|
match poll(&mut body) {
|
|
237
288
|
Poll::Ready(Some(Ok(frame))) => {
|
|
@@ -242,7 +293,9 @@ mod tests {
|
|
|
242
293
|
}
|
|
243
294
|
_ => panic!("expected data"),
|
|
244
295
|
}
|
|
245
|
-
|
|
296
|
+
let t = trailers(poll(&mut body));
|
|
297
|
+
assert_eq!(t["grpc-status"], "0");
|
|
298
|
+
assert_eq!(t["x-done"], "yes");
|
|
246
299
|
assert!(matches!(poll(&mut body), Poll::Ready(None)));
|
|
247
300
|
drop(body);
|
|
248
301
|
assert_eq!(stats.successful.load(Ordering::Relaxed), 1);
|
|
@@ -263,12 +316,23 @@ mod tests {
|
|
|
263
316
|
fn response_body_reports_handler_error() {
|
|
264
317
|
let (tx, rx) = mpsc::channel(4);
|
|
265
318
|
let mut body = response_body(rx, Arc::new(ServerStats::default()));
|
|
266
|
-
tx.try_send(StreamMessage::
|
|
319
|
+
tx.try_send(StreamMessage::error(Code::PermissionDenied, "denied"))
|
|
320
|
+
.ok();
|
|
267
321
|
let t = trailers(poll(&mut body));
|
|
268
322
|
assert_eq!(t["grpc-status"], "7");
|
|
269
323
|
assert_eq!(t["grpc-message"], "denied");
|
|
270
324
|
}
|
|
271
325
|
|
|
326
|
+
#[test]
|
|
327
|
+
fn response_body_starts_with_a_pending_message() {
|
|
328
|
+
let (tx, rx) = mpsc::channel(4);
|
|
329
|
+
let mut body = response_body(rx, Arc::new(ServerStats::default()));
|
|
330
|
+
body.pending = Some(StreamMessage::Data(b"x".to_vec()));
|
|
331
|
+
tx.try_send(StreamMessage::error(Code::Aborted, "stop")).ok();
|
|
332
|
+
assert!(matches!(poll(&mut body), Poll::Ready(Some(Ok(f))) if f.is_data()));
|
|
333
|
+
assert_eq!(trailers(poll(&mut body))["grpc-status"], "10");
|
|
334
|
+
}
|
|
335
|
+
|
|
272
336
|
#[test]
|
|
273
337
|
fn response_body_closed_channel_is_internal() {
|
|
274
338
|
let (tx, rx) = mpsc::channel(4);
|
|
@@ -21,10 +21,12 @@ use magnus::{Value, value::Opaque};
|
|
|
21
21
|
use tokio::sync::mpsc;
|
|
22
22
|
use tonic::Code;
|
|
23
23
|
use tonic::body::Body as TonicBody;
|
|
24
|
+
use tonic::transport::server::{TcpConnectInfo, TlsConnectInfo};
|
|
24
25
|
|
|
25
26
|
use crate::body::{ErrorBody, ResponseBody, ServerStats};
|
|
26
|
-
use crate::framing::{MessageReader, parse_grpc_timeout};
|
|
27
|
-
use crate::gvl::{DispatchContext, RequestError, StreamMessage};
|
|
27
|
+
use crate::framing::{MessageReader, append_metadata, parse_grpc_timeout};
|
|
28
|
+
use crate::gvl::{DispatchContext, Peer, RequestError, StreamMessage};
|
|
29
|
+
use crate::log::Level;
|
|
28
30
|
|
|
29
31
|
/// Messages buffered per direction before backpressure applies.
|
|
30
32
|
const STREAM_BUFFER: usize = 32;
|
|
@@ -62,8 +64,8 @@ impl tower::Service<Request<axum::body::Body>> for DispatchService {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
fn call(&mut self, req: Request<axum::body::Body>) -> Self::Future {
|
|
65
|
-
let
|
|
66
|
-
Box::pin(async move { Ok(
|
|
67
|
+
let inner = self.0.clone();
|
|
68
|
+
Box::pin(async move { Ok(inner.handle(req).await) })
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -83,11 +85,11 @@ fn grpc_response(body: TonicBody, compressed: bool) -> Response<TonicBody> {
|
|
|
83
85
|
|
|
84
86
|
impl DispatchInner {
|
|
85
87
|
fn reject(&self, path: &str, start: Instant, code: Code, message: &str) -> Response<TonicBody> {
|
|
86
|
-
self.stats.record(path, code as u32, message, start);
|
|
88
|
+
self.stats.record(path, code as u32, message, start, true);
|
|
87
89
|
grpc_response(TonicBody::new(ErrorBody::new(code, message)), false)
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
fn handle(&self, req: Request<axum::body::Body>) -> Response<TonicBody> {
|
|
92
|
+
async fn handle(&self, req: Request<axum::body::Body>) -> Response<TonicBody> {
|
|
91
93
|
let start = Instant::now();
|
|
92
94
|
self.stats.total.fetch_add(1, Ordering::Relaxed);
|
|
93
95
|
let path = req.uri().path().to_owned();
|
|
@@ -164,6 +166,8 @@ impl DispatchInner {
|
|
|
164
166
|
response_tx,
|
|
165
167
|
request_rx,
|
|
166
168
|
request_error: request_error.clone(),
|
|
169
|
+
log: self.stats.log.clone(),
|
|
170
|
+
peer: peer_of(&req),
|
|
167
171
|
});
|
|
168
172
|
|
|
169
173
|
match self.request_tx.try_send(ctx) {
|
|
@@ -190,17 +194,72 @@ impl DispatchInner {
|
|
|
190
194
|
gzip_request,
|
|
191
195
|
));
|
|
192
196
|
|
|
197
|
+
// Response headers wait for the handler's first output, so it can
|
|
198
|
+
// set initial metadata; unary calls send headers with the response
|
|
199
|
+
// as usual. The client's deadline applies while we wait.
|
|
200
|
+
let mut response_rx = response_rx;
|
|
201
|
+
let mut deadline = timeout.map(|t| Box::pin(tokio::time::sleep(t)));
|
|
202
|
+
let first = match deadline.as_mut() {
|
|
203
|
+
Some(deadline) => tokio::select! {
|
|
204
|
+
biased;
|
|
205
|
+
message = response_rx.recv() => Some(message),
|
|
206
|
+
_ = deadline.as_mut() => None,
|
|
207
|
+
},
|
|
208
|
+
None => Some(response_rx.recv().await),
|
|
209
|
+
};
|
|
210
|
+
let (headers, pending) = match first {
|
|
211
|
+
None => return self.reject(&path, start, Code::DeadlineExceeded, "deadline exceeded"),
|
|
212
|
+
Some(None) => {
|
|
213
|
+
return self.reject(&path, start, Code::Internal, "handler ended without a response");
|
|
214
|
+
}
|
|
215
|
+
Some(Some(StreamMessage::Headers(headers))) => (headers, None),
|
|
216
|
+
Some(Some(message)) => (Vec::new(), Some(message)),
|
|
217
|
+
};
|
|
218
|
+
|
|
193
219
|
let body = ResponseBody {
|
|
220
|
+
pending,
|
|
194
221
|
receiver: Some(response_rx),
|
|
195
222
|
stats: self.stats.clone(),
|
|
196
223
|
path,
|
|
197
224
|
start,
|
|
198
225
|
compress,
|
|
199
|
-
deadline
|
|
226
|
+
deadline,
|
|
200
227
|
max_message_size,
|
|
201
228
|
finished: false,
|
|
202
229
|
};
|
|
203
|
-
|
|
230
|
+
let path = body.path.clone();
|
|
231
|
+
let mut response = grpc_response(TonicBody::new(body), compress);
|
|
232
|
+
let skipped = append_metadata(response.headers_mut(), &headers);
|
|
233
|
+
if !skipped.is_empty() {
|
|
234
|
+
self.stats.log.log(
|
|
235
|
+
Level::Warn,
|
|
236
|
+
&format!(
|
|
237
|
+
"gRPC {path} dropped invalid or reserved response headers: {}",
|
|
238
|
+
skipped.join(", ")
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
response
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// The client's address and, if it presented one over TLS, its
|
|
247
|
+
/// certificate chain. tonic records both on the request.
|
|
248
|
+
fn peer_of<B>(req: &Request<B>) -> Peer {
|
|
249
|
+
let address = req
|
|
250
|
+
.extensions()
|
|
251
|
+
.get::<TcpConnectInfo>()
|
|
252
|
+
.and_then(TcpConnectInfo::remote_addr)
|
|
253
|
+
.map(|addr| addr.to_string());
|
|
254
|
+
let certificates = req
|
|
255
|
+
.extensions()
|
|
256
|
+
.get::<TlsConnectInfo<TcpConnectInfo>>()
|
|
257
|
+
.and_then(TlsConnectInfo::peer_certs)
|
|
258
|
+
.map(|chain| chain.iter().map(|der| der.to_vec()).collect())
|
|
259
|
+
.unwrap_or_default();
|
|
260
|
+
Peer {
|
|
261
|
+
address,
|
|
262
|
+
certificates,
|
|
204
263
|
}
|
|
205
264
|
}
|
|
206
265
|
|
|
@@ -190,6 +190,35 @@ pub fn sanitize_for_log(message: &str) -> String {
|
|
|
190
190
|
.collect()
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/// Headers a handler may not set: they belong to HTTP/2 or the gRPC
|
|
194
|
+
/// protocol itself.
|
|
195
|
+
fn is_reserved_metadata(name: &str) -> bool {
|
|
196
|
+
matches!(
|
|
197
|
+
name,
|
|
198
|
+
"content-type" | "te" | "user-agent" | "content-length" | "connection" | "transfer-encoding"
|
|
199
|
+
) || (name.starts_with("grpc-") && name != "grpc-status-details-bin")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Append handler-supplied metadata to `headers`, skipping names or values
|
|
203
|
+
/// that are invalid HTTP/2 headers or reserved by gRPC. Returns the names
|
|
204
|
+
/// that were skipped so the caller can report them.
|
|
205
|
+
pub fn append_metadata(headers: &mut HeaderMap, metadata: &[(String, String)]) -> Vec<String> {
|
|
206
|
+
let mut skipped = Vec::new();
|
|
207
|
+
for (name, value) in metadata {
|
|
208
|
+
let header = (!is_reserved_metadata(name))
|
|
209
|
+
.then(|| http::HeaderName::from_bytes(name.as_bytes()).ok())
|
|
210
|
+
.flatten()
|
|
211
|
+
.filter(|n| n.as_str() == name.as_str()); // must already be lower-case
|
|
212
|
+
match (header, HeaderValue::from_str(value)) {
|
|
213
|
+
(Some(header), Ok(value)) => {
|
|
214
|
+
headers.append(header, value);
|
|
215
|
+
}
|
|
216
|
+
_ => skipped.push(name.clone()),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
skipped
|
|
220
|
+
}
|
|
221
|
+
|
|
193
222
|
pub fn status_trailers(code: u32, message: &str) -> HeaderMap {
|
|
194
223
|
let mut trailers = HeaderMap::with_capacity(2);
|
|
195
224
|
trailers.insert("grpc-status", HeaderValue::from(code));
|
|
@@ -342,6 +371,27 @@ mod tests {
|
|
|
342
371
|
assert!(t.get("grpc-message").is_none());
|
|
343
372
|
}
|
|
344
373
|
|
|
374
|
+
#[test]
|
|
375
|
+
fn append_metadata_skips_reserved_and_invalid() {
|
|
376
|
+
let mut headers = HeaderMap::new();
|
|
377
|
+
let skipped = append_metadata(
|
|
378
|
+
&mut headers,
|
|
379
|
+
&[
|
|
380
|
+
("x-request-id".into(), "abc".into()),
|
|
381
|
+
("x-trace-bin".into(), "AAE=".into()),
|
|
382
|
+
("grpc-status-details-bin".into(), "CAU=".into()),
|
|
383
|
+
("grpc-status".into(), "0".into()),
|
|
384
|
+
("content-type".into(), "text/plain".into()),
|
|
385
|
+
("Upper".into(), "x".into()),
|
|
386
|
+
("x-bad".into(), "line\nbreak".into()),
|
|
387
|
+
],
|
|
388
|
+
);
|
|
389
|
+
assert_eq!(headers["x-request-id"], "abc");
|
|
390
|
+
assert_eq!(headers["x-trace-bin"], "AAE=");
|
|
391
|
+
assert_eq!(headers["grpc-status-details-bin"], "CAU=");
|
|
392
|
+
assert_eq!(skipped, ["grpc-status", "content-type", "Upper", "x-bad"]);
|
|
393
|
+
}
|
|
394
|
+
|
|
345
395
|
#[test]
|
|
346
396
|
fn sanitize_for_log_strips_controls() {
|
|
347
397
|
assert_eq!(sanitize_for_log("a\nb\tc"), "a b c");
|