Skip to content

feat: add H3 client config support #2609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion src/async_impl/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use super::request::{Request, RequestBuilder};
use super::response::Response;
use super::Body;
#[cfg(feature = "http3")]
use crate::async_impl::h3_client::connect::H3Connector;
use crate::async_impl::h3_client::connect::{H3Connector, H3ClientConfig};
#[cfg(feature = "http3")]
use crate::async_impl::h3_client::{H3Client, H3ResponseFuture};
use crate::connect::{
Expand Down Expand Up @@ -176,6 +176,14 @@ struct Config {
quic_receive_window: Option<VarInt>,
#[cfg(feature = "http3")]
quic_send_window: Option<u64>,
#[cfg(feature = "http3")]
h3_max_field_section_size: Option<u64>,
#[cfg(feature = "http3")]
h3_send_grease: Option<bool>,
#[cfg(feature = "http3")]
h3_enable_extended_connect: Option<bool>,
#[cfg(feature = "http3")]
h3_enable_datagram: Option<bool>,
dns_overrides: HashMap<String, Vec<SocketAddr>>,
dns_resolver: Option<Arc<dyn Resolve>>,
}
Expand Down Expand Up @@ -280,6 +288,14 @@ impl ClientBuilder {
quic_receive_window: None,
#[cfg(feature = "http3")]
quic_send_window: None,
#[cfg(feature = "http3")]
h3_max_field_section_size: None,
#[cfg(feature = "http3")]
h3_send_grease: None,
#[cfg(feature = "http3")]
h3_enable_extended_connect: None,
#[cfg(feature = "http3")]
h3_enable_datagram: None,
dns_resolver: None,
},
}
Expand Down Expand Up @@ -343,6 +359,10 @@ impl ClientBuilder {
quic_stream_receive_window,
quic_receive_window,
quic_send_window,
h3_max_field_section_size,
h3_send_grease,
h3_enable_extended_connect,
h3_enable_datagram,
local_address,
http_version_pref: &HttpVersionPref| {
let mut transport_config = TransportConfig::default();
Expand All @@ -365,11 +385,30 @@ impl ClientBuilder {
transport_config.send_window(send_window);
}

let mut h3_client_config = H3ClientConfig::default();

if let Some(max_field_section_size) = h3_max_field_section_size {
h3_client_config.max_field_section_size = Some(max_field_section_size);
}

if let Some(send_grease) = h3_send_grease {
h3_client_config.send_grease = Some(send_grease);
}

if let Some(enable_extended_connect) = h3_enable_extended_connect {
h3_client_config.enable_extended_connect = Some(enable_extended_connect);
}

if let Some(enable_datagram) = h3_enable_datagram {
h3_client_config.enable_datagram = Some(enable_datagram);
}

let res = H3Connector::new(
DynResolver::new(resolver),
tls,
local_address,
transport_config,
h3_client_config,
);

match res {
Expand Down Expand Up @@ -492,6 +531,10 @@ impl ClientBuilder {
config.quic_stream_receive_window,
config.quic_receive_window,
config.quic_send_window,
config.h3_max_field_section_size,
config.h3_send_grease,
config.h3_enable_extended_connect,
config.h3_enable_datagram,
config.local_address,
&config.http_version_pref,
)?;
Expand Down Expand Up @@ -687,6 +730,10 @@ impl ClientBuilder {
config.quic_stream_receive_window,
config.quic_receive_window,
config.quic_send_window,
config.h3_max_field_section_size,
config.h3_send_grease,
config.h3_enable_extended_connect,
config.h3_enable_datagram,
config.local_address,
&config.http_version_pref,
)?;
Expand Down Expand Up @@ -1962,6 +2009,48 @@ impl ClientBuilder {
self
}

/// The MAX_FIELD_SECTION_SIZE in HTTP/3 refers to the maximum size of the dynamic table used in HPACK compression.
/// HPACK is the compression algorithm used in HTTP/3 to reduce the size of the header fields in HTTP requests and responses.
///
/// In HTTP/3, the MAX_FIELD_SECTION_SIZE is set to 12.
/// This means that the dynamic table used for HPACK compression can have a maximum size of 2^12 bytes, which is 4KB.
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))]
pub fn http3_max_field_section_size(mut self, value: u64) -> ClientBuilder {
self.config.h3_max_field_section_size = Some(value.try_into().unwrap());
self
}

/// Just like in HTTP/2, HTTP/3 also uses the concept of "grease"
/// to prevent potential interoperability issues in the future.
/// In HTTP/3, the concept of grease is used to ensure that the protocol can evolve
/// and accommodate future changes without breaking existing implementations.
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))]
pub fn http3_send_grease(mut self, enabled: bool) -> ClientBuilder {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP/2 doesn't really have much of a concept of grease. Like, maybe someone somewhere wishes it did, but it wasn't embraced nearly as well as it was for HTTP/3. For this method, I'd make the first sentence brief, like "Enable whether to send HTTP/3 protocol grease on the connections." And then in a second paragraph, explain what it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for your feedback and guidance. I will optimize the doc and resubmit it.
I encountered issues with some cloud provider cdn where enabling QUIC caused request failures due to the absence of grease.

self.config.h3_send_grease = Some(enabled);
self
}

