Skip to content

Commit decbf61

Browse files
authored
transport: allow setting max_header_list_size (#1835)
There is a bug such that if the client sends a response with a header value that exceeds the max_header_list_size, then RPCs just hang (#1834). When tonic upgraded to hyper 1, it picked up [hyper#3622] which changed the default from 16MiB to 16KiB for this configuration value. Error messages in gRPC use headers. That means that services which ever sent error messages in excess of 16KiB (including in their error details!) will just hang. This commit adds the ability for the client to configure this value to something larger (perhaps the old default of 16MiB) to mitigate the above-referenced bug. [hyper#3622]: hyperium/hyper#3622
1 parent aff5eed commit decbf61

File tree

3 files changed

+84
-0
lines changed

3 files changed

+84
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::time::Duration;
2+
3+
use integration_tests::pb::{test_client, test_server, Input, Output};
4+
use tokio::sync::oneshot;
5+
use tonic::{
6+
transport::{Endpoint, Server},
7+
Request, Response, Status,
8+
};
9+
10+
/// This test checks that the max header list size is respected, and that
11+
/// it allows for error messages up to that size.
12+
#[tokio::test]
13+
async fn test_http_max_header_list_size_and_long_errors() {
14+
struct Svc;
15+
16+
// The default value is 16k.
17+
const N: usize = 20_000;
18+
19+
fn long_message() -> String {
20+
"a".repeat(N)
21+
}
22+
23+
#[tonic::async_trait]
24+
impl test_server::Test for Svc {
25+
async fn unary_call(&self, _: Request<Input>) -> Result<Response<Output>, Status> {
26+
Err(Status::internal(long_message()))
27+
}
28+
}
29+
30+
let svc = test_server::TestServer::new(Svc);
31+
32+
let (tx, rx) = oneshot::channel::<()>();
33+
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
34+
let addr = format!("http://{}", listener.local_addr().unwrap());
35+
36+
let jh = tokio::spawn(async move {
37+
let (nodelay, keepalive) = (true, Some(Duration::from_secs(1)));
38+
let listener =
39+
tonic::transport::server::TcpIncoming::from_listener(listener, nodelay, keepalive)
40+
.unwrap();
41+
Server::builder()
42+
.http2_max_pending_accept_reset_streams(Some(0))
43+
.add_service(svc)
44+
.serve_with_incoming_shutdown(listener, async { drop(rx.await) })
45+
.await
46+
.unwrap();
47+
});
48+
49+
tokio::time::sleep(Duration::from_millis(100)).await;
50+
51+
let channel = Endpoint::from_shared(addr)
52+
.unwrap()
53+
// Increase the max header list size to 2x the default value. If this is
54+
// not set, this test will hang. See
55+
// <https://github.com/hyperium/tonic/issues/1834>.
56+
.http2_max_header_list_size(u32::try_from(N * 2).unwrap())
57+
.connect()
58+
.await
59+
.unwrap();
60+
61+
let mut client = test_client::TestClient::new(channel);
62+
63+
let err = client.unary_call(Request::new(Input {})).await.unwrap_err();
64+
65+
assert_eq!(err.message(), long_message());
66+
67+
tx.send(()).unwrap();
68+
69+
jh.await.unwrap();
70+
}

tonic/src/transport/channel/endpoint.rs

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub struct Endpoint {
3333
pub(crate) http2_keep_alive_interval: Option<Duration>,
3434
pub(crate) http2_keep_alive_timeout: Option<Duration>,
3535
pub(crate) http2_keep_alive_while_idle: Option<bool>,
36+
pub(crate) http2_max_header_list_size: Option<u32>,
3637
pub(crate) connect_timeout: Option<Duration>,
3738
pub(crate) http2_adaptive_window: Option<bool>,
3839
pub(crate) executor: SharedExec,
@@ -290,6 +291,14 @@ impl Endpoint {
290291
}
291292
}
292293

294+
/// Sets the max header list size. Uses `hyper`'s default otherwise.
295+
pub fn http2_max_header_list_size(self, size: u32) -> Self {
296+
Endpoint {
297+
http2_max_header_list_size: Some(size),
298+
..self
299+
}
300+
}
301+
293302
/// Sets the executor used to spawn async tasks.
294303
///
295304
/// Uses `tokio::spawn` by default.
@@ -420,6 +429,7 @@ impl From<Uri> for Endpoint {
420429
http2_keep_alive_interval: None,
421430
http2_keep_alive_timeout: None,
422431
http2_keep_alive_while_idle: None,
432+
http2_max_header_list_size: None,
423433
connect_timeout: None,
424434
http2_adaptive_window: None,
425435
executor: SharedExec::tokio(),

tonic/src/transport/channel/service/connection.rs

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ impl Connection {
5151
settings.adaptive_window(val);
5252
}
5353

54+
if let Some(val) = endpoint.http2_max_header_list_size {
55+
settings.max_header_list_size(val);
56+
}
57+
5458
let stack = ServiceBuilder::new()
5559
.layer_fn(|s| {
5660
let origin = endpoint.origin.as_ref().unwrap_or(&endpoint.uri).clone();

0 commit comments

Comments
 (0)