Skip to content

Commit

Permalink
feat: Add http2 cargo feature (#2162)
Browse files Browse the repository at this point in the history
Technically a breaking change, since disabling default options will mean HTTP/2 is no longer enabled, and in 0.11.x, it was.
  • Loading branch information
yujincheng08 authored and seanmonstar committed Mar 20, 2024
1 parent eb94f26 commit fee78ec
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
- "feat.: default-tls and rustls-tls"
- "feat.: cookies"
- "feat.: blocking"
- "feat.: blocking only"
- "feat.: gzip"
- "feat.: brotli"
- "feat.: deflate"
Expand Down Expand Up @@ -138,6 +139,8 @@ jobs:
features: "--features cookies"
- name: "feat.: blocking"
features: "--features blocking"
- name: "feat.: blocking only"
features: "--no-default-features --features blocking"
- name: "feat.: gzip"
features: "--features gzip,stream"
- name: "feat.: brotli"
Expand Down
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -105,9 +107,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"
Expand Down
95 changes: 69 additions & 26 deletions src/async_impl/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -80,6 +80,7 @@ pub struct ClientBuilder {

enum HttpVersionPref {
Http1,
#[cfg(feature = "http2")]
Http2,
#[cfg(feature = "http3")]
Http3,
Expand Down Expand Up @@ -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<u32>,
#[cfg(feature = "http2")]
http2_initial_connection_window_size: Option<u32>,
#[cfg(feature = "http2")]
http2_adaptive_window: bool,
#[cfg(feature = "http2")]
http2_max_frame_size: Option<u32>,
#[cfg(feature = "http2")]
http2_keep_alive_interval: Option<Duration>,
#[cfg(feature = "http2")]
http2_keep_alive_timeout: Option<Duration>,
#[cfg(feature = "http2")]
http2_keep_alive_while_idle: bool,
local_address: Option<IpAddr>,
nodelay: bool,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -349,6 +364,7 @@ impl ClientBuilder {
HttpVersionPref::Http1 => {
tls.request_alpns(&["http/1.1"]);
}
#[cfg(feature = "http2")]
HttpVersionPref::Http2 => {
tls.request_alpns(&["h2"]);
}
Expand Down Expand Up @@ -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()];
}
Expand Down Expand Up @@ -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"))]
Expand Down Expand Up @@ -1089,6 +1110,8 @@ impl ClientBuilder {
}

/// Only use HTTP/2.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_prior_knowledge(mut self) -> ClientBuilder {
self.config.http_version_pref = HttpVersionPref::Http2;
self
Expand All @@ -1105,6 +1128,8 @@ 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_initial_stream_window_size(mut self, sz: impl Into<Option<u32>>) -> ClientBuilder {
self.config.http2_initial_stream_window_size = sz.into();
self
Expand All @@ -1113,6 +1138,8 @@ 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_initial_connection_window_size(
mut self,
sz: impl Into<Option<u32>>,
Expand All @@ -1125,6 +1152,8 @@ impl ClientBuilder {
///
/// Enabling this will override the limits set in `http2_initial_stream_window_size` and
/// `http2_initial_connection_window_size`.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_adaptive_window(mut self, enabled: bool) -> ClientBuilder {
self.config.http2_adaptive_window = enabled;
self
Expand All @@ -1133,6 +1162,8 @@ 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_max_frame_size(mut self, sz: impl Into<Option<u32>>) -> ClientBuilder {
self.config.http2_max_frame_size = sz.into();
self
Expand All @@ -1142,6 +1173,8 @@ impl ClientBuilder {
///
/// Pass `None` to disable HTTP2 keep-alive.
/// Default is currently disabled.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_keep_alive_interval(
mut self,
interval: impl Into<Option<Duration>>,
Expand All @@ -1155,6 +1188,8 @@ 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_keep_alive_timeout(mut self, timeout: Duration) -> ClientBuilder {
self.config.http2_keep_alive_timeout = Some(timeout);
self
Expand All @@ -1166,6 +1201,8 @@ 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_keep_alive_while_idle(mut self, enabled: bool) -> ClientBuilder {
self.config.http2_keep_alive_while_idle = enabled;
self
Expand Down Expand Up @@ -2008,6 +2045,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);
}
Expand Down Expand Up @@ -2177,7 +2215,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;
}
Expand Down Expand Up @@ -2234,6 +2275,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() {
Expand Down Expand Up @@ -2311,6 +2353,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;
}
Expand Down
10 changes: 10 additions & 0 deletions src/blocking/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,20 +445,26 @@ impl ClientBuilder {
}

/// Only use HTTP/2.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_prior_knowledge(self) -> ClientBuilder {
self.with_inner(|inner| inner.http2_prior_knowledge())
}

/// 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_initial_stream_window_size(self, sz: impl Into<Option<u32>>) -> ClientBuilder {
self.with_inner(|inner| inner.http2_initial_stream_window_size(sz))
}

/// 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_initial_connection_window_size(self, sz: impl Into<Option<u32>>) -> ClientBuilder {
self.with_inner(|inner| inner.http2_initial_connection_window_size(sz))
}
Expand All @@ -467,13 +473,17 @@ impl ClientBuilder {
///
/// Enabling this will override the limits set in `http2_initial_stream_window_size` and
/// `http2_initial_connection_window_size`.
#[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_adaptive_window(self, enabled: bool) -> ClientBuilder {
self.with_inner(|inner| inner.http2_adaptive_window(enabled))
}

/// 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn http2_max_frame_size(self, sz: impl Into<Option<u32>>) -> ClientBuilder {
self.with_inner(|inner| inner.http2_max_frame_size(sz))
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,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`.
Expand Down
1 change: 1 addition & 0 deletions tests/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ fn blocking_update_json_content_type_if_set_manually() {
}

#[test]
#[cfg(feature = "__tls")]
fn test_response_no_tls_info_for_http() {
let server = server::http(move |_req| async { http::Response::new("Hello".into()) });

Expand Down
5 changes: 4 additions & 1 deletion tests/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#![cfg(not(target_arch = "wasm32"))]
mod support;

use support::delay_server;
use support::server;

#[cfg(feature = "json")]
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit fee78ec

Please sign in to comment.