/// https://www.rfc-editor.org/info/rfc8441 defines an extended CONNECT method in Section 4,
/// enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL parameter.
/// That parameter is only defined for HTTP/2.
/// for extended CONNECT in HTTP/3; instead, the SETTINGS_ENABLE_WEBTRANSPORT setting implies that an endpoint supports extended CONNECT.
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))]
pub fn http3_enable_extended_connect(mut self, enabled: bool) -> ClientBuilder {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option, and the next one (datagrams), should probably only be added once the user can make use of them in a request. So for now, I'd leave them out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have already removed it.

self.config.h3_enable_extended_connect = Some(enabled);
self
}

/// Enable HTTP Datagrams, see https://datatracker.ietf.org/doc/rfc9297/ for details
#[cfg(feature = "http3")]
#[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))]
pub fn http3_enable_datagram(mut self, enabled: bool) -> ClientBuilder {
self.config.h3_enable_datagram = Some(enabled);
self
}

/// Adds a new Tower [`Layer`](https://docs.rs/tower/latest/tower/trait.Layer.html) to the
/// base connector [`Service`](https://docs.rs/tower/latest/tower/trait.Service.html) which
/// is responsible for connection establishment.
Expand Down
62 changes: 60 additions & 2 deletions src/async_impl/h3_client/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,48 @@ type H3Connection = (
SendRequest<OpenStreams, Bytes>,
);

/// H3 Client Config
#[derive(Clone)]
pub(crate) struct H3ClientConfig {
/// The MAX_FIELD_SECTION_SIZE in HTTP/3 refers to the maximum size of the dynamic table used in HPACK compression.
/// HPACK is the compression algorithm used in HTTP/3 to reduce the size of the header fields in HTTP requests and responses.

/// In HTTP/3, the MAX_FIELD_SECTION_SIZE is set to 12.
/// This means that the dynamic table used for HPACK compression can have a maximum size of 2^12 bytes, which is 4KB.
pub(crate) max_field_section_size: Option<u64>,

/// Just like in HTTP/2, HTTP/3 also uses the concept of "grease"
/// to prevent potential interoperability issues in the future.
/// In HTTP/3, the concept of grease is used to ensure that the protocol can evolve
/// and accommodate future changes without breaking existing implementations.
pub(crate) send_grease: Option<bool>,

/// https://www.rfc-editor.org/info/rfc8441 defines an extended CONNECT method in Section 4,
/// enabled by the SETTINGS_ENABLE_CONNECT_PROTOCOL parameter.
/// That parameter is only defined for HTTP/2.
/// for extended CONNECT in HTTP/3; instead, the SETTINGS_ENABLE_WEBTRANSPORT setting implies that an endpoint supports extended CONNECT.
pub(crate) enable_extended_connect: Option<bool>,

/// Enable HTTP Datagrams, see https://datatracker.ietf.org/doc/rfc9297/ for details
pub(crate) enable_datagram: Option<bool>,
}

impl Default for H3ClientConfig {
fn default() -> Self {
Self {
max_field_section_size: None,
send_grease: None,
enable_extended_connect: None,
enable_datagram: None,
}
}
}

#[derive(Clone)]
pub(crate) struct H3Connector {
resolver: DynResolver,
endpoint: Endpoint,
client_config: H3ClientConfig,
}

impl H3Connector {
Expand All @@ -29,6 +67,7 @@ impl H3Connector {
tls: rustls::ClientConfig,
local_addr: Option<IpAddr>,
transport_config: TransportConfig,
client_config: H3ClientConfig,
) -> Result<H3Connector, BoxError> {
let quic_client_config = Arc::new(QuicClientConfig::try_from(tls)?);
let mut config = ClientConfig::new(quic_client_config);
Expand All @@ -43,7 +82,11 @@ impl H3Connector {
let mut endpoint = Endpoint::client(socket_addr)?;
endpoint.set_default_client_config(config);

Ok(Self { resolver, endpoint })
Ok(Self {
resolver,
endpoint,
client_config,
})
}

pub async fn connect(&mut self, dest: Uri) -> Result<H3Connection, BoxError> {
Expand Down Expand Up @@ -79,7 +122,22 @@ impl H3Connector {
match self.endpoint.connect(addr, server_name)?.await {
Ok(new_conn) => {
let quinn_conn = Connection::new(new_conn);
return Ok(h3::client::new(quinn_conn).await?);

let mut h3_client_builder = h3::client::builder();
if let Some(max_field_section_size) = self.client_config.max_field_section_size {
h3_client_builder.max_field_section_size(max_field_section_size);
}
if let Some(send_grease) = self.client_config.send_grease {
h3_client_builder.send_grease(send_grease);
}
if let Some(enable_extended_connect) = self.client_config.enable_extended_connect {
h3_client_builder.enable_extended_connect(enable_extended_connect);
}
if let Some(enable_datagram) = self.client_config.enable_datagram {
h3_client_builder.enable_datagram(enable_datagram);
}

return Ok(h3_client_builder.build(quinn_conn).await?);
}
Err(e) => err = Some(e),
}
Expand Down
Loading