From baaefd89549ceb50c83c3f66cced7a4243fc8db0 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 12 Jan 2026 13:28:35 -0500 Subject: [PATCH] feat: Add HttpTransport trait abstraction Introduce a generic HttpTransport trait to decouple the SDK from specific HTTP client implementations. This follows the pattern established in eventsource-client. - Add transport.rs with HttpTransport trait and error types - Add transport_hyper.rs with hyper 1.0 implementation - Make hyper an optional dependency via feature flags - Use Bytes for request bodies (supports binary and empty) - Stream response bodies via ByteStream type alias This commit adds the abstraction without changing existing code. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/ci.yml | 2 +- contract-tests/Cargo.toml | 4 +- contract-tests/src/main.rs | 18 +- launchdarkly-server-sdk/Cargo.toml | 19 +- launchdarkly-server-sdk/src/config.rs | 12 +- .../src/data_source_builders.rs | 16 +- .../src/events/processor_builders.rs | 12 +- launchdarkly-server-sdk/src/lib.rs | 12 +- launchdarkly-server-sdk/src/transport.rs | 122 +++++++++ .../src/transport_hyper.rs | 246 ++++++++++++++++++ 10 files changed, 426 insertions(+), 37 deletions(-) create mode 100644 launchdarkly-server-sdk/src/transport.rs create mode 100644 launchdarkly-server-sdk/src/transport_hyper.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad2741e..b7d30fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: - name: "Run contract tests with hyper_rustls" uses: ./.github/actions/contract-tests with: - tls_feature: "rustls" + tls_feature: "hyper-rustls" token: ${{ secrets.GITHUB_TOKEN }} - name: "Run contract tests with hyper_tls" diff --git a/contract-tests/Cargo.toml b/contract-tests/Cargo.toml index 2e1cf4a..3020c67 100644 --- a/contract-tests/Cargo.toml +++ b/contract-tests/Cargo.toml @@ -23,6 +23,6 @@ reqwest = { version = "0.12.4", features = ["default", "blocking", "json"] } async-mutex = "1.4.0" [features] -default = ["rustls"] -rustls = ["hyper-rustls", "launchdarkly-server-sdk/rustls"] +default = ["hyper-rustls"] +hyper-rustls = ["dep:hyper-rustls", "launchdarkly-server-sdk/hyper-rustls"] tls = ["hyper-tls"] diff --git a/contract-tests/src/main.rs b/contract-tests/src/main.rs index e4b1b36..7d5e0f3 100644 --- a/contract-tests/src/main.rs +++ b/contract-tests/src/main.rs @@ -210,9 +210,9 @@ struct AppState { streaming_https_connector: StreamingHttpsConnector, } -#[cfg(feature = "rustls")] +#[cfg(feature = "hyper-rustls")] type HttpsConnector = hyper_rustls::HttpsConnector; -#[cfg(feature = "rustls")] +#[cfg(feature = "hyper-rustls")] type StreamingHttpsConnector = hyper_util::client::legacy::connect::HttpConnector; #[cfg(feature = "tls")] @@ -224,20 +224,22 @@ type StreamingHttpsConnector = hyper_tls::HttpsConnector; async fn main() -> std::io::Result<()> { env_logger::init(); - #[cfg(not(any(feature = "tls", feature = "rustls")))] + #[cfg(not(any(feature = "tls", feature = "hyper-rustls")))] { - compile_error!("one of the { \"tls\", \"rustls\" } features must be enabled"); + compile_error!("one of the { \"tls\", \"hyper-rustls\" } features must be enabled"); } - #[cfg(all(feature = "tls", feature = "rustls"))] + #[cfg(all(feature = "tls", feature = "hyper-rustls"))] { - compile_error!("only one of the { \"tls\", \"rustls\" } features can be enabled at a time"); + compile_error!( + "only one of the { \"tls\", \"hyper-rustls\" } features can be enabled at a time" + ); } let (tx, rx) = mpsc::channel::<()>(); - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] let streaming_https_connector = hyper_util::client::legacy::connect::HttpConnector::new(); - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] let connector = hyper_rustls::HttpsConnectorBuilder::new() .with_native_roots() .expect("Failed to load native root certificates") diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index 9e6e2b8..3fa8aaa 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -21,7 +21,7 @@ chrono = "0.4.19" crossbeam-channel = "0.5.1" data-encoding = "2.3.2" # eventsource-client = { version = "0.16.0", default-features = false } -eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client", branch = "feat/hyper-as-feature" } +eventsource-client = { git = "https://github.com/launchdarkly/rust-eventsource-client", default-features = false, branch = "feat/hyper-as-feature" } futures = "0.3.12" lazy_static = "1.4.0" log = "0.4.14" @@ -37,11 +37,11 @@ moka = { version = "0.12.1", features = ["sync"] } uuid = {version = "1.2.2", features = ["v4"] } http = "1.0" bytes = "1.11" -hyper = { version = "1.0", features = ["client", "http1", "http2"] } -hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"] } -http-body-util = { version = "0.1" } +hyper = { version = "1.0", features = ["client", "http1", "http2"], optional = true } +hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "tokio"], optional = true } +http-body-util = { version = "0.1", optional = true } hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "webpki-roots"], optional = true} -tower = { version = "0.4" } +tower = { version = "0.4", optional = true } rand = "0.9" flate2 = { version = "1.0.35", optional = true } aws-lc-rs = "1.14.1" @@ -59,14 +59,15 @@ reqwest = { version = "0.12.4", features = ["json"] } testing_logger = "0.1.1" [features] -default = ["rustls"] -rustls = ["hyper-rustls/http1", "hyper-rustls/http2", "eventsource-client/hyper-rustls"] +default = ["hyper-rustls"] +hyper = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:tower", "eventsource-client/hyper"] +hyper-rustls = ["dep:hyper-rustls", "hyper", "eventsource-client/hyper-rustls"] event-compression = ["flate2"] [[example]] name = "print_flags" -required-features = ["rustls"] +required-features = ["hyper-rustls"] [[example]] name = "progress" -required-features = ["rustls"] +required-features = ["hyper-rustls"] diff --git a/launchdarkly-server-sdk/src/config.rs b/launchdarkly-server-sdk/src/config.rs index b4d21ad..bb75eff 100644 --- a/launchdarkly-server-sdk/src/config.rs +++ b/launchdarkly-server-sdk/src/config.rs @@ -301,11 +301,11 @@ impl ConfigBuilder { Ok(Box::new(NullDataSourceBuilder::new())) } Some(builder) => Ok(builder), - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] None => Ok(Box::new( StreamingDataSourceBuilder::::new(), )), - #[cfg(not(feature = "rustls"))] + #[cfg(not(feature = "hyper-rustls"))] None => Err(BuildError::InvalidConfig( "data source builder required when rustls is disabled".into(), )), @@ -320,11 +320,13 @@ impl ConfigBuilder { Ok(Box::new(NullEventProcessorBuilder::new())) } Some(builder) => Ok(builder), - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] None => Ok(Box::new(EventProcessorBuilder::< - hyper_rustls::HttpsConnector, + hyper_rustls::HttpsConnector< + hyper_util::client::legacy::connect::HttpConnector, + >, >::new())), - #[cfg(not(feature = "rustls"))] + #[cfg(not(feature = "hyper-rustls"))] None => Err(BuildError::InvalidConfig( "event processor factory required when rustls is disabled".into(), )), diff --git a/launchdarkly-server-sdk/src/data_source_builders.rs b/launchdarkly-server-sdk/src/data_source_builders.rs index 02e105b..384d5c6 100644 --- a/launchdarkly-server-sdk/src/data_source_builders.rs +++ b/launchdarkly-server-sdk/src/data_source_builders.rs @@ -3,7 +3,7 @@ use crate::data_source::{DataSource, NullDataSource, PollingDataSource, Streamin use crate::feature_requester_builders::{FeatureRequesterFactory, HyperFeatureRequesterBuilder}; use eventsource_client as es; use http::Uri; -#[cfg(feature = "rustls")] +#[cfg(feature = "hyper-rustls")] use hyper_rustls::HttpsConnectorBuilder; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -93,7 +93,7 @@ impl DataSourceFactory for StreamingDataSourceBuilder { tags: Option, ) -> Result, BuildError> { let data_source_result = match &self.transport { - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] None => Ok(StreamingDataSource::new( endpoints.streaming_base_url(), sdk_key, @@ -101,7 +101,7 @@ impl DataSourceFactory for StreamingDataSourceBuilder { &tags, es::HyperTransport::new_https(), )), - #[cfg(not(feature = "rustls"))] + #[cfg(not(feature = "hyper-rustls"))] None => Err(BuildError::InvalidConfig( "https connector required when rustls is disabled".into(), )), @@ -243,7 +243,11 @@ impl PollingDataSourceBuilder { impl DataSourceFactory for PollingDataSourceBuilder where C: tower::Service + Clone + Send + Sync + 'static, - C::Response: hyper_util::client::legacy::connect::Connection + hyper::rt::Read + hyper::rt::Write + Send + Unpin, + C::Response: hyper_util::client::legacy::connect::Connection + + hyper::rt::Read + + hyper::rt::Write + + Send + + Unpin, C::Future: Send + Unpin + 'static, C::Error: Into>, { @@ -255,7 +259,7 @@ where ) -> Result, BuildError> { let feature_requester_builder: Result, BuildError> = match &self.connector { - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] None => { let connector = HttpsConnectorBuilder::new() .with_webpki_roots() @@ -270,7 +274,7 @@ where connector, ))) } - #[cfg(not(feature = "rustls"))] + #[cfg(not(feature = "hyper-rustls"))] None => Err(BuildError::InvalidConfig( "https connector required when rustls is disabled".into(), )), diff --git a/launchdarkly-server-sdk/src/events/processor_builders.rs b/launchdarkly-server-sdk/src/events/processor_builders.rs index 9bf40f7..927fb29 100644 --- a/launchdarkly-server-sdk/src/events/processor_builders.rs +++ b/launchdarkly-server-sdk/src/events/processor_builders.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::Duration; use http::Uri; -#[cfg(feature = "rustls")] +#[cfg(feature = "hyper-rustls")] use hyper_rustls::HttpsConnectorBuilder; use launchdarkly_server_sdk_evaluation::Reference; use thiserror::Error; @@ -88,7 +88,11 @@ pub struct EventProcessorBuilder { impl EventProcessorFactory for EventProcessorBuilder where C: tower::Service + Clone + Send + Sync + 'static, - C::Response: hyper_util::client::legacy::connect::Connection + hyper::rt::Read + hyper::rt::Write + Send + Unpin, + C::Response: hyper_util::client::legacy::connect::Connection + + hyper::rt::Read + + hyper::rt::Write + + Send + + Unpin, C::Future: Send + Unpin + 'static, C::Error: Into>, { @@ -119,7 +123,7 @@ where self.compress_events, ))) } else { - #[cfg(feature = "rustls")] + #[cfg(feature = "hyper-rustls")] { let connector = HttpsConnectorBuilder::new() .with_webpki_roots() @@ -136,7 +140,7 @@ where self.compress_events, ))) } - #[cfg(not(feature = "rustls"))] + #[cfg(not(feature = "hyper-rustls"))] Err(BuildError::InvalidConfig( "https connector is required when rustls is disabled".into(), )) diff --git a/launchdarkly-server-sdk/src/lib.rs b/launchdarkly-server-sdk/src/lib.rs index 06c791f..5847d9d 100644 --- a/launchdarkly-server-sdk/src/lib.rs +++ b/launchdarkly-server-sdk/src/lib.rs @@ -52,6 +52,11 @@ pub use stores::persistent_store_builders::{ pub use stores::store_types::{AllData, DataKind, SerializedItem, StorageItem}; pub use version::version_string; +// Re-export transport types +pub use transport::{HttpTransport, ResponseFuture, TransportError}; +#[cfg(feature = "hyper")] +pub use transport_hyper::HyperTransport; + mod client; mod config; mod data_source; @@ -66,6 +71,9 @@ mod sampler; mod service_endpoints; mod stores; mod test_common; +mod transport; +#[cfg(feature = "hyper")] +mod transport_hyper; mod version; static LAUNCHDARKLY_EVENT_SCHEMA_HEADER: &str = "x-launchdarkly-event-schema"; @@ -78,8 +86,8 @@ lazy_static! { format!("RustServerClient/{}", version_string()); // For cases where a statically empty header value are needed. - pub(crate) static ref EMPTY_HEADER: hyper::header::HeaderValue = - hyper::header::HeaderValue::from_static(""); + pub(crate) static ref EMPTY_HEADER: http::HeaderValue = + http::HeaderValue::from_static(""); } #[cfg(test)] diff --git a/launchdarkly-server-sdk/src/transport.rs b/launchdarkly-server-sdk/src/transport.rs new file mode 100644 index 0000000..e189923 --- /dev/null +++ b/launchdarkly-server-sdk/src/transport.rs @@ -0,0 +1,122 @@ +//! HTTP transport abstraction for LaunchDarkly SDK +//! +//! This module defines the [`HttpTransport`] trait which allows users to plug in +//! their own HTTP client implementation (hyper, reqwest, or custom). + +use bytes::Bytes; +use futures::Stream; +use std::error::Error as StdError; +use std::fmt; +use std::future::Future; +use std::pin::Pin; + +// Re-export http crate types for convenience +pub use http::{Request, Response}; + +/// A pinned, boxed stream of bytes returned by HTTP transports. +/// +/// This represents the streaming response body from an HTTP request. +pub type ByteStream = Pin> + Send>>; + +/// A pinned, boxed future for an HTTP response. +/// +/// This represents the future returned by [`HttpTransport::request`]. +pub type ResponseFuture = + Pin, TransportError>> + Send>>; + +/// Error type for HTTP transport operations. +/// +/// This wraps transport-specific errors (network failures, timeouts, etc.) in a +/// common error type that the SDK can handle uniformly. +#[derive(Debug)] +pub struct TransportError { + inner: Box, +} + +impl TransportError { + /// Create a new transport error from any error type. + pub fn new(err: impl StdError + Send + Sync + 'static) -> Self { + Self { + inner: Box::new(err), + } + } + + /// Get a reference to the inner error. + pub fn inner(&self) -> &(dyn StdError + Send + Sync + 'static) { + &*self.inner + } +} + +impl fmt::Display for TransportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "transport error: {}", self.inner) + } +} + +impl StdError for TransportError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + Some(&*self.inner) + } +} + +/// Trait for pluggable HTTP transport implementations. +/// +/// Implement this trait to provide HTTP request/response functionality for the +/// SDK. The transport is responsible for: +/// - Establishing HTTP connections (with TLS if needed) +/// - Sending HTTP requests +/// - Returning streaming HTTP responses +/// - Handling timeouts (if desired) +/// +/// The SDK normally uses [`crate::HyperTransport`] as the default implementation, +/// but you can provide your own implementation for custom requirements such as: +/// - Using a different HTTP client library (reqwest, custom, etc.) +/// - Adding request/response logging or metrics +/// - Implementing custom retry logic +/// - Using a proxy or custom TLS configuration +/// +/// # Example +/// +/// ```no_run +/// use launchdarkly_server_sdk::{HttpTransport, ResponseFuture, TransportError}; +/// use bytes::Bytes; +/// use http::{Request, Response}; +/// +/// #[derive(Clone)] +/// struct LoggingTransport { +/// inner: T, +/// } +/// +/// impl HttpTransport for LoggingTransport { +/// fn request(&self, request: Request) -> ResponseFuture { +/// println!("Making request to: {}", request.uri()); +/// self.inner.request(request) +/// } +/// } +/// ``` +pub trait HttpTransport: Clone + Send + Sync + 'static { + /// Execute an HTTP request and return a streaming response. + /// + /// # Arguments + /// + /// * `request` - The HTTP request to execute. The body type is `Bytes` + /// to support both binary content and empty bodies. Use `Bytes::new()` + /// for requests with no body (e.g., GET requests). + /// + /// # Returns + /// + /// A future that resolves to an HTTP response with a streaming body, or a + /// transport error if the request fails. + /// + /// The response includes: + /// - Status code + /// - Response headers + /// - A stream of body bytes + /// + /// # Notes + /// + /// - The transport should NOT follow redirects - the SDK handles this when needed + /// - The transport should NOT retry requests - the SDK handles this + /// - The transport MAY implement timeouts as desired + fn request(&self, request: Request) -> ResponseFuture; +} diff --git a/launchdarkly-server-sdk/src/transport_hyper.rs b/launchdarkly-server-sdk/src/transport_hyper.rs new file mode 100644 index 0000000..d73348e --- /dev/null +++ b/launchdarkly-server-sdk/src/transport_hyper.rs @@ -0,0 +1,246 @@ +//! Hyper v1 transport implementation for LaunchDarkly SDK +//! +//! This module provides a production-ready [`HyperTransport`] implementation that +//! integrates hyper v1 with the LaunchDarkly SDK. + +use crate::transport::{ByteStream, HttpTransport, ResponseFuture, TransportError}; +use bytes::Bytes; +use http::{Request, Response}; +use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; +use hyper::body::Incoming; +use hyper_util::client::legacy::Client as HyperClient; +use hyper_util::rt::TokioExecutor; + +/// A transport implementation using hyper v1.x +/// +/// This struct wraps a hyper client and implements the [`HttpTransport`] trait +/// for use with the LaunchDarkly SDK. +/// +/// # Default Configuration +/// +/// By default, `HyperTransport` uses: +/// - HTTP-only connector (no TLS) +/// - Both HTTP/1.1 and HTTP/2 protocol support +/// - No timeout configuration +/// +/// For HTTPS support, use [`HyperTransport::new_https()`] (requires the `rustls` feature) +/// or provide your own connector with [`HyperTransport::new_with_connector()`]. +/// +/// # Example +/// +/// ```ignore +/// use launchdarkly_server_sdk::{HyperTransport, ConfigBuilder, EventProcessorBuilder}; +/// +/// # #[cfg(feature = "hyper-rustls")] +/// # { +/// // Use default HTTPS transport +/// let transport = HyperTransport::new_https(); +/// +/// let config = ConfigBuilder::new("sdk-key") +/// .event_processor(EventProcessorBuilder::new().transport(transport.clone())) +/// .build(); +/// # } +/// ``` +#[derive(Clone)] +pub struct HyperTransport { + client: HyperClient>>, +} + +impl HyperTransport { + /// Create a new HyperTransport with default HTTP connector and no timeouts + /// + /// This creates a basic HTTP-only client that supports both HTTP/1 and HTTP/2. + /// For HTTPS support, use [`HyperTransport::new_https()`] instead. + /// + /// # Example + /// + /// ``` + /// use launchdarkly_server_sdk::HyperTransport; + /// + /// let transport = HyperTransport::new(); + /// ``` + pub fn new() -> Self { + let connector = hyper_util::client::legacy::connect::HttpConnector::new(); + let client = HyperClient::builder(TokioExecutor::new()).build(connector); + Self { client } + } + + /// Create a new HyperTransport with HTTPS support using rustls + /// + /// This creates an HTTPS client that supports both HTTP/1 and HTTP/2 protocols + /// with native certificate verification. + /// + /// This method is only available when the `rustls` feature is enabled. + /// + /// # Example + /// + /// ```no_run + /// # #[cfg(feature = "hyper-rustls")] + /// # { + /// use launchdarkly_server_sdk::HyperTransport; + /// + /// let transport = HyperTransport::new_https(); + /// # } + /// ``` + #[cfg(feature = "hyper-rustls")] + pub fn new_https() -> HyperTransport< + hyper_rustls::HttpsConnector, + > { + use hyper_rustls::HttpsConnectorBuilder; + + let connector = HttpsConnectorBuilder::new() + .with_webpki_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + let client = HyperClient::builder(TokioExecutor::new()).build(connector); + HyperTransport { client } + } +} + +impl HyperTransport { + /// Create a new HyperTransport with a custom connector + /// + /// This allows you to provide your own connector implementation, which is useful for: + /// - Custom TLS configuration + /// - Proxy support + /// - Connection pooling customization + /// - Custom DNS resolution + /// + /// # Example + /// + /// ```no_run + /// use launchdarkly_server_sdk::HyperTransport; + /// use hyper_util::client::legacy::connect::HttpConnector; + /// + /// let mut connector = HttpConnector::new(); + /// connector.set_nodelay(true); + /// + /// let transport = HyperTransport::new_with_connector(connector); + /// ``` + pub fn new_with_connector(connector: C) -> Self + where + C: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static, + { + let client = HyperClient::builder(TokioExecutor::new()).build(connector); + Self { client } + } +} + +impl Default for HyperTransport { + fn default() -> Self { + Self::new() + } +} + +impl HttpTransport for HyperTransport +where + C: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static, +{ + fn request(&self, request: Request) -> ResponseFuture { + let (parts, body) = request.into_parts(); + + // Convert Bytes to BoxBody for hyper + let boxed_body: BoxBody> = + if body.is_empty() { + // Use Empty for requests with no body (e.g., GET requests) + Empty::::new() + .map_err(|e| Box::new(e) as Box) + .boxed() + } else { + // Use Full for requests with a body + Full::new(body) + .map_err(|e| Box::new(e) as Box) + .boxed() + }; + + let hyper_req = hyper::Request::from_parts(parts, boxed_body); + let client = self.client.clone(); + + Box::pin(async move { + // Make the request + let resp = client + .request(hyper_req) + .await + .map_err(TransportError::new)?; + + let (parts, body) = resp.into_parts(); + + // Convert hyper's Incoming body to ByteStream + let byte_stream: ByteStream = Box::pin(body_to_stream(body)); + + Ok(Response::from_parts(parts, byte_stream)) + }) + } +} + +/// Convert hyper's Incoming body to a Stream of Bytes +fn body_to_stream( + body: Incoming, +) -> impl futures::Stream> + Send { + futures::stream::unfold(body, |mut body| async move { + match body.frame().await { + Some(Ok(frame)) => { + if let Ok(data) = frame.into_data() { + // Successfully got data frame + Some((Ok(data), body)) + } else { + // Skip non-data frames (trailers, etc.) + Some(( + Err(TransportError::new(std::io::Error::other("non-data frame"))), + body, + )) + } + } + Some(Err(e)) => { + // Error reading frame + Some((Err(TransportError::new(e)), body)) + } + None => { + // End of stream + None + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hyper_transport_new() { + let transport = HyperTransport::new(); + // If we can create it without panic, the test passes + // This verifies the default HTTP connector is set up correctly + drop(transport); + } + + #[test] + fn test_hyper_transport_default() { + let transport = HyperTransport::default(); + // Verify Default trait implementation + drop(transport); + } + + #[cfg(feature = "hyper-rustls")] + #[test] + fn test_hyper_transport_new_https() { + let transport = HyperTransport::new_https(); + // If we can create it without panic, the test passes + // This verifies the HTTPS connector with rustls is set up correctly + drop(transport); + } + + #[test] + fn test_new_with_connector() { + use hyper_util::client::legacy::connect::HttpConnector; + + let connector = HttpConnector::new(); + let transport = HyperTransport::new_with_connector(connector); + // Verify we can build with a custom connector + drop(transport); + } +}