Skip to main content

launchdarkly_sdk_transport/
transport_hyper.rs

1//! Hyper v1 transport implementation.
2//!
3//! This crate provides a production-ready [`HyperTransport`] implementation that
4//! integrates hyper v1 with any LaunchDarkly [`HttpTransport`] clients.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use launchdarkly_sdk_transport::HyperTransport;
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! let transport = HyperTransport::new()?;
13//! # Ok(())
14//! # }
15//! ```
16//!
17//! # Features
18//!
19//! This module is only available when the appropriate feature flags are enabled in your `Cargo.toml`.
20//!
21//! ## Available Features
22//!
23//! ### `hyper`
24//!
25//! Enables the hyper v1 transport implementation with HTTP-only support.
26//!
27//! **Use this when:**
28//! - You only need plain HTTP connections
29//! - You're handling TLS termination elsewhere (e.g., behind a load balancer)
30//! - You want to minimize dependencies
31//!
32//! **Cargo.toml:**
33//! ```toml
34//! [dependencies]
35//! launchdarkly-sdk-transport = { version = "0.0.1", features = ["hyper"] }
36//! ```
37//!
38//! ### `native-tls`
39//!
40//! Enables HTTPS support using the native TLS library. This feature automatically includes the
41//! `hyper` feature and adds TLS capabilities. Certificate validation relies on the operating
42//! system's native certificate store.
43//!
44//! **Cargo.toml:**
45//! ```toml
46//! [dependencies]
47//! launchdarkly-sdk-transport = { version = "0.0.1", features = ["native-tls"] }
48//! ```
49//!
50//! ### `hyper-rustls-native-roots`
51//!
52//! Use the operating system's native certificate store for TLS certificate validation, relying on
53//! the hyper-rustls crate.
54//!
55//! **Cargo.toml:**
56//! ```toml
57//! [dependencies]
58//! launchdarkly-sdk-transport = { version = "0.0.1", features = ["hyper-rustls-native-roots"] }
59//! ```
60//!
61//! ### `hyper-rustls-webpki-roots`
62//!
63//! Use Mozilla's curated WebPKI certificate bundle, compiled into the binary.
64//!
65//! **Cargo.toml:**
66//! ```toml
67//! [dependencies]
68//! launchdarkly-sdk-transport = { version = "0.0.1", features = ["hyper-rustls-webpki-roots"] }
69//! ```
70//!
71//! # Timeout Configuration
72//!
73//! ```no_run
74//! use launchdarkly_sdk_transport::HyperTransport;
75//! use std::time::Duration;
76//!
77//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
78//! let transport = HyperTransport::builder()
79//!     .connect_timeout(Duration::from_secs(10))
80//!     .read_timeout(Duration::from_secs(30))
81//!     .build_http()?;
82//!
83//! # Ok(())
84//! # }
85//! ```
86//!
87//! # TLS/HTTPS Configuration
88//!
89//! The HTTPS transport requires one root certificate provider to validate server
90//! certificates. The behavior of `build_https()` depends on which features are enabled:
91//!
92//! - **`hyper-rustls-native-roots`**: Uses the OS certificate store.
93//!
94//! - **`hyper-rustls-webpki-roots`**: Uses Mozilla's curated certificate bundle compiled
95//!   into the binary.
96//!
97//! - **`native-tls`**: Uses the platform's native TLS implementation.
98//!
99//! ## TLS Customization
100//!
101//! For custom TLS settings (custom CA certificates, client certificates, cipher suites, etc.),
102//! you can create a custom connector and pass it to [`HyperTransportBuilder::build_with_connector`].
103//!
104//! ```no_run
105//! # #[cfg(feature = "hyper-rustls-webpki-roots")]
106//! # {
107//! use launchdarkly_sdk_transport::HyperTransport;
108//! use hyper_rustls::HttpsConnectorBuilder;
109//!
110//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
111//! // Create a custom HTTPS connector with specific settings
112//! let https_connector = HttpsConnectorBuilder::new()
113//!     .with_webpki_roots()  // Use WebPKI roots instead of native
114//!     .https_only()         // Reject plain HTTP connections
115//!     .enable_http1()
116//!     .enable_http2()
117//!     .build();
118//!
119//! let transport = HyperTransport::builder()
120//!     .build_with_connector(https_connector)?;
121//! # Ok(())
122//! # }
123//! # }
124//! ```
125//!
126//! # Proxy Configuration
127//!
128//! By default, the transport automatically detects proxy settings from environment variables
129//! (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`). You can customize this behavior:
130//!
131//! ```no_run
132//! use launchdarkly_sdk_transport::HyperTransport;
133//!
134//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
135//! // Disable proxy completely
136//! let transport = HyperTransport::builder()
137//!     .disable_proxy()
138//!     .build_http()?;
139//!
140//! // Use a custom proxy URL
141//! let transport = HyperTransport::builder()
142//!     .proxy_url("http://proxy.example.com:8080".to_string())
143//!     .build_http()?;
144//!
145//! // Explicitly enable auto-detection (default behavior)
146//! let transport = HyperTransport::builder()
147//!     .auto_proxy()
148//!     .build_http()?;
149//! # Ok(())
150//! # }
151//! ```
152//!
153//! ## Proxy Environment Variables
154//!
155//! When using automatic proxy detection (the default), the transport checks these environment
156//! variables in order of precedence:
157//!
158//! - `http_proxy` / `HTTP_PROXY`: Proxy URL for HTTP requests
159//! - `https_proxy` / `HTTPS_PROXY`: Proxy URL for HTTPS requests
160//! - `no_proxy` / `NO_PROXY`: Comma-separated list of hosts to bypass the proxy
161//!
162//! **Note**: Lowercase variants take precedence over uppercase.
163//!
164//! ### Proxy Routing Behavior
165//!
166//! - **Both proxies set**: HTTP requests use `HTTP_PROXY`, HTTPS requests use `HTTPS_PROXY`
167//! - **Only HTTP_PROXY set**: All requests (HTTP and HTTPS) route through `HTTP_PROXY`
168//! - **Only HTTPS_PROXY set**: Only HTTPS requests use the proxy, HTTP requests connect directly
169//! - **Neither set**: All requests connect directly (no proxy)
170//!
171//! ### NO_PROXY Format
172//!
173//! The `NO_PROXY` variable accepts a comma-separated list of patterns:
174//!
175//! - Domain names: `example.com` (matches example.com and all subdomains)
176//! - IP addresses: `192.168.1.1`
177//! - CIDR blocks: `10.0.0.0/8`
178//! - Wildcard: `*` (disables proxy for all hosts)
179//!
180//! ### Proxy Authentication
181//!
182//! Proxy URLs can include authentication credentials:
183//!
184//! ```no_run
185//! use launchdarkly_sdk_transport::HyperTransport;
186//!
187//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
188//! let transport = HyperTransport::builder()
189//!     .proxy_url("http://username:password@proxy.example.com:8080".to_string())
190//!     .build_http()?;
191//! # Ok(())
192//! # }
193//! ```
194//!
195//! Or via environment variables:
196//!
197//! ```bash
198//! export HTTP_PROXY="http://username:password@proxy.example.com:8080"
199//! ```
200
201use crate::{ByteStream, HttpTransport, TransportError};
202use bytes::Bytes;
203use http::Uri;
204use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
205use hyper::body::Incoming;
206use hyper_http_proxy::{Intercept, Proxy, ProxyConnector};
207use hyper_timeout::TimeoutConnector;
208use hyper_util::client::legacy::Client as HyperClient;
209use hyper_util::rt::TokioExecutor;
210use no_proxy::NoProxy;
211use std::future::Future;
212use std::pin::Pin;
213use std::time::Duration;
214
215/// A transport implementation using hyper v1.x
216///
217/// This struct wraps a hyper client and implements the [`HttpTransport`] trait.
218///
219/// # Timeout Support
220///
221/// All three timeout types are fully supported via `hyper-timeout`:
222/// - `connect_timeout` - Timeout for establishing the TCP connection
223/// - `read_timeout` - Timeout for reading data from the connection
224/// - `write_timeout` - Timeout for writing data to the connection
225///
226/// Timeouts are configured using the builder pattern. See [`HyperTransportBuilder`] for details.
227///
228/// # Example
229///
230/// ```no_run
231/// use launchdarkly_sdk_transport::HyperTransport;
232///
233/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
234/// // Create transport with default HTTP connector
235/// let _transport = HyperTransport::new()?;
236///
237/// # Ok(())
238/// # }
239/// ```
240#[derive(Clone)]
241pub struct HyperTransport<
242    C = ProxyConnector<TimeoutConnector<hyper_util::client::legacy::connect::HttpConnector>>,
243> {
244    client: HyperClient<C, BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>>,
245}
246
247impl HyperTransport {
248    /// Create a new HyperTransport with default HTTP connector and no timeouts
249    ///
250    /// This creates a basic HTTP-only client that supports both HTTP/1 and HTTP/2.
251    /// For HTTPS support or timeout configuration, use [`HyperTransport::builder()`].
252    pub fn new() -> Result<Self, std::io::Error> {
253        HyperTransport::builder().build_http()
254    }
255
256    /// Create a new HyperTransport with HTTPS support using rustls
257    ///
258    /// This creates an HTTPS client that supports both HTTP/1 and HTTP/2 protocols using
259    /// rustls for TLS. The transport can handle both HTTP and HTTPS connections.
260    ///
261    /// This method is only available when the `hyper-rustls-native-roots` or
262    /// `hyper-rustls-webpki-roots` feature is enabled.
263    ///
264    /// For timeout configuration or custom TLS settings, use [`HyperTransport::builder()`] instead.
265    #[cfg(any(
266        feature = "hyper-rustls-native-roots",
267        feature = "hyper-rustls-webpki-roots"
268    ))]
269    #[cfg_attr(
270        docsrs,
271        doc(cfg(any(
272            feature = "hyper-rustls-native-roots",
273            feature = "hyper-rustls-webpki-roots"
274        )))
275    )]
276    pub fn new_https() -> Result<
277        HyperTransport<
278            ProxyConnector<
279                TimeoutConnector<
280                    hyper_rustls::HttpsConnector<
281                        hyper_util::client::legacy::connect::HttpConnector,
282                    >,
283                >,
284            >,
285        >,
286        std::io::Error,
287    > {
288        HyperTransport::builder().build_https()
289    }
290
291    /// Create a new HyperTransport with HTTPS support using hyper-tls
292    ///
293    /// This creates an HTTPS client that supports both HTTP/1 and HTTP/2 protocols using the
294    /// native TLS implementation. The transport can handle both HTTP and HTTPS connections.
295    ///
296    /// This method is only available when the `native-tls` feature is enabled.
297    ///
298    /// For timeout configuration or custom TLS settings, use [`HyperTransport::builder()`] instead.
299    #[cfg(feature = "native-tls")]
300    #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
301    pub fn new_https() -> Result<
302        HyperTransport<
303            ProxyConnector<
304                TimeoutConnector<
305                    hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
306                >,
307            >,
308        >,
309        std::io::Error,
310    > {
311        HyperTransport::builder().build_https()
312    }
313
314    /// Create a new builder for configuring HyperTransport
315    ///
316    /// The builder allows you to configure timeouts and choose between HTTP and HTTPS connectors.
317    ///
318    /// # Example
319    ///
320    /// ```no_run
321    /// use launchdarkly_sdk_transport::HyperTransport;
322    /// use std::time::Duration;
323    ///
324    /// let transport = HyperTransport::builder()
325    ///     .connect_timeout(Duration::from_secs(10))
326    ///     .read_timeout(Duration::from_secs(30))
327    ///     .build_http();
328    /// ```
329    pub fn builder() -> HyperTransportBuilder {
330        HyperTransportBuilder::default()
331    }
332}
333
334impl<C> HttpTransport for HyperTransport<C>
335where
336    C: hyper_util::client::legacy::connect::Connect + Clone + Send + Sync + 'static,
337{
338    fn request(
339        &self,
340        request: http::Request<Option<Bytes>>,
341    ) -> Pin<
342        Box<
343            dyn Future<Output = Result<http::Response<ByteStream>, TransportError>>
344                + Send
345                + Sync
346                + 'static,
347        >,
348    > {
349        // Convert http::Request<Option<Bytes>> to hyper::Request<BoxBody>
350        let (parts, body_opt) = request.into_parts();
351
352        // Convert Option<Bytes> to BoxBody
353        let body: BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>> = match body_opt {
354            Some(bytes) => {
355                // Use Full for non-empty bodies
356                Full::new(bytes)
357                    .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
358                    .boxed()
359            }
360            None => {
361                // Use Empty for no body
362                Empty::<Bytes>::new()
363                    .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
364                    .boxed()
365            }
366        };
367
368        let hyper_req = hyper::Request::from_parts(parts, body);
369
370        let client = self.client.clone();
371
372        Box::pin(async move {
373            // Make the request - timeouts are handled by TimeoutConnector
374            let resp = client
375                .request(hyper_req)
376                .await
377                .map_err(TransportError::new)?;
378
379            let (parts, body) = resp.into_parts();
380
381            // Convert hyper's Incoming body to ByteStream
382            let byte_stream: ByteStream = Box::pin(body_to_stream(body));
383
384            Ok(http::Response::from_parts(parts, byte_stream))
385        })
386    }
387}
388
389/// Builder for configuring a [`HyperTransport`].
390///
391/// This builder allows you to configure timeouts and choose between HTTP and HTTPS connectors.
392///
393/// # Example
394///
395/// ```no_run
396/// use launchdarkly_sdk_transport::HyperTransport;
397/// use std::time::Duration;
398///
399/// let transport = HyperTransport::builder()
400///     .connect_timeout(Duration::from_secs(10))
401///     .read_timeout(Duration::from_secs(30))
402///     .build_http();
403/// ```
404#[derive(Default)]
405pub struct HyperTransportBuilder {
406    connect_timeout: Option<Duration>,
407    read_timeout: Option<Duration>,
408    write_timeout: Option<Duration>,
409    proxy_config: Option<ProxyConfig>,
410}
411
412impl HyperTransportBuilder {
413    /// Disable proxy support completely.
414    ///
415    /// When this is set, the transport will connect directly to all hosts,
416    /// ignoring any proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, etc.).
417    ///
418    /// Use this when you need to ensure no proxy is used, even if proxy environment
419    /// variables are set in the environment.
420    ///
421    /// # Example
422    ///
423    /// ```no_run
424    /// use launchdarkly_sdk_transport::HyperTransport;
425    ///
426    /// let transport = HyperTransport::builder()
427    ///     .disable_proxy()
428    ///     .build_http();
429    /// ```
430    pub fn disable_proxy(mut self) -> Self {
431        self.proxy_config = Some(ProxyConfig::Disabled);
432        self
433    }
434
435    /// Configure the transport to automatically detect proxy settings from environment variables.
436    ///
437    /// This is the default behavior if no proxy configuration method is called.
438    ///
439    /// # Environment Variables
440    ///
441    /// The transport checks these variables (lowercase variants take precedence):
442    /// - `http_proxy` / `HTTP_PROXY`: Proxy for HTTP requests
443    /// - `https_proxy` / `HTTPS_PROXY`: Proxy for HTTPS requests
444    /// - `no_proxy` / `NO_PROXY`: Comma-separated bypass list
445    ///
446    /// # Routing Logic
447    ///
448    /// - If both HTTP_PROXY and HTTPS_PROXY are set, requests are routed based on their scheme
449    /// - If only HTTP_PROXY is set, **all requests** (HTTP and HTTPS) use that proxy
450    /// - If only HTTPS_PROXY is set, only HTTPS requests use the proxy
451    /// - NO_PROXY hosts bypass the proxy regardless of scheme
452    ///
453    /// # Example
454    ///
455    /// ```no_run
456    /// use launchdarkly_sdk_transport::HyperTransport;
457    ///
458    /// // Explicitly enable auto-detection
459    /// let transport = HyperTransport::builder()
460    ///     .auto_proxy()
461    ///     .build_http();
462    /// ```
463    ///
464    /// With environment variables:
465    /// ```bash
466    /// export HTTP_PROXY=http://proxy.corp.example.com:8080
467    /// export NO_PROXY=localhost,127.0.0.1,.internal.example.com
468    /// ```
469    pub fn auto_proxy(mut self) -> Self {
470        self.proxy_config = Some(ProxyConfig::Auto);
471        self
472    }
473
474    /// Configure the transport to use a custom proxy URL for all requests.
475    ///
476    /// When this is set, **all requests** will route through the specified proxy,
477    /// regardless of environment variables or request scheme. This overrides any
478    /// `HTTP_PROXY` or `HTTPS_PROXY` environment variables.
479    ///
480    /// # URL Format
481    ///
482    /// The proxy URL must include the scheme (`http://` or `https://`) and can optionally
483    /// include authentication credentials:
484    ///
485    /// - Without auth: `http://proxy.example.com:8080`
486    /// - With auth: `http://username:password@proxy.example.com:8080`
487    ///
488    /// # Example
489    ///
490    /// ```no_run
491    /// use launchdarkly_sdk_transport::HyperTransport;
492    ///
493    /// // Basic proxy
494    /// let transport = HyperTransport::builder()
495    ///     .proxy_url("http://proxy.example.com:8080".to_string())
496    ///     .build_http();
497    ///
498    /// // Proxy with authentication
499    /// let transport = HyperTransport::builder()
500    ///     .proxy_url("http://user:pass@proxy.example.com:8080".to_string())
501    ///     .build_http();
502    /// ```
503    ///
504    /// # Note
505    ///
506    /// Unlike `auto_proxy()`, this method does **not** respect the `NO_PROXY` environment
507    /// variable. All requests will use the specified proxy.
508    pub fn proxy_url(mut self, proxy_url: String) -> Self {
509        self.proxy_config = Some(ProxyConfig::Custom(proxy_url));
510        self
511    }
512
513    /// Set a connect timeout for establishing connections
514    ///
515    /// This timeout applies when establishing the TCP connection to the server.
516    /// There is no connect timeout by default.
517    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
518        self.connect_timeout = Some(timeout);
519        self
520    }
521
522    /// Set a read timeout for reading from connections
523    ///
524    /// This timeout applies when reading data from the connection.
525    /// There is no read timeout by default.
526    pub fn read_timeout(mut self, timeout: Duration) -> Self {
527        self.read_timeout = Some(timeout);
528        self
529    }
530
531    /// Set a write timeout for writing to connections
532    ///
533    /// This timeout applies when writing data to the connection.
534    /// There is no write timeout by default.
535    pub fn write_timeout(mut self, timeout: Duration) -> Self {
536        self.write_timeout = Some(timeout);
537        self
538    }
539
540    /// Build with an HTTP connector
541    ///
542    /// Creates a transport that supports HTTP/1 and HTTP/2 over plain HTTP.
543    pub fn build_http(self) -> Result<HyperTransport, std::io::Error> {
544        let connector = hyper_util::client::legacy::connect::HttpConnector::new();
545        self.build_with_connector(connector)
546    }
547
548    /// Build with an HTTPS connector using rustls
549    ///
550    /// Creates a transport that supports HTTP/1 and HTTP/2 over HTTPS using rustls for TLS.
551    /// The transport can handle both plain HTTP and HTTPS connections.
552    ///
553    /// This method is only available when the `hyper-rustls-native-roots` or
554    /// `hyper-rustls-webpki-roots` feature is enabled.
555    ///
556    /// For custom TLS configuration (custom CAs, client certificates, etc.), use
557    /// [`build_with_connector`](Self::build_with_connector) with a custom rustls connector.
558    ///
559    /// # Example
560    ///
561    /// ```no_run
562    /// # #[cfg(any(feature = "hyper-rustls-native-roots", feature = "hyper-rustls-webpki-roots"))]
563    /// # {
564    /// use launchdarkly_sdk_transport::HyperTransport;
565    /// use std::time::Duration;
566    ///
567    /// let transport = HyperTransport::builder()
568    ///     .connect_timeout(Duration::from_secs(10))
569    ///     .build_https()
570    ///     .expect("failed to build HTTPS transport");
571    /// # }
572    /// ```
573    #[cfg(any(
574        feature = "hyper-rustls-native-roots",
575        feature = "hyper-rustls-webpki-roots"
576    ))]
577    #[cfg_attr(
578        docsrs,
579        doc(cfg(any(
580            feature = "hyper-rustls-native-roots",
581            feature = "hyper-rustls-webpki-roots"
582        )))
583    )]
584    pub fn build_https(
585        self,
586    ) -> Result<
587        HyperTransport<
588            ProxyConnector<
589                TimeoutConnector<
590                    hyper_rustls::HttpsConnector<
591                        hyper_util::client::legacy::connect::HttpConnector,
592                    >,
593                >,
594            >,
595        >,
596        std::io::Error,
597    > {
598        // When only native roots are enabled, use them and propagate errors.
599        #[cfg(feature = "hyper-rustls-native-roots")]
600        let builder = hyper_rustls::HttpsConnectorBuilder::new()
601            .with_native_roots()
602            .map_err(std::io::Error::other)?;
603
604        // When only webpki roots are enabled, use them (infallible).
605        #[cfg(feature = "hyper-rustls-webpki-roots")]
606        let builder = hyper_rustls::HttpsConnectorBuilder::new().with_webpki_roots();
607
608        let connector = builder
609            .https_or_http()
610            .enable_http1()
611            .enable_http2()
612            .build();
613
614        self.build_with_connector(connector)
615    }
616
617    /// Build with an HTTPS connector using native TLS
618    ///
619    /// Creates a transport that supports HTTP/1 and HTTP/2 over HTTPS using native TLS.
620    /// The transport can handle both plain HTTP and HTTPS connections.
621    ///
622    /// This method is only available when the `native-tls` feature is enabled.
623    ///
624    /// For custom TLS configuration (custom CAs, client certificates, etc.), use
625    /// [`build_with_connector`](Self::build_with_connector) with a custom native TLS connector.
626    ///
627    /// # Example
628    ///
629    /// ```no_run
630    /// # #[cfg(feature = "native-tls")]
631    /// # {
632    /// use launchdarkly_sdk_transport::HyperTransport;
633    /// use std::time::Duration;
634    ///
635    /// let transport = HyperTransport::builder()
636    ///     .connect_timeout(Duration::from_secs(10))
637    ///     .build_https()
638    ///     .expect("failed to build HTTPS transport");
639    /// # }
640    /// ```
641    #[cfg(feature = "native-tls")]
642    #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
643    pub fn build_https(
644        self,
645    ) -> Result<
646        HyperTransport<
647            ProxyConnector<
648                TimeoutConnector<
649                    hyper_tls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
650                >,
651            >,
652        >,
653        std::io::Error,
654    > {
655        let connector = hyper_tls::HttpsConnector::new();
656
657        self.build_with_connector(connector)
658    }
659
660    /// Build with a custom connector
661    ///
662    /// This allows you to provide your own connector implementation, which is useful for:
663    /// - Custom TLS configuration
664    /// - Proxy support
665    /// - Connection pooling customization
666    /// - Custom DNS resolution
667    ///
668    /// The connector will be automatically wrapped with a `TimeoutConnector` that applies
669    /// the configured timeout settings.
670    ///
671    /// # Example
672    ///
673    /// ```no_run
674    /// use launchdarkly_sdk_transport::HyperTransport;
675    /// use hyper_util::client::legacy::connect::HttpConnector;
676    /// use std::time::Duration;
677    ///
678    /// let mut connector = HttpConnector::new();
679    /// // Configure the connector as needed
680    /// connector.set_nodelay(true);
681    ///
682    /// let transport = HyperTransport::builder()
683    ///     .read_timeout(Duration::from_secs(30))
684    ///     .build_with_connector(connector);
685    /// ```
686    pub fn build_with_connector<C>(
687        self,
688        connector: C,
689    ) -> Result<HyperTransport<ProxyConnector<TimeoutConnector<C>>>, std::io::Error>
690    where
691        C: tower::Service<http::Uri> + Clone + Send + Sync + 'static,
692        C::Response: hyper_util::client::legacy::connect::Connection
693            + hyper::rt::Read
694            + hyper::rt::Write
695            + Send
696            + Unpin,
697        C::Future: Send + 'static,
698        C::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
699    {
700        let mut timeout_connector = TimeoutConnector::new(connector);
701        timeout_connector.set_connect_timeout(self.connect_timeout);
702        timeout_connector.set_read_timeout(self.read_timeout);
703        timeout_connector.set_write_timeout(self.write_timeout);
704
705        // ProxyConnector::new() sets up TLS for connecting to HTTPS proxies.
706        // Use it when we have a TLS-backed proxy (native-tls or rustls with native roots).
707        // Otherwise use unsecured() which still supports HTTP proxies; TLS to target servers
708        // is handled by the inner connector.
709        #[cfg(any(
710            feature = "hyper-rustls-native-roots",
711            feature = "hyper-rustls-webpki-roots",
712            feature = "native-tls"
713        ))]
714        let mut proxy_connector = ProxyConnector::new(timeout_connector)?;
715        #[cfg(not(any(
716            feature = "hyper-rustls-native-roots",
717            feature = "hyper-rustls-webpki-roots",
718            feature = "native-tls"
719        )))]
720        let mut proxy_connector = ProxyConnector::unsecured(timeout_connector);
721
722        match self.proxy_config {
723            Some(ProxyConfig::Auto) | None => {
724                let http_proxy = std::env::var("http_proxy")
725                    .or_else(|_| std::env::var("HTTP_PROXY"))
726                    .unwrap_or_default();
727                let https_proxy = std::env::var("https_proxy")
728                    .or_else(|_| std::env::var("HTTPS_PROXY"))
729                    .unwrap_or_default();
730                let no_proxy = std::env::var("no_proxy")
731                    .or_else(|_| std::env::var("NO_PROXY"))
732                    .unwrap_or_default();
733                let no_proxy = NoProxy::from(no_proxy);
734
735                if !https_proxy.is_empty() {
736                    let https_uri = https_proxy
737                        .parse::<Uri>()
738                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
739                    let no_proxy = no_proxy.clone();
740                    let custom: Intercept = Intercept::Custom(
741                        (move |schema: Option<&str>,
742                               host: Option<&str>,
743                               _port: Option<u16>|
744                              -> bool {
745                            // This function should only enforce validation when it matches
746                            // the schema of the proxy.
747                            if !matches!(schema, Some("https")) {
748                                return false;
749                            }
750
751                            match host {
752                                None => false,
753                                Some(h) => !no_proxy.matches(h),
754                            }
755                        })
756                        .into(),
757                    );
758                    let proxy = Proxy::new(custom, https_uri);
759                    proxy_connector.add_proxy(proxy);
760                }
761
762                if !http_proxy.is_empty() {
763                    let http_uri = http_proxy
764                        .parse::<Uri>()
765                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
766                    // If http_proxy is set but https_proxy is not, then all hosts are eligible to
767                    // route through the http_proxy.
768                    let proxy_all = https_proxy.is_empty();
769                    let custom: Intercept = Intercept::Custom(
770                        (move |schema: Option<&str>,
771                               host: Option<&str>,
772                               _port: Option<u16>|
773                              -> bool {
774                            if !proxy_all && matches!(schema, Some("https")) {
775                                return false;
776                            }
777
778                            match host {
779                                None => false,
780                                Some(h) => !no_proxy.matches(h),
781                            }
782                        })
783                        .into(),
784                    );
785                    let proxy = Proxy::new(custom, http_uri);
786                    proxy_connector.add_proxy(proxy);
787                }
788            }
789            Some(ProxyConfig::Disabled) => {
790                // No proxies will be added, so the client will connect directly
791            }
792            Some(ProxyConfig::Custom(url)) => {
793                let uri = url
794                    .parse::<Uri>()
795                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
796                proxy_connector.add_proxy(Proxy::new(Intercept::All, uri));
797            }
798        };
799
800        let client = HyperClient::builder(TokioExecutor::new()).build(proxy_connector);
801
802        Ok(HyperTransport { client })
803    }
804}
805
806/// Proxy configuration for HyperTransport.
807///
808/// This determines whether and how the transport uses an HTTP/HTTPS proxy.
809#[derive(Default, Debug, Clone)]
810enum ProxyConfig {
811    /// Automatically detect proxy from environment variables (default).
812    ///
813    /// Checks `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables.
814    /// Lowercase variants take precedence over uppercase.
815    #[default]
816    Auto,
817
818    /// Explicitly disable proxy support.
819    ///
820    /// No proxy will be used even if environment variables are set.
821    Disabled,
822
823    /// Use a custom proxy URL.
824    ///
825    /// Format: `http://[user:pass@]host:port`
826    Custom(String),
827}
828
829/// Convert hyper's Incoming body to a Stream of Bytes
830fn body_to_stream(
831    body: Incoming,
832) -> impl futures::Stream<Item = Result<Bytes, TransportError>> + Send + Sync {
833    futures::stream::unfold(body, |mut body| async move {
834        loop {
835            match body.frame().await {
836                Some(Ok(frame)) => {
837                    if let Ok(data) = frame.into_data() {
838                        return Some((Ok(data), body));
839                    }
840                    // Non-data frame (e.g., trailers) - skip and continue to next frame
841                }
842                Some(Err(e)) => return Some((Err(TransportError::new(e)), body)),
843                None => return None,
844            }
845        }
846    })
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852    use futures::StreamExt;
853    use http::{Method, Request};
854    use std::time::Duration;
855
856    #[test]
857    fn test_hyper_transport_new() {
858        let transport = HyperTransport::new();
859        // If we can create it without panic, the test passes
860        // This verifies the default HTTP connector is set up correctly
861        drop(transport);
862    }
863
864    #[cfg(any(
865        feature = "hyper-rustls-native-roots",
866        feature = "hyper-rustls-webpki-roots"
867    ))]
868    #[test]
869    fn test_hyper_transport_new_https() {
870        let transport = HyperTransport::new_https().expect("transport failed to build");
871        // If we can create it without panic, the test passes
872        // This verifies the HTTPS connector with rustls is set up correctly
873        drop(transport);
874    }
875
876    #[cfg(feature = "native-tls")]
877    #[test]
878    fn test_hyper_transport_new_https() {
879        let transport = HyperTransport::new_https().expect("transport failed to build");
880        // If we can create it without panic, the test passes
881        // This verifies the HTTPS connector with native TLS is set up correctly
882        drop(transport);
883    }
884
885    #[test]
886    fn test_builder_default() {
887        let builder = HyperTransport::builder();
888        let transport = builder.build_http().expect("failed to build transport");
889        // Verify we can build with default settings
890        drop(transport);
891    }
892
893    #[test]
894    fn test_builder_with_all_timeouts() {
895        let transport = HyperTransport::builder()
896            .connect_timeout(Duration::from_secs(5))
897            .read_timeout(Duration::from_secs(30))
898            .write_timeout(Duration::from_secs(10))
899            .build_http()
900            .expect("failed to build transport");
901        // Verify we can build with all timeouts configured
902        drop(transport);
903    }
904
905    #[cfg(any(
906        feature = "hyper-rustls-native-roots",
907        feature = "hyper-rustls-webpki-roots"
908    ))]
909    #[test]
910    fn test_builder_https() {
911        let transport = HyperTransport::builder()
912            .connect_timeout(Duration::from_secs(5))
913            .read_timeout(Duration::from_secs(30))
914            .write_timeout(Duration::from_secs(10))
915            .build_https()
916            .expect("failed to build HTTPS transport");
917        // Verify we can build HTTPS transport with timeouts
918        drop(transport);
919    }
920
921    #[cfg(feature = "native-tls")]
922    #[test]
923    fn test_builder_https() {
924        let transport = HyperTransport::builder()
925            .connect_timeout(Duration::from_secs(5))
926            .read_timeout(Duration::from_secs(30))
927            .write_timeout(Duration::from_secs(10))
928            .build_https()
929            .expect("failed to build HTTPS transport");
930        drop(transport);
931    }
932
933    #[test]
934    fn test_builder_with_custom_connector() {
935        let mut connector = hyper_util::client::legacy::connect::HttpConnector::new();
936        connector.set_nodelay(true);
937
938        let transport = HyperTransport::builder()
939            .read_timeout(Duration::from_secs(30))
940            .build_with_connector(connector);
941        // Verify we can build with a custom connector
942        drop(transport);
943    }
944
945    #[test]
946    fn test_transport_is_clone() {
947        let transport = HyperTransport::new().expect("failed to build transport");
948        let _cloned = transport.clone();
949    }
950
951    #[tokio::test]
952    async fn test_http_transport_trait_implemented() {
953        let transport = HyperTransport::new().expect("failed to build transport");
954
955        // Create a basic request
956        let request = Request::builder()
957            .method(Method::GET)
958            .uri("http://httpbin.org/get")
959            .body(None)
960            .expect("failed to build request");
961
962        // Verify the trait is implemented by attempting to call it
963        // We're not actually making the request here, just verifying the types work
964        let _future = transport.request(request);
965        // The future exists and has the correct type signature
966    }
967
968    #[tokio::test]
969    async fn test_request_with_empty_body() {
970        // This test verifies that we can construct a request with no body
971        let transport = HyperTransport::new().expect("failed to build transport");
972
973        let request = Request::builder()
974            .method(Method::GET)
975            .uri("http://httpbin.org/get")
976            .body(None)
977            .expect("failed to build request");
978
979        // Just verify we can create the future - not actually making network call
980        let _future = transport.request(request);
981    }
982
983    #[tokio::test]
984    async fn test_request_with_string_body() {
985        // This test verifies that we can construct a request with a string body
986        let transport = HyperTransport::new().expect("failed to build transport");
987
988        let request = Request::builder()
989            .method(Method::POST)
990            .uri("http://httpbin.org/post")
991            .body(Some(Bytes::from("test body")))
992            .expect("failed to build request");
993
994        // Just verify we can create the future - not actually making network call
995        let _future = transport.request(request);
996    }
997
998    // Integration tests that actually make HTTP requests
999    // These require a running HTTP server, so they're marked as ignored by default
1000
1001    #[tokio::test]
1002    #[ignore] // Run with: cargo test -- --ignored
1003    async fn test_integration_http_request() {
1004        let transport = HyperTransport::builder()
1005            .connect_timeout(Duration::from_secs(10))
1006            .read_timeout(Duration::from_secs(30))
1007            .build_http()
1008            .expect("failed to build transport");
1009
1010        let request = Request::builder()
1011            .method(Method::GET)
1012            .uri("http://httpbin.org/get")
1013            .body(None)
1014            .expect("failed to build request");
1015
1016        let response = transport.request(request).await;
1017        assert!(response.is_ok(), "Request should succeed");
1018
1019        let response = response.unwrap();
1020        assert!(response.status().is_success(), "Status should be success");
1021
1022        // Verify we can read from the stream
1023        let mut stream = response.into_body();
1024        let mut received_data = false;
1025        while let Some(result) = stream.next().await {
1026            assert!(result.is_ok(), "Stream chunk should not error");
1027            received_data = true;
1028        }
1029        assert!(received_data, "Should have received some data");
1030    }
1031
1032    #[cfg(any(
1033        feature = "hyper-rustls-native-roots",
1034        feature = "hyper-rustls-webpki-roots"
1035    ))]
1036    #[tokio::test]
1037    #[ignore] // Run with: cargo test -- --ignored
1038    async fn test_integration_https_request() {
1039        let transport = HyperTransport::builder()
1040            .connect_timeout(Duration::from_secs(10))
1041            .read_timeout(Duration::from_secs(30))
1042            .build_https()
1043            .expect("failed to build HTTPS transport");
1044
1045        // Using example.com as it's highly reliable and well-maintained
1046        let request = Request::builder()
1047            .method(Method::GET)
1048            .uri("https://example.com/")
1049            .body(None)
1050            .expect("failed to build request");
1051
1052        let response = transport.request(request).await;
1053        assert!(
1054            response.is_ok(),
1055            "HTTPS request should succeed: {:?}",
1056            response.as_ref().err()
1057        );
1058
1059        let response = response.unwrap();
1060        assert!(
1061            response.status().is_success(),
1062            "Status should be success: {}",
1063            response.status()
1064        );
1065    }
1066
1067    #[cfg(feature = "native-tls")]
1068    #[tokio::test]
1069    #[ignore] // Run with: cargo test -- --ignored
1070    async fn test_integration_https_request() {
1071        let transport = HyperTransport::builder()
1072            .connect_timeout(Duration::from_secs(10))
1073            .read_timeout(Duration::from_secs(30))
1074            .build_https()
1075            .expect("failed to build HTTPS transport");
1076
1077        // Using example.com as it's highly reliable and well-maintained
1078        let request = Request::builder()
1079            .method(Method::GET)
1080            .uri("https://example.com/")
1081            .body(None)
1082            .expect("failed to build request");
1083
1084        let response: Result<http::Response<crate::ByteStream>, crate::TransportError> =
1085            transport.request(request).await;
1086        assert!(
1087            response.is_ok(),
1088            "HTTPS request should succeed: {:?}",
1089            response.as_ref().err()
1090        );
1091
1092        let response = response.unwrap();
1093        assert!(
1094            response.status().is_success(),
1095            "Status should be success: {}",
1096            response.status()
1097        );
1098    }
1099
1100    #[tokio::test]
1101    #[ignore] // Run with: cargo test -- --ignored
1102    async fn test_integration_request_with_body() {
1103        let transport = HyperTransport::new().expect("failed to build transport");
1104
1105        let body_content = r#"{"test": "data"}"#;
1106        let request = Request::builder()
1107            .method(Method::POST)
1108            .uri("http://httpbin.org/post")
1109            .header("Content-Type", "application/json")
1110            .body(Some(Bytes::from(body_content)))
1111            .expect("failed to build request");
1112
1113        let response = transport.request(request).await;
1114        assert!(response.is_ok(), "POST request should succeed");
1115
1116        let response = response.unwrap();
1117        assert!(response.status().is_success(), "Status should be success");
1118    }
1119
1120    #[tokio::test]
1121    #[ignore] // Run with: cargo test -- --ignored
1122    async fn test_integration_streaming_response() {
1123        let transport = HyperTransport::new().expect("failed to build transport");
1124
1125        let request = Request::builder()
1126            .method(Method::GET)
1127            .uri("http://httpbin.org/stream/10")
1128            .body(None)
1129            .expect("failed to build request");
1130
1131        let response = transport.request(request).await;
1132        assert!(response.is_ok(), "Streaming request should succeed");
1133
1134        let response = response.unwrap();
1135        assert!(response.status().is_success(), "Status should be success");
1136
1137        // Verify we receive multiple chunks
1138        let mut stream = response.into_body();
1139        let mut chunk_count = 0;
1140        while let Some(result) = stream.next().await {
1141            assert!(result.is_ok(), "Stream chunk should not error");
1142            let chunk = result.unwrap();
1143            assert!(!chunk.is_empty(), "Chunk should not be empty");
1144            chunk_count += 1;
1145        }
1146        assert!(chunk_count > 0, "Should have received multiple chunks");
1147    }
1148
1149    #[tokio::test]
1150    #[ignore] // Run with: cargo test -- --ignored
1151    async fn test_integration_connect_timeout() {
1152        // Use a non-routable IP to test connect timeout
1153        let transport = HyperTransport::builder()
1154            .connect_timeout(Duration::from_millis(100))
1155            .build_http()
1156            .expect("failed to build transport");
1157
1158        let request = Request::builder()
1159            .method(Method::GET)
1160            .uri("http://10.255.255.1/")
1161            .body(None)
1162            .expect("failed to build request");
1163
1164        let response = transport.request(request).await;
1165        assert!(response.is_err(), "Request should timeout");
1166    }
1167}