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