From d3b27acf371fdad3f8339a93a27a4f863576fac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 24 Apr 2024 13:58:10 -0700 Subject: [PATCH 01/47] refactor: Fix minimum `log` dependency specification (#2257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `log::log_enabled!` macro was added in version 0.4.6 of the crate. Hence, a version specification of 0.4 is insufficient. Fix it. Signed-off-by: Daniel Müller --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e0c4cab97..87ec823d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ 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" +log = "0.4.6" mime = "0.3.16" percent-encoding = "2.1" tokio = { version = "1.0", default-features = false, features = ["net", "time"] } From 81c0399c834f4c21fa7385c963eccbaa2facc90b Mon Sep 17 00:00:00 2001 From: mger1 Date: Thu, 25 Apr 2024 02:32:49 +0200 Subject: [PATCH 02/47] Allow overriding the DNS resolver for the blocking client. (#2260) --- src/blocking/client.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/blocking/client.rs b/src/blocking/client.rs index 7b020e733..9e6772910 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -16,6 +16,7 @@ use tokio::sync::{mpsc, oneshot}; use super::request::{Request, RequestBuilder}; use super::response::Response; use super::wait; +use crate::dns::Resolve; #[cfg(feature = "__tls")] use crate::tls; #[cfg(feature = "__tls")] @@ -920,6 +921,15 @@ impl ClientBuilder { self.with_inner(|inner| inner.resolve_to_addrs(domain, addrs)) } + /// Override the DNS resolver implementation. + /// + /// Pass an `Arc` wrapping a trait object implementing `Resolve`. + /// Overrides for specific names passed to `resolve` and `resolve_to_addrs` will + /// still be applied on top of this resolver. + pub fn dns_resolver(self, resolver: Arc) -> ClientBuilder { + self.with_inner(|inner| inner.dns_resolver(resolver)) + } + // private fn with_inner(mut self, func: F) -> ClientBuilder From 210c8a0c91bc0bdd5bdb2db9d5384cf97f9b9644 Mon Sep 17 00:00:00 2001 From: Nick Guletskii Date: Tue, 30 Apr 2024 15:07:05 +0300 Subject: [PATCH 03/47] wasm: Add an implementation for RequestBuilder::from_parts and RequestBuilder::build_split for wasm targets (#2268) --- src/wasm/request.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/wasm/request.rs b/src/wasm/request.rs index c373e5c8f..e6f51ebc1 100644 --- a/src/wasm/request.rs +++ b/src/wasm/request.rs @@ -115,6 +115,14 @@ impl RequestBuilder { RequestBuilder { client, request } } + /// Assemble a builder starting from an existing `Client` and a `Request`. + pub fn from_parts(client: crate::Client, request: crate::Request) -> crate::RequestBuilder { + crate::RequestBuilder { + client, + request: crate::Result::Ok(request), + } + } + /// Modify the query string of the URL. /// /// Modifies the URL of this request, adding the parameters provided. @@ -349,6 +357,15 @@ impl RequestBuilder { self.request } + /// Build a `Request`, which can be inspected, modified and executed with + /// `Client::execute()`. + /// + /// This is similar to [`RequestBuilder::build()`], but also returns the + /// embedded `Client`. + pub fn build_split(self) -> (Client, crate::Result) { + (self.client, self.request) + } + /// Constructs the Request and sends it to the target URL, returning a /// future Response. /// From 1b87711daa578fb75a2e33d1b3ec67aaf6be56df Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 30 Apr 2024 16:20:50 -0400 Subject: [PATCH 04/47] refactor: blocking feature doesn't need multi-threaded tokio runtime (#2269) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 87ec823d5..66c47b1e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ rustls-tls-manual-roots = ["__rustls"] rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls"] rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls"] -blocking = ["futures-channel/sink", "futures-util/io", "futures-util/sink", "tokio/rt-multi-thread", "tokio/sync"] +blocking = ["futures-channel/sink", "futures-util/io", "futures-util/sink", "tokio/sync"] charset = ["dep:encoding_rs"] From c654bb047274f9b8fec78442711abaf87c07ffdf Mon Sep 17 00:00:00 2001 From: lenstr Date: Wed, 1 May 2024 18:39:46 +0300 Subject: [PATCH 05/47] fix: correct Accept-Encoding header combinations in Accepts::as_str (#2271) --- src/async_impl/decoder.rs | 58 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/async_impl/decoder.rs b/src/async_impl/decoder.rs index 4e05428ae..f97141afa 100644 --- a/src/async_impl/decoder.rs +++ b/src/async_impl/decoder.rs @@ -484,9 +484,9 @@ impl Accepts { (true, true, true, false) => Some("gzip, br, zstd"), (true, true, false, false) => Some("gzip, br"), (true, false, true, true) => Some("gzip, zstd, deflate"), - (true, false, false, true) => Some("gzip, zstd, deflate"), + (true, false, false, true) => Some("gzip, deflate"), (false, true, true, true) => Some("br, zstd, deflate"), - (false, true, false, true) => Some("br, zstd, deflate"), + (false, true, false, true) => Some("br, deflate"), (true, false, true, false) => Some("gzip, zstd"), (true, false, false, false) => Some("gzip"), (false, true, true, false) => Some("br, zstd"), @@ -561,3 +561,57 @@ impl Default for Accepts { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_as_str() { + fn format_accept_encoding(accepts: &Accepts) -> String { + let mut encodings = vec![]; + if accepts.is_gzip() { + encodings.push("gzip"); + } + if accepts.is_brotli() { + encodings.push("br"); + } + if accepts.is_zstd() { + encodings.push("zstd"); + } + if accepts.is_deflate() { + encodings.push("deflate"); + } + encodings.join(", ") + } + + let state = [true, false]; + let mut permutations = Vec::new(); + + #[allow(unused_variables)] + for gzip in state { + for brotli in state { + for zstd in state { + for deflate in state { + permutations.push(Accepts { + #[cfg(feature = "gzip")] + gzip, + #[cfg(feature = "brotli")] + brotli, + #[cfg(feature = "zstd")] + zstd, + #[cfg(feature = "deflate")] + deflate, + }); + } + } + } + } + + for accepts in permutations { + let expected = format_accept_encoding(&accepts); + let got = accepts.as_str().unwrap_or(""); + assert_eq!(got, expected.as_str()); + } + } +} From 7e7011a657e2f97d3b52dda7bcd8af4353c06b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=96R=C3=96K=20Attila?= Date: Fri, 3 May 2024 17:37:36 +0200 Subject: [PATCH 06/47] Update cookie crates (#2274) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66c47b1e7..10a0e8815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,8 +142,8 @@ webpki-roots = { version = "0.26.0", optional = true } rustls-native-certs = { version = "0.7", optional = true } ## cookies -cookie_crate = { version = "0.17.0", package = "cookie", optional = true } -cookie_store = { version = "0.20.0", optional = true } +cookie_crate = { version = "0.18.0", package = "cookie", optional = true } +cookie_store = { version = "0.21.0", optional = true } ## compression async-compression = { version = "0.4.0", default-features = false, features = ["tokio"], optional = true } From 3da52832c7b143aa3689b2ec31b75ab43fd229fa Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sat, 11 May 2024 18:35:56 +0800 Subject: [PATCH 07/47] Fix typos in tests (#2284) --- tests/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/client.rs b/tests/client.rs index ea1eaa8ca..1639d68a0 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -60,7 +60,7 @@ async fn auto_headers() { } #[tokio::test] -async fn donot_set_conent_length_0_if_have_no_body() { +async fn donot_set_content_length_0_if_have_no_body() { let server = server::http(move |req| async move { let headers = req.headers(); assert_eq!(headers.get(CONTENT_LENGTH), None); @@ -70,7 +70,7 @@ async fn donot_set_conent_length_0_if_have_no_body() { http::Response::default() }); - let url = format!("http://{}/conent-length", server.addr()); + let url = format!("http://{}/content-length", server.addr()); let res = reqwest::Client::builder() .no_proxy() .build() @@ -466,7 +466,7 @@ async fn test_tls_info() { assert!(tls_info.is_none()); } -// NOTE: using the default "curernt_thread" runtime here would cause the test to +// NOTE: using the default "current_thread" runtime here would cause the test to // 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. From 03ddcac90ac50a79690a2739f7ac161e253cbb17 Mon Sep 17 00:00:00 2001 From: Adam Thibert Date: Wed, 15 May 2024 20:14:44 +0200 Subject: [PATCH 08/47] feat: Add `from_parts` and `build_split` methods to `blocking::RequestBuilder` struct (#2288) --- src/blocking/request.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/blocking/request.rs b/src/blocking/request.rs index 94f89b46a..9acc1e16a 100644 --- a/src/blocking/request.rs +++ b/src/blocking/request.rs @@ -167,6 +167,14 @@ impl RequestBuilder { } } + /// Assemble a builder starting from an existing `Client` and a `Request`. + pub fn from_parts(client: Client, request: Request) -> RequestBuilder { + RequestBuilder { + client, + request: crate::Result::Ok(request), + } + } + /// Add a `Header` to this Request. /// /// ```rust @@ -550,6 +558,15 @@ impl RequestBuilder { self.request } + /// Build a `Request`, which can be inspected, modified and executed with + /// `Client::execute()`. + /// + /// This is similar to [`RequestBuilder::build()`], but also returns the + /// embedded `Client`. + pub fn build_split(self) -> (Client, crate::Result) { + (self.client, self.request) + } + /// Constructs the Request and sends it the target URL, returning a Response. /// /// # Errors From 1f951d01bf3cda7d014698fd34adeba75706ac8c Mon Sep 17 00:00:00 2001 From: Luca Bruno Date: Mon, 20 May 2024 19:07:13 +0200 Subject: [PATCH 09/47] chore: update sync_wrapper to 1.0 (#2281) This updates `sync_wrapper` to its latest version (`1.0.1`). Ref: https://github.com/Actyx/sync_wrapper/compare/v0.1.2...v1.0.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10a0e8815..53e028880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,7 @@ serde_urlencoded = "0.7.1" tower-service = "0.3" futures-core = { version = "0.3.0", default-features = false } futures-util = { version = "0.3.0", default-features = false } -sync_wrapper = "0.1.2" +sync_wrapper = "1.0" # Optional deps... From c7db546e3e5a7086416bd487ae557c9e4248af08 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 3 Jun 2024 09:29:02 -0400 Subject: [PATCH 10/47] chore: ignore expected cfgs (#2298) --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 53e028880..1831be3ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -213,6 +213,9 @@ features = [ wasm-bindgen = { version = "0.2.68", features = ["serde-serialize"] } wasm-bindgen-test = "0.3" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("http3"))', 'cfg(reqwest_unstable)'] } + [[example]] name = "blocking" path = "examples/blocking.rs" From af38f9eb07b5322fcb17b5841689ec9e4461c755 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 3 Jun 2024 10:20:35 -0400 Subject: [PATCH 11/47] feat: upgrade to rustls 0.23 (#2299) --- Cargo.toml | 8 ++++---- src/async_impl/client.rs | 13 ++++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1831be3ec..5e0cdb241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,7 @@ __tls = ["dep:rustls-pemfile", "tokio/io-util"] # Enables common rustls code. # Equivalent to rustls-tls-manual-roots but shorter :) -__rustls = ["dep:hyper-rustls", "dep:tokio-rustls", "dep:rustls", "__tls", "dep:rustls-pemfile", "rustls-pki-types"] +__rustls = ["dep:hyper-rustls", "dep:tokio-rustls", "dep:rustls", "__tls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # When enabled, disable using the cached SYS_PROXIES. __internal_proxy_sys_no_cache = [] @@ -134,10 +134,10 @@ native-tls-crate = { version = "0.2.10", optional = true, package = "native-tls" tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls -hyper-rustls = { version = "0.26.0", default-features = false, optional = true } -rustls = { version = "0.22.2", optional = true } +hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } +rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "ring", "tls12"] } rustls-pki-types = { version = "1.1.0", features = ["alloc"] ,optional = true } -tokio-rustls = { version = "0.25", optional = true } +tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring", "tls12"] } webpki-roots = { version = "0.26.0", optional = true } rustls-native-certs = { version = "0.7", optional = true } diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 22519f535..1930eff59 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -565,10 +565,17 @@ impl ClientBuilder { return Err(crate::error::builder("empty supported tls versions")); } + // Allow user to have installed a runtime default. + // If not, we use ring. + let provider = rustls::crypto::CryptoProvider::get_default() + .map(|arc| arc.clone()) + .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())); + // Build TLS config - let config_builder = - rustls::ClientConfig::builder_with_protocol_versions(&versions) - .with_root_certificates(root_cert_store); + let config_builder = rustls::ClientConfig::builder_with_provider(provider) + .with_protocol_versions(&versions) + .map_err(|_| crate::error::builder("invalid TLS versions"))? + .with_root_certificates(root_cert_store); // Finalize TLS config let mut tls = if let Some(id) = config.identity { From 8dc1c0803caa4ee9bde7576762c05cd8ee9168f9 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 3 Jun 2024 15:32:32 -0400 Subject: [PATCH 12/47] feat: re-enable experimentl HTTP/3 feature (#2300) --- .github/workflows/ci.yml | 4 +-- Cargo.toml | 38 ++++++++++++++--------------- src/async_impl/body.rs | 20 ++++++++++++--- src/async_impl/client.rs | 24 ++++++++++++------ src/async_impl/h3_client/connect.rs | 4 ++- src/async_impl/h3_client/mod.rs | 8 +++--- src/async_impl/h3_client/pool.rs | 12 ++++++--- src/async_impl/response.rs | 2 +- 8 files changed, 71 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74fbae5ee..ec18a401c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,8 +212,8 @@ jobs: with: toolchain: 'stable' - #- name: Check - # run: RUSTFLAGS="--cfg reqwest_unstable" cargo check --features http3 + - name: Check + run: RUSTFLAGS="--cfg reqwest_unstable" cargo check --features http3 docs: name: Docs diff --git a/Cargo.toml b/Cargo.toml index 5e0cdb241..46e7307fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ rustls-tls-manual-roots = ["__rustls"] rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls"] rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls"] -blocking = ["futures-channel/sink", "futures-util/io", "futures-util/sink", "tokio/sync"] +blocking = ["dep:futures-channel", "futures-channel?/sink", "futures-util/io", "futures-util/sink", "tokio/sync"] charset = ["dep:encoding_rs"] @@ -75,8 +75,7 @@ socks = ["dep:tokio-socks"] macos-system-configuration = ["dep:system-configuration"] # Experimental HTTP/3 client. -# Disabled while waiting for quinn to upgrade. -#http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep:futures-channel"] +http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep:slab", "dep:futures-channel"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at anytime. @@ -94,13 +93,13 @@ __internal_proxy_sys_no_cache = [] [dependencies] base64 = "0.22" http = "1" -url = "2.2" +url = "2.4" bytes = "1.0" serde = "1.0" serde_urlencoded = "0.7.1" tower-service = "0.3" -futures-core = { version = "0.3.0", default-features = false } -futures-util = { version = "0.3.0", default-features = false } +futures-core = { version = "0.3.28", default-features = false } +futures-util = { version = "0.3.28", default-features = false } sync_wrapper = "1.0" # Optional deps... @@ -114,15 +113,15 @@ mime_guess = { version = "2.0", default-features = false, optional = true } encoding_rs = { version = "0.8", optional = true } http-body = "1" http-body-util = "0.1" -hyper = { version = "1", features = ["http1", "client"] } +hyper = { version = "1.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.6" +once_cell = "1.18" +log = "0.4.17" mime = "0.3.16" -percent-encoding = "2.1" +percent-encoding = "2.3" tokio = { version = "1.0", default-features = false, features = ["net", "time"] } -pin-project-lite = "0.2.0" +pin-project-lite = "0.2.11" ipnet = "2.3" # Optional deps... @@ -147,7 +146,7 @@ cookie_store = { version = "0.21.0", optional = true } ## compression async-compression = { version = "0.4.0", default-features = false, features = ["tokio"], optional = true } -tokio-util = { version = "0.7.1", default-features = false, features = ["codec", "io"], optional = true } +tokio-util = { version = "0.7.9", default-features = false, features = ["codec", "io"], optional = true } ## socks tokio-socks = { version = "0.5.1", optional = true } @@ -156,23 +155,24 @@ tokio-socks = { version = "0.5.1", optional = true } hickory-resolver = { version = "0.24", optional = true, features = ["tokio-runtime"] } # HTTP/3 experimental support -h3 = { version = "0.0.4", optional = true } -h3-quinn = { version = "0.0.5", optional = true } -quinn = { version = "0.10", default-features = false, features = ["tls-rustls", "ring", "runtime-tokio"], optional = true } +h3 = { version = "0.0.5", optional = true } +h3-quinn = { version = "0.0.6", optional = true } +quinn = { version = "0.11.1", default-features = false, features = ["rustls", "ring", "runtime-tokio"], optional = true } +slab = { version = "0.4.9", optional = true } # just to get minimal versions working with quinn futures-channel = { version = "0.3", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] env_logger = "0.10" hyper = { version = "1.1.0", default-features = false, features = ["http1", "http2", "client", "server"] } -hyper-util = { version = "0.1", features = ["http1", "http2", "client", "client-legacy", "server-auto", "tokio"] } +hyper-util = { version = "0.1.3", features = ["http1", "http2", "client", "client-legacy", "server-auto", "tokio"] } serde = { version = "1.0", features = ["derive"] } libflate = "1.0" brotli_crate = { package = "brotli", version = "3.3.0" } zstd_crate = { package = "zstd", version = "0.13" } doc-comment = "0.3" tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] } -futures-util = { version = "0.3.0", default-features = false, features = ["std", "alloc"] } +futures-util = { version = "0.3.28", default-features = false, features = ["std", "alloc"] } [target.'cfg(windows)'.dependencies] winreg = "0.52.0" @@ -190,7 +190,7 @@ wasm-bindgen-futures = "0.4.18" wasm-streams = { version = "0.4", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] -version = "0.3.25" +version = "0.3.28" features = [ "AbortController", "AbortSignal", @@ -214,7 +214,7 @@ wasm-bindgen = { version = "0.2.68", features = ["serde-serialize"] } wasm-bindgen-test = "0.3" [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("http3"))', 'cfg(reqwest_unstable)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(reqwest_unstable)'] } [[example]] name = "blocking" diff --git a/src/async_impl/body.rs b/src/async_impl/body.rs index a70a853b1..638df76fb 100644 --- a/src/async_impl/body.rs +++ b/src/async_impl/body.rs @@ -393,11 +393,25 @@ where pub(crate) type ResponseBody = http_body_util::combinators::BoxBody>; -pub(crate) fn response( - body: hyper::body::Incoming, +pub(crate) fn boxed(body: B) -> ResponseBody +where + B: hyper::body::Body + Send + Sync + 'static, + B::Error: Into>, +{ + use http_body_util::BodyExt; + + body.map_err(box_err).boxed() +} + +pub(crate) fn response( + body: B, deadline: Option>>, read_timeout: Option, -) -> ResponseBody { +) -> ResponseBody +where + B: hyper::body::Body + Send + Sync + 'static, + B::Error: Into>, +{ use http_body_util::BodyExt; match (deadline, read_timeout) { diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 1930eff59..3e2edaeae 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1805,7 +1805,7 @@ impl ClientBuilder { /// The default is false. #[cfg(feature = "http3")] #[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))] - pub fn set_tls_enable_early_data(mut self, enabled: bool) -> ClientBuilder { + pub fn tls_early_data(mut self, enabled: bool) -> ClientBuilder { self.config.tls_enable_early_data = enabled; self } @@ -1817,7 +1817,7 @@ impl ClientBuilder { /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html #[cfg(feature = "http3")] #[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))] - pub fn set_quic_max_idle_timeout(mut self, value: Duration) -> ClientBuilder { + pub fn http3_max_idle_timeout(mut self, value: Duration) -> ClientBuilder { self.config.quic_max_idle_timeout = Some(value); self } @@ -1828,10 +1828,14 @@ impl ClientBuilder { /// Please see docs in [`TransportConfig`] in [`quinn`]. /// /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + /// + /// # Panics + /// + /// Panics if the value is over 2^62. #[cfg(feature = "http3")] #[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))] - pub fn set_quic_stream_receive_window(mut self, value: VarInt) -> ClientBuilder { - self.config.quic_stream_receive_window = Some(value); + pub fn http3_stream_receive_window(mut self, value: u64) -> ClientBuilder { + self.config.quic_stream_receive_window = Some(value.try_into().unwrap()); self } @@ -1841,10 +1845,14 @@ impl ClientBuilder { /// Please see docs in [`TransportConfig`] in [`quinn`]. /// /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html + /// + /// # Panics + /// + /// Panics if the value is over 2^62. #[cfg(feature = "http3")] #[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))] - pub fn set_quic_receive_window(mut self, value: VarInt) -> ClientBuilder { - self.config.quic_receive_window = Some(value); + pub fn http3_conn_receive_window(mut self, value: u64) -> ClientBuilder { + self.config.quic_receive_window = Some(value.try_into().unwrap()); self } @@ -1855,7 +1863,7 @@ impl ClientBuilder { /// [`TransportConfig`]: https://docs.rs/quinn/latest/quinn/struct.TransportConfig.html #[cfg(feature = "http3")] #[cfg_attr(docsrs, doc(cfg(all(reqwest_unstable, feature = "http3",))))] - pub fn set_quic_send_window(mut self, value: u64) -> ClientBuilder { + pub fn http3_send_window(mut self, value: u64) -> ClientBuilder { self.config.quic_send_window = Some(value); self } @@ -2540,7 +2548,7 @@ impl Future for PendingRequest { crate::error::request(e).with_url(self.url.clone()) )); } - Poll::Ready(Ok(res)) => res, + Poll::Ready(Ok(res)) => res.map(super::body::boxed), Poll::Pending => return Poll::Pending, }, #[cfg(feature = "http3")] diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index ec732f66a..1d7f4a3cf 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -6,6 +6,7 @@ use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::Uri; use hyper_util::client::legacy::connect::dns::Name; +use quinn::crypto::rustls::QuicClientConfig; use quinn::{ClientConfig, Endpoint, TransportConfig}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; @@ -29,7 +30,8 @@ impl H3Connector { local_addr: Option, transport_config: TransportConfig, ) -> Result { - let mut config = ClientConfig::new(Arc::new(tls)); + let quic_client_config = Arc::new(QuicClientConfig::try_from(tls)?); + let mut config = ClientConfig::new(quic_client_config); // FIXME: Replace this when there is a setter. config.transport_config(Arc::new(transport_config)); diff --git a/src/async_impl/h3_client/mod.rs b/src/async_impl/h3_client/mod.rs index ce877cdfe..aa1bc4422 100644 --- a/src/async_impl/h3_client/mod.rs +++ b/src/async_impl/h3_client/mod.rs @@ -4,13 +4,13 @@ pub(crate) mod connect; pub(crate) mod dns; mod pool; +use crate::async_impl::body::ResponseBody; use crate::async_impl::h3_client::pool::{Key, Pool, PoolClient}; use crate::error::{BoxError, Error, Kind}; use crate::{error, Body}; use connect::H3Connector; use futures_util::future; use http::{Request, Response}; -use hyper::Body as HyperBody; use log::trace; use std::future::Future; use std::pin::Pin; @@ -49,7 +49,7 @@ impl H3Client { mut self, key: Key, req: Request, - ) -> Result, Error> { + ) -> Result, Error> { let mut pooled = match self.get_pooled_client(key).await { Ok(client) => client, Err(e) => return Err(error::request(e)), @@ -76,11 +76,11 @@ impl H3Client { } pub(crate) struct H3ResponseFuture { - inner: Pin, Error>> + Send>>, + inner: Pin, Error>> + Send>>, } impl Future for H3ResponseFuture { - type Output = Result, Error>; + type Output = Result, Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.inner.as_mut().poll(cx) diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index d6442c81a..9e708dbfb 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::time::Instant; +use crate::async_impl::body::ResponseBody; use crate::error::{BoxError, Error, Kind}; use crate::Body; use bytes::Buf; @@ -13,7 +14,6 @@ use h3::client::SendRequest; use h3_quinn::{Connection, OpenStreams}; use http::uri::{Authority, Scheme}; use http::{Request, Response, Uri}; -use hyper::body as HyperBody; use log::trace; pub(super) type Key = (Scheme, Authority); @@ -125,7 +125,9 @@ impl PoolClient { pub async fn send_request( &mut self, req: Request, - ) -> Result, BoxError> { + ) -> Result, BoxError> { + use http_body_util::{BodyExt, Full}; + let (head, req_body) = req.into_parts(); let req = Request::from_parts(head, ()); let mut stream = self.inner.send_request(req).await?; @@ -146,7 +148,11 @@ impl PoolClient { resp_body.extend(chunk.chunk()) } - Ok(resp.map(|_| HyperBody::from(resp_body))) + let resp_body = Full::new(resp_body.into()) + .map_err(|never| match never {}) + .boxed(); + + Ok(resp.map(|_| resp_body)) } } diff --git a/src/async_impl/response.rs b/src/async_impl/response.rs index 4088bd647..6e2544f96 100644 --- a/src/async_impl/response.rs +++ b/src/async_impl/response.rs @@ -35,7 +35,7 @@ pub struct Response { impl Response { pub(super) fn new( - res: hyper::Response, + res: hyper::Response, url: Url, accepts: Accepts, total_timeout: Option>>, From a75b26a857a06653ebfc93aa6ba7f669a81a70b3 Mon Sep 17 00:00:00 2001 From: asonix Date: Tue, 4 Jun 2024 07:33:29 -0500 Subject: [PATCH 13/47] feat: add cargo feature to use rustls without ring (#2301) --- .github/workflows/ci.yml | 6 ++++++ Cargo.toml | 20 +++++++++++++------- src/async_impl/client.rs | 14 +++++++++++++- src/async_impl/request.rs | 2 ++ tests/badssl.rs | 1 + tests/client.rs | 1 + tests/proxy.rs | 1 + tests/redirect.rs | 1 + tests/timeouts.rs | 1 + tests/upgrade.rs | 1 + 10 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec18a401c..846feccf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,8 +72,10 @@ jobs: - "feat.: rustls-tls" - "feat.: rustls-tls-manual-roots" - "feat.: rustls-tls-native-roots" + - "feat.: rustls-tls-no-provider" - "feat.: native-tls" - "feat.: default-tls and rustls-tls" + - "feat.: rustls-tls and rustls-tls-no-provider" - "feat.: cookies" - "feat.: blocking" - "feat.: blocking only" @@ -131,8 +133,12 @@ jobs: features: "--no-default-features --features rustls-tls-manual-roots" - name: "feat.: rustls-tls-native-roots" features: "--no-default-features --features rustls-tls-native-roots" + - name: "feat.: rustls-tls-no-provider" + features: "--no-default-features --features rustls-tls-no-provider" - name: "feat.: native-tls" features: "--features native-tls" + - name: "feat.: rustls-tls and rustls-tls-no-provider" + features: "--features rustls-tls,rustls-tls-no-provider" - name: "feat.: default-tls and rustls-tls" features: "--features rustls-tls" - name: "feat.: cookies" diff --git a/Cargo.toml b/Cargo.toml index 46e7307fc..7af59bdcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,9 +41,12 @@ native-tls-alpn = ["native-tls", "native-tls-crate?/alpn", "hyper-tls?/alpn"] native-tls-vendored = ["native-tls", "native-tls-crate?/vendored"] rustls-tls = ["rustls-tls-webpki-roots"] -rustls-tls-manual-roots = ["__rustls"] -rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls"] -rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls"] +rustls-tls-no-provider = ["rustls-tls-manual-roots-no-provider"] + +rustls-tls-manual-roots = ["__rustls", "__rustls-ring"] +rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls", "__rustls-ring"] +rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls", "__rustls-ring"] +rustls-tls-manual-roots-no-provider = ["__rustls"] blocking = ["dep:futures-channel", "futures-channel?/sink", "futures-util/io", "futures-util/sink", "tokio/sync"] @@ -77,6 +80,7 @@ macos-system-configuration = ["dep:system-configuration"] # Experimental HTTP/3 client. http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep:slab", "dep:futures-channel"] + # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at anytime. @@ -86,6 +90,7 @@ __tls = ["dep:rustls-pemfile", "tokio/io-util"] # Enables common rustls code. # Equivalent to rustls-tls-manual-roots but shorter :) __rustls = ["dep:hyper-rustls", "dep:tokio-rustls", "dep:rustls", "__tls", "dep:rustls-pemfile", "dep:rustls-pki-types"] +__rustls-ring = ["hyper-rustls?/ring", "tokio-rustls?/ring", "rustls?/ring", "quinn?/ring"] # When enabled, disable using the cached SYS_PROXIES. __internal_proxy_sys_no_cache = [] @@ -133,10 +138,10 @@ native-tls-crate = { version = "0.2.10", optional = true, package = "native-tls" tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls -hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "ring", "tls12"] } +hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "native-tokio", "tls12"] } +rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] } rustls-pki-types = { version = "1.1.0", features = ["alloc"] ,optional = true } -tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring", "tls12"] } +tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12"] } webpki-roots = { version = "0.26.0", optional = true } rustls-native-certs = { version = "0.7", optional = true } @@ -157,7 +162,7 @@ hickory-resolver = { version = "0.24", optional = true, features = ["tokio-runti # HTTP/3 experimental support h3 = { version = "0.0.5", optional = true } h3-quinn = { version = "0.0.6", optional = true } -quinn = { version = "0.11.1", default-features = false, features = ["rustls", "ring", "runtime-tokio"], optional = true } +quinn = { version = "0.11.1", default-features = false, features = ["rustls", "runtime-tokio"], optional = true } slab = { version = "0.4.9", optional = true } # just to get minimal versions working with quinn futures-channel = { version = "0.3", optional = true } @@ -173,6 +178,7 @@ zstd_crate = { package = "zstd", version = "0.13" } doc-comment = "0.3" tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] } futures-util = { version = "0.3.28", default-features = false, features = ["std", "alloc"] } +rustls = { version = "0.23", default-features = false, features = ["ring"] } [target.'cfg(windows)'.dependencies] winreg = "0.52.0" diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 3e2edaeae..994fa6fda 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -569,7 +569,13 @@ impl ClientBuilder { // If not, we use ring. let provider = rustls::crypto::CryptoProvider::get_default() .map(|arc| arc.clone()) - .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider())); + .unwrap_or_else(|| { + #[cfg(not(feature = "__rustls-ring"))] + panic!("No provider set"); + + #[cfg(feature = "__rustls-ring")] + Arc::new(rustls::crypto::ring::default_provider()) + }); // Build TLS config let config_builder = rustls::ClientConfig::builder_with_provider(provider) @@ -1312,6 +1318,8 @@ impl ClientBuilder { /// # Example /// /// ``` + /// # #[cfg(all(feature = "__rustls", not(feature = "__rustls-ring")))] + /// # let _ = rustls::crypto::ring::default_provider().install_default(); /// use std::net::IpAddr; /// let local_addr = IpAddr::from([12, 4, 1, 8]); /// let client = reqwest::Client::builder() @@ -1331,6 +1339,8 @@ impl ClientBuilder { /// # Example /// /// ``` + /// # #[cfg(all(feature = "__rustls", not(feature = "__rustls-ring")))] + /// # let _ = rustls::crypto::ring::default_provider().install_default(); /// let interface = "lo"; /// let client = reqwest::Client::builder() /// .interface(interface) @@ -2762,6 +2772,8 @@ fn add_cookie_header(headers: &mut HeaderMap, cookie_store: &dyn cookie::CookieS #[cfg(test)] mod tests { + #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] + #[tokio::test] async fn execute_request_rejects_invalid_urls() { let url_str = "hxxps://www.rust-lang.org/"; diff --git a/src/async_impl/request.rs b/src/async_impl/request.rs index 665710430..aa900ca02 100644 --- a/src/async_impl/request.rs +++ b/src/async_impl/request.rs @@ -649,6 +649,8 @@ impl TryFrom for HttpRequest { #[cfg(test)] mod tests { + #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] + use super::{Client, HttpRequest, Request, RequestBuilder, Version}; use crate::Method; use serde::Serialize; diff --git a/tests/badssl.rs b/tests/badssl.rs index 9b001d070..e889e864f 100644 --- a/tests/badssl.rs +++ b/tests/badssl.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] #[cfg(all(feature = "__tls", not(feature = "rustls-tls-manual-roots")))] #[tokio::test] diff --git a/tests/client.rs b/tests/client.rs index 1639d68a0..f97b26302 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use support::server; diff --git a/tests/proxy.rs b/tests/proxy.rs index 231de25d8..9231a3267 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use support::server; diff --git a/tests/redirect.rs b/tests/redirect.rs index c98c799ef..c496d90d3 100644 --- a/tests/redirect.rs +++ b/tests/redirect.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use http_body_util::BodyExt; use reqwest::Body; diff --git a/tests/timeouts.rs b/tests/timeouts.rs index c18fecdbe..c3649ea9f 100644 --- a/tests/timeouts.rs +++ b/tests/timeouts.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use support::server; diff --git a/tests/upgrade.rs b/tests/upgrade.rs index 5ea72acc2..7a67c0457 100644 --- a/tests/upgrade.rs +++ b/tests/upgrade.rs @@ -1,4 +1,5 @@ #![cfg(not(target_arch = "wasm32"))] +#![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] mod support; use support::server; use tokio::io::{AsyncReadExt, AsyncWriteExt}; From 57acdc1ffb63dd82ebc35e04d1deeed2b6848461 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 4 Jun 2024 11:25:28 -0400 Subject: [PATCH 14/47] fix http3 compilation if http2 is disabled (#2304) --- src/async_impl/client.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 994fa6fda..b321a609a 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -2404,7 +2404,7 @@ impl PendingRequest { self.project().headers } - #[cfg(feature = "http2")] + #[cfg(any(feature = "http2", feature = "http3"))] fn retry_error(mut self: Pin<&mut Self>, err: &(dyn std::error::Error + 'static)) -> bool { use log::trace; @@ -2464,7 +2464,7 @@ impl PendingRequest { } } -#[cfg(feature = "http2")] +#[cfg(any(feature = "http2", feature = "http3"))] fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { // pop the legacy::Error let err = if let Some(err) = err.source() { @@ -2482,6 +2482,7 @@ fn is_retryable_error(err: &(dyn std::error::Error + 'static)) -> bool { } } + #[cfg(feature = "http2")] if let Some(cause) = err.source() { if let Some(err) = cause.downcast_ref::() { // They sent us a graceful shutdown, try with a new connection! From 695bc0463726bb243e235f17c7f8833974835ec8 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 4 Jun 2024 11:27:08 -0400 Subject: [PATCH 15/47] fix: http3 resolving ipv6 addresses (#2305) --- src/async_impl/h3_client/connect.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/async_impl/h3_client/connect.rs b/src/async_impl/h3_client/connect.rs index 1d7f4a3cf..2cce1cf47 100644 --- a/src/async_impl/h3_client/connect.rs +++ b/src/async_impl/h3_client/connect.rs @@ -47,7 +47,11 @@ impl H3Connector { } pub async fn connect(&mut self, dest: Uri) -> Result { - let host = dest.host().ok_or("destination must have a host")?; + let host = dest + .host() + .ok_or("destination must have a host")? + .trim_start_matches('[') + .trim_end_matches(']'); let port = dest.port_u16().unwrap_or(443); let addrs = if let Some(addr) = IpAddr::from_str(host).ok() { From 8cc7cd4d35836085452051dc5be55e305f754923 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 11 Jun 2024 08:40:15 -0400 Subject: [PATCH 16/47] msrv: pin url --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 846feccf8..1ac80134d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -297,6 +297,7 @@ jobs: cargo update cargo update -p log --precise 0.4.18 cargo update -p tokio --precise 1.29.1 + cargo update -p url --precise 2.5.0 - uses: Swatinem/rust-cache@v2 From c56fbae99862763b4d834ca749ea2d1c6b8bb9ce Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 11 Jun 2024 08:53:31 -0400 Subject: [PATCH 17/47] tests: remove a proxy parse error check --- src/proxy.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/proxy.rs b/src/proxy.rs index 17670cf4a..f5e8f66ac 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1984,11 +1984,6 @@ mod test { url::ParseError::InvalidIpv6Address, ); } - - #[test] - fn invalid_domain_character() { - check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter); - } } } } From ccb5e40a5d68c93dd696963bb7ca5b09ce116c81 Mon Sep 17 00:00:00 2001 From: asonix Date: Sun, 9 Jun 2024 11:43:07 -0500 Subject: [PATCH 18/47] Don't compile hyper-tls with native-roots unless rustls-tls-native-roots is enabled --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7af59bdcb..4258b9dc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,8 +44,8 @@ rustls-tls = ["rustls-tls-webpki-roots"] rustls-tls-no-provider = ["rustls-tls-manual-roots-no-provider"] rustls-tls-manual-roots = ["__rustls", "__rustls-ring"] -rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls", "__rustls-ring"] -rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls", "__rustls-ring"] +rustls-tls-webpki-roots = ["dep:webpki-roots", "hyper-rustls?/webpki-tokio", "__rustls", "__rustls-ring"] +rustls-tls-native-roots = ["dep:rustls-native-certs", "hyper-rustls?/native-tokio", "__rustls", "__rustls-ring"] rustls-tls-manual-roots-no-provider = ["__rustls"] blocking = ["dep:futures-channel", "futures-channel?/sink", "futures-util/io", "futures-util/sink", "tokio/sync"] @@ -138,7 +138,7 @@ native-tls-crate = { version = "0.2.10", optional = true, package = "native-tls" tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls -hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "native-tokio", "tls12"] } +hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "tls12"] } rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] } rustls-pki-types = { version = "1.1.0", features = ["alloc"] ,optional = true } tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12"] } From e5ce0b5ea38d685dd07ecd6b1b456ca2b7c236e4 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Tue, 11 Jun 2024 12:25:57 +0200 Subject: [PATCH 19/47] fix: don't enable hyper-rustls/http2 unless http2 is already enabled --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4258b9dc9..2c019cc30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ default = ["default-tls", "charset", "http2", "macos-system-configuration"] # functionality for it. default-tls = ["dep:hyper-tls", "dep:native-tls-crate", "__tls", "dep:tokio-native-tls"] -http2 = ["h2", "hyper/http2", "hyper-util/http2"] +http2 = ["h2", "hyper/http2", "hyper-util/http2", "hyper-rustls?/http2"] # Enables native-tls specific functionality not available by default. native-tls = ["default-tls"] @@ -138,7 +138,7 @@ native-tls-crate = { version = "0.2.10", optional = true, package = "native-tls" tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls -hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "http2", "tls12"] } +hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "tls12"] } rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] } rustls-pki-types = { version = "1.1.0", features = ["alloc"] ,optional = true } tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12"] } From 404df599c963e336b5419e4c8eec72ed5f18e4fc Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 4 Jun 2024 11:05:20 -0400 Subject: [PATCH 20/47] test: add http3 test server support --- .github/workflows/ci.yml | 5 +- tests/client.rs | 32 +++++++++++ tests/support/server.cert | Bin 0 -> 996 bytes tests/support/server.key | Bin 0 -> 1192 bytes tests/support/server.rs | 109 +++++++++++++++++++++++++++++++++++++- 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 tests/support/server.cert create mode 100644 tests/support/server.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ac80134d..e86e3ace4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,10 @@ jobs: toolchain: 'stable' - name: Check - run: RUSTFLAGS="--cfg reqwest_unstable" cargo check --features http3 + run: cargo test --features http3 + env: + RUSTFLAGS: --cfg reqwest_unstable + RUSTDOCFLAGS: --cfg reqwest_unstable docs: name: Docs diff --git a/tests/client.rs b/tests/client.rs index f97b26302..5fa9a3532 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -84,6 +84,37 @@ async fn donot_set_content_length_0_if_have_no_body() { assert_eq!(res.status(), reqwest::StatusCode::OK); } +#[cfg(feature = "http3")] +#[tokio::test] +async fn http3_request_full() { + //use http_body_util::BodyExt; + + let server = server::http3(move |_req| async move { + /* + assert_eq!(req.headers()[CONTENT_LENGTH], "5"); + let reqb = req.collect().await.unwrap().to_bytes(); + assert_eq!(reqb, "hello"); + */ + http::Response::default() + }); + + let url = format!("https://{}/content-length", server.addr()); + let res = reqwest::Client::builder() + .http3_prior_knowledge() + .danger_accept_invalid_certs(true) + .build() + .expect("client builder") + .post(url) + .version(http::Version::HTTP_3) + .body("hello") + .send() + .await + .expect("request"); + + assert_eq!(res.version(), http::Version::HTTP_3); + assert_eq!(res.status(), reqwest::StatusCode::OK); +} + #[tokio::test] async fn user_agent() { let server = server::http(move |req| async move { @@ -384,6 +415,7 @@ async fn http2_upgrade() { } #[cfg(feature = "default-tls")] +#[cfg_attr(feature = "http3", ignore = "enabling http3 seems to break this, why?")] #[tokio::test] async fn test_allowed_methods() { let resp = reqwest::Client::builder() diff --git a/tests/support/server.cert b/tests/support/server.cert new file mode 100644 index 0000000000000000000000000000000000000000..e573f2a52aefc858adf2c7379e841e4816407f90 GIT binary patch literal 996 zcmXqLVt!!I#N@GnnTe5!iHY%q0WTY;R+~rLcV0$DZdL{Z4MQ~p6*lHj7G@sVjLL%4 zqRi4 zG!!=wg_y)wl3H9+oLW?tTBMhppKH*>sDx}UBP#=Q6C*zZP@IdYiII_EQ%1?p$BsWY zUJBc^C)lHX`u3(5hYK=iG!!kI9Byr>D*9>HT-o}xo13KCHM5T1*0^pa>m8V=VR>X* zz%ro79NOx zS~$B>RA|YO#cgLU%CuI#d$h)M`Y!PY#>z*3&y!x2)_%^*GVSCWcC$BAx*ga*d%V?~ z$M(}v`+5JTgOv|;xck-$x;&T}sj;eC@N1ITX>-S@^N*LU)tmT}HE&hZoNtF_=qhc0 z^7ZcHRBzGbC%u7dI|4Xq<1r0}Mi0VMfOPEKCLr2HYSXKMM;p6Waj;SrCVhMT|vcQu?h3 zC&q58cLwV`zU_0f&pjsUXJ7}CR%S6bFl`WdYQj9HPk#Tk`!D%dyxbhnx7Y2q+(M}( z5(Z+1A_hW0g~0gchR3(FV-sVwfig(HAd8HFR1+^uO0Og}Iira)CqFqcCnLYO1UVXk z$p;vXjSMQ{VNo@u&zF4M!r{H@RzpnuUnwDfmEh*?8~gS%zf;&|d+v?p5B92fkFff} zxW|t>v!?3J7v0f$ZW`A<-y;$V#?Ow_eA^w@Vtk?a>wY7jmUCZO4u>!Z)X!e$8ht@( zrU1vEf}gYQ1Pe^o+NrmSdGo@%5B6Ww`FMH#ouvDF>}IFDx@{iHWaHG4WigX&lAe9t z5M|-Rh1hx*bop zmlqwIx3l?#WZ8z2I|1IlFJEYxpG!J#@RxJi+SPkhdTrLm-#hVXSG?=i|GfSG6W46q z&+$XlT`RM5&6T`K-N7rQtP;1mx!veIxLcfQ{w(*#H!pa;UpoF!<%zMJz{@lRM>WrS zZ}lWLu}iX{?Z1DeFSxQu;gZ9KrDjrx*J#|UuW@gB5`R-{a=G=9Uz1HL|8wtGeDJ-S z@AeMf#)e({R=DLpkZ3hAh}qAV^6;LsW~%9rR_WKDs{)1B_{}m`^jdFItZe`Q0C=Fg literal 0 HcmV?d00001 diff --git a/tests/support/server.key b/tests/support/server.key new file mode 100644 index 0000000000000000000000000000000000000000..757035e2419512e11770773dd25aa01ce6ee1f5d GIT binary patch literal 1192 zcmV;Z1Xueof&`=j0RRGm0RaHAXmt7GLHV)LRLA ztuB%I1#YT>oc6+)Eh4w%_1)uTOBG|Mu7Kk{zX~3dikh>n4T#Wqgk&k;x6H&n)$H2{ zkn>Mau$GgM6>v4qw0|5X50)hbmJi1x81*ltkFap}_N8J=^j7YyaXLU+9&doUf zBxB+q%5%dnTS9{`px_BL6AU`0rvT};Rlom<`XdUE&j4+DF|R+;g?}$W?TKK0yM;{Z zSt4Hd-}0En%wz#S;caStN!x6{_kv2x3{yLjq&;5p?W+-vy#XN@J{=Z=PwL62?G8cT z=EUg<$jiVx!(g${9Z0|kf>z7Orettk(gxia&rGp2E2>*$JlaM^yf<@CZE27gqevLk zvWKUgMKI3jNbY>W1rU=*CM)K;H(+?jU|CLo{R)}?2^yk!dU~WXwSsk8yygUE{YY`L z4M&pI3&y5i1GH|>FrOr2Loc%d0)c@5h_qz-Z;u1I!_kPkFBa=jPaZunOd8R6C+h$*tgXY{0l;Pnt?H5^h@} zKCV^H4uyy`J~myYBu=O)fzkWktq&TMGhl&L{byr=_CK;OuSG(m#+@!I(lng$0)c@5 z$A+|DX&2wzT^>2Ry$ILS_36QG{Z2J-*&Cl0)c@5vQew5Wn8Tp1Ke(qO&$G_Q)F@}&wKwS zK?TMM8S{2VJDWwX3~!=zj|r%-AMY>U_{GGH>4+`x{S6;3A0Tb2j`l-2=jft41LPnS zt-}U?!0@uGStzj>akh+c552C&*7+<8CMom$tD$UP{*FR8h6>Eq)a6mCpcv*b_k%-gdzf4(ZT&`)=Ew?otI<*O~O=ZvT#c#W@O8uzyUh~I3a?pssb zE_tQ+b&o1oRfx^eOX~>4p?cStLol0)c===y47xI%(PDmpENf7E=Zm4RiBP z1rE!v3ZBMQ=1N7EJcja3yye^C1lVsybDu%EZdj?UFr+jTYDm#c zBCK*p{#Ho>sT7oN*`cg)=iCvR*e#W{K3wa02FgC#b45v5V`QmBdA#_8c(ZjcGHCb~ Gk-Qv-7eT`S literal 0 HcmV?d00001 diff --git a/tests/support/server.rs b/tests/support/server.rs index f9c45b4d2..43742b60e 100644 --- a/tests/support/server.rs +++ b/tests/support/server.rs @@ -52,6 +52,7 @@ where F2: FnOnce(&mut Builder) -> Bu + Send + 'static, { // Spawn new runtime in thread to prevent reactor execution context conflict + let test_name = thread::current().name().unwrap_or("").to_string(); thread::spawn(move || { let rt = runtime::Builder::new_current_thread() .enable_all() @@ -68,7 +69,7 @@ where let (panic_tx, panic_rx) = std_mpsc::channel(); let tname = format!( "test({})-support-server", - thread::current().name().unwrap_or("") + test_name, ); thread::Builder::new() .name(tname) @@ -110,3 +111,109 @@ where .join() .unwrap() } + +#[cfg(feature = "http3")] +pub fn http3(func: F1) -> Server +where + F1: Fn(http::Request>) -> Fut + + Clone + + Send + + 'static, + Fut: Future> + Send + 'static, +{ + use bytes::Buf; + use http_body_util::BodyExt; + use quinn::crypto::rustls::QuicServerConfig; + use std::sync::Arc; + + // Spawn new runtime in thread to prevent reactor execution context conflict + let test_name = thread::current().name().unwrap_or("").to_string(); + thread::spawn(move || { + let rt = runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("new rt"); + + let cert = std::fs::read("tests/support/server.cert").unwrap().into(); + let key = std::fs::read("tests/support/server.key").unwrap().try_into().unwrap(); + + let mut tls_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert], key) + .unwrap(); + tls_config.max_early_data_size = u32::MAX; + tls_config.alpn_protocols = vec![b"h3".into()]; + + let server_config = quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(tls_config).unwrap())); + let endpoint = rt.block_on(async move { + quinn::Endpoint::server(server_config, "[::1]:0".parse().unwrap()).unwrap() + }); + let addr = endpoint.local_addr().unwrap(); + + let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); + let (panic_tx, panic_rx) = std_mpsc::channel(); + let tname = format!( + "test({})-support-server", + test_name, + ); + thread::Builder::new() + .name(tname) + .spawn(move || { + rt.block_on(async move { + + loop { + tokio::select! { + _ = &mut shutdown_rx => { + break; + } + Some(accepted) = endpoint.accept() => { + let conn = accepted.await.expect("accepted"); + let mut h3_conn = h3::server::Connection::new(h3_quinn::Connection::new(conn)).await.unwrap(); + let func = func.clone(); + tokio::spawn(async move { + while let Ok(Some((req, stream))) = h3_conn.accept().await { + let func = func.clone(); + tokio::spawn(async move { + let (mut tx, rx) = stream.split(); + let body = futures_util::stream::unfold(rx, |mut rx| async move { + match rx.recv_data().await { + Ok(Some(mut buf)) => { + Some((Ok(hyper::body::Frame::data(buf.copy_to_bytes(buf.remaining()))), rx)) + }, + Ok(None) => None, + Err(err) => { + Some((Err(err), rx)) + } + } + }); + let body = BodyExt::boxed(http_body_util::StreamBody::new(body)); + let resp = func(req.map(move |()| body)).await; + let (parts, mut body) = resp.into_parts(); + let resp = http::Response::from_parts(parts, ()); + tx.send_response(resp).await.unwrap(); + + while let Some(Ok(frame)) = body.frame().await { + if let Ok(data) = frame.into_data() { + tx.send_data(data).await.unwrap(); + } + } + tx.finish().await.unwrap(); + }); + } + }); + } + } + } + let _ = panic_tx.send(()); + }); + }) + .expect("thread spawn"); + Server { + addr, + panic_rx, + shutdown_tx: Some(shutdown_tx), + } + }) + .join() + .unwrap() +} From ce3b30e1581d4b4e7e55a5551761f13381435d72 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 11 Jun 2024 08:38:40 -0400 Subject: [PATCH 21/47] http3: send content-length if known --- src/async_impl/h3_client/pool.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/async_impl/h3_client/pool.rs b/src/async_impl/h3_client/pool.rs index 9e708dbfb..100a0935d 100644 --- a/src/async_impl/h3_client/pool.rs +++ b/src/async_impl/h3_client/pool.rs @@ -127,9 +127,18 @@ impl PoolClient { req: Request, ) -> Result, BoxError> { use http_body_util::{BodyExt, Full}; + use hyper::body::Body as _; let (head, req_body) = req.into_parts(); - let req = Request::from_parts(head, ()); + let mut req = Request::from_parts(head, ()); + + if let Some(n) = req_body.size_hint().exact() { + if n > 0 { + req.headers_mut() + .insert(http::header::CONTENT_LENGTH, n.into()); + } + } + let mut stream = self.inner.send_request(req).await?; match req_body.as_bytes() { From c32d87723ee1a6ff501b69d027bd387369937e4f Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 17 Jun 2024 10:29:15 -0400 Subject: [PATCH 22/47] tests: enable http3 content-length test --- tests/client.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/client.rs b/tests/client.rs index 5fa9a3532..9bb27001c 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -87,14 +87,12 @@ async fn donot_set_content_length_0_if_have_no_body() { #[cfg(feature = "http3")] #[tokio::test] async fn http3_request_full() { - //use http_body_util::BodyExt; + use http_body_util::BodyExt; let server = server::http3(move |_req| async move { - /* assert_eq!(req.headers()[CONTENT_LENGTH], "5"); let reqb = req.collect().await.unwrap().to_bytes(); assert_eq!(reqb, "hello"); - */ http::Response::default() }); From a7880d625726ec07edc5bd9d0368b86e0de17c86 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 17 Jun 2024 10:49:44 -0400 Subject: [PATCH 23/47] tests: fix http3 tests --- tests/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client.rs b/tests/client.rs index 9bb27001c..ce97456bf 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -89,7 +89,7 @@ async fn donot_set_content_length_0_if_have_no_body() { async fn http3_request_full() { use http_body_util::BodyExt; - let server = server::http3(move |_req| async move { + let server = server::http3(move |req| async move { assert_eq!(req.headers()[CONTENT_LENGTH], "5"); let reqb = req.collect().await.unwrap().to_bytes(); assert_eq!(reqb, "hello"); From 29d4cff234b37065632512f002b9785700c51aa8 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 17 Jun 2024 11:14:12 -0400 Subject: [PATCH 24/47] v0.12.5 --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dea63e5a..428f37073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v0.12.5 + +- Add `blocking::ClientBuilder::dns_resolver()` method to change DNS resolver in blocking client. +- Add `http3` feature back, still requiring `reqwest_unstable`. +- Add `rustls-tls-no-provider` Cargo feature to use rustls without a crypto provider. +- Fix `Accept-Encoding` header combinations. +- Fix http3 resolving IPv6 addresses. +- Internal: upgrade to rustls 0.23. + ## v0.12.4 - Add `zstd` support, enabled with `zstd` Cargo feature. diff --git a/Cargo.toml b/Cargo.toml index 2c019cc30..7f81cb9ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest" -version = "0.12.4" +version = "0.12.5" description = "higher level HTTP client library" keywords = ["http", "request", "client"] categories = ["web-programming::http-client", "wasm"] From c4ebb073438026e09c99469be02fc1f1a254058a Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Fri, 21 Jun 2024 10:29:46 -0400 Subject: [PATCH 25/47] refactor: change Debug of Error to output url as str --- src/async_impl/response.rs | 2 +- src/error.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async_impl/response.rs b/src/async_impl/response.rs index 6e2544f96..17be37030 100644 --- a/src/async_impl/response.rs +++ b/src/async_impl/response.rs @@ -432,7 +432,7 @@ impl Response { impl fmt::Debug for Response { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Response") - .field("url", self.url()) + .field("url", &self.url().as_str()) .field("status", &self.status()) .field("headers", self.headers()) .finish() diff --git a/src/error.rs b/src/error.rs index 12fca8b9c..b1c666479 100644 --- a/src/error.rs +++ b/src/error.rs @@ -172,7 +172,7 @@ impl fmt::Debug for Error { builder.field("kind", &self.inner.kind); if let Some(ref url) = self.inner.url { - builder.field("url", url); + builder.field("url", &url.as_str()); } if let Some(ref source) = self.inner.source { builder.field("source", source); From 892569e10b69d1a0e5db1f53202d1ebf09924fc1 Mon Sep 17 00:00:00 2001 From: Simon Chopin Date: Wed, 3 Jul 2024 14:07:55 +0200 Subject: [PATCH 26/47] tests: bypass the proxy if testing DNS override If an explicit proxy is configured in the environment, then the request will go through it rather than actually resolving the domain. Either we're hitting the target domain on a weird port which will likely fail, or the proxy straight up denies that weird request. We're hitting this issue in the Ubuntu CI. Amazingly enough, the tests actually passed *once* there, although the exact circumstances that allowed this are still a bit of a mystery. --- tests/client.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/client.rs b/tests/client.rs index ce97456bf..18aaf4e99 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -246,6 +246,7 @@ async fn overridden_dns_resolution_with_gai() { server.addr().port() ); let client = reqwest::Client::builder() + .no_proxy() .resolve(overridden_domain, server.addr()) .build() .expect("client builder"); @@ -270,6 +271,7 @@ async fn overridden_dns_resolution_with_gai_multiple() { // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs // algorithm decide which address to use. let client = reqwest::Client::builder() + .no_proxy() .resolve_to_addrs( overridden_domain, &[ @@ -302,6 +304,7 @@ async fn overridden_dns_resolution_with_hickory_dns() { server.addr().port() ); let client = reqwest::Client::builder() + .no_proxy() .resolve(overridden_domain, server.addr()) .hickory_dns(true) .build() @@ -328,6 +331,7 @@ async fn overridden_dns_resolution_with_hickory_dns_multiple() { // the server runs on IPv4 localhost, so provide both IPv4 and IPv6 and let the happy eyeballs // algorithm decide which address to use. let client = reqwest::Client::builder() + .no_proxy() .resolve_to_addrs( overridden_domain, &[ From 9e577f5ed982736747b0a0c60dd1234b92dc3351 Mon Sep 17 00:00:00 2001 From: Thomas B <9094255+Ten0@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:02:54 +0200 Subject: [PATCH 27/47] fix(blocking): Fix `.header()` unsetting `sensitive` on `HeaderValue` (#2353) Fixes #2352 --- src/blocking/request.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/blocking/request.rs b/src/blocking/request.rs index 9acc1e16a..8ac112444 100644 --- a/src/blocking/request.rs +++ b/src/blocking/request.rs @@ -211,7 +211,12 @@ impl RequestBuilder { match >::try_from(key) { Ok(key) => match >::try_from(value) { Ok(mut value) => { - value.set_sensitive(sensitive); + // We want to potentially make an unsensitive header + // to be sensitive, not the reverse. So, don't turn off + // a previously sensitive header. + if sensitive { + value.set_sensitive(true); + } req.headers_mut().append(key, value); } Err(e) => error = Some(crate::error::builder(e.into())), From c66053535c8337f80b96b41b96fbce7c1289395a Mon Sep 17 00:00:00 2001 From: Thales Date: Mon, 15 Jul 2024 15:27:04 -0300 Subject: [PATCH 28/47] feat: Implement accept_invalid_hostnames for rustls (#2249) Signed-off-by: Thales Fragoso --- CHANGELOG.md | 4 +++ src/async_impl/client.rs | 52 +++++++++++++++++++----------- src/blocking/client.rs | 14 ++++++-- src/tls.rs | 69 +++++++++++++++++++++++++++++++++++++++- tests/badssl.rs | 2 +- 5 files changed, 117 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428f37073..3d0860163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- Implement `danger_accept_invalid_hostnames` for `rustls`. + ## v0.12.5 - Add `blocking::ClientBuilder::dns_resolver()` method to change DNS resolver in blocking client. diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index b321a609a..3bb650244 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -91,7 +91,7 @@ struct Config { // NOTE: When adding a new field, update `fmt::Debug for ClientBuilder` accepts: Accepts, headers: HeaderMap, - #[cfg(feature = "native-tls")] + #[cfg(feature = "__tls")] hostname_verification: bool, #[cfg(feature = "__tls")] certs_verification: bool, @@ -188,7 +188,7 @@ impl ClientBuilder { error: None, accepts: Accepts::default(), headers, - #[cfg(feature = "native-tls")] + #[cfg(feature = "__tls")] hostname_verification: true, #[cfg(feature = "__tls")] certs_verification: true, @@ -388,10 +388,7 @@ impl ClientBuilder { } } - #[cfg(feature = "native-tls")] - { - tls.danger_accept_invalid_hostnames(!config.hostname_verification); - } + tls.danger_accept_invalid_hostnames(!config.hostname_verification); tls.danger_accept_invalid_certs(!config.certs_verification); @@ -500,7 +497,7 @@ impl ClientBuilder { } #[cfg(feature = "__rustls")] TlsBackend::Rustls => { - use crate::tls::NoVerifier; + use crate::tls::{IgnoreHostname, NoVerifier}; // Set root certificates. let mut root_cert_store = rustls::RootCertStore::empty(); @@ -578,10 +575,25 @@ impl ClientBuilder { }); // Build TLS config + let signature_algorithms = provider.signature_verification_algorithms; let config_builder = rustls::ClientConfig::builder_with_provider(provider) .with_protocol_versions(&versions) - .map_err(|_| crate::error::builder("invalid TLS versions"))? - .with_root_certificates(root_cert_store); + .map_err(|_| crate::error::builder("invalid TLS versions"))?; + + let config_builder = if !config.certs_verification { + config_builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + } else if !config.hostname_verification { + config_builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(IgnoreHostname::new( + root_cert_store, + signature_algorithms, + ))) + } else { + config_builder.with_root_certificates(root_cert_store) + }; // Finalize TLS config let mut tls = if let Some(id) = config.identity { @@ -590,12 +602,6 @@ impl ClientBuilder { config_builder.with_no_client_auth() }; - // Certificate verifier - if !config.certs_verification { - tls.dangerous() - .set_certificate_verifier(Arc::new(NoVerifier)); - } - tls.enable_sni = config.tls_sni; // ALPN protocol @@ -1476,9 +1482,17 @@ impl ClientBuilder { /// /// # Optional /// - /// This requires the optional `native-tls` feature to be enabled. - #[cfg(feature = "native-tls")] - #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] pub fn danger_accept_invalid_hostnames( mut self, accept_invalid_hostname: bool, @@ -2243,7 +2257,7 @@ impl Config { f.field("tcp_nodelay", &true); } - #[cfg(feature = "native-tls")] + #[cfg(feature = "__tls")] { if !self.hostname_verification { f.field("danger_accept_invalid_hostnames", &true); diff --git a/src/blocking/client.rs b/src/blocking/client.rs index 9e6772910..d4b973ee6 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -670,9 +670,17 @@ impl ClientBuilder { /// /// # Optional /// - /// This requires the optional `native-tls` feature to be enabled. - #[cfg(feature = "native-tls")] - #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] + /// This requires the optional `default-tls`, `native-tls`, or `rustls-tls(-...)` + /// feature to be enabled. + #[cfg(feature = "__tls")] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "default-tls", + feature = "native-tls", + feature = "rustls-tls" + ))) + )] pub fn danger_accept_invalid_hostnames(self, accept_invalid_hostname: bool) -> ClientBuilder { self.with_inner(|inner| inner.danger_accept_invalid_hostnames(accept_invalid_hostname)) } diff --git a/src/tls.rs b/src/tls.rs index 8f979b15b..83f3feee8 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -47,7 +47,9 @@ #[cfg(feature = "__rustls")] use rustls::{ client::danger::HandshakeSignatureValid, client::danger::ServerCertVerified, - client::danger::ServerCertVerifier, DigitallySignedStruct, Error as TLSError, SignatureScheme, + client::danger::ServerCertVerifier, crypto::WebPkiSupportedAlgorithms, + server::ParsedCertificate, DigitallySignedStruct, Error as TLSError, RootCertStore, + SignatureScheme, }; #[cfg(feature = "__rustls")] use rustls_pki_types::{ServerName, UnixTime}; @@ -571,6 +573,71 @@ impl ServerCertVerifier for NoVerifier { } } +#[cfg(feature = "__rustls")] +#[derive(Debug)] +pub(crate) struct IgnoreHostname { + roots: RootCertStore, + signature_algorithms: WebPkiSupportedAlgorithms, +} + +#[cfg(feature = "__rustls")] +impl IgnoreHostname { + pub(crate) fn new( + roots: RootCertStore, + signature_algorithms: WebPkiSupportedAlgorithms, + ) -> Self { + Self { + roots, + signature_algorithms, + } + } +} + +#[cfg(feature = "__rustls")] +impl ServerCertVerifier for IgnoreHostname { + fn verify_server_cert( + &self, + end_entity: &rustls_pki_types::CertificateDer<'_>, + intermediates: &[rustls_pki_types::CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + let cert = ParsedCertificate::try_from(end_entity)?; + + rustls::client::verify_server_cert_signed_by_trust_anchor( + &cert, + &self.roots, + intermediates, + now, + self.signature_algorithms.all, + )?; + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.signature_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls_pki_types::CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.signature_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.signature_algorithms.supported_schemes() + } +} + /// Hyper extension carrying extra TLS layer information. /// Made available to clients on responses when `tls_info` is set. #[derive(Clone)] diff --git a/tests/badssl.rs b/tests/badssl.rs index e889e864f..28d284324 100644 --- a/tests/badssl.rs +++ b/tests/badssl.rs @@ -75,7 +75,7 @@ async fn test_badssl_no_built_in_roots() { assert!(result.is_err()); } -#[cfg(feature = "native-tls")] +#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[tokio::test] async fn test_badssl_wrong_host() { let text = reqwest::Client::builder() From b9d62a0323d96f11672a61a17bf8849baec00275 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Fri, 19 Jul 2024 13:12:20 -0700 Subject: [PATCH 29/47] Remove Sync requirement from reqwest::Body::wrap_stream() (#2361) Closes #2273 --- Cargo.toml | 2 +- src/async_impl/body.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f81cb9ec..022cc11d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ serde_urlencoded = "0.7.1" tower-service = "0.3" futures-core = { version = "0.3.28", default-features = false } futures-util = { version = "0.3.28", default-features = false } -sync_wrapper = "1.0" +sync_wrapper = { version = "1.0", features = ["futures"] } # Optional deps... diff --git a/src/async_impl/body.rs b/src/async_impl/body.rs index 638df76fb..e6e659f1b 100644 --- a/src/async_impl/body.rs +++ b/src/async_impl/body.rs @@ -88,7 +88,7 @@ impl Body { #[cfg_attr(docsrs, doc(cfg(feature = "stream")))] pub fn wrap_stream(stream: S) -> Body where - S: futures_core::stream::TryStream + Send + Sync + 'static, + S: futures_core::stream::TryStream + Send + 'static, S::Error: Into>, Bytes: From, { @@ -98,7 +98,7 @@ impl Body { #[cfg(any(feature = "stream", feature = "multipart", feature = "blocking"))] pub(crate) fn stream(stream: S) -> Body where - S: futures_core::stream::TryStream + Send + Sync + 'static, + S: futures_core::stream::TryStream + Send + 'static, S::Error: Into>, Bytes: From, { @@ -106,11 +106,11 @@ impl Body { use http_body::Frame; use http_body_util::StreamBody; - let body = http_body_util::BodyExt::boxed(StreamBody::new( + let body = http_body_util::BodyExt::boxed(StreamBody::new(sync_wrapper::SyncStream::new( stream .map_ok(|d| Frame::data(Bytes::from(d))) .map_err(Into::into), - )); + ))); Body { inner: Inner::Streaming(body), } From b63cc56208a021ba2b02200424085c10db4093b5 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Sat, 3 Aug 2024 06:33:57 -0700 Subject: [PATCH 30/47] refactor: clean up unused warnings with features (#2371) --- src/async_impl/body.rs | 2 ++ src/async_impl/decoder.rs | 67 ++++++++++++++++++++++++++++++++++++--- src/error.rs | 7 ++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/async_impl/body.rs b/src/async_impl/body.rs index e6e659f1b..8a35585e2 100644 --- a/src/async_impl/body.rs +++ b/src/async_impl/body.rs @@ -48,6 +48,7 @@ pin_project! { } /// Converts any `impl Body` into a `impl Stream` of just its DATA frames. +#[cfg(any(feature = "stream", feature = "multipart",))] pub(crate) struct DataStream(pub(crate) B); impl Body { @@ -434,6 +435,7 @@ where // ===== impl DataStream ===== +#[cfg(any(feature = "stream", feature = "multipart",))] impl futures_core::Stream for DataStream where B: HttpBody + Unpin, diff --git a/src/async_impl/decoder.rs b/src/async_impl/decoder.rs index f97141afa..d742e6d35 100644 --- a/src/async_impl/decoder.rs +++ b/src/async_impl/decoder.rs @@ -1,4 +1,10 @@ use std::fmt; +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate" +))] use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; @@ -15,9 +21,16 @@ use async_compression::tokio::bufread::ZstdDecoder; #[cfg(feature = "deflate")] use async_compression::tokio::bufread::ZlibDecoder; -use bytes::Bytes; +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate", + feature = "blocking", +))] use futures_core::Stream; -use futures_util::stream::Peekable; + +use bytes::Bytes; use http::HeaderMap; use hyper::body::Body as HttpBody; use hyper::body::Frame; @@ -38,7 +51,6 @@ use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::io::StreamReader; use super::body::ResponseBody; -use crate::error; #[derive(Clone, Copy, Debug)] pub(super) struct Accepts { @@ -74,7 +86,13 @@ pub(crate) struct Decoder { inner: Inner, } -type PeekableIoStream = Peekable; +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate" +))] +type PeekableIoStream = futures_util::stream::Peekable; #[cfg(any( feature = "gzip", @@ -114,11 +132,30 @@ enum Inner { Pending(Pin>), } +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate" +))] /// A future attempt to poll the response body for EOF so we know whether to use gzip or not. struct Pending(PeekableIoStream, DecoderType); +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate", + feature = "blocking", +))] pub(crate) struct IoStream(B); +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate" +))] enum DecoderType { #[cfg(feature = "gzip")] Gzip, @@ -376,11 +413,24 @@ impl HttpBody for Decoder { } } +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate", + feature = "blocking", +))] fn empty() -> ResponseBody { use http_body_util::{combinators::BoxBody, BodyExt, Empty}; BoxBody::new(Empty::new().map_err(|never| match never {})) } +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate" +))] impl Future for Pending { type Output = Result; @@ -429,6 +479,13 @@ impl Future for Pending { } } +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate", + feature = "blocking", +))] impl Stream for IoStream where B: HttpBody + Unpin, @@ -447,7 +504,7 @@ where continue; } } - Some(Err(err)) => Poll::Ready(Some(Err(error::into_io(err.into())))), + Some(Err(err)) => Poll::Ready(Some(Err(crate::error::into_io(err.into())))), None => Poll::Ready(None), }; } diff --git a/src/error.rs b/src/error.rs index b1c666479..ca7413fd6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -287,6 +287,13 @@ pub(crate) fn upgrade>(e: E) -> Error { // io::Error helpers +#[cfg(any( + feature = "gzip", + feature = "zstd", + feature = "brotli", + feature = "deflate", + feature = "blocking", +))] pub(crate) fn into_io(e: BoxError) -> io::Error { io::Error::new(io::ErrorKind::Other, e) } From 5715050256882dc9daf90403c1c2d58e7cd78eaf Mon Sep 17 00:00:00 2001 From: Jan <59206115+Threated@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:36:18 +0200 Subject: [PATCH 31/47] fix: Make HTTP(S)_PROXY variables take precedence over ALL_PROXY (#2370) --- src/lib.rs | 5 ++++- src/proxy.rs | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d62cb8210..cf3d39d0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,8 +139,11 @@ //! //! System proxies look in environment variables to set HTTP or HTTPS proxies. //! -//! `HTTP_PROXY` or `http_proxy` provide http proxies for http connections while +//! `HTTP_PROXY` or `http_proxy` provide HTTP proxies for HTTP connections while //! `HTTPS_PROXY` or `https_proxy` provide HTTPS proxies for HTTPS connections. +//! `ALL_PROXY` or `all_proxy` provide proxies for both HTTP and HTTPS connections. +//! If both the all proxy and HTTP or HTTPS proxy variables are set the more specific +//! HTTP or HTTPS proxies take precedence. //! //! These can be overwritten by adding a [`Proxy`] to `ClientBuilder` //! i.e. `let proxy = reqwest::Proxy::http("https://secure.example")?;` diff --git a/src/proxy.rs b/src/proxy.rs index f5e8f66ac..98708c775 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -897,6 +897,13 @@ fn insert_proxy(proxies: &mut SystemProxyMap, scheme: impl Into, addr: S fn get_from_environment() -> SystemProxyMap { let mut proxies = HashMap::new(); + if !(insert_from_env(&mut proxies, "http", "ALL_PROXY") + && insert_from_env(&mut proxies, "https", "ALL_PROXY")) + { + insert_from_env(&mut proxies, "http", "all_proxy"); + insert_from_env(&mut proxies, "https", "all_proxy"); + } + if is_cgi() { if log::log_enabled!(log::Level::Warn) && env::var_os("HTTP_PROXY").is_some() { log::warn!("HTTP_PROXY environment variable ignored in CGI"); @@ -909,13 +916,6 @@ fn get_from_environment() -> SystemProxyMap { insert_from_env(&mut proxies, "https", "https_proxy"); } - if !(insert_from_env(&mut proxies, "http", "ALL_PROXY") - && insert_from_env(&mut proxies, "https", "ALL_PROXY")) - { - insert_from_env(&mut proxies, "http", "all_proxy"); - insert_from_env(&mut proxies, "https", "all_proxy"); - } - proxies } @@ -1300,7 +1300,10 @@ mod tests { assert_eq!(p.host(), "127.0.0.1"); assert_eq!(all_proxies.len(), 2); - assert!(all_proxies.values().all(|p| p.host() == "127.0.0.2")); + // Set by ALL_PROXY + assert_eq!(all_proxies["https"].host(), "127.0.0.2"); + // Overwritten by the more specific HTTP_PROXY + assert_eq!(all_proxies["http"].host(), "127.0.0.1"); } #[cfg(any(target_os = "windows", target_os = "macos"))] From 23f267892e64e78fe7d8078417f6b678d192d45a Mon Sep 17 00:00:00 2001 From: FlowerCode Date: Mon, 12 Aug 2024 21:24:54 +0800 Subject: [PATCH 32/47] Use `windows-registry` instead of `winreg` for reading proxy settings on Windows (#2380) --- Cargo.toml | 2 +- src/proxy.rs | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 022cc11d7..a7108e2c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,7 +181,7 @@ futures-util = { version = "0.3.28", default-features = false, features = ["std" rustls = { version = "0.23", default-features = false, features = ["ring"] } [target.'cfg(windows)'.dependencies] -winreg = "0.52.0" +windows-registry = "0.2" [target.'cfg(target_os = "macos")'.dependencies] system-configuration = { version = "0.5.1", optional = true } diff --git a/src/proxy.rs b/src/proxy.rs index 98708c775..5be207a8a 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -29,10 +29,6 @@ use system_configuration::{ sys::schema_definitions::kSCPropNetProxiesHTTPSPort, sys::schema_definitions::kSCPropNetProxiesHTTPSProxy, }; -#[cfg(target_os = "windows")] -use winreg::enums::HKEY_CURRENT_USER; -#[cfg(target_os = "windows")] -use winreg::RegKey; /// Configuration of a proxy that a `Client` should pass requests to. /// @@ -937,12 +933,11 @@ fn is_cgi() -> bool { #[cfg(target_os = "windows")] fn get_from_platform_impl() -> Result, Box> { - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let internet_setting: RegKey = - hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; + let internet_setting = windows_registry::CURRENT_USER + .open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; // ensure the proxy is enable, if the value doesn't exist, an error will returned. - let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?; - let proxy_server: String = internet_setting.get_value("ProxyServer")?; + let proxy_enable = internet_setting.get_u32("ProxyEnable")?; + let proxy_server = internet_setting.get_string("ProxyServer")?; Ok((proxy_enable == 1).then_some(proxy_server)) } From 6a1cdb4622b27cfbf9dc6c2c66085c73abe0fbca Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 12 Aug 2024 09:25:36 -0400 Subject: [PATCH 33/47] chore: update Brotli to v6 (#2379) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a7108e2c1..7adeb5bf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -173,7 +173,7 @@ hyper = { version = "1.1.0", default-features = false, features = ["http1", "htt hyper-util = { version = "0.1.3", features = ["http1", "http2", "client", "client-legacy", "server-auto", "tokio"] } serde = { version = "1.0", features = ["derive"] } libflate = "1.0" -brotli_crate = { package = "brotli", version = "3.3.0" } +brotli_crate = { package = "brotli", version = "6.0.0" } zstd_crate = { package = "zstd", version = "0.13" } doc-comment = "0.3" tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread"] } From 86a18a3e2cf8396dd084284c122e30e26cf7c9fd Mon Sep 17 00:00:00 2001 From: zeling Date: Mon, 12 Aug 2024 09:13:11 -0700 Subject: [PATCH 34/47] Enable happy eyeballs when using `hickory-dns` (#2378) Happy Eyeballs algorithm is implemented for hyper, however it is not working correctly for IPv6 only hosts because the default resolver option in hickory is `Ipv4ThenIpv6`, meaning it only sends AAAA queries if it cannot resolve an IPv4 address. Thus the address list `hyper` receives is almost always IPv4 only given most servers have an IPv4 address. To make the Happy Eyeballs algorithm work correctly, we need the resolver to resolve both IP versions. This also aligns with the default GAI resolver behavior for both glibc and musl. --- src/dns/hickory.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/dns/hickory.rs b/src/dns/hickory.rs index 042707006..dd9ab877b 100644 --- a/src/dns/hickory.rs +++ b/src/dns/hickory.rs @@ -1,6 +1,8 @@ //! DNS resolution via the [hickory-resolver](https://github.com/hickory-dns/hickory-dns) crate -use hickory_resolver::{lookup_ip::LookupIpIntoIter, system_conf, TokioAsyncResolver}; +use hickory_resolver::{ + config::LookupIpStrategy, lookup_ip::LookupIpIntoIter, system_conf, TokioAsyncResolver, +}; use once_cell::sync::OnceCell; use std::io; @@ -46,13 +48,16 @@ impl Iterator for SocketAddrs { } /// Create a new resolver with the default configuration, -/// which reads from `/etc/resolve.conf`. +/// which reads from `/etc/resolve.conf`. The options are +/// overriden to look up for both IPv4 and IPv6 addresses +/// to work with "happy eyeballs" algorithm. fn new_resolver() -> io::Result { - let (config, opts) = system_conf::read_system_conf().map_err(|e| { + let (config, mut opts) = system_conf::read_system_conf().map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("error reading DNS system conf: {e}"), ) })?; + opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; Ok(TokioAsyncResolver::tokio(config, opts)) } From dd036dbcde69125dc68873b14c263b96ff1696b8 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 13 Aug 2024 07:23:44 -0700 Subject: [PATCH 35/47] chore: suppress wasm web-sys deprecation warnings (#2386) --- src/wasm/body.rs | 4 ++++ src/wasm/client.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/wasm/body.rs b/src/wasm/body.rs index 751cf586e..241aa8173 100644 --- a/src/wasm/body.rs +++ b/src/wasm/body.rs @@ -182,6 +182,10 @@ impl fmt::Debug for Body { } } +// Can use new methods in web-sys when requiring v0.2.93. +// > `init.method(m)` to `init.set_method(m)` +// For now, ignore their deprecation. +#[allow(deprecated)] #[cfg(test)] mod tests { use crate::Body; diff --git a/src/wasm/client.rs b/src/wasm/client.rs index 9f22a896b..e33ed26d1 100644 --- a/src/wasm/client.rs +++ b/src/wasm/client.rs @@ -182,6 +182,10 @@ impl fmt::Debug for ClientBuilder { } } +// Can use new methods in web-sys when requiring v0.2.93. +// > `init.method(m)` to `init.set_method(m)` +// For now, ignore their deprecation. +#[allow(deprecated)] async fn fetch(req: Request) -> crate::Result { // Build the js Request let mut init = web_sys::RequestInit::new(); From a53c944b24f690adc74d5f09ab47a2e6efe1ca52 Mon Sep 17 00:00:00 2001 From: murongshaozong Date: Tue, 13 Aug 2024 15:07:40 +0800 Subject: [PATCH 36/47] chore: fix some comments Signed-off-by: murongshaozong --- src/blocking/body.rs | 2 +- src/dns/hickory.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocking/body.rs b/src/blocking/body.rs index dd44c6fa2..4782b368c 100644 --- a/src/blocking/body.rs +++ b/src/blocking/body.rs @@ -296,7 +296,7 @@ async fn send_future(sender: Sender) -> Result<(), crate::Error> { // // We need to know whether there is any data to send before // we check the transmission channel (with poll_ready below) - // because somestimes the receiver disappears as soon as is + // because sometimes the receiver disappears as soon as is // considers the data is completely transmitted, which may // be true. // diff --git a/src/dns/hickory.rs b/src/dns/hickory.rs index dd9ab877b..44b943827 100644 --- a/src/dns/hickory.rs +++ b/src/dns/hickory.rs @@ -49,7 +49,7 @@ impl Iterator for SocketAddrs { /// Create a new resolver with the default configuration, /// which reads from `/etc/resolve.conf`. The options are -/// overriden to look up for both IPv4 and IPv6 addresses +/// overridden to look up for both IPv4 and IPv6 addresses /// to work with "happy eyeballs" algorithm. fn new_resolver() -> io::Result { let (config, mut opts) = system_conf::read_system_conf().map_err(|e| { From dddf87734018ddad8c9bc084b5ff351bb31ea9c0 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 12 Aug 2024 23:19:56 -0400 Subject: [PATCH 37/47] chore: bump h3 dependency Not certain what causes CI failure, so doing one dependency update per PR --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7adeb5bf4..e438e4b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ http3 = ["rustls-tls-manual-roots", "dep:h3", "dep:h3-quinn", "dep:quinn", "dep: # Internal (PRIVATE!) features used to aid testing. -# Don't rely on these whatsoever. They may disappear at anytime. +# Don't rely on these whatsoever. They may disappear at any time. # Enables common types used for TLS. Useless on its own. __tls = ["dep:rustls-pemfile", "tokio/io-util"] @@ -160,8 +160,8 @@ tokio-socks = { version = "0.5.1", optional = true } hickory-resolver = { version = "0.24", optional = true, features = ["tokio-runtime"] } # HTTP/3 experimental support -h3 = { version = "0.0.5", optional = true } -h3-quinn = { version = "0.0.6", optional = true } +h3 = { version = "0.0.6", optional = true } +h3-quinn = { version = "0.0.7", optional = true } quinn = { version = "0.11.1", default-features = false, features = ["rustls", "runtime-tokio"], optional = true } slab = { version = "0.4.9", optional = true } # just to get minimal versions working with quinn futures-channel = { version = "0.3", optional = true } From 8c7f338507e89279b15e051a00c68124e5f1e353 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 13 Aug 2024 16:28:37 -0400 Subject: [PATCH 38/47] chore: bump dev-dependency libflate (#2382) --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e86e3ace4..fd14e28b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -272,7 +272,7 @@ jobs: run: | cargo clean cargo update -Z minimal-versions - cargo update -p proc-macro2 --precise 1.0.60 + cargo update -p proc-macro2 --precise 1.0.62 cargo check cargo check --all-features diff --git a/Cargo.toml b/Cargo.toml index e438e4b2f..1cced917b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,7 +172,7 @@ env_logger = "0.10" hyper = { version = "1.1.0", default-features = false, features = ["http1", "http2", "client", "server"] } hyper-util = { version = "0.1.3", features = ["http1", "http2", "client", "client-legacy", "server-auto", "tokio"] } serde = { version = "1.0", features = ["derive"] } -libflate = "1.0" +libflate = "2.1" brotli_crate = { package = "brotli", version = "6.0.0" } zstd_crate = { package = "zstd", version = "0.13" } doc-comment = "0.3" From bfd31beb1c3c06a047fea933243513945620983a Mon Sep 17 00:00:00 2001 From: David Sankel Date: Wed, 14 Aug 2024 23:49:08 +0530 Subject: [PATCH 39/47] docs: Improve RequestBuilder::multipart's documentation (#2388) Making it clear that Content-Type is set will help prevent callers from building maleformed requests where that field is present multiple times. --- src/async_impl/request.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/async_impl/request.rs b/src/async_impl/request.rs index aa900ca02..76b40a788 100644 --- a/src/async_impl/request.rs +++ b/src/async_impl/request.rs @@ -309,6 +309,9 @@ impl RequestBuilder { /// # Ok(()) /// # } /// ``` + /// + /// In additional the request's body, the Content-Type and Content-Length fields are + /// appropriately set. #[cfg(feature = "multipart")] #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] pub fn multipart(self, mut multipart: multipart::Form) -> RequestBuilder { From 85dd6dad826449a92975ccbb815b9aaebfede310 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Thu, 15 Aug 2024 06:39:12 -0700 Subject: [PATCH 40/47] dns: improve error message for hickory-dns and warn in docs (#2389) --- src/async_impl/client.rs | 30 ++++++++---------------------- src/dns/hickory.rs | 29 ++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 3bb650244..0a7281c42 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -1720,14 +1720,7 @@ impl ClientBuilder { self } - /// Enables the [hickory-dns](hickory_resolver) async resolver instead of a default threadpool - /// using `getaddrinfo`. - /// - /// If the `hickory-dns` feature is turned on, the default option is enabled. - /// - /// # Optional - /// - /// This requires the optional `hickory-dns` feature to be enabled + #[doc(hidden)] #[cfg(feature = "hickory-dns")] #[cfg_attr(docsrs, doc(cfg(feature = "hickory-dns")))] #[deprecated(note = "use `hickory_dns` instead")] @@ -1744,6 +1737,11 @@ impl ClientBuilder { /// # Optional /// /// This requires the optional `hickory-dns` feature to be enabled + /// + /// # Warning + /// + /// The hickory resolver does not work exactly the same, or on all the platforms + /// that the default resolver does #[cfg(feature = "hickory-dns")] #[cfg_attr(docsrs, doc(cfg(feature = "hickory-dns")))] pub fn hickory_dns(mut self, enable: bool) -> ClientBuilder { @@ -1751,22 +1749,10 @@ impl ClientBuilder { self } - /// Disables the hickory-dns async resolver. - /// - /// This method exists even if the optional `hickory-dns` feature is not enabled. - /// This can be used to ensure a `Client` doesn't use the hickory-dns async resolver - /// even if another dependency were to enable the optional `hickory-dns` feature. + #[doc(hidden)] #[deprecated(note = "use `no_hickory_dns` instead")] pub fn no_trust_dns(self) -> ClientBuilder { - #[cfg(feature = "hickory-dns")] - { - self.hickory_dns(false) - } - - #[cfg(not(feature = "hickory-dns"))] - { - self - } + self.no_hickory_dns() } /// Disables the hickory-dns async resolver. diff --git a/src/dns/hickory.rs b/src/dns/hickory.rs index 44b943827..a94160b2d 100644 --- a/src/dns/hickory.rs +++ b/src/dns/hickory.rs @@ -1,11 +1,12 @@ //! DNS resolution via the [hickory-resolver](https://github.com/hickory-dns/hickory-dns) crate use hickory_resolver::{ - config::LookupIpStrategy, lookup_ip::LookupIpIntoIter, system_conf, TokioAsyncResolver, + config::LookupIpStrategy, error::ResolveError, lookup_ip::LookupIpIntoIter, system_conf, + TokioAsyncResolver, }; use once_cell::sync::OnceCell; -use std::io; +use std::fmt; use std::net::SocketAddr; use std::sync::Arc; @@ -24,6 +25,9 @@ struct SocketAddrs { iter: LookupIpIntoIter, } +#[derive(Debug)] +struct HickoryDnsSystemConfError(ResolveError); + impl Resolve for HickoryDnsResolver { fn resolve(&self, name: Name) -> Resolving { let resolver = self.clone(); @@ -51,13 +55,20 @@ impl Iterator for SocketAddrs { /// which reads from `/etc/resolve.conf`. The options are /// overridden to look up for both IPv4 and IPv6 addresses /// to work with "happy eyeballs" algorithm. -fn new_resolver() -> io::Result { - let (config, mut opts) = system_conf::read_system_conf().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("error reading DNS system conf: {e}"), - ) - })?; +fn new_resolver() -> Result { + let (config, mut opts) = system_conf::read_system_conf().map_err(HickoryDnsSystemConfError)?; opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; Ok(TokioAsyncResolver::tokio(config, opts)) } + +impl fmt::Display for HickoryDnsSystemConfError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("error reading DNS system conf for hickory-dns") + } +} + +impl std::error::Error for HickoryDnsSystemConfError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} From 646b1f80842fb513c31ad3d78374b13932c721c4 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 15 Aug 2024 09:40:12 -0400 Subject: [PATCH 41/47] chore: update macOS system-configuration dep (#2368) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1cced917b..421082ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -184,7 +184,7 @@ rustls = { version = "0.23", default-features = false, features = ["ring"] } windows-registry = "0.2" [target.'cfg(target_os = "macos")'.dependencies] -system-configuration = { version = "0.5.1", optional = true } +system-configuration = { version = "0.6.0", optional = true } # wasm From 522216e890ccb13c43fb0d92af9598b3bedaaec5 Mon Sep 17 00:00:00 2001 From: Aleksey Sidorov Date: Mon, 19 Aug 2024 13:14:25 +0000 Subject: [PATCH 42/47] feat: Add `impl Service>` for `Client` and `&'_ Client` (#2356) --- CHANGELOG.md | 1 + src/async_impl/client.rs | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0860163..b8c54903f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased - Implement `danger_accept_invalid_hostnames` for `rustls`. +- Add `impl Service>` for `Client` and `&'_ Client`. ## v0.12.5 diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 0a7281c42..d7defc578 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -2161,6 +2161,62 @@ impl tower_service::Service for &'_ Client { } } +impl tower_service::Service> for Client { + type Response = http::Response; + type Error = crate::Error; + type Future = MappedPending; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + match req.try_into() { + Ok(req) => MappedPending::new(self.execute_request(req)), + Err(err) => MappedPending::new(Pending::new_err(err)), + } + } +} + +impl tower_service::Service> for &'_ Client { + type Response = http::Response; + type Error = crate::Error; + type Future = MappedPending; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + match req.try_into() { + Ok(req) => MappedPending::new(self.execute_request(req)), + Err(err) => MappedPending::new(Pending::new_err(err)), + } + } +} + +pin_project! { + pub struct MappedPending { + #[pin] + inner: Pending, + } +} + +impl MappedPending { + fn new(inner: Pending) -> MappedPending { + Self { inner } + } +} + +impl Future for MappedPending { + type Output = Result, crate::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let inner = self.project().inner; + inner.poll(cx).map_ok(Into::into) + } +} + impl fmt::Debug for ClientBuilder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut builder = f.debug_struct("ClientBuilder"); From b2a28f5ec370aa02a16ce12430172344c67aed0f Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 19 Aug 2024 09:37:02 -0400 Subject: [PATCH 43/47] v0.12.6 --- CHANGELOG.md | 8 ++++++-- Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c54903f..2d46d754f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ -## Unreleased +## v0.12.6 -- Implement `danger_accept_invalid_hostnames` for `rustls`. +- Add support for `danger_accept_invalid_hostnames` for `rustls`. - Add `impl Service>` for `Client` and `&'_ Client`. +- Add support for `!Sync` bodies in `Body::wrap_stream()`. +- Enable happy eyeballs when `hickory-dns` is used. +- Fix `Proxy` so that `HTTP(S)_PROXY` values take precendence over `ALL_PROXY`. +- Fix `blocking::RequestBuilder::header()` from unsetting `sensitive` on passed header values. ## v0.12.5 diff --git a/Cargo.toml b/Cargo.toml index 421082ad5..3f5a4a424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest" -version = "0.12.5" +version = "0.12.6" description = "higher level HTTP client library" keywords = ["http", "request", "client"] categories = ["web-programming::http-client", "wasm"] From 68127f0425e510d2319f4fc22f97f84e5612e63d Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 19 Aug 2024 15:31:02 -0400 Subject: [PATCH 44/47] Revert "feat: Add `impl Service>` for `Client` and `&'_ Client` (#2356)" This reverts commit 522216e890ccb13c43fb0d92af9598b3bedaaec5. --- src/async_impl/client.rs | 56 ---------------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index d7defc578..0a7281c42 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -2161,62 +2161,6 @@ impl tower_service::Service for &'_ Client { } } -impl tower_service::Service> for Client { - type Response = http::Response; - type Error = crate::Error; - type Future = MappedPending; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: http::Request) -> Self::Future { - match req.try_into() { - Ok(req) => MappedPending::new(self.execute_request(req)), - Err(err) => MappedPending::new(Pending::new_err(err)), - } - } -} - -impl tower_service::Service> for &'_ Client { - type Response = http::Response; - type Error = crate::Error; - type Future = MappedPending; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: http::Request) -> Self::Future { - match req.try_into() { - Ok(req) => MappedPending::new(self.execute_request(req)), - Err(err) => MappedPending::new(Pending::new_err(err)), - } - } -} - -pin_project! { - pub struct MappedPending { - #[pin] - inner: Pending, - } -} - -impl MappedPending { - fn new(inner: Pending) -> MappedPending { - Self { inner } - } -} - -impl Future for MappedPending { - type Output = Result, crate::Error>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let inner = self.project().inner; - inner.poll(cx).map_ok(Into::into) - } -} - impl fmt::Debug for ClientBuilder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut builder = f.debug_struct("ClientBuilder"); From 88bd9be6df0ad19957f88f6a1ab5592917f4f9ca Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 19 Aug 2024 17:03:14 -0400 Subject: [PATCH 45/47] v0.12.7 --- CHANGELOG.md | 4 ++++ Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d46d754f..1e6ecd9d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.12.7 + +- Revert adding `impl Service>` for `Client`. + ## v0.12.6 - Add support for `danger_accept_invalid_hostnames` for `rustls`. diff --git a/Cargo.toml b/Cargo.toml index 3f5a4a424..2d7f65ce7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reqwest" -version = "0.12.6" +version = "0.12.7" description = "higher level HTTP client library" keywords = ["http", "request", "client"] categories = ["web-programming::http-client", "wasm"] From 193ed1fae73e9c44846a089506884d3b8f2865a6 Mon Sep 17 00:00:00 2001 From: Jubilee Young Date: Sat, 24 Aug 2024 17:34:19 -0700 Subject: [PATCH 46/47] chore: Depend on wasm-bindgen 0.2.89 or higher wasm-bindgen 0.2.68 is not compatible with a wasm ABI change that rustc wishes to enable by default for wasm32-unknown-unknown, currently gated behind passing the -Zwasm-c-abi flag to rustc. wasm-bindgen 0.2.89 should exhibit seamless behavior before and after the ABI change to match the C ABI, so depend on that. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d7f65ce7..044757de8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,7 +191,7 @@ system-configuration = { version = "0.6.0", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.45" serde_json = "1.0" -wasm-bindgen = "0.2.68" +wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.18" wasm-streams = { version = "0.4", optional = true } @@ -216,7 +216,7 @@ features = [ ] [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen = { version = "0.2.68", features = ["serde-serialize"] } +wasm-bindgen = { version = "0.2.89", features = ["serde-serialize"] } wasm-bindgen-test = "0.3" [lints.rust] From cc3dd510c0cb50ee03be217ea864d84e744658d1 Mon Sep 17 00:00:00 2001 From: NaokiM03 <37442712+NaokiM03@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:27:55 +0900 Subject: [PATCH 47/47] Add file function to async::multipart (#2106) * Add file function to async_impl::multipart * Add test for asynchronous file function in multipart * Fix doc of file function in blocking::multipart * Fix test Follow up on this pull request https://github.com/seanmonstar/reqwest/pull/2059 * Fix doc test --- src/async_impl/multipart.rs | 58 +++++++++++++++++++++++++++++++++++++ src/blocking/multipart.rs | 2 +- tests/multipart.rs | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs index 75198ca0a..525876dde 100644 --- a/src/async_impl/multipart.rs +++ b/src/async_impl/multipart.rs @@ -3,9 +3,16 @@ use std::borrow::Cow; use std::fmt; use std::pin::Pin; +#[cfg(feature = "stream")] +use std::io; +#[cfg(feature = "stream")] +use std::path::Path; + use bytes::Bytes; use mime_guess::Mime; use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC}; +#[cfg(feature = "stream")] +use tokio::fs::File; use futures_core::Stream; use futures_util::{future, stream, StreamExt}; @@ -82,6 +89,33 @@ impl Form { self.part(name, Part::text(value)) } + /// Adds a file field. + /// + /// The path will be used to try to guess the filename and mime. + /// + /// # Examples + /// + /// ```no_run + /// # async fn run() -> std::io::Result<()> { + /// let form = reqwest::multipart::Form::new() + /// .file("key", "/path/to/file").await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + #[cfg(feature = "stream")] + #[cfg_attr(docsrs, doc(cfg(feature = "stream")))] + pub async fn file(self, name: T, path: U) -> io::Result
+ where + T: Into>, + U: AsRef, + { + Ok(self.part(name, Part::file(path).await?)) + } + /// Adds a customized Part. pub fn part(self, name: T, part: Part) -> Form where @@ -218,6 +252,30 @@ impl Part { Part::new(value.into(), Some(length)) } + /// Makes a file parameter. + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + #[cfg(feature = "stream")] + #[cfg_attr(docsrs, doc(cfg(feature = "stream")))] + pub async fn file>(path: T) -> io::Result { + let path = path.as_ref(); + let file_name = path + .file_name() + .map(|filename| filename.to_string_lossy().into_owned()); + let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); + let mime = mime_guess::from_ext(ext).first_or_octet_stream(); + let file = File::open(path).await?; + let field = Part::stream(file).mime(mime); + + Ok(if let Some(file_name) = file_name { + field.file_name(file_name) + } else { + field + }) + } + fn new(value: Body, body_length: Option) -> Part { Part { meta: PartMetadata::new(), diff --git a/src/blocking/multipart.rs b/src/blocking/multipart.rs index 8f7c7bc84..4f18a2aae 100644 --- a/src/blocking/multipart.rs +++ b/src/blocking/multipart.rs @@ -104,7 +104,7 @@ impl Form { /// /// ```no_run /// # fn run() -> std::io::Result<()> { - /// let files = reqwest::blocking::multipart::Form::new() + /// let form = reqwest::blocking::multipart::Form::new() /// .file("key", "/path/to/file")?; /// # Ok(()) /// # } diff --git a/tests/multipart.rs b/tests/multipart.rs index 8b5149e1d..bac6da314 100644 --- a/tests/multipart.rs +++ b/tests/multipart.rs @@ -175,3 +175,58 @@ fn blocking_file_part() { assert_eq!(res.url().as_str(), &url); assert_eq!(res.status(), reqwest::StatusCode::OK); } + +#[cfg(feature = "stream")] +#[tokio::test] +async fn async_impl_file_part() { + let _ = env_logger::try_init(); + + let form = reqwest::multipart::Form::new() + .file("foo", "Cargo.lock") + .await + .unwrap(); + + let fcontents = std::fs::read_to_string("Cargo.lock").unwrap(); + + let expected_body = format!( + "\ + --{0}\r\n\ + Content-Disposition: form-data; name=\"foo\"; filename=\"Cargo.lock\"\r\n\ + Content-Type: application/octet-stream\r\n\r\n\ + {1}\r\n\ + --{0}--\r\n\ + ", + form.boundary(), + fcontents + ); + + let ct = format!("multipart/form-data; boundary={}", form.boundary()); + + let server = server::http(move |req| { + let ct = ct.clone(); + let expected_body = expected_body.clone(); + async move { + assert_eq!(req.method(), "POST"); + assert_eq!(req.headers()["content-type"], ct); + assert_eq!(req.headers()["transfer-encoding"], "chunked"); + + let full = req.collect().await.unwrap().to_bytes(); + + assert_eq!(full, expected_body.as_bytes()); + + http::Response::default() + } + }); + + let url = format!("http://{}/multipart/3", server.addr()); + + let res = reqwest::Client::new() + .post(&url) + .multipart(form) + .send() + .await + .unwrap(); + + assert_eq!(res.url().as_str(), &url); + assert_eq!(res.status(), reqwest::StatusCode::OK); +}