From 7074cedfd111506969a34205560bd09e8be46bf8 Mon Sep 17 00:00:00 2001 From: Wahaj Ahmed Date: Wed, 27 May 2026 14:56:12 +0200 Subject: [PATCH] fix(proxy): add max_header_list_size config for HTTP/2 client Adds client-side configurability for the h2 max header list size, which defaults to 16 KiB in Hyper 1.x and is insufficient for gRPC services with large trailers. Changes: - linkerd/http/h2: Add max_header_list_size to ClientParams struct and include it in override_from method - linkerd/app/env: Add http2::parse_client() for client params parsing and wire it to LINKERD2_PROXY_OUTBOUND_CONNECT_HTTP2_* and LINKERD2_PROXY_INBOUND_CONNECT_HTTP2_* env vars - linkerd/proxy/http: Apply max_header_list_size in h2 Connect::call Env vars (follow server pattern): *_MAX_HEADER_LIST_SIZE *_MAX_FRAME_SIZE *_MAX_SEND_BUF_SIZE *_KEEP_ALIVE_TIMEOUT / *_KEEP_ALIVE_INTERVAL / *_KEEP_ALIVE_WHILE_IDLE Fixes linkerd/linkerd2#15199 --- linkerd/app/src/env.rs | 22 +++------ linkerd/app/src/env/http2.rs | 95 +++++++++++++++++++++++++++++++++++- linkerd/http/h2/src/lib.rs | 4 ++ linkerd/proxy/http/src/h2.rs | 4 ++ 4 files changed, 110 insertions(+), 15 deletions(-) diff --git a/linkerd/app/src/env.rs b/linkerd/app/src/env.rs index 11d9f3f434..df33a1600f 100644 --- a/linkerd/app/src/env.rs +++ b/linkerd/app/src/env.rs @@ -540,13 +540,10 @@ pub fn parse_config(strings: &S) -> Result OUTBOUND_CONNECT_BASE, DEFAULT_OUTBOUND_CONNECT_BACKOFF, )?, - http2: h2::ClientParams { - flow_control: Some(h2::FlowControl::Fixed { - initial_stream_window_size, - initial_connection_window_size, - }), - ..Default::default() - }, + http2: http2::parse_client( + strings, + "LINKERD2_PROXY_OUTBOUND_CONNECT_HTTP2", + )?, http1: h1::PoolSettings { max_idle, idle_timeout: connection_pool_timeout @@ -634,13 +631,10 @@ pub fn parse_config(strings: &S) -> Result INBOUND_CONNECT_BASE, DEFAULT_INBOUND_CONNECT_BACKOFF, )?, - http2: h2::ClientParams { - flow_control: Some(h2::FlowControl::Fixed { - initial_stream_window_size, - initial_connection_window_size, - }), - ..Default::default() - }, + http2: http2::parse_client( + strings, + "LINKERD2_PROXY_INBOUND_CONNECT_HTTP2", + )?, http1: h1::PoolSettings { max_idle, idle_timeout: connection_pool_timeout, diff --git a/linkerd/app/src/env/http2.rs b/linkerd/app/src/env/http2.rs index 42c5806ca6..2d034fabfc 100644 --- a/linkerd/app/src/env/http2.rs +++ b/linkerd/app/src/env/http2.rs @@ -29,6 +29,28 @@ pub(super) fn parse_server( }) } +pub(super) fn parse_client( + strings: &S, + base: &str, +) -> Result { + Ok(h2::ClientParams { + flow_control: Some(parse_flow_control(strings, base)?), + keep_alive: parse_client_keep_alive(strings, &format!("{base}_KEEP_ALIVE"))?, + max_concurrent_reset_streams: parse( + strings, + &format!("{base}_MAX_CONCURRENT_RESET_STREAMS"), + parse_number, + )?, + max_frame_size: parse(strings, &format!("{base}_MAX_FRAME_SIZE"), parse_number)?, + max_header_list_size: parse( + strings, + &format!("{base}_MAX_HEADER_LIST_SIZE"), + parse_number, + )?, + max_send_buf_size: parse(strings, &format!("{base}_MAX_SEND_BUF_SIZE"), parse_number)?, + }) +} + fn parse_flow_control(strings: &S, base: &str) -> Result { if let Some(true) = parse( strings, @@ -77,6 +99,25 @@ fn parse_keep_alive( Ok(None) } +fn parse_client_keep_alive( + strings: &S, + base: &str, +) -> Result, EnvError> { + if let (Some(timeout), Some(interval)) = ( + parse(strings, &format!("{base}_TIMEOUT"), parse_duration)?, + parse(strings, &format!("{base}_INTERVAL"), parse_duration)?, + ) { + let while_idle = parse( + strings, + &format!("{base}_WHILE_IDLE"), + parse_bool, + )?.unwrap_or(false); + return Ok(Some(h2::ClientKeepAlive { interval, timeout, while_idle })); + } + + Ok(None) +} + #[cfg(test)] mod tests { use super::*; @@ -147,4 +188,56 @@ mod tests { } ); } -} + + #[test] + fn client_params() { + let mut env = HashMap::default(); + + // Produces empty params if no relevant env vars are set. + let default = h2::ClientParams { + flow_control: Some(h2::FlowControl::Fixed { + initial_stream_window_size: super::super::DEFAULT_INITIAL_STREAM_WINDOW_SIZE, + initial_connection_window_size: + super::super::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE, + }), + ..Default::default() + }; + assert_eq!(parse_client(&env, "TEST").unwrap(), default); + + // Set all the fields. + env.insert("TEST_MAX_FRAME_SIZE", "4"); + env.insert("TEST_MAX_HEADER_LIST_SIZE", "5"); + env.insert("TEST_MAX_SEND_BUF_SIZE", "7"); + env.insert("TEST_KEEP_ALIVE_TIMEOUT", "1s"); + env.insert("TEST_KEEP_ALIVE_INTERVAL", "2s"); + env.insert("TEST_KEEP_ALIVE_WHILE_IDLE", "true"); + env.insert("TEST_INITIAL_STREAM_WINDOW_SIZE", "1"); + env.insert("TEST_INITIAL_CONNECTION_WINDOW_SIZE", "2"); + let expected = h2::ClientParams { + flow_control: Some(h2::FlowControl::Fixed { + initial_stream_window_size: 1, + initial_connection_window_size: 2, + }), + keep_alive: Some(h2::ClientKeepAlive { + interval: Duration::from_secs(2), + timeout: Duration::from_secs(1), + while_idle: true, + }), + max_frame_size: Some(4), + max_header_list_size: Some(5), + max_send_buf_size: Some(7), + ..Default::default() + }; + assert_eq!(parse_client(&env, "TEST").unwrap(), expected); + + // Enable adaptive flow control, overriding other flow control settings. + env.insert("TEST_ADAPTIVE_FLOW_CONTROL", "true"); + assert_eq!( + parse_client(&env, "TEST").unwrap(), + h2::ClientParams { + flow_control: Some(h2::FlowControl::Adaptive), + ..expected.clone() + } + ); + } +} \ No newline at end of file diff --git a/linkerd/http/h2/src/lib.rs b/linkerd/http/h2/src/lib.rs index 6bbc4244ff..48283e32af 100644 --- a/linkerd/http/h2/src/lib.rs +++ b/linkerd/http/h2/src/lib.rs @@ -22,6 +22,7 @@ pub struct ClientParams { pub max_concurrent_reset_streams: Option, pub max_frame_size: Option, pub max_send_buf_size: Option, + pub max_header_list_size: Option, } #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -58,6 +59,9 @@ impl ClientParams { .or(self.max_concurrent_reset_streams), max_frame_size: overrides.max_frame_size.or(self.max_frame_size), max_send_buf_size: overrides.max_send_buf_size.or(self.max_send_buf_size), + max_header_list_size: overrides + .max_header_list_size + .or(self.max_header_list_size), } } } diff --git a/linkerd/proxy/http/src/h2.rs b/linkerd/proxy/http/src/h2.rs index 62c0ed58db..e46884925c 100644 --- a/linkerd/proxy/http/src/h2.rs +++ b/linkerd/proxy/http/src/h2.rs @@ -75,6 +75,7 @@ where max_concurrent_reset_streams, max_frame_size, max_send_buf_size, + max_header_list_size, } = self.params; let connect = self @@ -122,6 +123,9 @@ where if let Some(sz) = max_send_buf_size { builder.max_send_buf_size(sz); } + if let Some(sz) = max_header_list_size { + builder.max_header_list_size(sz); + } let (tx, conn) = builder .handshake(hyper_util::rt::TokioIo::new(io))