Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"]

Expand Down
146 changes: 125 additions & 21 deletions src/client/conn.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,14 +16,39 @@ use crate::common::future::poll_fn;

type BoxError = Box<dyn std::error::Error + Send + Sync>;

/// todo
/// 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<Empty<Bytes>> = http1();
/// let client = ServiceBuilder::new()
/// .layer(layer)
/// .service(connector);
/// ```
#[cfg(feature = "http1")]
pub struct Http1Layer<B> {
builder: hyper::client::conn::http1::Builder,
_body: PhantomData<fn(B)>,
}

/// todo
/// 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<B>() -> Http1Layer<B> {
Http1Layer {
Expand Down Expand Up @@ -60,7 +89,11 @@ impl<B> From<hyper::client::conn::http1::Builder> for Http1Layer<B> {
}
}

/// 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<M, B> {
inner: M,
Expand Down Expand Up @@ -118,21 +151,56 @@ impl<M: Clone, B> Clone for Http1Connect<M, B> {
}
}

/// todo
/// 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<Empty<Bytes>> = http2();
/// let client = ServiceBuilder::new()
/// .layer(layer)
/// .service(connector);
/// ```
#[cfg(feature = "http2")]
pub struct Http2Layer<B> {
pub struct Http2Layer<B, E> {
builder: hyper::client::conn::http2::Builder<E>,
_body: PhantomData<fn(B)>,
}

/// todo
/// 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<B>() -> Http2Layer<B> {
Http2Layer { _body: PhantomData }
pub fn http2<B, E>(executor: E) -> Http2Layer<B, E>
where
E: Clone,
{
Http2Layer {
builder: hyper::client::conn::http2::Builder::new(executor),
_body: PhantomData,
}
}

#[cfg(feature = "http2")]
impl<M, B> tower_layer::Layer<M> for Http2Layer<B> {
type Service = Http2Connect<M, B>;
impl<M, B, E> tower_layer::Layer<M> for Http2Layer<B, E>
where
E: Clone,
{
type Service = Http2Connect<M, B, E>;
fn layer(&self, inner: M) -> Self::Service {
Http2Connect {
inner,
Expand All @@ -143,25 +211,40 @@ impl<M, B> tower_layer::Layer<M> for Http2Layer<B> {
}

#[cfg(feature = "http2")]
impl<B> Clone for Http2Layer<B> {
impl<B, E: Clone> Clone for Http2Layer<B, E> {
fn clone(&self) -> Self {
Self {
builder: self.builder.clone(),
_body: self._body.clone(),
}
}
}

/// todo
#[cfg(feature = "http2")]
impl<B, E> From<hyper::client::conn::http2::Builder<E>> for Http2Layer<B, E> {
fn from(builder: hyper::client::conn::http2::Builder<E>) -> Self {
Self {
builder,
_body: PhantomData,
}
}
}

/// 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<M, B> {
pub struct Http2Connect<M, B, E> {
inner: M,
builder: hyper::client::conn::http2::Builder<crate::rt::TokioExecutor>,
builder: hyper::client::conn::http2::Builder<E>,
_body: PhantomData<fn(B)>,
}

#[cfg(feature = "http2")]
impl<M, Dst, B> Service<Dst> for Http2Connect<M, B>
impl<M, Dst, B, E> Service<Dst> for Http2Connect<M, B, E>
where
M: Service<Dst>,
M::Future: Send + 'static,
Expand All @@ -170,6 +253,7 @@ where
B: hyper::body::Body + Unpin + Send + 'static,
B::Data: Send + 'static,
B::Error: Into<BoxError>,
E: hyper::rt::bounds::Http2ClientConnExec<B, M::Response> + Unpin + Clone + Send + 'static,
{
type Response = Http2ClientService<B>;
type Error = BoxError;
Expand Down Expand Up @@ -199,7 +283,7 @@ where
}

#[cfg(feature = "http2")]
impl<M: Clone, B> Clone for Http2Connect<M, B> {
impl<M: Clone, B, E: Clone> Clone for Http2Connect<M, B, E> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
Expand All @@ -209,7 +293,14 @@ impl<M: Clone, B> Clone for Http2Connect<M, B> {
}
}

/// 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<B> {
Expand All @@ -218,7 +309,10 @@ pub struct Http1ClientService<B> {

#[cfg(feature = "http1")]
impl<B> Http1ClientService<B> {
/// todo
/// 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<B>) -> Self {
Self { tx }
}
Expand Down Expand Up @@ -248,7 +342,14 @@ where
}
}

/// todo
/// 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<B> {
Expand All @@ -257,7 +358,10 @@ pub struct Http2ClientService<B> {

#[cfg(feature = "http2")]
impl<B> Http2ClientService<B> {
/// todo
/// 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<B>) -> Self {
Self { tx }
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/legacy/connect/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ impl<R> HttpConnector<R> {
/// - 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",
Expand Down
2 changes: 1 addition & 1 deletion src/client/pool/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ where
// impl Map

impl Map<builder::StartHere, builder::StartHere> {
/// Create a [`Builder`] to configure a new `Map`.
/// Create a `Builder` to configure a new `Map`.
pub fn builder<Dst>() -> builder::Builder<Dst, builder::WantsKeyer, builder::WantsServiceMaker>
{
builder::Builder::new()
Expand Down
4 changes: 4 additions & 0 deletions src/client/pool/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
60 changes: 58 additions & 2 deletions src/client/proxy/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,15 @@ 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.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
Expand Down Expand Up @@ -698,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"));
Expand Down Expand Up @@ -866,4 +878,48 @@ 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"));

assert!(matcher.contains("www.foo.bar"));
assert!(matcher.contains("WWW.FOO.BAR"));
assert!(matcher.contains("Www.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());

// 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());
}
}