From 0dd0beca6fa5f5dc507cfb1ff0a1ff252f0dd0f8 Mon Sep 17 00:00:00 2001 From: Dahale Aditya Dnyaneshwar Date: Sun, 4 Oct 2026 22:04:34 +0530 Subject: [PATCH] feat(client): decouple tokio/net and mio from client-legacy Previously, client-legacy unconditionally enabled tokio/net, socket2, and libc, which forced the mio dependency even on platforms that do not support it (such as Fuchsia and WASM), and for downstream libraries (like hyper-rustls) that provide custom connectors without requiring TCP networking. This change: - Re-introduces the tcp feature ([tokio, tokio/net, dep:socket2, dep:libc]) matching hyper 0.14 convention. - Decouples tokio/net, socket2, and libc from client-legacy. - Gates HttpConnector, dns, and build_http behind #[cfg(feature = tcp)]. - Adds tcp to the full feature shorthand. - Adds an integration test ensuring client-legacy works with a custom connector without requiring the tcp feature. Closes hyperium/hyper#3842 --- .github/workflows/CI.yml | 3 + Cargo.toml | 11 ++- src/client/legacy/client.rs | 7 +- src/client/legacy/connect/mod.rs | 8 ++- tests/client_no_tcp.rs | 118 +++++++++++++++++++++++++++++++ tests/legacy_client.rs | 6 +- tests/proxy.rs | 2 +- 7 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 tests/client_no_tcp.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index e7d27a47..51bbd4f7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -58,6 +58,9 @@ jobs: # full excludes that opt-in, so also check that TokioExecutor does not # propagate spans by default. Limit this extra run to runtime tests. - run: "cargo test --features full --lib rt::" + # Test client-legacy without the tcp feature to ensure custom connectors + # work on targets where mio/tcp is unsupported. + - run: "cargo test --features client-legacy,http1,tokio --test client_no_tcp" msrv: name: Check MSRV (${{ matrix.rust }}) on ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index d2c2dcb8..3198c9df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,12 +74,13 @@ full = [ "service", "http1", "http2", + "tcp", "tokio", "tracing", ] client = ["hyper/client", "dep:tracing", "dep:futures-channel", "dep:tower-service"] -client-legacy = ["client", "tokio/net", "dep:socket2", "tokio/sync", "dep:libc", "dep:futures-util", "dep:httparse", "dep:sync_wrapper"] +client-legacy = ["client", "tokio/sync", "dep:futures-util", "dep:httparse", "dep:sync_wrapper"] client-pool = ["client", "dep:futures-util", "dep:tower-layer", "tokio/sync"] client-proxy = ["client", "dep:base64", "dep:ipnet", "dep:percent-encoding"] client-proxy-system = ["dep:system-configuration", "dep:windows-registry"] @@ -93,6 +94,8 @@ service = ["dep:tower-service"] http1 = ["hyper/http1"] http2 = ["hyper/http2"] +tcp = ["tokio", "tokio/net", "dep:socket2", "dep:libc"] + tokio = ["dep:tokio", "tokio/rt", "tokio/time"] tracing = ["dep:tracing"] @@ -105,7 +108,11 @@ __internal_happy_eyeballs_tests = [] [[example]] name = "client" -required-features = ["client-legacy", "http1", "tokio"] +required-features = ["client-legacy", "http1", "tcp"] + +[[example]] +name = "client_tracing" +required-features = ["client-legacy", "http1", "tcp", "tracing"] [[example]] name = "server" diff --git a/src/client/legacy/client.rs b/src/client/legacy/client.rs index d6e48955..68b3e81e 100644 --- a/src/client/legacy/client.rs +++ b/src/client/legacy/client.rs @@ -20,7 +20,7 @@ use hyper::{Method, Request, Response, Uri, Version, body::Body}; use sync_wrapper::SyncWrapper; use tracing::{debug, trace, warn}; -#[cfg(feature = "tokio")] +#[cfg(feature = "tcp")] use super::connect::HttpConnector; use super::connect::capture::CaptureConnectionExtension; use super::connect::{Alpn, Connect, Connected, Connection}; @@ -1555,6 +1555,8 @@ impl Builder { { #[cfg(feature = "http2")] self.h2_builder.timer(timer); + #[cfg(not(feature = "http2"))] + let _ = timer; self } @@ -1611,7 +1613,8 @@ impl Builder { } /// Build a client with this configuration and the default `HttpConnector`. - #[cfg(feature = "tokio")] + #[cfg(feature = "tcp")] + #[cfg_attr(docsrs, doc(cfg(feature = "tcp")))] pub fn build_http(&self) -> Client where B: Body + Send, diff --git a/src/client/legacy/connect/mod.rs b/src/client/legacy/connect/mod.rs index fff8262f..dae7a371 100644 --- a/src/client/legacy/connect/mod.rs +++ b/src/client/legacy/connect/mod.rs @@ -72,12 +72,14 @@ use std::{ use ::http::Extensions; -#[cfg(feature = "tokio")] +#[cfg(feature = "tcp")] +#[cfg_attr(docsrs, doc(cfg(feature = "tcp")))] pub use self::http::{HttpConnector, HttpInfo}; -#[cfg(feature = "tokio")] +#[cfg(feature = "tcp")] +#[cfg_attr(docsrs, doc(cfg(feature = "tcp")))] pub mod dns; -#[cfg(feature = "tokio")] +#[cfg(feature = "tcp")] mod http; pub mod proxy; diff --git a/tests/client_no_tcp.rs b/tests/client_no_tcp.rs new file mode 100644 index 00000000..3c1f51c0 --- /dev/null +++ b/tests/client_no_tcp.rs @@ -0,0 +1,118 @@ +//! Integration test verifying that `client-legacy` can be compiled and used with +//! a custom connector (e.g. for custom in-memory streams, TLS, or non-TCP platforms +//! like WASM and Fuchsia) without enabling the `tcp` feature or pulling in `mio`. + +#![cfg(all( + feature = "client-legacy", + feature = "http1", + feature = "tokio", + not(feature = "tcp") +))] + +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; + +use bytes::Bytes; +use http::{Request, StatusCode, Uri}; +use http_body_util::{BodyExt, Empty}; +use hyper::rt::{Read, ReadBufCursor, Write}; +use hyper_util::client::legacy::Client; +use hyper_util::client::legacy::connect::{Connected, Connection}; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use tower_service::Service; + +struct MockStream(TokioIo); + +impl Read for MockStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: ReadBufCursor<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_read(cx, buf) + } +} + +impl Write for MockStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_shutdown(cx) + } +} + +impl Connection for MockStream { + fn connected(&self) -> Connected { + Connected::new() + } +} + +#[derive(Clone)] +struct CustomConnector { + mock: Arc>>, +} + +impl Service for CustomConnector { + type Response = MockStream; + type Error = std::io::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _dst: Uri) -> Self::Future { + let mock = self + .mock + .lock() + .unwrap() + .take() + .expect("mock connection only called once"); + Box::pin(async move { Ok(MockStream(TokioIo::new(mock))) }) + } +} + +#[tokio::test] +async fn test_client_legacy_with_custom_connector_without_tcp() { + let mock_io = tokio_test::io::Builder::new() + .write(b"GET /test HTTP/1.1\r\nhost: example.com\r\n\r\n") + .read(b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello") + .build(); + + let connector = CustomConnector { + mock: Arc::new(Mutex::new(Some(mock_io))), + }; + + let client = Client::builder(TokioExecutor::new()) + .pool_max_idle_per_host(0) + .build(connector); + + let req = Request::builder() + .uri("http://example.com/test") + .body(Empty::::new()) + .unwrap(); + + let mut resp = client.request(req).await.expect("request succeeds"); + assert_eq!(resp.status(), StatusCode::OK); + + let body = resp.body_mut().collect().await.unwrap().to_bytes(); + assert_eq!(&body[..], b"hello"); +} diff --git a/tests/legacy_client.rs b/tests/legacy_client.rs index aac04cdd..f86df282 100644 --- a/tests/legacy_client.rs +++ b/tests/legacy_client.rs @@ -1,4 +1,8 @@ -#![cfg(all(feature = "client-legacy", any(feature = "http1", feature = "http2")))] +#![cfg(all( + feature = "client-legacy", + feature = "tcp", + any(feature = "http1", feature = "http2") +))] mod test_utils; diff --git a/tests/proxy.rs b/tests/proxy.rs index 8aa8070c..70ef52d5 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -1,4 +1,4 @@ -#![cfg(all(feature = "client-legacy", feature = "client-proxy"))] +#![cfg(all(feature = "client-legacy", feature = "client-proxy", feature = "tcp"))] use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream};