From 0be605960a76835c737b34f5a187f0f312c9f185 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 9 Sep 2025 10:00:09 -0400 Subject: [PATCH 1/9] feat(client): add Layer types for client::conn --- src/client/conn.rs | 297 +++++++++++++++++++++++++++++++++++++++++++++ src/client/mod.rs | 3 + 2 files changed, 300 insertions(+) create mode 100644 src/client/conn.rs diff --git a/src/client/conn.rs b/src/client/conn.rs new file mode 100644 index 00000000..f2e2ef99 --- /dev/null +++ b/src/client/conn.rs @@ -0,0 +1,297 @@ +//! todo + +use std::future::Future; +use std::marker::PhantomData; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use http::{Request, Response}; +use tower_service::Service; + +use crate::common::future::poll_fn; + +type BoxError = Box; + +/// todo +#[cfg(feature = "http1")] +pub struct Http1Layer { + builder: hyper::client::conn::http1::Builder, + _body: PhantomData, +} + +/// todo +#[cfg(feature = "http1")] +pub fn http1() -> Http1Layer { + Http1Layer { + builder: hyper::client::conn::http1::Builder::new(), + _body: PhantomData, + } +} + +#[cfg(feature = "http1")] +impl tower_layer::Layer for Http1Layer { + type Service = Http1Connect; + fn layer(&self, inner: M) -> Self::Service { + Http1Connect { + inner, + builder: self.builder.clone(), + _body: self._body, + } + } +} + +#[cfg(feature = "http1")] +impl Clone for Http1Layer { + fn clone(&self) -> Self { + Self { + builder: self.builder.clone(), + _body: self._body.clone(), + } + } +} + +#[cfg(feature = "http1")] +impl From for Http1Layer { + fn from(builder: hyper::client::conn::http1::Builder) -> Self { + Self { + builder, + _body: PhantomData, + } + } +} + +/// todo +#[cfg(feature = "http1")] +pub struct Http1Connect { + inner: M, + builder: hyper::client::conn::http1::Builder, + _body: PhantomData, +} + +#[cfg(feature = "http1")] +impl Service for Http1Connect +where + M: Service, + M::Future: Send + 'static, + M::Response: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static, + M::Error: Into, + B: hyper::body::Body + Send + 'static, + B::Data: Send + 'static, + B::Error: Into>, +{ + type Response = Http1ClientService; + type Error = BoxError; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, dst: Dst) -> Self::Future { + let fut = self.inner.call(dst); + let builder = self.builder.clone(); + Box::pin(async move { + let io = fut.await.map_err(Into::into)?; + let (mut tx, conn) = builder.handshake(io).await?; + //todo: pass in Executor + tokio::spawn(async move { + if let Err(e) = conn.await { + eprintln!("connection error: {:?}", e); + } + }); + // todo: wait for ready? or factor out to other middleware? + poll_fn(|cx| tx.poll_ready(cx)).await?; + + Ok(Http1ClientService::new(tx)) + }) + } +} + +#[cfg(feature = "http1")] +impl Clone for Http1Connect { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + builder: self.builder.clone(), + _body: self._body.clone(), + } + } +} + +/// todo +#[cfg(feature = "http2")] +pub struct Http2Layer { + _body: PhantomData, +} + +/// todo +#[cfg(feature = "http2")] +pub fn http2() -> Http2Layer { + Http2Layer { _body: PhantomData } +} + +#[cfg(feature = "http2")] +impl tower_layer::Layer for Http2Layer { + type Service = Http2Connect; + fn layer(&self, inner: M) -> Self::Service { + Http2Connect { + inner, + builder: hyper::client::conn::http2::Builder::new(crate::rt::TokioExecutor::new()), + _body: self._body, + } + } +} + +#[cfg(feature = "http2")] +impl Clone for Http2Layer { + fn clone(&self) -> Self { + Self { + _body: self._body.clone(), + } + } +} + +/// todo +#[cfg(feature = "http2")] +#[derive(Debug)] +pub struct Http2Connect { + inner: M, + builder: hyper::client::conn::http2::Builder, + _body: PhantomData, +} + +#[cfg(feature = "http2")] +impl Service for Http2Connect +where + M: Service, + M::Future: Send + 'static, + M::Response: hyper::rt::Read + hyper::rt::Write + Unpin + Send + 'static, + M::Error: Into, + B: hyper::body::Body + Unpin + Send + 'static, + B::Data: Send + 'static, + B::Error: Into, +{ + type Response = Http2ClientService; + type Error = BoxError; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, dst: Dst) -> Self::Future { + let fut = self.inner.call(dst); + let builder = self.builder.clone(); + Box::pin(async move { + let io = fut.await.map_err(Into::into)?; + let (mut tx, conn) = builder.handshake(io).await?; + tokio::spawn(async move { + if let Err(e) = conn.await { + eprintln!("connection error: {:?}", e); + } + }); + + // todo: wait for ready? or factor out to other middleware? + poll_fn(|cx| tx.poll_ready(cx)).await?; + Ok(Http2ClientService::new(tx)) + }) + } +} + +#[cfg(feature = "http2")] +impl Clone for Http2Connect { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + builder: self.builder.clone(), + _body: self._body.clone(), + } + } +} + +/// A thin adapter over hyper HTTP/1 client SendRequest. +#[cfg(feature = "http1")] +#[derive(Debug)] +pub struct Http1ClientService { + tx: hyper::client::conn::http1::SendRequest, +} + +#[cfg(feature = "http1")] +impl Http1ClientService { + /// todo + pub fn new(tx: hyper::client::conn::http1::SendRequest) -> Self { + Self { tx } + } + + /// Checks if the connection side has been closed. + pub fn is_closed(&self) -> bool { + self.tx.is_closed() + } +} + +#[cfg(feature = "http1")] +impl Service> for Http1ClientService +where + B: hyper::body::Body + Send + 'static, +{ + type Response = Response; + type Error = hyper::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.tx.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let fut = self.tx.send_request(req); + Box::pin(fut) + } +} + +/// todo +#[cfg(feature = "http2")] +#[derive(Debug)] +pub struct Http2ClientService { + tx: hyper::client::conn::http2::SendRequest, +} + +#[cfg(feature = "http2")] +impl Http2ClientService { + /// todo + pub fn new(tx: hyper::client::conn::http2::SendRequest) -> Self { + Self { tx } + } + + /// Checks if the connection side has been closed. + pub fn is_closed(&self) -> bool { + self.tx.is_closed() + } +} + +#[cfg(feature = "http2")] +impl Service> for Http2ClientService +where + B: hyper::body::Body + Send + 'static, +{ + type Response = Response; + type Error = hyper::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.tx.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let fut = self.tx.send_request(req); + Box::pin(fut) + } +} + +#[cfg(feature = "http2")] +impl Clone for Http2ClientService { + fn clone(&self) -> Self { + Self { + tx: self.tx.clone(), + } + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index 0d896030..294db016 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,8 @@ //! HTTP client utilities +#[cfg(any(feature = "http1", feature = "http2"))] +pub mod conn; + /// Legacy implementations of `connect` module and `Client` #[cfg(feature = "client-legacy")] pub mod legacy; From 818748fbd3a223a82870e91d2b78c0590c0d66d1 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 2 Dec 2025 15:21:16 -0500 Subject: [PATCH 2/9] docs(pool): add module level docs for pools (#248) --- Cargo.toml | 2 +- src/client/pool/mod.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 525b15ee..7bf208df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ full = [ client = ["hyper/client", "tokio/net", "dep:tracing", "dep:futures-channel", "dep:tower-service"] client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc", "dep:futures-util"] -client-pool = ["dep:futures-util", "dep:tower-layer"] +client-pool = ["client", "dep:futures-util", "dep:tower-layer"] client-proxy = ["client", "dep:base64", "dep:ipnet", "dep:percent-encoding"] client-proxy-system = ["dep:system-configuration", "dep:windows-registry"] diff --git a/src/client/pool/mod.rs b/src/client/pool/mod.rs index 9f1a8fce..fd505416 100644 --- a/src/client/pool/mod.rs +++ b/src/client/pool/mod.rs @@ -1,4 +1,8 @@ //! Composable pool services +//! +//! This module contains various concepts of a connection pool separated into +//! their own concerns. This allows for users to compose the layers, along with +//! any other layers, when constructing custom connection pools. pub mod cache; pub mod map; From d5740116a55cbf7af13d1142b365c56b1d684f3a Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Tue, 2 Dec 2025 14:39:43 -0500 Subject: [PATCH 3/9] v0.1.19 --- CHANGELOG.md | 8 ++++++++ Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cebafd..9965bd6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.1.19 (2025-12-03) + +- Add `client::pool` module for composable pools. Enable with the `client-pool` feature. +- Add `pool::singleton` for sharing a single cloneable connection. +- Add `pool::cache` for caching a list of connections. +- Add `pool::negotiate` for combining two pools with upgrade and fallback negotiation. +- Add `pool::map` for customizable mapping of keys and connections. + # 0.1.18 (2025-11-13) - Fix `rt::TokioTimer` to support Tokio's paused time. diff --git a/Cargo.toml b/Cargo.toml index 7bf208df..d13bf7e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" description = "hyper utilities" readme = "README.md" homepage = "https://hyper.rs" From 1b3fa96b1bc9057cbff49b52847c85dd79845b73 Mon Sep 17 00:00:00 2001 From: Chen Hongzhi Date: Tue, 23 Dec 2025 03:55:54 +0800 Subject: [PATCH 4/9] fix(matcher): improve domain matching case insensitivity (#251) Make NO_PROXY domain matching case-insensitive --- src/client/proxy/matcher.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/client/proxy/matcher.rs b/src/client/proxy/matcher.rs index a883133e..037cf278 100644 --- a/src/client/proxy/matcher.rs +++ b/src/client/proxy/matcher.rs @@ -515,7 +515,10 @@ impl DomainMatcher { fn contains(&self, domain: &str) -> bool { let domain_len = domain.len(); for d in &self.0 { - if d == domain || d.strip_prefix('.') == Some(domain) { + if d == domain + || d.strip_prefix('.') + .map_or(false, |s| s.eq_ignore_ascii_case(domain)) + { return true; } else if domain.ends_with(d) { if d.starts_with('.') { @@ -866,4 +869,33 @@ mod tests { assert!(m.intercept(&"http://rick.roll".parse().unwrap()).is_none()); } + + #[test] + fn test_domain_matcher_case_insensitive() { + let domains = vec![".foo.bar".into()]; + let matcher = DomainMatcher(domains); + + assert!(matcher.contains("foo.bar")); + assert!(matcher.contains("FOO.BAR")); + assert!(matcher.contains("Foo.Bar")); + } + + #[test] + fn test_no_proxy_case_insensitive() { + let p = p! { + all = "http://proxy.local", + no = ".example.com", + }; + + // should bypass proxy (case insensitive match) + assert!(p + .intercept(&"http://example.com".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://EXAMPLE.COM".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://Example.com".parse().unwrap()) + .is_none()); + } } From 8c4f4a0b4b0cf83ae6b7d86705f002f4efc13869 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 23 Dec 2025 22:09:11 +0800 Subject: [PATCH 5/9] fix(matcher): improve subdomain matching case insensitivity (#252) --- src/client/proxy/matcher.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/client/proxy/matcher.rs b/src/client/proxy/matcher.rs index 037cf278..41990089 100644 --- a/src/client/proxy/matcher.rs +++ b/src/client/proxy/matcher.rs @@ -515,12 +515,15 @@ impl DomainMatcher { fn contains(&self, domain: &str) -> bool { let domain_len = domain.len(); for d in &self.0 { - if d == domain + if d.eq_ignore_ascii_case(domain) || d.strip_prefix('.') .map_or(false, |s| s.eq_ignore_ascii_case(domain)) { return true; - } else if domain.ends_with(d) { + } else if domain + .get(domain_len.saturating_sub(d.len())..) + .map_or(false, |s| s.eq_ignore_ascii_case(d)) + { if d.starts_with('.') { // If the first character of d is a dot, that means the first character of domain // must also be a dot, so we are looking at a subdomain of d and that matches @@ -701,13 +704,19 @@ mod tests { // domains match with leading `.` assert!(matcher.contains("foo.bar")); + assert!(matcher.contains("FOO.BAR")); + // subdomains match with leading `.` assert!(matcher.contains("www.foo.bar")); + assert!(matcher.contains("WWW.FOO.BAR")); // domains match with no leading `.` assert!(matcher.contains("bar.foo")); + assert!(matcher.contains("Bar.foo")); + // subdomains match with no leading `.` assert!(matcher.contains("www.bar.foo")); + assert!(matcher.contains("WWW.BAR.FOO")); // non-subdomain string prefixes don't match assert!(!matcher.contains("notfoo.bar")); @@ -878,6 +887,10 @@ mod tests { assert!(matcher.contains("foo.bar")); assert!(matcher.contains("FOO.BAR")); assert!(matcher.contains("Foo.Bar")); + + assert!(matcher.contains("www.foo.bar")); + assert!(matcher.contains("WWW.FOO.BAR")); + assert!(matcher.contains("Www.Foo.Bar")); } #[test] @@ -897,5 +910,16 @@ mod tests { assert!(p .intercept(&"http://Example.com".parse().unwrap()) .is_none()); + + // subdomain should bypass proxy (case insensitive match) + assert!(p + .intercept(&"http://www.example.com".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://WWW.EXAMPLE.COM".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://Www.Example.Com".parse().unwrap()) + .is_none()); } } From d5503b2b476b1274f8faf18f99217068359ec5c4 Mon Sep 17 00:00:00 2001 From: Yusuke Tanaka Date: Sun, 28 Dec 2025 21:54:24 +0900 Subject: [PATCH 6/9] docs(client): correct malformed reference link in set_interface (#254) --- src/client/legacy/connect/http.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/legacy/connect/http.rs b/src/client/legacy/connect/http.rs index 56324e79..0236cc85 100644 --- a/src/client/legacy/connect/http.rs +++ b/src/client/legacy/connect/http.rs @@ -397,7 +397,7 @@ impl HttpConnector { /// - macOS, iOS, visionOS, watchOS, and tvOS /// /// [VRF]: https://www.kernel.org/doc/Documentation/networking/vrf.txt - /// [`man 7 socket`] https://man7.org/linux/man-pages/man7/socket.7.html + /// [`man 7 socket`]: https://man7.org/linux/man-pages/man7/socket.7.html /// [`man 7p ip`]: https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html #[cfg(any( target_os = "android", From 863ecb0526cb7e0a2bddb264ff2286a19c55a1fd Mon Sep 17 00:00:00 2001 From: Samuel Cobb Date: Mon, 5 Jan 2026 10:46:51 +0000 Subject: [PATCH 7/9] feat(conn): make http2 executor generic over the executor --- src/client/conn.rs | 62 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/client/conn.rs b/src/client/conn.rs index f2e2ef99..8b92b523 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -13,6 +13,7 @@ use crate::common::future::poll_fn; type BoxError = Box; /// todo +/// Constructed using a FROM operator on a http1 builder only #[cfg(feature = "http1")] pub struct Http1Layer { builder: hyper::client::conn::http1::Builder, @@ -20,6 +21,7 @@ pub struct Http1Layer { } /// todo +/// Constructs a base layer with the default http1 builder #[cfg(feature = "http1")] pub fn http1() -> Http1Layer { Http1Layer { @@ -45,7 +47,7 @@ impl Clone for Http1Layer { fn clone(&self) -> Self { Self { builder: self.builder.clone(), - _body: self._body.clone(), + _body: self._body, } } } @@ -61,6 +63,7 @@ impl From for Http1Layer { } /// todo +/// #[cfg(feature = "http1")] pub struct Http1Connect { inner: M, @@ -119,20 +122,33 @@ impl Clone for Http1Connect { } /// todo +/// A http2 middleware for maintaining http2 connections +/// Typically constructed from a hyper::client::conn::http2::Builder #[cfg(feature = "http2")] -pub struct Http2Layer { +pub struct Http2Layer { + builder: hyper::client::conn::http2::Builder, _body: PhantomData, -} + } /// todo +/// Constructs a layer with a default builder and a given executor. #[cfg(feature = "http2")] -pub fn http2() -> Http2Layer { - Http2Layer { _body: PhantomData } -} +pub fn http2(executor: E) -> Http2Layer +where + E: Clone, +{ + Http2Layer { + builder: hyper::client::conn::http2::Builder::new(executor), + _body: PhantomData, + } + } #[cfg(feature = "http2")] -impl tower_layer::Layer for Http2Layer { - type Service = Http2Connect; +impl tower_layer::Layer for Http2Layer +where + E: Clone, +{ + type Service = Http2Connect; fn layer(&self, inner: M) -> Self::Service { Http2Connect { inner, @@ -143,25 +159,38 @@ impl tower_layer::Layer for Http2Layer { } #[cfg(feature = "http2")] -impl Clone for Http2Layer { +impl Clone for Http2Layer { fn clone(&self) -> Self { Self { + builder: self.builder.clone(), _body: self._body.clone(), } } } + +#[cfg(feature = "http2")] +impl From> for Http2Layer { + fn from(builder: hyper::client::conn::http2::Builder) -> Self { + Self { + builder, + _body: PhantomData, + } + } + } + /// todo +/// The http2 connection type #[cfg(feature = "http2")] #[derive(Debug)] -pub struct Http2Connect { +pub struct Http2Connect { inner: M, - builder: hyper::client::conn::http2::Builder, + builder: hyper::client::conn::http2::Builder, _body: PhantomData, } #[cfg(feature = "http2")] -impl Service for Http2Connect +impl Service for Http2Connect where M: Service, M::Future: Send + 'static, @@ -170,6 +199,7 @@ where B: hyper::body::Body + Unpin + Send + 'static, B::Data: Send + 'static, B::Error: Into, + E: hyper::rt::bounds::Http2ClientConnExec + Unpin + Clone + Send + 'static, { type Response = Http2ClientService; type Error = BoxError; @@ -199,7 +229,7 @@ where } #[cfg(feature = "http2")] -impl Clone for Http2Connect { +impl Clone for Http2Connect { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -218,7 +248,7 @@ pub struct Http1ClientService { #[cfg(feature = "http1")] impl Http1ClientService { - /// todo + /// Constructs a new client HTTP/2 client service from a provided SendRequest. pub fn new(tx: hyper::client::conn::http1::SendRequest) -> Self { Self { tx } } @@ -248,7 +278,7 @@ where } } -/// todo +/// A thin adapter over hyper HTTP/2 client SendRequest. #[cfg(feature = "http2")] #[derive(Debug)] pub struct Http2ClientService { @@ -257,7 +287,7 @@ pub struct Http2ClientService { #[cfg(feature = "http2")] impl Http2ClientService { - /// todo + /// Constructs a new client HTTP/2 client service from a provided SendRequest. pub fn new(tx: hyper::client::conn::http2::SendRequest) -> Self { Self { tx } } From 34e1c266c316cdc7c06178ca4d53d4dc73719aa9 Mon Sep 17 00:00:00 2001 From: Samuel Cobb Date: Mon, 5 Jan 2026 14:03:29 +0000 Subject: [PATCH 8/9] feat(conn): docs --- src/client/conn.rs | 118 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 96 insertions(+), 22 deletions(-) diff --git a/src/client/conn.rs b/src/client/conn.rs index 8b92b523..74c7f9ac 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -1,4 +1,8 @@ -//! todo +//! Tower layers and services for HTTP/1 and HTTP/2 client connections. +//! +//! This module provides Tower-compatible layers that wrap Hyper's low-level +//! HTTP client connection types, making them easier to compose with other +//! middleware and connection pooling strategies. use std::future::Future; use std::marker::PhantomData; @@ -12,16 +16,39 @@ use crate::common::future::poll_fn; type BoxError = Box; -/// todo -/// Constructed using a FROM operator on a http1 builder only +/// A Tower [`Layer`](tower_layer::Layer) for creating HTTP/1 client connections. +/// +/// This layer wraps a connection service (typically a TCP or TLS connector) and +/// performs the HTTP/1 handshake, producing an [`Http1ClientService`] that can +/// send requests. +/// +/// Use [`http1()`] to create a layer with default settings, or construct from +/// a [`hyper::client::conn::http1::Builder`] for custom configuration. +/// +/// # Example +/// +/// ```ignore +/// use hyper_util::client::conn::http1; +/// use hyper::{client::connect::HttpConnector, body::Bytes}; +/// use tower:: ServiceBuilder; +/// use http_body_util::Empty; +/// +/// let connector = HttpConnector::new(); +/// let layer: Http1Layer> = http1(); +/// let client = ServiceBuilder::new() +/// .layer(layer) +/// .service(connector); +/// ``` #[cfg(feature = "http1")] pub struct Http1Layer { builder: hyper::client::conn::http1::Builder, _body: PhantomData, } -/// todo -/// Constructs a base layer with the default http1 builder +/// Creates an [`Http1Layer`] with default HTTP/1 settings. +/// +/// For custom settings, construct an [`Http1Layer`] from a +/// [`hyper::client::conn::http1::Builder`] using `.into()`. #[cfg(feature = "http1")] pub fn http1() -> Http1Layer { Http1Layer { @@ -62,8 +89,11 @@ impl From for Http1Layer { } } -/// todo +/// A Tower [`Service`] that establishes HTTP/1 connections. /// +/// This service wraps an underlying connection service (e.g., TCP or TLS) and +/// performs the HTTP/1 handshake when called. The resulting service can be used +/// to send HTTP requests over the established connection. #[cfg(feature = "http1")] pub struct Http1Connect { inner: M, @@ -121,17 +151,39 @@ impl Clone for Http1Connect { } } -/// todo -/// A http2 middleware for maintaining http2 connections -/// Typically constructed from a hyper::client::conn::http2::Builder +/// A Tower [`Layer`](tower_layer::Layer) for creating HTTP/2 client connections. +/// +/// This layer wraps a connection service (typically a TCP or TLS connector) and +/// performs the HTTP/2 handshake, producing an [`Http2ClientService`] that can +/// send requests. +/// +/// Use [`http2()`] to create a layer with a specific executor, or construct from +/// a [`hyper::client::conn::http2::Builder`] for custom configuration. +/// +/// # Example +/// +/// ```ignore +/// use hyper_util::client::conn::http2; +/// use hyper::{client::connect::HttpConnector, body::Bytes}; +/// use tower:: ServiceBuilder; +/// use http_body_util::Empty; +/// +/// let connector = HttpConnector::new(); +/// let layer: Http2Layer> = http2(); +/// let client = ServiceBuilder::new() +/// .layer(layer) +/// .service(connector); +/// ``` #[cfg(feature = "http2")] pub struct Http2Layer { builder: hyper::client::conn::http2::Builder, _body: PhantomData, - } +} -/// todo -/// Constructs a layer with a default builder and a given executor. +/// Creates an [`Http2Layer`] with default HTTP/1 settings. +/// +/// For custom settings, construct an [`Http2Layer`] from a +/// [`hyper::client::conn::http2::Builder`] using `.into()`. #[cfg(feature = "http2")] pub fn http2(executor: E) -> Http2Layer where @@ -141,7 +193,7 @@ where builder: hyper::client::conn::http2::Builder::new(executor), _body: PhantomData, } - } +} #[cfg(feature = "http2")] impl tower_layer::Layer for Http2Layer @@ -168,7 +220,6 @@ impl Clone for Http2Layer { } } - #[cfg(feature = "http2")] impl From> for Http2Layer { fn from(builder: hyper::client::conn::http2::Builder) -> Self { @@ -176,11 +227,14 @@ impl From> for Http2Layer { builder, _body: PhantomData, } - } - } + } +} -/// todo -/// The http2 connection type +/// A Tower [`Service`] that establishes HTTP/2 connections. +/// +/// This service wraps an underlying connection service (e.g., TCP or TLS) and +/// performs the HTTP/2 handshake when called. The resulting service can be used +/// to send HTTP requests over the established connection. #[cfg(feature = "http2")] #[derive(Debug)] pub struct Http2Connect { @@ -239,7 +293,14 @@ impl Clone for Http2Connect { } } -/// A thin adapter over hyper HTTP/1 client SendRequest. +/// A Tower [`Service`] that sends HTTP/1 requests over an established connection. +/// +/// This is a thin wrapper around [`hyper::client::conn::http1::SendRequest`] that implements +/// the Tower `Service` trait, making it composable with other Tower middleware. +/// +/// The service maintains a single HTTP/1 connection and can be used to send multiple +/// sequential requests. For concurrent requests or connection pooling, wrap this service +/// with appropriate middleware. #[cfg(feature = "http1")] #[derive(Debug)] pub struct Http1ClientService { @@ -248,7 +309,10 @@ pub struct Http1ClientService { #[cfg(feature = "http1")] impl Http1ClientService { - /// Constructs a new client HTTP/2 client service from a provided SendRequest. + /// Constructs a new HTTP/1 client service from a Hyper `SendRequest`. + /// + /// Typically you won't call this directly; instead, use [`Http1Connect`] to + /// establish connections and produce this service. pub fn new(tx: hyper::client::conn::http1::SendRequest) -> Self { Self { tx } } @@ -278,7 +342,14 @@ where } } -/// A thin adapter over hyper HTTP/2 client SendRequest. +/// A Tower [`Service`] that sends HTTP/2 requests over an established connection. +/// +/// This is a thin wrapper around [`hyper::client::conn::http2::SendRequest`] that implements +/// the Tower `Service` trait, making it composable with other Tower middleware. +/// +/// The service maintains a single HTTP/2 connection and supports multiplexing multiple +/// concurrent requests over that connection. The service can be cloned to send requests +/// concurrently, or used with the [`Singleton`](crate::client::pool::singleton::Singleton) pool service. #[cfg(feature = "http2")] #[derive(Debug)] pub struct Http2ClientService { @@ -287,7 +358,10 @@ pub struct Http2ClientService { #[cfg(feature = "http2")] impl Http2ClientService { - /// Constructs a new client HTTP/2 client service from a provided SendRequest. + /// Constructs a new HTTP/2 client service from a Hyper `SendRequest`. + /// + /// Typically you won't call this directly; instead, use [`Http2Connect`] to + /// establish connections and produce this service. pub fn new(tx: hyper::client::conn::http2::SendRequest) -> Self { Self { tx } } From 18c1683da029bde3dacc6a501094614b312ce558 Mon Sep 17 00:00:00 2001 From: Samuel Cobb Date: Mon, 5 Jan 2026 14:03:43 +0000 Subject: [PATCH 9/9] fix(docs): fix map doc warning --- src/client/pool/map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/pool/map.rs b/src/client/pool/map.rs index 83b56480..8ff4179d 100644 --- a/src/client/pool/map.rs +++ b/src/client/pool/map.rs @@ -48,7 +48,7 @@ where // impl Map impl Map { - /// Create a [`Builder`] to configure a new `Map`. + /// Create a `Builder` to configure a new `Map`. pub fn builder() -> builder::Builder { builder::Builder::new()