From bfc54fc3a13d071c8beb6bcc49b066d96bfecb10 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Mon, 11 Mar 2024 20:24:30 +0800 Subject: [PATCH] feat: Add http2 cargo feature (#2162) Technically a breaking change, since disabling default options will mean HTTP/2 is no longer enabled, and in 0.11.x, it was. --- Cargo.toml | 10 +++-- src/async_impl/client.rs | 87 ++++++++++++++++++++++++++++------------ src/lib.rs | 1 + tests/client.rs | 5 ++- 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ca66c043..a26b06e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,14 @@ features = [ ] [features] -default = ["default-tls"] +default = ["default-tls", "http2"] # Note: this doesn't enable the 'native-tls' feature, which adds specific # functionality for it. default-tls = ["hyper-tls", "native-tls-crate", "__tls", "tokio-native-tls"] +http2 = ["h2", "hyper/http2", "hyper-util/http2"] + # Enables native-tls specific functionality not available by default. native-tls = ["default-tls"] native-tls-alpn = ["native-tls", "native-tls-crate/alpn", "hyper-tls/alpn"] @@ -103,9 +105,9 @@ mime_guess = { version = "2.0", default-features = false, optional = true } encoding_rs = "0.8" http-body = "1" http-body-util = "0.1" -hyper = { version = "1", features = ["http1", "http2", "client"] } -hyper-util = { version = "0.1.3", features = ["http1", "http2", "client", "client-legacy", "tokio"] } -h2 = "0.4" +hyper = { version = "1", features = ["http1", "client"] } +hyper-util = { version = "0.1.3", features = ["http1", "client", "client-legacy", "tokio"] } +h2 = { version = "0.4", optional = true } once_cell = "1" log = "0.4" mime = "0.3.16" diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index adaa59da6..912a5d4bd 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -46,7 +46,7 @@ use crate::Certificate; #[cfg(any(feature = "native-tls", feature = "__rustls"))] use crate::Identity; use crate::{IntoUrl, Method, Proxy, StatusCode, Url}; -use log::{debug, trace}; +use log::debug; #[cfg(feature = "http3")] use quinn::TransportConfig; #[cfg(feature = "http3")] @@ -80,6 +80,7 @@ pub struct ClientBuilder { enum HttpVersionPref { Http1, + #[cfg(feature = "http2")] Http2, #[cfg(feature = "http3")] Http3, @@ -126,12 +127,19 @@ struct Config { http1_allow_obsolete_multiline_headers_in_responses: bool, http1_ignore_invalid_headers_in_responses: bool, http1_allow_spaces_after_header_name_in_responses: bool, + #[cfg(feature = "http2")] http2_initial_stream_window_size: Option, + #[cfg(feature = "http2")] http2_initial_connection_window_size: Option, + #[cfg(feature = "http2")] http2_adaptive_window: bool, + #[cfg(feature = "http2")] http2_max_frame_size: Option, + #[cfg(feature = "http2")] http2_keep_alive_interval: Option, + #[cfg(feature = "http2")] http2_keep_alive_timeout: Option, + #[cfg(feature = "http2")] http2_keep_alive_while_idle: bool, local_address: Option, nodelay: bool, @@ -211,12 +219,19 @@ impl ClientBuilder { http1_allow_obsolete_multiline_headers_in_responses: false, http1_ignore_invalid_headers_in_responses: false, http1_allow_spaces_after_header_name_in_responses: false, + #[cfg(feature = "http2")] http2_initial_stream_window_size: None, + #[cfg(feature = "http2")] http2_initial_connection_window_size: None, + #[cfg(feature = "http2")] http2_adaptive_window: false, + #[cfg(feature = "http2")] http2_max_frame_size: None, + #[cfg(feature = "http2")] http2_keep_alive_interval: None, + #[cfg(feature = "http2")] http2_keep_alive_timeout: None, + #[cfg(feature = "http2")] http2_keep_alive_while_idle: false, local_address: None, nodelay: true, @@ -349,6 +364,7 @@ impl ClientBuilder { HttpVersionPref::Http1 => { tls.request_alpns(&["http/1.1"]); } + #[cfg(feature = "http2")] HttpVersionPref::Http2 => { tls.request_alpns(&["h2"]); } @@ -541,6 +557,7 @@ impl ClientBuilder { HttpVersionPref::Http1 => { tls.alpn_protocols = vec!["http/1.1".into()]; } + #[cfg(feature = "http2")] HttpVersionPref::Http2 => { tls.alpn_protocols = vec!["h2".into()]; } @@ -596,32 +613,36 @@ impl ClientBuilder { let mut builder = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()); - if matches!(config.http_version_pref, HttpVersionPref::Http2) { - builder.http2_only(true); - } - - if let Some(http2_initial_stream_window_size) = config.http2_initial_stream_window_size { - builder.http2_initial_stream_window_size(http2_initial_stream_window_size); - } - if let Some(http2_initial_connection_window_size) = - config.http2_initial_connection_window_size + #[cfg(feature = "http2")] { - builder.http2_initial_connection_window_size(http2_initial_connection_window_size); - } - if config.http2_adaptive_window { - builder.http2_adaptive_window(true); - } - if let Some(http2_max_frame_size) = config.http2_max_frame_size { - builder.http2_max_frame_size(http2_max_frame_size); - } - if let Some(http2_keep_alive_interval) = config.http2_keep_alive_interval { - builder.http2_keep_alive_interval(http2_keep_alive_interval); - } - if let Some(http2_keep_alive_timeout) = config.http2_keep_alive_timeout { - builder.http2_keep_alive_timeout(http2_keep_alive_timeout); - } - if config.http2_keep_alive_while_idle { - builder.http2_keep_alive_while_idle(true); + if matches!(config.http_version_pref, HttpVersionPref::Http2) { + builder.http2_only(true); + } + + if let Some(http2_initial_stream_window_size) = config.http2_initial_stream_window_size + { + builder.http2_initial_stream_window_size(http2_initial_stream_window_size); + } + if let Some(http2_initial_connection_window_size) = + config.http2_initial_connection_window_size + { + builder.http2_initial_connection_window_size(http2_initial_connection_window_size); + } + if config.http2_adaptive_window { + builder.http2_adaptive_window(true); + } + if let Some(http2_max_frame_size) = config.http2_max_frame_size { + builder.http2_max_frame_size(http2_max_frame_size); + } + if let Some(http2_keep_alive_interval) = config.http2_keep_alive_interval { + builder.http2_keep_alive_interval(http2_keep_alive_interval); + } + if let Some(http2_keep_alive_timeout) = config.http2_keep_alive_timeout { + builder.http2_keep_alive_timeout(http2_keep_alive_timeout); + } + if config.http2_keep_alive_while_idle { + builder.http2_keep_alive_while_idle(true); + } } #[cfg(not(target_arch = "wasm32"))] @@ -1089,6 +1110,7 @@ impl ClientBuilder { } /// Only use HTTP/2. + #[cfg(feature = "http2")] pub fn http2_prior_knowledge(mut self) -> ClientBuilder { self.config.http_version_pref = HttpVersionPref::Http2; self @@ -1105,6 +1127,7 @@ impl ClientBuilder { /// Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control. /// /// Default is currently 65,535 but may change internally to optimize for common uses. + #[cfg(feature = "http2")] pub fn http2_initial_stream_window_size(mut self, sz: impl Into>) -> ClientBuilder { self.config.http2_initial_stream_window_size = sz.into(); self @@ -1113,6 +1136,7 @@ impl ClientBuilder { /// Sets the max connection-level flow control for HTTP2 /// /// Default is currently 65,535 but may change internally to optimize for common uses. + #[cfg(feature = "http2")] pub fn http2_initial_connection_window_size( mut self, sz: impl Into>, @@ -1125,6 +1149,7 @@ impl ClientBuilder { /// /// Enabling this will override the limits set in `http2_initial_stream_window_size` and /// `http2_initial_connection_window_size`. + #[cfg(feature = "http2")] pub fn http2_adaptive_window(mut self, enabled: bool) -> ClientBuilder { self.config.http2_adaptive_window = enabled; self @@ -1133,6 +1158,7 @@ impl ClientBuilder { /// Sets the maximum frame size to use for HTTP2. /// /// Default is currently 16,384 but may change internally to optimize for common uses. + #[cfg(feature = "http2")] pub fn http2_max_frame_size(mut self, sz: impl Into>) -> ClientBuilder { self.config.http2_max_frame_size = sz.into(); self @@ -1142,6 +1168,7 @@ impl ClientBuilder { /// /// Pass `None` to disable HTTP2 keep-alive. /// Default is currently disabled. + #[cfg(feature = "http2")] pub fn http2_keep_alive_interval( mut self, interval: impl Into>, @@ -1155,6 +1182,7 @@ impl ClientBuilder { /// If the ping is not acknowledged within the timeout, the connection will be closed. /// Does nothing if `http2_keep_alive_interval` is disabled. /// Default is currently disabled. + #[cfg(feature = "http2")] pub fn http2_keep_alive_timeout(mut self, timeout: Duration) -> ClientBuilder { self.config.http2_keep_alive_timeout = Some(timeout); self @@ -1166,6 +1194,7 @@ impl ClientBuilder { /// If enabled, pings are also sent when no streams are active. /// Does nothing if `http2_keep_alive_interval` is disabled. /// Default is `false`. + #[cfg(feature = "http2")] pub fn http2_keep_alive_while_idle(mut self, enabled: bool) -> ClientBuilder { self.config.http2_keep_alive_while_idle = enabled; self @@ -1973,6 +2002,7 @@ impl Config { f.field("http1_only", &true); } + #[cfg(feature = "http2")] if matches!(self.http_version_pref, HttpVersionPref::Http2) { f.field("http2_prior_knowledge", &true); } @@ -2142,7 +2172,10 @@ impl PendingRequest { self.project().headers } + #[cfg(feature = "http2")] fn retry_error(mut self: Pin<&mut Self>, err: &(dyn std::error::Error + 'static)) -> bool { + use log::trace; + if !is_retryable_error(err) { return false; } @@ -2199,6 +2232,7 @@ impl PendingRequest { } } +#[cfg(feature = "http2")] fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { // pop the legacy::Error let err = if let Some(err) = err.source() { @@ -2276,6 +2310,7 @@ impl Future for PendingRequest { let res = match self.as_mut().in_flight().get_mut() { ResponseFuture::Default(r) => match Pin::new(r).poll(cx) { Poll::Ready(Err(e)) => { + #[cfg(feature = "http2")] if self.as_mut().retry_error(&e) { continue; } diff --git a/src/lib.rs b/src/lib.rs index f3da39a94..e7f5545b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,6 +173,7 @@ //! The following are a list of [Cargo features][cargo-features] that can be //! enabled or disabled: //! +//! - **http2** *(enabled by default)*: Enables HTTP/2 support. //! - **default-tls** *(enabled by default)*: Provides TLS support to connect //! over HTTPS. //! - **native-tls**: Enables TLS functionality provided by `native-tls`. diff --git a/tests/client.rs b/tests/client.rs index 9843547a2..8a17ca3da 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,7 +1,6 @@ #![cfg(not(target_arch = "wasm32"))] mod support; -use support::delay_server; use support::server; #[cfg(feature = "json")] @@ -442,6 +441,7 @@ async fn test_tls_info() { // fail, because the only thread would block until `panic_rx` receives a // notification while the client needs to be driven to get the graceful shutdown // done. +#[cfg(feature = "http2")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn highly_concurrent_requests_to_http2_server_with_low_max_concurrent_streams() { let client = reqwest::Client::builder() @@ -472,8 +472,11 @@ async fn highly_concurrent_requests_to_http2_server_with_low_max_concurrent_stre futures_util::future::join_all(futs).await; } +#[cfg(feature = "http2")] #[tokio::test] async fn highly_concurrent_requests_to_slow_http2_server_with_low_max_concurrent_streams() { + use support::delay_server; + let client = reqwest::Client::builder() .http2_prior_knowledge() .build()