Skip to main content

rama/http/client/
mod.rs

1//! rama http client support
2//!
3//! Contains re-exports from `rama-http-backend::client`
4//! and adds `EasyHttpWebClient`, an opiniated http web client which
5//! supports most common use cases and provides sensible defaults.
6use std::{fmt, io};
7
8use crate::{
9    Layer, Service,
10    error::BoxError,
11    extensions::ExtensionsRef,
12    graceful::WeakScope,
13    http::{Body as ResponseBody, Request, Response, StreamingBody},
14    net::client::EstablishedClientConnection,
15    service::BoxService,
16    telemetry::tracing,
17};
18
19#[doc(inline)]
20pub use ::rama_http::service::client::blocking::{
21    Body as BlockingBody, Client as BlockingHttpClient, Response as BlockingResponse,
22};
23#[doc(inline)]
24pub use ::rama_http_backend::client::*;
25use rama_core::{
26    error::{ErrorContext, ErrorExt as _, extra::OpaqueError},
27    extensions::Egress,
28    layer::MapErr,
29};
30use rama_http::{
31    layer::{
32        forward_proxy::{HttpForwardProxyLayer, HttpForwardProxyService},
33        map_response_body::MapResponseBody,
34    },
35    proxy::PlaintextHttpProxyMode,
36};
37
38#[cfg(any(feature = "boring", feature = "rustls"))]
39use crate::tls::client::TlsClientConfig;
40
41#[cfg(feature = "boring")]
42use {crate::tls::boring::client::TlsClientSessionCache, std::sync::Arc};
43
44#[cfg(all(feature = "boring", feature = "quic"))]
45use crate::quic::tls::BoringTlsProvider;
46
47#[cfg(all(
48    feature = "quic",
49    any(
50        feature = "boring",
51        feature = "masque",
52        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
53    )
54))]
55use crate::quic::TransportConfig;
56
57#[cfg(all(feature = "masque", feature = "quic"))]
58use crate::{
59    proxy::masque::connect_udp::{OUTER_IDLE_TIMEOUT, OUTER_INITIAL_MTU},
60    quic::{IdleTimeout, proto::VarInt},
61};
62
63pub mod builder;
64#[doc(inline)]
65pub use builder::EasyHttpConnectorBuilder;
66#[cfg(all(feature = "masque", feature = "quic"))]
67#[doc(inline)]
68pub use builder::MasqueQuicEndpoints;
69
70#[cfg(feature = "socks5")]
71mod proxy_connector;
72#[cfg(feature = "socks5")]
73#[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
74#[doc(inline)]
75pub use proxy_connector::{MaybeProxiedConnection, ProxyConnector, ProxyConnectorLayer};
76
77/// An opiniated http client that can be used to serve HTTP requests.
78///
79/// Use [`EasyHttpWebClient::connector_builder()`] to easily create a client with
80/// a common Http connector setup (tcp + proxy + tls + http) or bring your
81/// own http connector.
82///
83/// [`Default`] uses Rama's default multiplexing connection pool. Build the
84/// connector explicitly with
85/// [`EasyHttpConnectorBuilder::without_connection_pool`] when connection reuse
86/// is unwanted.
87///
88/// You can fork this http client in case you have use cases not possible with this service example.
89/// E.g. perhaps you wish to have middleware in into outbound requests, after they
90/// passed through your "connector" setup. All this and more is possible by defining your own
91/// http client. Rama is here to empower you, the building blocks are there, go crazy
92/// with your own service fork and use the full power of Rust at your fingertips ;)
93pub struct EasyHttpWebClient<BodyIn, ConnResponse, L> {
94    connector: BoxService<Request<BodyIn>, ConnResponse, OpaqueError>,
95    forward_proxy_layer: HttpForwardProxyLayer,
96    plaintext_http_proxy_mode: Option<PlaintextHttpProxyMode>,
97    jit_layers: L,
98}
99
100impl<BodyIn, ConnResponse, L> fmt::Debug for EasyHttpWebClient<BodyIn, ConnResponse, L> {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.debug_struct("EasyHttpWebClient").finish()
103    }
104}
105
106impl<BodyIn, ConnResponse, L: Clone> Clone for EasyHttpWebClient<BodyIn, ConnResponse, L> {
107    fn clone(&self) -> Self {
108        Self {
109            connector: self.connector.clone(),
110            forward_proxy_layer: self.forward_proxy_layer.clone(),
111            plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
112            jit_layers: self.jit_layers.clone(),
113        }
114    }
115}
116
117impl EasyHttpWebClient<(), (), ()> {
118    /// Create a [`EasyHttpConnectorBuilder`] to easily create a [`EasyHttpWebClient`] with a custom connector
119    #[must_use]
120    pub fn connector_builder() -> EasyHttpConnectorBuilder {
121        EasyHttpConnectorBuilder::new()
122    }
123
124    /// Create a cloneable blocking HTTP(S) client with its own dedicated
125    /// runtime thread and Rama's default web connector stack.
126    ///
127    /// ```no_run
128    /// use rama::http::client::EasyHttpWebClient;
129    ///
130    /// # fn main() -> Result<(), rama::error::BoxError> {
131    /// let client = EasyHttpWebClient::try_blocking()?;
132    /// let client_for_worker = client.clone();
133    ///
134    /// let text = client_for_worker
135    ///     .get("https://example.com/")
136    ///     .send()?
137    ///     .try_into_string()?;
138    /// # _ = text;
139    /// # Ok(())
140    /// # }
141    /// ```
142    pub fn try_blocking() -> io::Result<BlockingHttpWebClient> {
143        BlockingHttpClient::try_new(EasyHttpWebClient::default())
144    }
145}
146
147/// Rama's default asynchronous HTTP(S) client, including its default
148/// multiplexing connection pool.
149///
150/// With BoringSSL or Rustls plus `ring`/`aws-lc`, HTTP/3 is available through
151/// alternative-service discovery or an explicit HTTP/3 request. Its shared UDP
152/// endpoint is bound lazily, so constructing the client requires no runtime.
153///
154/// Like a browser it resumes TLS sessions (with BoringSSL, also over TCP), which
155/// lets servers link the connections of one client. It acts for a single user:
156/// proxies relay traffic instead of sharing one client between their users.
157pub type DefaultHttpWebClient<Body = crate::http::Body> = EasyHttpWebClient<
158    Body,
159    EstablishedClientConnection<
160        rama_http::layer::alt_svc::AltSvc<
161            BindBodyToConn<
162                crate::net::client::pool::MultiplexedConnection<
163                    HttpClientService<Body>,
164                    HttpConnId,
165                >,
166            >,
167        >,
168        Request<Body>,
169    >,
170    (),
171>;
172
173/// A blocking HTTP(S) client using Rama's default pooled web connector stack.
174pub type BlockingHttpWebClient = BlockingHttpClient<DefaultHttpWebClient>;
175
176impl<Body> Default for DefaultHttpWebClient<Body>
177where
178    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
179{
180    #[inline(always)]
181    fn default() -> Self {
182        Self::default_with_scope(WeakScope::detached())
183    }
184}
185
186impl<Body> DefaultHttpWebClient<Body>
187where
188    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
189{
190    core::cfg_select! {
191        all(feature = "boring", feature = "quic") => {
192            pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
193                let scope = scope.into();
194                #[expect(
195                    clippy::expect_used,
196                    reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
197                )]
198                Self::boringssl_default(scope, TlsClientConfig::default_http(), None)
199                    .expect("default BoringSSL HTTP/3 configuration is valid")
200            }
201
202            #[cfg(feature = "masque")]
203            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
204            /// A client to MASQUE proxies, for the tunnels another client's HTTP/3 runs in:
205            /// the default client, with room in its own HTTP/3 connections for the tunneled
206            /// QUIC packets ([`masque_quic_transport`]).
207            pub fn default_masque_proxy_client_with_scope(scope: impl Into<WeakScope>) -> Self {
208                let scope = scope.into();
209                #[expect(
210                    clippy::expect_used,
211                    reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
212                )]
213                Self::try_default_masque_proxy_client_using_boringssl(
214                    scope,
215                    TlsClientConfig::default_http(),
216                )
217                .expect("default BoringSSL HTTP/3 configuration is valid")
218            }
219
220            #[cfg(feature = "masque")]
221            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
222            /// [`Self::default_masque_proxy_client_with_scope`], with `proxy_tls` towards the
223            /// proxies, such as [`TlsClientConfig::default_http`] with a server verification of
224            /// its own.
225            pub fn try_default_masque_proxy_client_using_boringssl(
226                scope: impl Into<WeakScope>,
227                proxy_tls: TlsClientConfig,
228            ) -> Result<Self, BoxError> {
229                let scope = scope.into();
230                Self::boringssl_default(scope, proxy_tls, Some(masque_quic_transport()))
231            }
232
233            #[cfg(feature = "masque")]
234            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
235            /// The default client, with its HTTP/3 also through MASQUE proxies: in
236            /// CONNECT-UDP tunnels ([RFC 9298]) opened by a
237            /// [`Self::default_masque_proxy_client_with_scope`] client.
238            ///
239            /// Each HTTP/3 connection has a UDP socket or tunnel of its own.
240            ///
241            /// [RFC 9298]: https://datatracker.ietf.org/doc/html/rfc9298
242            pub fn default_masque_with_scope(scope: impl Into<WeakScope>) -> Self {
243                let scope = scope.into();
244                #[expect(
245                    clippy::expect_used,
246                    reason = "fixed default H3 limits and BoringSSL TLS configuration are valid"
247                )]
248                Self::try_default_masque_using_boringssl(
249                    scope,
250                    TlsClientConfig::default_http(),
251                    TlsClientConfig::default_http(),
252                )
253                .expect("default BoringSSL HTTP/3 configuration is valid")
254            }
255
256            #[cfg(feature = "masque")]
257            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
258            /// [`Self::default_masque_with_scope`], with `tls` towards origins and
259            /// `proxy_tls` towards proxies, MASQUE and HTTPS proxies alike.
260            pub fn try_default_masque_using_boringssl(
261                scope: impl Into<WeakScope>,
262                tls: TlsClientConfig,
263                proxy_tls: TlsClientConfig,
264            ) -> Result<Self, BoxError> {
265                let scope = scope.into();
266                let h3 = Http3Connector::builder(scope.clone())
267                    .with_tls_provider(Arc::new(BoringTlsProvider))
268                    .with_tls_config(tls.clone());
269                let proxy_client =
270                    DefaultHttpWebClient::<crate::http::Body>::try_default_masque_proxy_client_using_boringssl(
271                        scope.clone(),
272                        proxy_tls.clone(),
273                    )?;
274                Ok(EasyHttpConnectorBuilder::new()
275                    .with_default_transport_connector()
276                    .with_default_dns_connector()
277                    .with_tls_proxy_support_using_boringssl_config(proxy_tls)
278                    .with_proxy_support()
279                    .with_tls_support_using_boringssl(tls)
280                    .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
281                    .with_default_http_connector(scope)
282                    .with_http3_masque_support(h3, proxy_client)?
283                    .with_default_connection_pool()
284                    .build_client())
285            }
286
287            fn boringssl_default(
288                scope: WeakScope,
289                tls: TlsClientConfig,
290                transport: Option<TransportConfig>,
291            ) -> Result<Self, BoxError> {
292                let h3 = Http3Connector::builder(scope.clone())
293                    .with_tls_provider(Arc::new(BoringTlsProvider))
294                    .with_tls_config(tls.clone())
295                    .maybe_with_transport(transport)
296                    .build_lazy()?;
297                Ok(EasyHttpConnectorBuilder::new()
298                    .with_default_transport_connector()
299                    .with_default_dns_connector()
300                    .with_tls_proxy_support_using_boringssl()
301                    .with_proxy_support()
302                    .with_tls_support_using_boringssl(tls)
303                    .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
304                    .with_default_http_connector(scope)
305                    .with_http3_support(h3)
306                    .with_default_connection_pool()
307                    .build_client())
308            }
309        }
310        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"), feature = "quic") => {
311            pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
312                let scope = scope.into();
313                #[expect(
314                    clippy::expect_used,
315                    reason = "fixed default H3 limits and Rustls TLS configuration are valid"
316                )]
317                Self::rustls_default(scope, TlsClientConfig::default_http(), None)
318                    .expect("default Rustls HTTP/3 configuration is valid")
319            }
320
321            #[cfg(feature = "masque")]
322            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
323            /// A client to MASQUE proxies, for the tunnels another client's HTTP/3 runs in:
324            /// the default client, with room in its own HTTP/3 connections for the tunneled
325            /// QUIC packets ([`masque_quic_transport`]).
326            pub fn default_masque_proxy_client_with_scope(scope: impl Into<WeakScope>) -> Self {
327                let scope = scope.into();
328                #[expect(
329                    clippy::expect_used,
330                    reason = "fixed default H3 limits and Rustls TLS configuration are valid"
331                )]
332                Self::try_default_masque_proxy_client_using_rustls(
333                    scope,
334                    TlsClientConfig::default_http(),
335                )
336                .expect("default Rustls HTTP/3 configuration is valid")
337            }
338
339            #[cfg(feature = "masque")]
340            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
341            /// [`Self::default_masque_proxy_client_with_scope`], with `proxy_tls` towards the
342            /// proxies, such as [`TlsClientConfig::default_http`] with a server verification of
343            /// its own.
344            pub fn try_default_masque_proxy_client_using_rustls(
345                scope: impl Into<WeakScope>,
346                proxy_tls: TlsClientConfig,
347            ) -> Result<Self, BoxError> {
348                let scope = scope.into();
349                Self::rustls_default(scope, proxy_tls, Some(masque_quic_transport()))
350            }
351
352            #[cfg(feature = "masque")]
353            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
354            /// The default client, with its HTTP/3 also through MASQUE proxies: in
355            /// CONNECT-UDP tunnels ([RFC 9298]) opened by a
356            /// [`Self::default_masque_proxy_client_with_scope`] client.
357            ///
358            /// Each HTTP/3 connection has a UDP socket or tunnel of its own.
359            ///
360            /// [RFC 9298]: https://datatracker.ietf.org/doc/html/rfc9298
361            pub fn default_masque_with_scope(scope: impl Into<WeakScope>) -> Self {
362                let scope = scope.into();
363                #[expect(
364                    clippy::expect_used,
365                    reason = "fixed default H3 limits and Rustls TLS configuration are valid"
366                )]
367                Self::try_default_masque_using_rustls(
368                    scope,
369                    TlsClientConfig::default_http(),
370                    TlsClientConfig::default_http(),
371                )
372                .expect("default Rustls HTTP/3 configuration is valid")
373            }
374
375            #[cfg(feature = "masque")]
376            #[cfg_attr(docsrs, doc(cfg(feature = "masque")))]
377            /// [`Self::default_masque_with_scope`], with `tls` towards origins and
378            /// `proxy_tls` towards proxies, MASQUE and HTTPS proxies alike.
379            pub fn try_default_masque_using_rustls(
380                scope: impl Into<WeakScope>,
381                tls: TlsClientConfig,
382                proxy_tls: TlsClientConfig,
383            ) -> Result<Self, BoxError> {
384                let scope = scope.into();
385                let h3 = Http3Connector::builder(scope.clone())
386                    .with_tls_config(tls.clone());
387                let proxy_client =
388                    DefaultHttpWebClient::<crate::http::Body>::try_default_masque_proxy_client_using_rustls(
389                        scope.clone(),
390                        proxy_tls.clone(),
391                    )?;
392                Ok(EasyHttpConnectorBuilder::new()
393                    .with_default_transport_connector()
394                    .with_default_dns_connector()
395                    .with_tls_proxy_support_using_rustls_config(proxy_tls)
396                    .with_proxy_support()
397                    .with_tls_support_using_rustls(tls)
398                    .with_default_http_connector(scope)
399                    .with_http3_masque_support(h3, proxy_client)?
400                    .with_default_connection_pool()
401                    .build_client())
402            }
403
404            fn rustls_default(
405                scope: WeakScope,
406                tls: TlsClientConfig,
407                transport: Option<TransportConfig>,
408            ) -> Result<Self, BoxError> {
409                let h3 = Http3Connector::builder(scope.clone())
410                    .with_tls_config(tls.clone())
411                    .maybe_with_transport(transport)
412                    .build_lazy()?;
413                Ok(EasyHttpConnectorBuilder::new()
414                    .with_default_transport_connector()
415                    .with_default_dns_connector()
416                    .with_tls_proxy_support_using_rustls()
417                    .with_proxy_support()
418                    .with_tls_support_using_rustls(tls)
419                    .with_default_http_connector(scope)
420                    .with_http3_support(h3)
421                    .with_default_connection_pool()
422                    .build_client())
423            }
424        }
425        feature = "boring" => {
426            pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
427                let scope = scope.into();
428                EasyHttpConnectorBuilder::new()
429                    .with_default_transport_connector()
430                    .with_default_dns_connector()
431                    .with_tls_proxy_support_using_boringssl()
432                    .with_proxy_support()
433                    .with_tls_support_using_boringssl(TlsClientConfig::default_http())
434                    .with_tls_session_store(Arc::new(TlsClientSessionCache::default()))
435                    .with_default_http_connector(scope)
436                    .with_default_connection_pool()
437                    .build_client()
438            }
439        }
440        feature = "rustls" => {
441            pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
442                let scope = scope.into();
443                EasyHttpConnectorBuilder::new()
444                    .with_default_transport_connector()
445                    .with_default_dns_connector()
446                    .with_tls_proxy_support_using_rustls()
447                    .with_proxy_support()
448                    .with_tls_support_using_rustls(TlsClientConfig::default_http())
449                    .with_default_http_connector(scope)
450                    .with_default_connection_pool()
451                    .build_client()
452            }
453        }
454        _ => {
455            pub fn default_with_scope(scope: impl Into<WeakScope>) -> Self {
456                let scope = scope.into();
457                EasyHttpConnectorBuilder::new()
458                    .with_default_transport_connector()
459                    .with_default_dns_connector()
460                    .without_tls_proxy_support()
461                    .with_proxy_support()
462                    .without_tls_support()
463                    .with_default_http_connector(scope)
464                    .with_default_connection_pool()
465                    .build_client()
466            }
467        }
468    }
469}
470
471#[cfg(all(feature = "masque", feature = "quic"))]
472#[cfg_attr(docsrs, doc(cfg(all(feature = "masque", feature = "quic"))))]
473/// The QUIC transport for HTTP/3 connections that carry MASQUE tunnels, for both the proxy
474/// and its clients: room for a tunneled QUIC packet in each datagram ([`OUTER_INITIAL_MTU`]),
475/// and an idle timeout that leaves quiet tunnels to their own policy
476/// ([`OUTER_IDLE_TIMEOUT`]).
477#[must_use]
478pub fn masque_quic_transport() -> TransportConfig {
479    let idle = u32::try_from(OUTER_IDLE_TIMEOUT.as_millis()).unwrap_or(u32::MAX);
480    TransportConfig::default()
481        .with_initial_mtu(OUTER_INITIAL_MTU)
482        .with_max_idle_timeout(IdleTimeout::from(VarInt::from_u32(idle)))
483}
484
485impl<BodyIn, ConnResponse> EasyHttpWebClient<BodyIn, ConnResponse, ()>
486where
487    BodyIn: Send + 'static,
488{
489    /// Create a new [`EasyHttpWebClient`] using the provided connector.
490    ///
491    /// Custom proxy connectors must honor [`PlaintextHttpProxyMode`] and publish
492    /// [`EstablishedProxyRoute`](crate::net::client::EstablishedProxyRoute).
493    /// Connection wrappers must preserve that metadata through [`ExtensionsRef`].
494    #[must_use]
495    pub fn new<S>(connector: S) -> Self
496    where
497        S: Service<Request<BodyIn>, Output = ConnResponse, Error: Into<BoxError>>,
498    {
499        Self {
500            connector: MapErr::into_opaque_error(connector).boxed(),
501            forward_proxy_layer: HttpForwardProxyLayer::new(),
502            plaintext_http_proxy_mode: None,
503            jit_layers: (),
504        }
505    }
506}
507
508impl<BodyIn, ConnResponse, L> EasyHttpWebClient<BodyIn, ConnResponse, L> {
509    /// Convert this asynchronous web client into a cloneable blocking client
510    /// with its own dedicated runtime thread.
511    pub fn try_into_blocking(self) -> io::Result<BlockingHttpClient<Self>> {
512        BlockingHttpClient::try_new(self)
513    }
514
515    /// Convert this asynchronous web client into a blocking client using a
516    /// caller-supplied runtime.
517    #[must_use]
518    pub fn into_blocking_with_runtime(
519        self,
520        runtime: &crate::rt::blocking::Runtime,
521    ) -> BlockingHttpClient<Self> {
522        BlockingHttpClient::with_runtime(self, runtime)
523    }
524
525    /// Set the connector that this [`EasyHttpWebClient`] will use.
526    ///
527    /// Custom proxy connectors follow the metadata contract described in [`Self::new`].
528    #[must_use]
529    pub fn with_connector<S, BodyInNew, ConnResponseNew>(
530        self,
531        connector: S,
532    ) -> EasyHttpWebClient<BodyInNew, ConnResponseNew, L>
533    where
534        S: Service<Request<BodyInNew>, Output = ConnResponseNew, Error: Into<BoxError>>,
535        BodyInNew: Send + 'static,
536    {
537        EasyHttpWebClient {
538            connector: MapErr::into_opaque_error(connector).boxed(),
539            forward_proxy_layer: self.forward_proxy_layer,
540            plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
541            jit_layers: self.jit_layers,
542        }
543    }
544
545    /// [`Layer`] which will be applied just in time (JIT) before the request is sent, but after
546    /// the connection has been established. Rama's built-in forward-proxy
547    /// policy is the innermost JIT service so it can act on the actual
548    /// connection after caller middleware has processed the request, and can
549    /// isolate a proxy challenge before caller middleware sees the response.
550    ///
551    /// Simplified flow of how the [`EasyHttpWebClient`] works:
552    /// 1. External: let response = client.serve(request)
553    /// 2. Internal: let http_connection = self.connector.serve(request)
554    /// 3. Internal: wrap the connection in Rama's forward-proxy policy
555    /// 4. Internal: let response = jit_layers.layer(http_connection).serve(request)
556    pub fn with_jit_layer<T>(self, jit_layers: T) -> EasyHttpWebClient<BodyIn, ConnResponse, T> {
557        EasyHttpWebClient {
558            connector: self.connector,
559            forward_proxy_layer: self.forward_proxy_layer,
560            plaintext_http_proxy_mode: self.plaintext_http_proxy_mode,
561            jit_layers,
562        }
563    }
564
565    crate::utils::macros::generate_set_and_with! {
566        /// Enable or disable automatic Basic or Bearer credentials on requests
567        /// sent directly to an HTTP forward proxy.
568        ///
569        /// This is enabled by default and acts only when the established connection
570        /// is positively identified as an HTTP forward-proxy connection. It never
571        /// adds credentials to direct, SOCKS, or HTTP CONNECT-tunneled requests.
572        ///
573        /// Disabling this only disables insertion. Caller-provided
574        /// `Proxy-Authorization` headers are preserved on established HTTP
575        /// forward routes and always stripped on every other route, including
576        /// connections without established route metadata.
577        pub fn forward_proxy_auth(mut self, enabled: bool) -> Self {
578            self.forward_proxy_layer.set_proxy_auth(enabled);
579            self
580        }
581    }
582
583    /// Disable automatic Basic or Bearer credentials on HTTP forward-proxy
584    /// requests. The credential-stripping policy described by
585    /// [`Self::with_forward_proxy_auth`] still applies.
586    #[must_use]
587    pub fn without_forward_proxy_auth(self) -> Self {
588        self.with_forward_proxy_auth(false)
589    }
590
591    crate::utils::macros::generate_set_and_with! {
592        /// Enable or disable carrying plaintext HTTP through an HTTP(S) proxy with
593        /// CONNECT instead of using ordinary forward-proxy semantics
594        /// (absolute-form on HTTP/1).
595        ///
596        /// If this method is not called, a request-level
597        /// [`PlaintextHttpProxyMode`]
598        /// is honored and
599        /// otherwise forwarding is the connector default. Calling this method
600        /// explicitly selects Tunnel (`true`) or Forward (`false`) for the client.
601        /// Tunneling does not encrypt the origin traffic: a plaintext `http://`
602        /// request remains plaintext inside the proxy tunnel.
603        pub fn tunnel_plaintext_http(mut self, enabled: bool) -> Self {
604            self.plaintext_http_proxy_mode = Some(if enabled {
605                PlaintextHttpProxyMode::Tunnel
606            } else {
607                PlaintextHttpProxyMode::Forward
608            });
609            self
610        }
611    }
612
613    crate::utils::macros::generate_set_and_with! {
614        /// Enable or disable isolation of `407 Proxy Authentication Required`
615        /// responses received from an established HTTP forward proxy.
616        ///
617        /// Ordinary clients expose such responses by default. Intermediaries should
618        /// enable this option so an upstream proxy's challenge, headers, and body
619        /// cannot be forwarded to a different downstream proxy client.
620        pub fn isolate_forward_proxy_auth_error(mut self, enabled: bool) -> Self {
621            self.forward_proxy_layer.set_isolate_auth_error(enabled);
622            self
623        }
624    }
625}
626
627impl<Body, ConnectionBody, Connection, IncomingBody, L> Service<Request<Body>>
628    for EasyHttpWebClient<Body, EstablishedClientConnection<Connection, Request<ConnectionBody>>, L>
629where
630    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
631    Connection: Service<Request<ConnectionBody>, Output = Response<IncomingBody>, Error = BoxError>
632        + ExtensionsRef,
633    IncomingBody: Send + Sync + 'static,
634    ResponseBody: From<IncomingBody>,
635    // Body type this connection will be able to send, this is not necessarily the same one that
636    // was used in the request that created this connection
637    ConnectionBody:
638        StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
639    L: Layer<
640            HttpForwardProxyService<MapResponseBody<Connection, fn(IncomingBody) -> ResponseBody>>,
641            Service: Service<Request<ConnectionBody>, Output = Response, Error = BoxError>,
642        > + Send
643        + Sync
644        + 'static,
645{
646    type Output = Response;
647    type Error = OpaqueError;
648
649    async fn serve(&self, req: Request<Body>) -> Result<Self::Output, Self::Error> {
650        let uri = req.uri().clone();
651
652        if let Some(mode) = self.plaintext_http_proxy_mode {
653            req.extensions().insert(mode);
654        }
655
656        let EstablishedClientConnection {
657            input: req,
658            conn: http_connection,
659        } = self.connector.serve(req).await.into_opaque_error()?;
660
661        // Publish connection metadata for JIT middleware. The forward-proxy
662        // layer refreshes it after those layers run; the backend independently
663        // refreshes it for callers that use the backend without this client.
664        req.extensions()
665            .insert(Egress(http_connection.extensions().clone()));
666
667        let http_connection = MapResponseBody::into_boxed_streaming_body(http_connection);
668        let http_connection = self.forward_proxy_layer.layer(http_connection);
669        let http_connection = self.jit_layers.layer(http_connection);
670
671        // NOTE: stack might change request version based on connector data,
672        tracing::trace!(url.full = %uri, "send http req to connector stack");
673
674        let result = http_connection.serve(req).await;
675
676        match result {
677            Ok(resp) => {
678                tracing::trace!(url.full = %uri, "response received from connector stack");
679                Ok(resp)
680            }
681            Err(err) => Err(err
682                .context("http request failure")
683                .context_field("uri", uri)
684                .into_opaque_error()),
685        }
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use std::{
692        convert::Infallible,
693        num::NonZeroUsize,
694        sync::{
695            Arc,
696            atomic::{AtomicUsize, Ordering},
697        },
698        time::Duration,
699    };
700
701    use rama_core::extensions::Extensions;
702    use rama_core::{error::BoxErrorExt as _, service::service_fn};
703    use rama_http::{Body, BodyExtractExt, Version};
704    use rama_http_backend::server::HttpServer;
705    use rama_net::{
706        address::ProxyAddress,
707        client::{
708            ConnectRequest, ConnectionError, ConnectionErrorDomain, ConnectionErrorKind,
709            ConnectorService, ConnectorTarget, EstablishedProxyRoute, ProxyRoute,
710            ProxyRouteFailureCache, ProxyRouteFailureCacheConfig, ProxyRouteFailureCacheScope,
711            ProxyRoutes,
712        },
713        test_utils::client::{MockConnectorService, MockSocket},
714    };
715    use rama_utils::octets::kib;
716    use serde::{Deserialize, Serialize};
717    use tokio::time::sleep;
718
719    #[cfg(feature = "boring")]
720    use {
721        crate::{
722            net::address::SocketAddress,
723            tcp::{TcpStream, server::TcpListener},
724            tls::{
725                boring::{TlsStream, server::TlsAcceptorLayer},
726                client::{NegotiatedTlsParameters, ServerVerifyMode, TlsServerVerify},
727                server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
728            },
729        },
730        rama_core::{extensions::ExtensionsRef as _, layer::MapInputLayer},
731        rama_http::header::CONNECTION,
732    };
733
734    use super::*;
735
736    #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
737    struct Output {
738        conn: usize,
739        resp: usize,
740    }
741
742    #[tokio::test]
743    async fn default_client_request_future_stays_within_stack_budget() {
744        let client = DefaultHttpWebClient::<Body>::default();
745        let request = Request::builder()
746            .uri("https://example.com/")
747            .body(Body::empty())
748            .unwrap();
749        let request = client.serve(request);
750        let inner_size = std::mem::size_of_val(&request);
751        let request = tokio::time::timeout(Duration::from_secs(1), request);
752        let size = std::mem::size_of_val(&request);
753        eprintln!("default client request: {inner_size} bytes; with timeout: {size} bytes");
754        assert!(
755            size <= kib(16),
756            "default client request future is {size} bytes"
757        );
758    }
759
760    #[test]
761    fn default_client_can_be_constructed_and_cloned_without_a_runtime() {
762        let client = DefaultHttpWebClient::<Body>::default();
763        drop(client.clone());
764        drop(client);
765    }
766
767    #[derive(Debug, Clone, Default, ExtensionsRef)]
768    struct EmptyHttpConnection {
769        extensions: Extensions,
770    }
771
772    impl Service<Request> for EmptyHttpConnection {
773        type Output = Response;
774        type Error = BoxError;
775
776        async fn serve(&self, _request: Request) -> Result<Self::Output, Self::Error> {
777            Ok(Response::new(Body::empty()))
778        }
779    }
780
781    #[derive(Debug, Clone)]
782    struct InspectConnectionRouteLayer(EstablishedProxyRoute);
783
784    impl<S: ExtensionsRef> Layer<S> for InspectConnectionRouteLayer {
785        type Service = S;
786
787        fn layer(&self, inner: S) -> Self::Service {
788            assert_eq!(
789                inner.extensions().get_ref::<EstablishedProxyRoute>(),
790                Some(&self.0),
791            );
792            inner
793        }
794    }
795
796    fn dummy_server<Input: Send + 'static>()
797    -> impl Service<
798        Input,
799        Output = EstablishedClientConnection<MockSocket, Input>,
800        Error = Infallible,
801    > + Clone {
802        let created_connections = Arc::new(AtomicUsize::new(0));
803        MockConnectorService::new(move || {
804            let created_connections = created_connections.clone();
805            let conn = created_connections.fetch_add(1, Ordering::Relaxed);
806
807            // count responses created on this specific connection
808            let created_response = Arc::new(AtomicUsize::new(0));
809
810            HttpServer::auto(WeakScope::detached()).service(service_fn(move |_req: Request| {
811                let created_response = created_response.clone();
812                let resp = created_response.fetch_add(1, Ordering::Relaxed);
813                async move {
814                    sleep(Duration::from_millis(5)).await;
815                    let out = Output { conn, resp };
816                    let resp = Response::new(Body::from(serde_json::to_vec(&out).unwrap()));
817                    Ok::<_, Infallible>(resp)
818                }
819            }))
820        })
821    }
822
823    #[tokio::test]
824    async fn custom_connector_receives_plaintext_http_proxy_mode() {
825        let connector = service_fn(|request: Request| async move {
826            assert_eq!(
827                request.extensions().get_ref::<PlaintextHttpProxyMode>(),
828                Some(&PlaintextHttpProxyMode::Tunnel)
829            );
830
831            let conn = EmptyHttpConnection::default();
832            Ok::<_, Infallible>(EstablishedClientConnection {
833                input: request,
834                conn,
835            })
836        });
837        let client = EasyHttpWebClient::new(connector).with_tunnel_plaintext_http(true);
838        let request = Request::builder()
839            .uri("http://example.com/")
840            .body(Body::empty())
841            .unwrap();
842
843        let response = client.serve(request).await.unwrap();
844        assert_eq!(response.status(), crate::http::StatusCode::OK);
845    }
846
847    #[tokio::test]
848    async fn jit_layer_can_read_established_connection_extensions() {
849        let connector = service_fn(|request: Request| async move {
850            let conn = EmptyHttpConnection::default();
851            conn.extensions().insert(EstablishedProxyRoute::Direct);
852            Ok::<_, Infallible>(EstablishedClientConnection {
853                input: request,
854                conn,
855            })
856        });
857        let client = EasyHttpWebClient::new(connector)
858            .with_jit_layer(InspectConnectionRouteLayer(EstablishedProxyRoute::Direct));
859        let request = Request::builder()
860            .uri("http://example.com/")
861            .extension(ProxyRoute::Direct)
862            .body(Body::empty())
863            .unwrap();
864
865        let response = client.serve(request).await.unwrap();
866        assert_eq!(response.status(), crate::http::StatusCode::OK);
867    }
868
869    #[tokio::test]
870    async fn jit_request_metadata_cannot_change_proxy_credentials_target_or_challenge_isolation() {
871        use rama_core::layer::{MapInputLayer, MapOutputLayer};
872        use rama_http::{HeaderValue, StatusCode, header::PROXY_AUTHORIZATION};
873
874        #[derive(Debug, Clone, ExtensionsRef)]
875        struct InspectProxyConnection {
876            extensions: Extensions,
877        }
878
879        impl Service<Request> for InspectProxyConnection {
880            type Output = Response;
881            type Error = BoxError;
882
883            async fn serve(&self, request: Request) -> Result<Response, BoxError> {
884                let route = self.extensions.get_ref::<EstablishedProxyRoute>();
885                let is_forward = route.is_some_and(EstablishedProxyRoute::is_http_forward);
886                assert_eq!(
887                    request
888                        .extensions()
889                        .egress()
890                        .unwrap()
891                        .0
892                        .get_ref::<EstablishedProxyRoute>(),
893                    route,
894                );
895                assert_eq!(
896                    request.extensions().get_ref::<ProxyRoute>(),
897                    Some(&ProxyRoute::Proxy(
898                        "http://wrong:request-secret@requested.example:8080"
899                            .parse()
900                            .unwrap(),
901                    )),
902                    "forward policy must preserve the caller's requested route",
903                );
904                let mut target = Vec::new();
905                rama_http::proto::h1::head::encode_request_target(
906                    request.method(),
907                    request.uri(),
908                    request.extensions(),
909                    &mut target,
910                )
911                .unwrap();
912                if is_forward {
913                    assert_eq!(
914                        request.headers().get(PROXY_AUTHORIZATION).unwrap(),
915                        "Basic dXBzdHJlYW06c2VjcmV0",
916                    );
917                    assert_eq!(&target[..], b"http://origin.example/resource");
918                } else {
919                    assert!(request.headers().get(PROXY_AUTHORIZATION).is_none());
920                    assert_eq!(&target[..], b"/resource");
921                }
922                Ok(Response::builder()
923                    .status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
924                    .header("proxy-authenticate", "Basic realm=private-upstream")
925                    .body(Body::from("private upstream challenge"))
926                    .unwrap())
927            }
928        }
929
930        let proxy: ProxyAddress = "http://upstream:secret@proxy.example:8080".parse().unwrap();
931        for isolate in [false, true] {
932            for route in [
933                None,
934                Some(EstablishedProxyRoute::Direct),
935                Some(EstablishedProxyRoute::Tunnel(proxy.clone())),
936                Some(EstablishedProxyRoute::Tunnel(
937                    "socks5://proxy.example:1080".parse().unwrap(),
938                )),
939                Some(EstablishedProxyRoute::Forward(proxy.clone())),
940            ] {
941                let is_forward = route
942                    .as_ref()
943                    .is_some_and(EstablishedProxyRoute::is_http_forward);
944                let connector = service_fn(move |request: Request| {
945                    let route = route.clone();
946                    async move {
947                        let extensions = Extensions::new();
948                        if let Some(route) = route {
949                            extensions.insert(route);
950                        }
951                        Ok::<_, Infallible>(EstablishedClientConnection {
952                            input: request,
953                            conn: InspectProxyConnection { extensions },
954                        })
955                    }
956                });
957                let stale_route = if is_forward {
958                    EstablishedProxyRoute::Direct
959                } else {
960                    EstablishedProxyRoute::Forward(proxy.clone())
961                };
962                let observed_responses = Arc::new(AtomicUsize::new(0));
963                let client = EasyHttpWebClient::new(connector)
964                    .with_isolate_forward_proxy_auth_error(isolate)
965                    .with_jit_layer((
966                        MapInputLayer::new(move |mut request: Request| {
967                            request.extensions().insert(stale_route.clone());
968                            let stale_egress = Extensions::new();
969                            stale_egress.insert(stale_route.clone());
970                            request.extensions().insert(Egress(stale_egress));
971                            request.headers_mut().insert(
972                                PROXY_AUTHORIZATION,
973                                HeaderValue::from_static("Basic downstream-secret"),
974                            );
975                            request
976                        }),
977                        MapOutputLayer::new({
978                            let observed_responses = observed_responses.clone();
979                            move |response: Response| {
980                                observed_responses.fetch_add(1, Ordering::Relaxed);
981                                response
982                            }
983                        }),
984                    ));
985                let request = Request::builder()
986                    .uri("http://origin.example/resource")
987                    .body(Body::empty())
988                    .unwrap();
989                request.extensions().insert(ProxyRoute::Proxy(
990                    "http://wrong:request-secret@requested.example:8080"
991                        .parse()
992                        .unwrap(),
993                ));
994                let result = client.serve(request).await;
995                if isolate && is_forward {
996                    assert!(result.is_err());
997                    assert_eq!(observed_responses.load(Ordering::Relaxed), 0);
998                } else {
999                    assert_eq!(
1000                        result.unwrap().status(),
1001                        StatusCode::PROXY_AUTHENTICATION_REQUIRED
1002                    );
1003                    assert_eq!(observed_responses.load(Ordering::Relaxed), 1);
1004                }
1005            }
1006        }
1007    }
1008
1009    #[test]
1010    fn blocking_client_drives_the_composed_http_stack() {
1011        let client = EasyHttpWebClient::connector_builder()
1012            .with_custom_transport_connector(dummy_server())
1013            .without_dns_connector()
1014            .without_tls_proxy_support()
1015            .without_proxy_support()
1016            .without_tls_support()
1017            .with_default_http_connector(WeakScope::detached())
1018            .without_connection_pool()
1019            .build_client()
1020            .try_into_blocking()
1021            .unwrap();
1022
1023        let cloned = client.clone();
1024        drop(client);
1025        let response = cloned.get("http://example.com").send().unwrap();
1026        assert_eq!(
1027            response.try_into_json::<Output>().unwrap(),
1028            Output { conn: 0, resp: 0 }
1029        );
1030    }
1031
1032    #[test]
1033    fn default_blocking_http_client_is_cloneable_and_pooled() {
1034        fn assert_default_client(_: &DefaultHttpWebClient) {}
1035
1036        let client = EasyHttpWebClient::try_blocking().unwrap();
1037        assert_default_client(client.get_ref());
1038        let cloned = client.clone();
1039        drop(client);
1040        let request = cloned.get("https://example.com").build().unwrap();
1041        assert_eq!(request.uri(), &"https://example.com".parse().unwrap());
1042    }
1043
1044    #[cfg(feature = "ws")]
1045    #[test]
1046    fn default_blocking_http_client_builds_websocket_requests() {
1047        use crate::http::ws::handshake::client::BlockingHttpClientWebSocketExt as _;
1048
1049        let client = EasyHttpWebClient::try_blocking().unwrap();
1050        let _from_url = client
1051            .websocket("wss://example.com/chat")
1052            .with_header("authorization", "Bearer secret");
1053
1054        let request = Request::builder()
1055            .uri("wss://example.com/chat")
1056            .body(Body::empty())
1057            .unwrap();
1058        let _from_request = client.websocket_with_request(request);
1059    }
1060
1061    #[tokio::test]
1062    async fn no_pool_tries_proxy_routes_in_order() {
1063        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1064        let transport = service_fn({
1065            let attempts = attempts.clone();
1066            let direct = dummy_server::<ConnectRequest>();
1067            move |input: ConnectRequest| {
1068                let attempts = attempts.clone();
1069                let direct = direct.clone();
1070                async move {
1071                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1072                    attempts.lock().push(route.clone());
1073                    if route.proxy_address().is_some() {
1074                        Err(ConnectionError::transport(
1075                            BoxError::from_static_str("proxy unavailable"),
1076                            ConnectionErrorKind::Unavailable,
1077                        ))
1078                    } else {
1079                        direct.connect(input).await
1080                    }
1081                }
1082            }
1083        });
1084        let client = EasyHttpWebClient::connector_builder()
1085            .with_custom_transport_connector(transport)
1086            .without_dns_connector()
1087            .without_tls_proxy_support()
1088            .with_custom_proxy_connector(())
1089            .without_tls_support()
1090            .with_default_http_connector(WeakScope::detached())
1091            .without_connection_pool()
1092            .build_client();
1093        let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
1094        let request = || {
1095            let request = Request::builder()
1096                .uri("http://example.com")
1097                .body(Body::empty())
1098                .unwrap();
1099            request
1100                .extensions()
1101                .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1102            request
1103        };
1104
1105        for _ in 0..2 {
1106            client
1107                .serve(request())
1108                .await
1109                .context("serve request through direct fallback")
1110                .unwrap();
1111        }
1112
1113        assert_eq!(
1114            attempts.lock().as_slice(),
1115            [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
1116        );
1117    }
1118
1119    #[tokio::test]
1120    async fn no_proxy_tls_support_rejects_https_proxy() {
1121        let client = EasyHttpWebClient::connector_builder()
1122            .with_custom_transport_connector(dummy_server())
1123            .without_dns_connector()
1124            .without_tls_proxy_support()
1125            .with_proxy_support()
1126            .without_tls_support()
1127            .with_default_http_connector(WeakScope::detached())
1128            .without_connection_pool()
1129            .build_client();
1130        let request = Request::builder()
1131            .uri("http://example.com")
1132            .body(Body::empty())
1133            .unwrap();
1134        request.extensions().insert(ProxyRoutes::new([
1135            ProxyRoute::Proxy(
1136                "https://proxy.example:8443"
1137                    .parse::<ProxyAddress>()
1138                    .unwrap(),
1139            ),
1140            ProxyRoute::Direct,
1141        ]));
1142
1143        let error =
1144            ConnectionError::from(client.serve(request).await.unwrap_err().into_box_error());
1145        assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
1146        assert_eq!(error.kind(), ConnectionErrorKind::Protocol);
1147    }
1148
1149    #[tokio::test]
1150    async fn easy_client_pools_plaintext_proxy_versions_separately() {
1151        let proxy: ProxyAddress = "http://proxy.example:8080".parse().unwrap();
1152        let dials = Arc::new(AtomicUsize::new(0));
1153        let transport = service_fn({
1154            let inner = dummy_server::<ConnectRequest>();
1155            let proxy = proxy.clone();
1156            let dials = dials.clone();
1157            move |input: ConnectRequest| {
1158                let inner = inner.clone();
1159                let proxy = proxy.clone();
1160                let dials = dials.clone();
1161                async move {
1162                    assert_eq!(
1163                        input.extensions.get_ref::<ConnectorTarget>(),
1164                        Some(&ConnectorTarget(proxy.address.clone())),
1165                    );
1166                    assert_eq!(
1167                        input
1168                            .extensions
1169                            .get_ref::<ProxyRoute>()
1170                            .and_then(ProxyRoute::proxy_address),
1171                        Some(&proxy),
1172                    );
1173                    dials.fetch_add(1, Ordering::Relaxed);
1174                    inner.connect(input).await
1175                }
1176            }
1177        });
1178        let client = EasyHttpWebClient::connector_builder()
1179            .with_custom_transport_connector(transport)
1180            .without_dns_connector()
1181            .without_tls_proxy_support()
1182            .with_proxy_support()
1183            .without_tls_support()
1184            .with_default_http_connector(WeakScope::detached())
1185            .with_default_connection_pool()
1186            .build_client();
1187
1188        for (version, expected_conn) in [(Version::HTTP_11, 0), (Version::HTTP_2, 1)] {
1189            let request = Request::builder()
1190                .uri("http://example.com")
1191                .version(version)
1192                .body(Body::empty())
1193                .unwrap();
1194            request
1195                .extensions()
1196                .insert(ProxyRoutes::from(proxy.clone()));
1197
1198            let response = client.serve(request).await.unwrap();
1199            assert_eq!(response.version(), version);
1200            assert_eq!(
1201                response.try_into_json::<Output>().await.unwrap(),
1202                Output {
1203                    conn: expected_conn,
1204                    resp: 0,
1205                },
1206            );
1207        }
1208        assert_eq!(dials.load(Ordering::Relaxed), 2);
1209    }
1210
1211    #[cfg(feature = "socks5")]
1212    #[tokio::test]
1213    async fn umbrella_proxy_connector_falls_back_across_supported_plan() {
1214        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1215        let transport = service_fn({
1216            let attempts = attempts.clone();
1217            let direct = dummy_server::<ConnectRequest>();
1218            move |input: ConnectRequest| {
1219                let attempts = attempts.clone();
1220                let direct = direct.clone();
1221                async move {
1222                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap().clone();
1223                    attempts.lock().push(route.clone());
1224                    if route.proxy_address().is_some() {
1225                        Err(ConnectionError::transport(
1226                            BoxError::from_static_str("proxy unavailable"),
1227                            ConnectionErrorKind::Unavailable,
1228                        ))
1229                    } else {
1230                        direct.connect(input).await
1231                    }
1232                }
1233            }
1234        });
1235        let client = EasyHttpWebClient::connector_builder()
1236            .with_custom_transport_connector(transport)
1237            .without_dns_connector()
1238            .without_tls_proxy_support()
1239            .with_proxy_support()
1240            .without_tls_support()
1241            .with_default_http_connector(WeakScope::detached())
1242            .without_connection_pool()
1243            .build_client();
1244        let request = Request::builder()
1245            .uri("http://example.com")
1246            .body(Body::empty())
1247            .unwrap();
1248        let socks = ProxyRoute::Proxy(
1249            "socks5://socks.example:1080"
1250                .parse::<ProxyAddress>()
1251                .unwrap(),
1252        );
1253        let http = ProxyRoute::Proxy("http://http.example:8080".parse::<ProxyAddress>().unwrap());
1254        request.extensions().insert(ProxyRoutes::new([
1255            socks.clone(),
1256            http.clone(),
1257            ProxyRoute::Direct,
1258        ]));
1259
1260        let response = client.serve(request).await.unwrap();
1261        let output = response.try_into_json::<Output>().await.unwrap();
1262        assert_eq!(output, Output { conn: 0, resp: 0 });
1263        assert_eq!(
1264            attempts.lock().as_slice(),
1265            [socks, http, ProxyRoute::Direct]
1266        );
1267    }
1268
1269    #[tokio::test]
1270    async fn default_pool_caches_failed_route_and_reuses_selected_connection() {
1271        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1272        let transport = service_fn({
1273            let attempts = attempts.clone();
1274            let direct = dummy_server::<ConnectRequest>();
1275            move |input: ConnectRequest| {
1276                let attempts = attempts.clone();
1277                let direct = direct.clone();
1278                async move {
1279                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1280                    attempts.lock().push(route.clone());
1281                    if route.proxy_address().is_some() {
1282                        Err(ConnectionError::transport(
1283                            BoxError::from_static_str("proxy unavailable"),
1284                            ConnectionErrorKind::Unavailable,
1285                        ))
1286                    } else {
1287                        direct.connect(input).await
1288                    }
1289                }
1290            }
1291        });
1292        let client = EasyHttpWebClient::connector_builder()
1293            .with_custom_transport_connector(transport)
1294            .without_dns_connector()
1295            .without_tls_proxy_support()
1296            .with_custom_proxy_connector(())
1297            .without_tls_support()
1298            .with_default_http_connector(WeakScope::detached())
1299            .with_default_connection_pool()
1300            .build_client();
1301        let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse::<ProxyAddress>().unwrap());
1302        let request = || {
1303            let request = Request::builder()
1304                .uri("http://example.com")
1305                .body(Body::empty())
1306                .unwrap();
1307            request
1308                .extensions()
1309                .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1310            request
1311        };
1312
1313        for expected_response_index in 0..2 {
1314            let response = client.serve(request()).await.unwrap();
1315            let output = response.try_into_json::<Output>().await.unwrap();
1316            assert_eq!(output.conn, 0);
1317            assert_eq!(output.resp, expected_response_index);
1318        }
1319
1320        assert_eq!(attempts.lock().as_slice(), [proxy, ProxyRoute::Direct]);
1321    }
1322
1323    #[tokio::test]
1324    async fn easy_client_can_disable_proxy_route_failure_cache() {
1325        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1326        let transport = service_fn({
1327            let attempts = attempts.clone();
1328            let direct = dummy_server::<ConnectRequest>();
1329            move |input: ConnectRequest| {
1330                let attempts = attempts.clone();
1331                let direct = direct.clone();
1332                async move {
1333                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1334                    attempts.lock().push(route.clone());
1335                    if route.proxy_address().is_some() {
1336                        Err(ConnectionError::transport(
1337                            BoxError::from_static_str("proxy unavailable"),
1338                            ConnectionErrorKind::Unavailable,
1339                        ))
1340                    } else {
1341                        direct.connect(input).await
1342                    }
1343                }
1344            }
1345        });
1346        let client = EasyHttpWebClient::connector_builder()
1347            .with_custom_transport_connector(transport)
1348            .without_dns_connector()
1349            .without_tls_proxy_support()
1350            .with_custom_proxy_connector(())
1351            .without_tls_support()
1352            .with_default_http_connector(WeakScope::detached())
1353            .without_proxy_route_failure_cache()
1354            .without_connection_pool()
1355            .build_client();
1356        let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1357
1358        for _ in 0..2 {
1359            let request = Request::builder()
1360                .uri("http://example.com")
1361                .body(Body::empty())
1362                .unwrap();
1363            request
1364                .extensions()
1365                .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1366            client.serve(request).await.unwrap();
1367        }
1368
1369        assert_eq!(
1370            attempts.lock().as_slice(),
1371            [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1372        );
1373    }
1374
1375    #[tokio::test]
1376    async fn proxy_free_easy_client_omits_proxy_route_failure_cache() {
1377        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1378        let transport = service_fn({
1379            let attempts = attempts.clone();
1380            let direct = dummy_server::<ConnectRequest>();
1381            move |input: ConnectRequest| {
1382                let attempts = attempts.clone();
1383                let direct = direct.clone();
1384                async move {
1385                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1386                    attempts.lock().push(route.clone());
1387                    if route.proxy_address().is_some() {
1388                        Err(ConnectionError::transport(
1389                            BoxError::from_static_str("proxy unavailable"),
1390                            ConnectionErrorKind::Unavailable,
1391                        ))
1392                    } else {
1393                        direct.connect(input).await
1394                    }
1395                }
1396            }
1397        });
1398        let client = EasyHttpWebClient::connector_builder()
1399            .with_custom_transport_connector(transport)
1400            .without_dns_connector()
1401            .without_tls_proxy_support()
1402            .without_proxy_support()
1403            .without_tls_support()
1404            .with_default_http_connector(WeakScope::detached())
1405            .without_connection_pool()
1406            .build_client();
1407        let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1408
1409        for _ in 0..2 {
1410            let request = Request::builder()
1411                .uri("http://example.com")
1412                .body(Body::empty())
1413                .unwrap();
1414            request
1415                .extensions()
1416                .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1417            client.serve(request).await.unwrap();
1418        }
1419
1420        assert_eq!(
1421            attempts.lock().as_slice(),
1422            [proxy.clone(), ProxyRoute::Direct, proxy, ProxyRoute::Direct]
1423        );
1424    }
1425
1426    #[tokio::test]
1427    async fn easy_client_accepts_custom_proxy_route_failure_cache() {
1428        let attempts = Arc::new(parking_lot::Mutex::new(Vec::new()));
1429        let transport = service_fn({
1430            let attempts = attempts.clone();
1431            let direct = dummy_server::<ConnectRequest>();
1432            move |input: ConnectRequest| {
1433                let attempts = attempts.clone();
1434                let direct = direct.clone();
1435                async move {
1436                    let route = input.extensions.get_ref::<ProxyRoute>().unwrap();
1437                    attempts.lock().push(route.clone());
1438                    if route.proxy_address().is_some() {
1439                        Err(ConnectionError::transport(
1440                            BoxError::from_static_str("proxy unavailable"),
1441                            ConnectionErrorKind::Unavailable,
1442                        ))
1443                    } else {
1444                        direct.connect(input).await
1445                    }
1446                }
1447            }
1448        });
1449        let mut failure_cache_config = ProxyRouteFailureCacheConfig::default();
1450        failure_cache_config.scope = ProxyRouteFailureCacheScope::PerProxy;
1451        let failure_cache = ProxyRouteFailureCache::try_new(failure_cache_config).unwrap();
1452        let client = EasyHttpWebClient::connector_builder()
1453            .with_custom_transport_connector(transport)
1454            .without_dns_connector()
1455            .without_tls_proxy_support()
1456            .without_proxy_support()
1457            .without_tls_support()
1458            .with_default_http_connector(WeakScope::detached())
1459            .with_proxy_route_failure_cache(failure_cache)
1460            .without_connection_pool()
1461            .build_client();
1462        let proxy = ProxyRoute::Proxy("http://proxy.example:8080".parse().unwrap());
1463
1464        for destination in ["one.example", "two.example"] {
1465            let request = Request::builder()
1466                .uri(format!("http://{destination}"))
1467                .body(Body::empty())
1468                .unwrap();
1469            request
1470                .extensions()
1471                .insert(ProxyRoutes::new([proxy.clone(), ProxyRoute::Direct]));
1472            client.serve(request).await.unwrap();
1473        }
1474
1475        assert_eq!(
1476            attempts.lock().as_slice(),
1477            [proxy, ProxyRoute::Direct, ProxyRoute::Direct]
1478        );
1479    }
1480
1481    #[cfg(all(feature = "rustls", any(feature = "aws-lc", feature = "ring")))]
1482    #[tokio::test]
1483    async fn rustls_https_proxy_alpn_is_scoped_across_connect() {
1484        use crate::{
1485            extensions::ExtensionsRef as _,
1486            net::{
1487                Protocol,
1488                address::HostWithPort,
1489                client::{EstablishedClientConnection, ProxyRoute},
1490                stream::service::EchoService,
1491            },
1492            tls::{
1493                client::{NegotiatedTlsParameters, TlsClientConfig},
1494                rustls::{client::TlsConnector, server::TlsAcceptorLayer},
1495                server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1496            },
1497        };
1498        use rama_core::ServiceInput;
1499        use rama_crypto::cert::generate_server_auth;
1500        use rama_http::io::upgrade::handle_upgrade;
1501        use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1502        use rama_net::http::TargetHttpVersion;
1503        use std::sync::Arc;
1504
1505        let (proxy_chain, proxy_key) =
1506            generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1507        let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1508        let (origin_chain, origin_key) =
1509            generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1510        let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1511
1512        let origin_server =
1513            TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1514                cert_chain: origin_chain,
1515                private_key: origin_key,
1516                ocsp: None,
1517            }))
1518            .into_layer(EchoService::new());
1519        let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1520        let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1521
1522        let connect_version = Arc::new(parking_lot::Mutex::new(None));
1523        let observed_version = connect_version.clone();
1524        let proxy_http =
1525            HttpServer::auto(WeakScope::detached()).service(service_fn(move |req: Request| {
1526                let origin_server = origin_server.clone();
1527                let origin_done_tx = origin_done_tx.clone();
1528                let observed_version = observed_version.clone();
1529                async move {
1530                    assert_eq!(req.method(), rama_http::Method::CONNECT);
1531                    *observed_version.lock() = Some(req.version());
1532                    let upgrade = handle_upgrade(&req);
1533                    tokio::spawn(async move {
1534                        let tunnel = upgrade.await.expect("server CONNECT upgrade");
1535                        // The client deliberately drops immediately after the
1536                        // handshake assertions, so the TLS server may finish
1537                        // with an EOF/close-notify error.
1538                        let _origin_result = origin_server.serve(tunnel).await;
1539                        if let Some(tx) = origin_done_tx.lock().take() {
1540                            tx.send(()).expect("origin completion receiver");
1541                        }
1542                    });
1543                    Ok::<_, Infallible>(Response::new(Body::empty()))
1544                }
1545            }));
1546        let proxy_server = TlsAcceptorLayer::new(
1547            TlsServerConfig::new()
1548                .with_single_cert(ServerAuthData {
1549                    cert_chain: proxy_chain,
1550                    private_key: proxy_key,
1551                    ocsp: None,
1552                })
1553                .with_alpn_http_2(),
1554        )
1555        .into_layer(proxy_http);
1556
1557        let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1558        let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1559        let transport = service_fn(move |input: ConnectRequest| {
1560            let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1561            async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1562        });
1563
1564        let proxy_config = TlsClientConfig::new()
1565            .with_alpn_http_2()
1566            .with_server_name(crate::net::address::Host::from_static("localhost"))
1567            .try_with_server_trust_anchors([proxy_trust])
1568            .expect("proxy trust");
1569        let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1570        let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1571        let origin_config = TlsClientConfig::new()
1572            .with_alpn(Default::default())
1573            .with_server_name(crate::net::address::Host::from_static("localhost"))
1574            .try_with_server_trust_anchors([origin_trust])
1575            .expect("origin trust");
1576        let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1577
1578        let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1579            .with_application_protocol(Protocol::HTTPS);
1580        input
1581            .extensions
1582            .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1583        let client = async move {
1584            let established = Box::pin(connector.serve(input))
1585                .await
1586                .expect("two TLS handshakes");
1587
1588            assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1589            assert_eq!(
1590                established
1591                    .conn
1592                    .extensions()
1593                    .get_ref::<NegotiatedTlsParameters>()
1594                    .expect("origin TLS parameters")
1595                    .application_layer_protocol,
1596                None
1597            );
1598            assert!(
1599                established
1600                    .conn
1601                    .extensions()
1602                    .get_ref::<TargetHttpVersion>()
1603                    .is_none(),
1604                "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1605            );
1606            drop(established);
1607        };
1608        let (proxy_result, ()) =
1609            Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1610                tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1611            }))
1612            .await
1613            .expect("proxy/origin exchange");
1614        proxy_result.expect("proxy server");
1615        tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1616            .await
1617            .expect("origin server shutdown")
1618            .expect("origin completion signal");
1619    }
1620
1621    #[cfg(feature = "boring")]
1622    #[tokio::test]
1623    async fn boring_https_proxy_alpn_is_scoped_across_connect() {
1624        use crate::{
1625            extensions::ExtensionsRef as _,
1626            net::{
1627                Protocol,
1628                address::HostWithPort,
1629                client::{EstablishedClientConnection, ProxyRoute},
1630                stream::service::EchoService,
1631            },
1632            tls::{
1633                boring::{client::TlsConnector, server::TlsAcceptorLayer},
1634                client::{NegotiatedTlsParameters, TlsClientConfig},
1635                server::{GeneratedServerAuthConfig, ServerAuthData, TlsServerConfig},
1636            },
1637        };
1638        use rama_core::ServiceInput;
1639        use rama_crypto::cert::generate_server_auth;
1640        use rama_http::io::upgrade::handle_upgrade;
1641        use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
1642        use rama_net::http::TargetHttpVersion;
1643        use std::sync::Arc;
1644
1645        let (proxy_chain, proxy_key) =
1646            generate_server_auth(GeneratedServerAuthConfig::default()).expect("proxy auth");
1647        let proxy_trust = proxy_chain.last().expect("proxy trust anchor").clone();
1648        let (origin_chain, origin_key) =
1649            generate_server_auth(GeneratedServerAuthConfig::default()).expect("origin auth");
1650        let origin_trust = origin_chain.last().expect("origin trust anchor").clone();
1651
1652        let origin_server =
1653            TlsAcceptorLayer::new(TlsServerConfig::new().with_single_cert(ServerAuthData {
1654                cert_chain: origin_chain,
1655                private_key: origin_key,
1656                ocsp: None,
1657            }))
1658            .into_layer(EchoService::new());
1659        let (origin_done_tx, origin_done_rx) = tokio::sync::oneshot::channel();
1660        let origin_done_tx = Arc::new(parking_lot::Mutex::new(Some(origin_done_tx)));
1661
1662        let connect_version = Arc::new(parking_lot::Mutex::new(None));
1663        let observed_version = connect_version.clone();
1664        let proxy_http =
1665            HttpServer::auto(WeakScope::detached()).service(service_fn(move |req: Request| {
1666                let origin_server = origin_server.clone();
1667                let origin_done_tx = origin_done_tx.clone();
1668                let observed_version = observed_version.clone();
1669                async move {
1670                    assert_eq!(req.method(), rama_http::Method::CONNECT);
1671                    *observed_version.lock() = Some(req.version());
1672                    let upgrade = handle_upgrade(&req);
1673                    tokio::spawn(async move {
1674                        let tunnel = upgrade.await.expect("server CONNECT upgrade");
1675                        // The client deliberately drops immediately after the
1676                        // handshake assertions, so the TLS server may finish
1677                        // with an EOF/close-notify error.
1678                        let _origin_result = origin_server.serve(tunnel).await;
1679                        if let Some(tx) = origin_done_tx.lock().take() {
1680                            tx.send(()).expect("origin completion receiver");
1681                        }
1682                    });
1683                    Ok::<_, Infallible>(Response::new(Body::empty()))
1684                }
1685            }));
1686        let proxy_server = TlsAcceptorLayer::new(
1687            TlsServerConfig::new()
1688                .with_single_cert(ServerAuthData {
1689                    cert_chain: proxy_chain,
1690                    private_key: proxy_key,
1691                    ocsp: None,
1692                })
1693                .with_alpn_http_2(),
1694        )
1695        .into_layer(proxy_http);
1696
1697        let (client_io, server_io) = tokio::io::duplex(64 * 1024);
1698        let client_io = Arc::new(parking_lot::Mutex::new(Some(client_io)));
1699        let transport = service_fn(move |input: ConnectRequest| {
1700            let conn = ServiceInput::new(client_io.lock().take().expect("one proxy connection"));
1701            async move { Ok::<_, ConnectionError>(EstablishedClientConnection { input, conn }) }
1702        });
1703
1704        let proxy_config = TlsClientConfig::new()
1705            .with_alpn_http_2()
1706            .with_server_name(crate::net::address::Host::from_static("localhost"))
1707            .try_with_server_trust_anchors([proxy_trust])
1708            .expect("proxy trust");
1709        let proxy_tls = TlsConnector::tunnel(transport, None).with_base_config(proxy_config);
1710        let proxy = HttpProxyConnectorLayer::default().into_layer(proxy_tls);
1711        let origin_config = TlsClientConfig::new()
1712            .with_alpn(Default::default())
1713            .with_server_name(crate::net::address::Host::from_static("localhost"))
1714            .try_with_server_trust_anchors([origin_trust])
1715            .expect("origin trust");
1716        let connector = TlsConnector::auto(proxy).with_base_config(origin_config);
1717
1718        let input = ConnectRequest::new(HostWithPort::try_from("localhost:443").unwrap())
1719            .with_application_protocol(Protocol::HTTPS);
1720        input
1721            .extensions
1722            .insert(ProxyRoute::Proxy("https://localhost:8443".parse().unwrap()));
1723        let client = async move {
1724            let established = connector.serve(input).await.expect("two TLS handshakes");
1725
1726            assert_eq!(*connect_version.lock(), Some(Version::HTTP_2));
1727            assert_eq!(
1728                established
1729                    .conn
1730                    .extensions()
1731                    .get_ref::<NegotiatedTlsParameters>()
1732                    .expect("origin TLS parameters")
1733                    .application_layer_protocol,
1734                None
1735            );
1736            assert!(
1737                established
1738                    .conn
1739                    .extensions()
1740                    .get_ref::<TargetHttpVersion>()
1741                    .is_none(),
1742                "proxy HTTP/2 must not leak past CONNECT into a no-ALPN origin"
1743            );
1744            drop(established);
1745        };
1746        let (proxy_result, ()) =
1747            Box::pin(tokio::time::timeout(Duration::from_secs(5), async move {
1748                tokio::join!(proxy_server.serve(ServiceInput::new(server_io)), client)
1749            }))
1750            .await
1751            .expect("proxy/origin exchange");
1752        proxy_result.expect("proxy server");
1753        tokio::time::timeout(Duration::from_secs(5), origin_done_rx)
1754            .await
1755            .expect("origin server shutdown")
1756            .expect("origin completion signal");
1757    }
1758
1759    #[cfg(feature = "boring")]
1760    #[tokio::test]
1761    async fn default_client_resumes_tls_sessions() {
1762        let scope = WeakScope::detached();
1763        let resumed = Arc::new(parking_lot::Mutex::new(Vec::new()));
1764        let record = resumed.clone();
1765        let server = TlsAcceptorLayer::new(
1766            TlsServerConfig::new()
1767                .with_server_auth(
1768                    ServerAuthData::new_generated(GeneratedServerAuthConfig::default()).unwrap(),
1769                )
1770                .with_alpn_http_1(),
1771        )
1772        .with_session_resumption(true)
1773        .into_layer(
1774            MapInputLayer::new(move |stream: TlsStream<TcpStream>| {
1775                let params = stream.extensions().get_ref::<NegotiatedTlsParameters>();
1776                record.lock().push(params.and_then(|params| params.resumed));
1777                stream
1778            })
1779            .into_layer(HttpServer::new_http1(scope.clone()).service(service_fn(
1780                async |_: Request| {
1781                    // A fresh connection per request, so each one handshakes.
1782                    Ok::<_, Infallible>(
1783                        Response::builder()
1784                            .header(CONNECTION, "close")
1785                            .body(Body::empty())
1786                            .unwrap(),
1787                    )
1788                },
1789            ))),
1790        );
1791        let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), scope)
1792            .await
1793            .unwrap();
1794        let port = listener.local_addr().unwrap().port();
1795        let server = tokio::spawn(listener.serve(server));
1796
1797        let client = DefaultHttpWebClient::<Body>::default();
1798        for _ in 0..2 {
1799            let request = Request::builder()
1800                .uri(format!("https://localhost:{port}/"))
1801                .body(Body::empty())
1802                .unwrap();
1803            request
1804                .extensions()
1805                .insert(TlsServerVerify(ServerVerifyMode::Disable));
1806            let response = tokio::time::timeout(Duration::from_secs(5), client.serve(request))
1807                .await
1808                .unwrap()
1809                .unwrap();
1810            response.try_into_string().await.unwrap();
1811        }
1812        server.abort();
1813        assert_eq!(*resumed.lock(), [Some(false), Some(true)]);
1814    }
1815
1816    #[cfg(feature = "boring")]
1817    #[test]
1818    fn proxy_failure_cache_keeps_tls_client_future_bounded() {
1819        let client = EasyHttpWebClient::connector_builder()
1820            .with_default_transport_connector()
1821            .with_default_dns_connector()
1822            .without_tls_proxy_support()
1823            .with_proxy_support()
1824            .with_tls_support_using_boringssl_and_default_http_version(
1825                crate::tls::client::TlsClientConfig::default_http(),
1826                Version::HTTP_11,
1827            )
1828            .with_default_http_connector(WeakScope::detached())
1829            .without_connection_pool()
1830            .build_client();
1831        let request = Request::builder()
1832            .uri("https://example.com")
1833            .body(Body::empty())
1834            .unwrap();
1835
1836        let future = client.serve(request);
1837        let future_size = std::mem::size_of_val(&future);
1838
1839        assert!(
1840            future_size < 64 * 1024,
1841            "easy TLS client future is unexpectedly large: {future_size} bytes"
1842        );
1843    }
1844
1845    #[tokio::test]
1846    async fn connection_is_in_use_until_response_body_is_consumed() {
1847        let client = EasyHttpWebClient::connector_builder()
1848            .with_custom_transport_connector(dummy_server())
1849            .without_dns_connector()
1850            .without_tls_proxy_support()
1851            .without_proxy_support()
1852            .without_tls_support()
1853            .with_default_http_connector(WeakScope::detached())
1854            .with_connection_pool(HttpPooledConnectorConfig {
1855                max_streams_per_connection: NonZeroUsize::new(1),
1856                max_connections_total: NonZeroUsize::new(4),
1857                ..Default::default()
1858            })
1859            .build_client();
1860
1861        let req = || {
1862            Request::builder()
1863                .uri("http://example.com")
1864                .version(Version::HTTP_2)
1865                .body(Body::empty())
1866                .unwrap()
1867        };
1868
1869        // Get the first response but DO NOT consume its body yet: the connection
1870        // is logically still in use until the body is drained. Then issue a second
1871        // request before draining the first.
1872        let res1 = client.serve(req()).await.unwrap();
1873        let res2 = client.serve(req()).await.unwrap();
1874
1875        // Drain in reverse so `res1`'s body is still outstanding when `req2` runs.
1876        let out2 = res2.try_into_json::<Output>().await.unwrap();
1877        let out1 = res1.try_into_json::<Output>().await.unwrap();
1878
1879        assert_eq!(out1.conn, 0, "first request uses the first connection");
1880        // With `max_concurrent_streams = 1`, connection 0's response body is still
1881        // in flight, so the second request must NOT reuse it.
1882        assert_eq!(
1883            out2.conn, 1,
1884            "second request must not reuse a connection whose response body is still in flight"
1885        );
1886    }
1887
1888    // These things are already tested inside the pool itself, but here we add some high level tests
1889    // in case we ever swap the underlying pool implementation.
1890
1891    #[tokio::test]
1892    async fn default_pool_multiplexes_on_h2() {
1893        let client = EasyHttpWebClient::connector_builder()
1894            .with_custom_transport_connector(dummy_server())
1895            .without_dns_connector()
1896            .without_tls_proxy_support()
1897            .without_proxy_support()
1898            .without_tls_support()
1899            .with_default_http_connector(WeakScope::detached())
1900            .with_default_connection_pool()
1901            .build_client();
1902
1903        let req = || {
1904            Request::builder()
1905                .uri("http://example.com")
1906                .version(Version::HTTP_2)
1907                .body(Body::empty())
1908                .unwrap()
1909        };
1910        let (res1, res2, res3) = tokio::join!(
1911            client.serve(req()),
1912            client.serve(req()),
1913            client.serve(req()),
1914        );
1915
1916        // Should only create single connection and send all requests over the same one
1917        for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1918            let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1919            assert_eq!(out.conn, 0);
1920            assert_eq!(out.resp, i);
1921        }
1922    }
1923
1924    #[tokio::test]
1925    async fn default_pool_does_not_multiplexes_on_h1() {
1926        let client = EasyHttpWebClient::connector_builder()
1927            .with_custom_transport_connector(dummy_server())
1928            .without_dns_connector()
1929            .without_tls_proxy_support()
1930            .without_proxy_support()
1931            .without_tls_support()
1932            .with_default_http_connector(WeakScope::detached())
1933            .with_default_connection_pool()
1934            .build_client();
1935
1936        let req = || {
1937            Request::builder()
1938                .uri("http://example.com")
1939                .version(Version::HTTP_11)
1940                .body(Body::empty())
1941                .unwrap()
1942        };
1943        let (res1, res2, res3) = tokio::join!(
1944            client.serve(req()),
1945            client.serve(req()),
1946            client.serve(req()),
1947        );
1948
1949        // Should create a new connection for each request since they are all inprogress at the same
1950        // time and h1 does not support multiplexing
1951        for (i, res) in [res1, res2, res3].into_iter().enumerate() {
1952            let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1953            assert_eq!(out.conn, i);
1954            assert_eq!(out.resp, 0);
1955        }
1956    }
1957
1958    #[tokio::test]
1959    async fn multiplex_on_h2_respects_limits() {
1960        let client = EasyHttpWebClient::connector_builder()
1961            .with_custom_transport_connector(dummy_server())
1962            .without_dns_connector()
1963            .without_tls_proxy_support()
1964            .without_proxy_support()
1965            .without_tls_support()
1966            .with_default_http_connector(WeakScope::detached())
1967            .with_connection_pool(HttpPooledConnectorConfig {
1968                max_streams_per_connection: NonZeroUsize::new(2),
1969                ..Default::default()
1970            })
1971            .build_client();
1972
1973        let req = || {
1974            Request::builder()
1975                .uri("http://example.com")
1976                .version(Version::HTTP_2)
1977                .body(Body::empty())
1978                .unwrap()
1979        };
1980        let (res1, res2, res3, res4) = tokio::join!(
1981            client.serve(req()),
1982            client.serve(req()),
1983            client.serve(req()),
1984            client.serve(req()),
1985        );
1986
1987        // Should create a connection for every two request
1988        for (i, res) in [res1, res2, res3, res4].into_iter().enumerate() {
1989            let out = res.unwrap().try_into_json::<Output>().await.unwrap();
1990            assert_eq!(out.conn, i / 2);
1991            assert_eq!(out.resp, i % 2);
1992        }
1993    }
1994}