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}