Skip to main content

rama/http/client/
builder.rs

1use rama_core::rt::Executor;
2
3use super::{
4    HttpConnectRequestAdapter, HttpConnector, HttpPooledConnector, HttpPooledConnectorConfig,
5};
6use crate::{
7    Layer, Service,
8    dns::client::{
9        DnsConnector, DnsConnectorLayer, GlobalDnsResolver, resolver::DnsAddressResolver,
10    },
11    error::BoxError,
12    extensions::ExtensionsRef,
13    http::{
14        Request, StreamingBody, client::proxy::layer::HttpProxyConnector,
15        layer::version_adapter::RequestVersionAdapter,
16    },
17    net::client::{
18        ConnectRequest, ConnectionError, ConnectorService, EstablishedClientConnection,
19        MapEstablishedConnection, ProxyRouteFailureCache, ProxyRouteFailureCacheConnector,
20        ProxyRoutesConnector, pool::PooledConnector,
21    },
22    service::BoxService,
23    tcp::client::service::TcpConnector,
24};
25use rama_http::layer::{
26    alt_svc::{AltSvc, AltSvcCache, AltSvcLayer},
27    http_service::HttpServiceConnector,
28};
29use rama_utils::macros::generate_set_and_with;
30use std::time::Duration;
31
32#[cfg(feature = "boring")]
33use crate::tls::boring::client as boring_client;
34
35#[cfg(feature = "rustls")]
36use crate::tls::rustls::client as rustls_client;
37#[cfg(any(feature = "rustls", feature = "boring"))]
38use {
39    crate::http::conn::FallbackHttpVersion, rama_core::layer::AddInputExtension,
40    rama_tls::client::TlsClientConfig,
41};
42
43#[cfg(feature = "socks5")]
44use crate::{http::client::proxy_connector::ProxyConnector, proxy::socks5::Socks5ProxyConnector};
45
46/// Builder that is designed to easily create a connector for [`super::EasyHttpWebClient`] from most basic use cases
47#[derive(Default)]
48pub struct EasyHttpConnectorBuilder<C = (), S = (), D = ()> {
49    connector: C,
50    stage: S,
51    // Retain the layer so both transports apply exactly the same DNS policy.
52    dns: D,
53}
54
55#[non_exhaustive]
56#[derive(Debug, Default)]
57pub struct TransportStage;
58
59#[non_exhaustive]
60#[derive(Debug, Default)]
61pub struct DnsStage;
62
63#[non_exhaustive]
64#[derive(Debug, Default)]
65pub struct ProxyTunnelStage<const TLS_PROXY: bool = true>;
66
67#[non_exhaustive]
68#[derive(Debug, Default)]
69pub struct ProxyStage<const PROXY: bool = true>;
70
71#[non_exhaustive]
72#[derive(Debug, Default)]
73pub struct TlsStage<const PROXY: bool = true>;
74
75#[non_exhaustive]
76#[derive(Debug, Default)]
77pub struct HttpStage<const PROXY: bool = true>;
78
79#[non_exhaustive]
80#[derive(Debug, Default)]
81pub struct ProxyRouteFailureCacheStage;
82
83#[non_exhaustive]
84#[derive(Debug, Default)]
85pub struct PoolStage;
86
87impl EasyHttpConnectorBuilder {
88    #[must_use]
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    #[must_use]
94    pub fn with_default_transport_connector(
95        self,
96    ) -> EasyHttpConnectorBuilder<TcpConnector, TransportStage> {
97        let connector = TcpConnector::default();
98        EasyHttpConnectorBuilder {
99            dns: self.dns,
100            connector,
101            stage: Default::default(),
102        }
103    }
104
105    /// Add a custom transport connector that will be used by this client for the transport layer
106    pub fn with_custom_transport_connector<C>(
107        self,
108        connector: C,
109    ) -> EasyHttpConnectorBuilder<C, TransportStage> {
110        EasyHttpConnectorBuilder {
111            dns: self.dns,
112            connector,
113            stage: Default::default(),
114        }
115    }
116}
117
118impl<T, Stage, D> EasyHttpConnectorBuilder<T, Stage, D> {
119    /// Add a custom connector to this Stage.
120    ///
121    /// Adding a custom connector to a stage will not change the state
122    /// so this can be used to modify behaviour at a specific stage.
123    pub fn with_custom_connector<L>(
124        self,
125        connector_layer: L,
126    ) -> EasyHttpConnectorBuilder<L::Service, Stage, D>
127    where
128        L: Layer<T>,
129    {
130        self.map_connector(|c| connector_layer.into_layer(c))
131    }
132
133    /// Map the current connector using the given fn.
134    ///
135    /// Mapping a connector to a stage will not change the state
136    /// so this can be used to modify behaviour at a specific stage.
137    pub fn map_connector<T2>(
138        self,
139        map_fn: impl FnOnce(T) -> T2,
140    ) -> EasyHttpConnectorBuilder<T2, Stage, D> {
141        let connector = map_fn(self.connector);
142        EasyHttpConnectorBuilder {
143            dns: self.dns,
144            connector,
145            stage: self.stage,
146        }
147    }
148}
149
150impl<T> EasyHttpConnectorBuilder<T, TransportStage> {
151    /// Add the global DNS policy to both the stream and built-in QUIC transports.
152    pub fn with_default_dns_connector(
153        self,
154    ) -> EasyHttpConnectorBuilder<DnsConnector<T>, DnsStage, DnsConnectorLayer> {
155        self.with_dns_address_resolver(GlobalDnsResolver::new())
156    }
157
158    /// Add a custom address resolver to both the stream and built-in QUIC transports.
159    pub fn with_dns_address_resolver<R: DnsAddressResolver + Clone>(
160        self,
161        resolver: R,
162    ) -> EasyHttpConnectorBuilder<DnsConnector<T, R>, DnsStage, DnsConnectorLayer<R>> {
163        self.with_dns_connector(DnsConnectorLayer::with_resolver(resolver))
164    }
165
166    /// Omit DNS resolution. Transports require IP targets or their own resolution.
167    pub fn without_dns_connector(self) -> EasyHttpConnectorBuilder<T, DnsStage> {
168        self.with_dns_connector(())
169    }
170
171    /// Retain a custom DNS layer for both transport stacks.
172    ///
173    /// The layer is applied to the stream transport here and to the built-in
174    /// QUIC connector when `with_http3_support` is called. A layer specific to
175    /// the stream transport can instead be paired with `with_http3_connector`,
176    /// whose supplied connector owns its DNS policy independently.
177    pub fn with_dns_connector<L: Layer<T>>(
178        self,
179        layer: L,
180    ) -> EasyHttpConnectorBuilder<L::Service, DnsStage, L> {
181        let connector = layer.layer(self.connector);
182        EasyHttpConnectorBuilder {
183            connector,
184            stage: DnsStage,
185            dns: layer,
186        }
187    }
188}
189
190impl<T, D> EasyHttpConnectorBuilder<T, DnsStage, D> {
191    /// Add a custom proxy TLS connector used to establish TLS to an HTTPS proxy.
192    ///
193    /// The layer must attach `rama_tls::client::NegotiatedTlsParameters` to
194    /// the established connection. Rama uses this as positive proof that TLS
195    /// was negotiated and to select the proxy-side HTTP version; missing
196    /// evidence fails closed before proxy HTTP is sent.
197    pub fn with_custom_tls_proxy_connector<L>(
198        self,
199        connector_layer: L,
200    ) -> EasyHttpConnectorBuilder<L::Service, ProxyTunnelStage<true>, D>
201    where
202        L: Layer<T>,
203    {
204        let connector = connector_layer.into_layer(self.connector);
205        EasyHttpConnectorBuilder {
206            dns: self.dns,
207            connector,
208            stage: Default::default(),
209        }
210    }
211
212    #[cfg(feature = "boring")]
213    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
214    /// Support a tls tunnel to the proxy itself using boringssl
215    ///
216    /// Note that a tls proxy is not needed to make a https connection
217    /// to the final target. It only has an influence on the initial connection
218    /// to the proxy itself
219    pub fn with_tls_proxy_support_using_boringssl(
220        self,
221    ) -> EasyHttpConnectorBuilder<
222        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
223        ProxyTunnelStage<true>,
224        D,
225    > {
226        let connector = boring_client::TlsConnector::tunnel(self.connector, None);
227        EasyHttpConnectorBuilder {
228            dns: self.dns,
229            connector,
230            stage: Default::default(),
231        }
232    }
233
234    #[cfg(feature = "boring")]
235    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
236    /// Support a tls tunnel to the proxy itself using boringssl and the provided config
237    ///
238    /// Note that a tls proxy is not needed to make a https connection
239    /// to the final target. It only has an influence on the initial connection
240    /// to the proxy itself
241    pub fn with_tls_proxy_support_using_boringssl_config(
242        self,
243        config: TlsClientConfig,
244    ) -> EasyHttpConnectorBuilder<
245        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
246        ProxyTunnelStage<true>,
247        D,
248    > {
249        let connector =
250            boring_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
251        EasyHttpConnectorBuilder {
252            dns: self.dns,
253            connector,
254            stage: Default::default(),
255        }
256    }
257
258    #[cfg(feature = "rustls")]
259    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
260    /// Support a tls tunnel to the proxy itself using rustls
261    ///
262    /// Note that a tls proxy is not needed to make a https connection
263    /// to the final target. It only has an influence on the initial connection
264    /// to the proxy itself
265    pub fn with_tls_proxy_support_using_rustls(
266        self,
267    ) -> EasyHttpConnectorBuilder<
268        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
269        ProxyTunnelStage<true>,
270        D,
271    > {
272        let connector = rustls_client::TlsConnector::tunnel(self.connector, None);
273
274        EasyHttpConnectorBuilder {
275            dns: self.dns,
276            connector,
277            stage: Default::default(),
278        }
279    }
280
281    #[cfg(feature = "rustls")]
282    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
283    /// Support a tls tunnel to the proxy itself using rustls and the provided config
284    ///
285    /// Note that a tls proxy is not needed to make a https connection
286    /// to the final target. It only has an influence on the initial connection
287    /// to the proxy itself
288    pub fn with_tls_proxy_support_using_rustls_config(
289        self,
290        config: TlsClientConfig,
291    ) -> EasyHttpConnectorBuilder<
292        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
293        ProxyTunnelStage<true>,
294        D,
295    > {
296        let connector =
297            rustls_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
298
299        EasyHttpConnectorBuilder {
300            dns: self.dns,
301            connector,
302            stage: Default::default(),
303        }
304    }
305
306    /// Don't support a tls tunnel to the proxy itself
307    ///
308    /// Note that a tls proxy is not needed to make a https connection
309    /// to the final target. It only has an influence on the initial connection
310    /// to the proxy itself
311    pub fn without_tls_proxy_support(
312        self,
313    ) -> EasyHttpConnectorBuilder<T, ProxyTunnelStage<false>, D> {
314        EasyHttpConnectorBuilder {
315            dns: self.dns,
316            connector: self.connector,
317            stage: Default::default(),
318        }
319    }
320}
321
322impl<T, D, const TLS_PROXY: bool> EasyHttpConnectorBuilder<T, ProxyTunnelStage<TLS_PROXY>, D> {
323    /// Add a custom proxy connector that will be used by this client
324    pub fn with_custom_proxy_connector<L>(
325        self,
326        connector_layer: L,
327    ) -> EasyHttpConnectorBuilder<L::Service, ProxyStage<true>, D>
328    where
329        L: Layer<T>,
330    {
331        let connector = connector_layer.into_layer(self.connector);
332        EasyHttpConnectorBuilder {
333            dns: self.dns,
334            connector,
335            stage: Default::default(),
336        }
337    }
338
339    #[cfg(not(feature = "socks5"))]
340    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
341    ///
342    /// Note that a tls proxy is not needed to make a https connection
343    /// to the final target. It only has an influence on the initial connection
344    /// to the proxy itself
345    ///
346    /// Note to also enable socks proxy support enable feature `socks5`
347    ///
348    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
349    pub fn with_proxy_support(
350        self,
351    ) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage<true>, D> {
352        self.with_http_proxy_support()
353    }
354
355    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
356    ///
357    /// Note that a tls proxy is not needed to make a https connection
358    /// to the final target. It only has an influence on the initial connection
359    /// to the proxy itself
360    ///
361    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
362    pub fn with_http_proxy_support(
363        self,
364    ) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage<true>, D> {
365        let connector =
366            HttpProxyConnector::optional(self.connector).with_tls_proxy_support(TLS_PROXY);
367
368        EasyHttpConnectorBuilder {
369            dns: self.dns,
370            connector,
371            stage: Default::default(),
372        }
373    }
374
375    #[cfg(feature = "socks5")]
376    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
377    /// Add support for usage of a socks5(h) [`ProxyAddress`] to this client
378    ///
379    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
380    pub fn with_socks5_proxy_support(
381        self,
382    ) -> EasyHttpConnectorBuilder<Socks5ProxyConnector<T>, ProxyStage<true>, D> {
383        let connector = Socks5ProxyConnector::optional(self.connector);
384
385        EasyHttpConnectorBuilder {
386            dns: self.dns,
387            connector,
388            stage: Default::default(),
389        }
390    }
391
392    /// Make a client without proxy support
393    pub fn without_proxy_support(self) -> EasyHttpConnectorBuilder<T, ProxyStage<false>, D> {
394        EasyHttpConnectorBuilder {
395            dns: self.dns,
396            connector: self.connector,
397            stage: Default::default(),
398        }
399    }
400}
401
402impl<T: Clone, D, const TLS_PROXY: bool>
403    EasyHttpConnectorBuilder<T, ProxyTunnelStage<TLS_PROXY>, D>
404{
405    #[cfg(feature = "socks5")]
406    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
407    /// Add support for usage of a http(s) and socks5(h) [`ProxyAddress`] to this client
408    ///
409    /// Note that a tls proxy is not needed to make a https connection
410    /// to the final target. It only has an influence on the initial connection
411    /// to the proxy itself
412    ///
413    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
414    pub fn with_proxy_support(
415        self,
416    ) -> EasyHttpConnectorBuilder<ProxyConnector<T>, ProxyStage<true>, D> {
417        use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
418        use rama_socks5::Socks5ProxyConnectorLayer;
419
420        let connector = ProxyConnector::optional(
421            self.connector,
422            Socks5ProxyConnectorLayer::required(),
423            HttpProxyConnectorLayer::required().with_tls_proxy_support(TLS_PROXY),
424        );
425
426        EasyHttpConnectorBuilder {
427            dns: self.dns,
428            connector,
429            stage: Default::default(),
430        }
431    }
432}
433
434impl<T, D, const PROXY: bool> EasyHttpConnectorBuilder<T, ProxyStage<PROXY>, D> {
435    /// Add a custom tls connector that will be used by the client.
436    ///
437    /// TLS connectors publish connection reuse policies on their established
438    /// transports. Custom secure connectors without this metadata are not pooled;
439    /// implement the generic connection reuse policy to support safe pooling.
440    ///
441    /// The final HTTP transition applies a [`RequestVersionAdapter`] outside
442    /// the complete connection attempt so it can apply the negotiated version
443    /// to the original HTTP request.
444    pub fn with_custom_tls_connector<L>(
445        self,
446        connector_layer: L,
447    ) -> EasyHttpConnectorBuilder<L::Service, TlsStage<PROXY>, D>
448    where
449        L: Layer<T>,
450    {
451        let connector = connector_layer.into_layer(self.connector);
452
453        EasyHttpConnectorBuilder {
454            dns: self.dns,
455            connector,
456            stage: Default::default(),
457        }
458    }
459
460    #[cfg(feature = "boring")]
461    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
462    /// Support https connections by using boringssl for tls
463    ///
464    /// The final HTTP transition automatically applies the HTTP version
465    /// negotiated through TLS to the original request.
466    pub fn with_tls_support_using_boringssl(
467        self,
468        config: TlsClientConfig,
469    ) -> EasyHttpConnectorBuilder<boring_client::TlsConnector<T>, TlsStage<PROXY>, D> {
470        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
471
472        EasyHttpConnectorBuilder {
473            dns: self.dns,
474            connector,
475            stage: Default::default(),
476        }
477    }
478
479    #[cfg(feature = "boring")]
480    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
481    /// Same as [`Self::with_tls_support_using_boringssl`] but also
482    /// setting a fallback HTTP version in case no ALPN is negotiated.
483    /// The fallback does not constrain the ALPN protocols offered by TLS.
484    ///
485    /// This is a fairly important detail for proxy purposes given otherwise
486    /// you might come in situations where the ingress traffic is negotiated to `h2`,
487    /// but the egress traffic has no negotiation which would without a default
488    /// http version remain on h2... In such a case you can get failed
489    /// requests if the egress server does not handle multiple http versions.
490    pub fn with_tls_support_using_boringssl_and_default_http_version(
491        self,
492        config: TlsClientConfig,
493        default_http_version: rama_http::Version,
494    ) -> EasyHttpConnectorBuilder<
495        AddInputExtension<boring_client::TlsConnector<T>, FallbackHttpVersion>,
496        TlsStage<PROXY>,
497        D,
498    > {
499        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
500        let connector =
501            AddInputExtension::new(connector, FallbackHttpVersion(default_http_version))
502                .with_overwrite(false);
503
504        EasyHttpConnectorBuilder {
505            dns: self.dns,
506            connector,
507            stage: Default::default(),
508        }
509    }
510
511    #[cfg(feature = "rustls")]
512    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
513    /// Support https connections by using ruslts for tls
514    ///
515    /// The final HTTP transition automatically applies the HTTP version
516    /// negotiated through TLS to the original request.
517    pub fn with_tls_support_using_rustls(
518        self,
519        config: TlsClientConfig,
520    ) -> EasyHttpConnectorBuilder<rustls_client::TlsConnector<T>, TlsStage<PROXY>, D> {
521        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
522
523        EasyHttpConnectorBuilder {
524            dns: self.dns,
525            connector,
526            stage: Default::default(),
527        }
528    }
529
530    #[cfg(feature = "rustls")]
531    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
532    /// Same as [`Self::with_tls_support_using_rustls`] but also
533    /// setting a fallback HTTP version in case no ALPN is negotiated.
534    /// The fallback does not constrain the ALPN protocols offered by TLS.
535    ///
536    /// This is a fairly important detail for proxy purposes given otherwise
537    /// you might come in situations where the ingress traffic is negotiated to `h2`,
538    /// but the egress traffic has no negotiation which would without a default
539    /// http version remain on h2... In such a case you can get failed
540    /// requests if the egress server does not handle multiple http versions.
541    pub fn with_tls_support_using_rustls_and_default_http_version(
542        self,
543        config: TlsClientConfig,
544        default_http_version: rama_http::Version,
545    ) -> EasyHttpConnectorBuilder<
546        AddInputExtension<rustls_client::TlsConnector<T>, FallbackHttpVersion>,
547        TlsStage<PROXY>,
548        D,
549    > {
550        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
551        let connector =
552            AddInputExtension::new(connector, FallbackHttpVersion(default_http_version))
553                .with_overwrite(false);
554
555        EasyHttpConnectorBuilder {
556            dns: self.dns,
557            connector,
558            stage: Default::default(),
559        }
560    }
561
562    /// Don't support https on this connector
563    pub fn without_tls_support(self) -> EasyHttpConnectorBuilder<T, TlsStage<PROXY>, D> {
564        EasyHttpConnectorBuilder {
565            dns: self.dns,
566            connector: self.connector,
567            stage: Default::default(),
568        }
569    }
570}
571
572impl<T, D, const PROXY: bool> EasyHttpConnectorBuilder<T, TlsStage<PROXY>, D> {
573    /// Add http support to this connector
574    pub fn with_default_http_connector<Body>(
575        self,
576        exec: Executor,
577    ) -> EasyHttpConnectorBuilder<HttpConnector<T, Body>, HttpStage<PROXY>, D> {
578        let connector = HttpConnector::new(self.connector, exec);
579
580        EasyHttpConnectorBuilder {
581            dns: self.dns,
582            connector,
583            stage: HttpStage,
584        }
585    }
586
587    /// Add a custom http connector that will be run just after tls
588    pub fn with_custom_http_connector<L>(
589        self,
590        connector_layer: L,
591    ) -> EasyHttpConnectorBuilder<L::Service, HttpStage<PROXY>, D>
592    where
593        L: Layer<T>,
594    {
595        let connector = connector_layer.into_layer(self.connector);
596
597        EasyHttpConnectorBuilder {
598            dns: self.dns,
599            connector,
600            stage: HttpStage,
601        }
602    }
603}
604
605impl<T, Body, D, const PROXY: bool>
606    EasyHttpConnectorBuilder<HttpConnector<T, Body>, HttpStage<PROXY>, D>
607{
608    /// Add a separately configured QUIC transport below the common HTTP handshake.
609    /// The connector supplies its own transport, DNS and TLS policy. Its policy
610    /// publishes connection reuse metadata for its effective policy. Custom
611    /// connectors without that metadata are not pooled.
612    /// Use [`Self::with_http3_support`] for the built-in connector.
613    pub fn with_http3_connector<C>(
614        self,
615        connector: C,
616    ) -> EasyHttpConnectorBuilder<
617        HttpConnector<super::HttpTransportConnector<T, C>, Body>,
618        HttpStage<PROXY>,
619        D,
620    > {
621        EasyHttpConnectorBuilder {
622            dns: self.dns,
623            connector: self.connector.with_http3_connector(connector),
624            stage: self.stage,
625        }
626    }
627
628    /// Enable the built-in QUIC connector with the same DNS layer as the stream transport.
629    ///
630    /// The retained layer must support the QUIC connector as its inner service.
631    /// This does not copy a custom stream transport or its routing policy.
632    /// Use [`Self::with_http3_connector`] for an equivalent custom QUIC path.
633    pub fn with_http3_support(
634        self,
635        connector: super::Http3Connector,
636    ) -> EasyHttpConnectorBuilder<
637        HttpConnector<super::HttpTransportConnector<T, D::Service>, Body>,
638        HttpStage<PROXY>,
639        D,
640    >
641    where
642        D: Layer<super::Http3Connector>,
643    {
644        let connector = self.dns.layer(connector);
645        self.with_http3_connector(connector)
646    }
647}
648
649type DefaultHttpConnector<T> = RequestVersionAdapter<
650    HttpConnectRequestAdapter<
651        MapEstablishedConnection<
652            HttpServiceConnector<ProxyRoutesConnector<T>>,
653            AltSvcConnectionLayer,
654        >,
655    >,
656>;
657
658/// HTTP response observation configured by the easy-client builder.
659///
660/// Protocol selection and response observation share the configured cache but
661/// remain independently composable services.
662#[doc(hidden)]
663#[derive(Clone, Debug)]
664pub struct AltSvcConnectionLayer {
665    cache: Option<AltSvcCache>,
666}
667
668impl<S> Layer<S> for AltSvcConnectionLayer {
669    type Service = AltSvc<S>;
670
671    fn layer(&self, inner: S) -> Self::Service {
672        match &self.cache {
673            Some(cache) => AltSvcLayer::new(cache.clone()).layer(inner),
674            None => AltSvc::passthrough(inner),
675        }
676    }
677}
678
679type ConfiguredConnectionBuilder<T> = EasyHttpConnectorBuilder<DefaultHttpConnector<T>, PoolStage>;
680
681type ConfiguredConnectionPoolBuilder<T> =
682    EasyHttpConnectorBuilder<DefaultHttpConnector<HttpPooledConnector<T>>, PoolStage>;
683
684type ErasedConnector<C> =
685    BoxService<ConnectRequest, EstablishedClientConnection<C, ConnectRequest>, ConnectionError>;
686
687type DefaultConnectionBuilder<C> =
688    ConfiguredConnectionBuilder<ProxyRouteFailureCacheConnector<ErasedConnector<C>>>;
689
690type DefaultConnectionPoolBuilder<C> =
691    ConfiguredConnectionPoolBuilder<ProxyRouteFailureCacheConnector<ErasedConnector<C>>>;
692
693// Keep the configured connector and its future behind one dynamic boundary
694// before adding route caching and fallback. This prevents deeply nested TLS
695// connector futures from overflowing ordinary thread stacks while dispatching
696// only once per new connection (and behind the pool when pooling is enabled).
697struct ConnectorServiceAdapter<T>(T);
698
699impl<T> Service<ConnectRequest> for ConnectorServiceAdapter<T>
700where
701    T: ConnectorService<ConnectRequest>,
702{
703    type Output = EstablishedClientConnection<T::Connection, ConnectRequest>;
704    type Error = ConnectionError;
705
706    fn serve(
707        &self,
708        input: ConnectRequest,
709    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
710        self.0.connect(input)
711    }
712}
713
714fn erase_connector<T>(connector: T) -> ErasedConnector<T::Connection>
715where
716    T: ConnectorService<ConnectRequest>,
717{
718    ConnectorServiceAdapter(connector).boxed()
719}
720
721fn finalize_http_connector<T>(connector: T) -> DefaultHttpConnector<T> {
722    let connector = ProxyRoutesConnector::new(connector);
723    let cache = AltSvcCache::default();
724    let connector = HttpServiceConnector::new(connector).with_cache(cache.clone());
725    adapt_http_service_connector(connector, Some(cache))
726}
727
728fn adapt_http_service_connector<T>(
729    connector: HttpServiceConnector<ProxyRoutesConnector<T>>,
730    cache: Option<AltSvcCache>,
731) -> DefaultHttpConnector<T> {
732    let connector = MapEstablishedConnection::new(connector, AltSvcConnectionLayer { cache });
733    RequestVersionAdapter::new(HttpConnectRequestAdapter::new(connector))
734}
735
736impl<T> EasyHttpConnectorBuilder<DefaultHttpConnector<T>, PoolStage> {
737    /// Disable advertised alternatives and response learning for every HTTP version.
738    #[must_use]
739    pub fn without_alt_svc(self) -> Self {
740        self.maybe_with_alt_svc_cache(None)
741    }
742
743    generate_set_and_with! {
744        /// Share an alternative-service cache across HTTP clients; `None` disables discovery.
745        pub fn alt_svc_cache(mut self, cache: Option<AltSvcCache>) -> Self {
746            let connector = self.connector.get_mut().get_mut();
747            connector.get_mut().maybe_set_cache(cache.clone());
748            connector.layer_mut().cache = cache;
749            self
750        }
751    }
752}
753
754fn finish_without_connection_pool<T, Stage, D>(
755    builder: EasyHttpConnectorBuilder<T, Stage, D>,
756) -> ConfiguredConnectionBuilder<T>
757where
758    T: ConnectorService<ConnectRequest>,
759{
760    EasyHttpConnectorBuilder {
761        dns: (),
762        connector: finalize_http_connector(builder.connector),
763        stage: Default::default(),
764    }
765}
766
767fn finish_with_connection_pool<T, Stage, D>(
768    builder: EasyHttpConnectorBuilder<T, Stage, D>,
769    config: HttpPooledConnectorConfig,
770) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
771where
772    T: ConnectorService<ConnectRequest>,
773{
774    let connector = config.try_build_connector(builder.connector)?;
775    Ok(EasyHttpConnectorBuilder {
776        dns: (),
777        connector: finalize_http_connector(connector),
778        stage: Default::default(),
779    })
780}
781
782fn finish_with_default_connection_pool<T, Stage, D>(
783    builder: EasyHttpConnectorBuilder<T, Stage, D>,
784) -> ConfiguredConnectionPoolBuilder<T>
785where
786    T: ConnectorService<ConnectRequest>,
787{
788    let connector = HttpPooledConnectorConfig::build_default_connector(builder.connector);
789    EasyHttpConnectorBuilder {
790        dns: (),
791        connector: finalize_http_connector(connector),
792        stage: Default::default(),
793    }
794}
795
796fn finish_with_custom_connection_pool<T, Stage, D, P, R>(
797    builder: EasyHttpConnectorBuilder<T, Stage, D>,
798    pool: P,
799    req_to_conn_id: R,
800    wait_for_pool_timeout: Option<Duration>,
801) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
802    let connector = PooledConnector::new(builder.connector, pool, req_to_conn_id)
803        .maybe_with_wait_for_pool_timeout(wait_for_pool_timeout);
804    EasyHttpConnectorBuilder {
805        dns: (),
806        connector,
807        stage: Default::default(),
808    }
809}
810
811impl<T, D, const PROXY: bool> EasyHttpConnectorBuilder<T, HttpStage<PROXY>, D> {
812    /// Explicitly use the given shared proxy route failure cache.
813    ///
814    /// This selects the failure-cache policy for the final connection stage.
815    /// The configured connector is type-erased at this boundary to keep the
816    /// combined connector future stack-safe.
817    #[must_use]
818    pub fn with_proxy_route_failure_cache(
819        self,
820        cache: ProxyRouteFailureCache,
821    ) -> EasyHttpConnectorBuilder<
822        ProxyRouteFailureCacheConnector<ErasedConnector<T::Connection>>,
823        ProxyRouteFailureCacheStage,
824        D,
825    >
826    where
827        T: ConnectorService<ConnectRequest>,
828    {
829        EasyHttpConnectorBuilder {
830            dns: self.dns,
831            connector: ProxyRouteFailureCacheConnector::new(erase_connector(self.connector), cache),
832            stage: ProxyRouteFailureCacheStage,
833        }
834    }
835
836    /// Disable negative caching of temporarily failing proxy routes.
837    #[must_use]
838    pub fn without_proxy_route_failure_cache(
839        self,
840    ) -> EasyHttpConnectorBuilder<T, ProxyRouteFailureCacheStage, D> {
841        EasyHttpConnectorBuilder {
842            dns: self.dns,
843            connector: self.connector,
844            stage: ProxyRouteFailureCacheStage,
845        }
846    }
847}
848
849impl<T, D> EasyHttpConnectorBuilder<T, HttpStage<true>, D> {
850    /// Finish the default HTTP connector stack without adding a connection pool.
851    ///
852    /// This still installs HTTP request adaptation and ordered proxy-route
853    /// fallback. It also installs the default proxy-route failure cache. The
854    /// only omitted component is the pool itself.
855    pub fn without_connection_pool(self) -> DefaultConnectionBuilder<T::Connection>
856    where
857        T: ConnectorService<ConnectRequest>,
858    {
859        finish_without_connection_pool(
860            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
861        )
862    }
863
864    /// Use the default connection pool for this [`super::EasyHttpWebClient`]
865    ///
866    /// This will create a [`MultiplexPool`](crate::net::client::pool::MultiplexPool)
867    /// using the provided limits and will use
868    /// [`super::HttpConnIdentifier`] to group connections on
869    /// protocol, authority, selected route, physical transport, any HTTP
870    /// version requirement, and the selected plaintext HTTP proxy mode. This
871    /// keeps forward-proxy connections separate from CONNECT tunnels to the
872    /// same proxy. The default proxy-route failure cache is installed behind
873    /// the pool, so reusable connections bypass negative-cache checks.
874    ///
875    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
876    ///
877    /// If you need a different pool or custom way to group connection you can
878    /// use [`EasyHttpConnectorBuilder::with_custom_connection_pool()`] to provide
879    /// you own.
880    ///
881    /// This also applies a [`RequestVersionAdapter`] layer to make sure that request versions
882    /// are adapted when pooled connections are used, which you almost always need, but in case
883    /// that is unwanted, you can use [`Self::with_custom_connection_pool`] instead.
884    pub fn try_with_connection_pool(
885        self,
886        config: HttpPooledConnectorConfig,
887    ) -> Result<DefaultConnectionPoolBuilder<T::Connection>, BoxError>
888    where
889        T: ConnectorService<ConnectRequest>,
890    {
891        finish_with_connection_pool(
892            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
893            config,
894        )
895    }
896
897    /// Use Rama's default connection pool and default proxy-route failure
898    /// cache.
899    ///
900    /// This operation is infallible because Rama's built-in pool limits are
901    /// known to be valid and non-zero.
902    pub fn with_default_connection_pool(self) -> DefaultConnectionPoolBuilder<T::Connection>
903    where
904        T: ConnectorService<ConnectRequest>,
905    {
906        finish_with_default_connection_pool(
907            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
908        )
909    }
910
911    /// Configure this client to use the provided [`Pool`] and [`ReqToConnId`]
912    ///
913    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
914    ///
915    /// Warning: this does not apply a [`RequestVersionAdapter`] layer to make sure that request versions
916    /// are adapted when pooled connections are used, which you almost always. This should be manually added
917    /// by using [`Self::with_custom_connector`] after configuring this pool and providing a [`RequestVersionAdapter`] there.
918    /// Unlike [`Self::try_with_connection_pool`], this fully generic method also does not install the HTTP
919    /// connect-request adapter or proxy-route connector. It installs the default proxy-route failure cache behind
920    /// the custom pool. Callers that want route-aware fallback around a custom pool can compose those layers
921    /// explicitly around their [`PooledConnector`].
922    ///
923    /// When the connector supports plaintext HTTP through an HTTP proxy, the
924    /// custom [`ReqToConnId`] must keep ordinary forward-proxy connections
925    /// separate from CONNECT tunnels to the same proxy. Rama's
926    /// [`super::HttpConnIdentifier`] includes this
927    /// distinction automatically.
928    ///
929    /// [`Pool`]: rama_net::client::pool::Pool
930    /// [`ReqToConnId`]: rama_net::client::pool::ReqToConnID
931    ///
932    /// The supplied `ReqToConnID` partitions routes and application protocols.
933    /// The pool must also honor connection-owned reuse policies published by
934    /// transport connectors. Return a non-reusable `ConnID` to require a fresh
935    /// connection that must not return to the pool.
936    pub fn with_custom_connection_pool<P, R>(
937        self,
938        pool: P,
939        req_to_conn_id: R,
940        wait_for_pool_timeout: Option<Duration>,
941    ) -> EasyHttpConnectorBuilder<
942        PooledConnector<ProxyRouteFailureCacheConnector<ErasedConnector<T::Connection>>, P, R>,
943        PoolStage,
944    >
945    where
946        T: ConnectorService<ConnectRequest>,
947    {
948        finish_with_custom_connection_pool(
949            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
950            pool,
951            req_to_conn_id,
952            wait_for_pool_timeout,
953        )
954    }
955}
956
957impl<T, D> EasyHttpConnectorBuilder<T, HttpStage<false>, D> {
958    /// Finish the proxy-free HTTP connector stack without a connection pool.
959    ///
960    /// No proxy-route failure cache is installed. Call
961    /// [`Self::with_proxy_route_failure_cache`] before this method to
962    /// explicitly add one for a custom transport.
963    pub fn without_connection_pool(self) -> ConfiguredConnectionBuilder<T>
964    where
965        T: ConnectorService<ConnectRequest>,
966    {
967        finish_without_connection_pool(self)
968    }
969
970    /// Use the default connection pool without a proxy-route failure cache.
971    pub fn try_with_connection_pool(
972        self,
973        config: HttpPooledConnectorConfig,
974    ) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
975    where
976        T: ConnectorService<ConnectRequest>,
977    {
978        finish_with_connection_pool(self, config)
979    }
980
981    /// Use Rama's known-valid default connection pool configuration without a
982    /// proxy-route failure cache.
983    pub fn with_default_connection_pool(self) -> ConfiguredConnectionPoolBuilder<T>
984    where
985        T: ConnectorService<ConnectRequest>,
986    {
987        finish_with_default_connection_pool(self)
988    }
989
990    /// Use a custom connection pool without a proxy-route failure cache.
991    ///
992    /// The supplied `ReqToConnID` partitions routes and application protocols.
993    /// The pool must also honor connection-owned reuse policies published by
994    /// transport connectors. Return a non-reusable `ConnID` to require a fresh
995    /// connection that must not return to the pool.
996    pub fn with_custom_connection_pool<P, R>(
997        self,
998        pool: P,
999        req_to_conn_id: R,
1000        wait_for_pool_timeout: Option<Duration>,
1001    ) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
1002        finish_with_custom_connection_pool(self, pool, req_to_conn_id, wait_for_pool_timeout)
1003    }
1004}
1005
1006impl<T, D> EasyHttpConnectorBuilder<T, ProxyRouteFailureCacheStage, D> {
1007    /// Finish the default HTTP connector stack without a connection pool.
1008    pub fn without_connection_pool(self) -> ConfiguredConnectionBuilder<T>
1009    where
1010        T: ConnectorService<ConnectRequest>,
1011    {
1012        finish_without_connection_pool(self)
1013    }
1014
1015    /// Use the default connection pool with the selected failure-cache policy.
1016    pub fn try_with_connection_pool(
1017        self,
1018        config: HttpPooledConnectorConfig,
1019    ) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
1020    where
1021        T: ConnectorService<ConnectRequest>,
1022    {
1023        finish_with_connection_pool(self, config)
1024    }
1025
1026    /// Use Rama's known-valid default connection pool configuration with the
1027    /// selected failure-cache policy.
1028    pub fn with_default_connection_pool(self) -> ConfiguredConnectionPoolBuilder<T>
1029    where
1030        T: ConnectorService<ConnectRequest>,
1031    {
1032        finish_with_default_connection_pool(self)
1033    }
1034
1035    /// Use a custom connection pool with the selected failure-cache policy.
1036    ///
1037    /// For a proxy-capable connector, the custom
1038    /// [`ReqToConnID`](rama_net::client::pool::ReqToConnID) must partition
1039    /// plaintext HTTP forward-proxy connections from CONNECT tunnels to the
1040    /// same proxy. [`super::HttpConnIdentifier`] does so by
1041    /// default.
1042    ///
1043    /// The supplied `ReqToConnID` partitions routes and application protocols.
1044    /// The pool must also honor connection-owned reuse policies published by
1045    /// transport connectors. Return a non-reusable `ConnID` to require a fresh
1046    /// connection that must not return to the pool.
1047    pub fn with_custom_connection_pool<P, R>(
1048        self,
1049        pool: P,
1050        req_to_conn_id: R,
1051        wait_for_pool_timeout: Option<Duration>,
1052    ) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
1053        finish_with_custom_connection_pool(self, pool, req_to_conn_id, wait_for_pool_timeout)
1054    }
1055}
1056
1057impl<T> EasyHttpConnectorBuilder<T, PoolStage> {
1058    /// Build a [`super::EasyHttpWebClient`] using the currently configured connector
1059    pub fn build_client<Body, ModifiedBody, ConnResponse>(
1060        self,
1061    ) -> super::EasyHttpWebClient<Body, T::Output, ()>
1062    where
1063        Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
1064        ModifiedBody:
1065            StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
1066        T: Service<
1067                Request<Body>,
1068                Output = EstablishedClientConnection<ConnResponse, Request<ModifiedBody>>,
1069                Error: Into<BoxError>,
1070            >,
1071        ConnResponse: ExtensionsRef,
1072    {
1073        super::EasyHttpWebClient::new(self.connector)
1074    }
1075}
1076
1077impl<T, S, D> EasyHttpConnectorBuilder<T, S, D> {
1078    /// Build a connector from the currently configured setup
1079    pub fn build_connector(self) -> T {
1080        self.connector
1081    }
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::*;
1087    use crate::http::{Body, client::HttpConnIdentifier};
1088    use rama_net::{
1089        Protocol,
1090        address::HostWithPort,
1091        client::{
1092            ConnectRequest,
1093            pool::{ConnID as _, ReqToConnID},
1094        },
1095    };
1096    use rama_tls::client::{ServerVerifyMode, TlsPoolId, TlsServerVerify};
1097    use rama_utils::octets::kib;
1098
1099    use rama_core::layer::layer_fn;
1100    #[cfg(any(
1101        feature = "boring",
1102        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1103    ))]
1104    use {
1105        crate::http::client::Http3Connector,
1106        crate::quic::Endpoint,
1107        rama_core::futures::{Stream, stream},
1108        rama_http::layer::http_service::HttpServiceAttempt,
1109        rama_net::{
1110            address::{Domain, SocketAddress},
1111            client::ConnectionErrorKind,
1112            http::TargetHttpVersion as RequestedVersion,
1113        },
1114        std::{
1115            net::{Ipv4Addr, Ipv6Addr, SocketAddr},
1116            sync::{
1117                Arc,
1118                atomic::{AtomicUsize, Ordering},
1119            },
1120        },
1121    };
1122    #[cfg(feature = "tls")]
1123    use {
1124        crate::http::{
1125            HeaderMap, Version,
1126            conn::{HttpOrigin, SelectedHttpService},
1127            header::ALT_SVC,
1128        },
1129        rama_core::{ServiceInput, service::service_fn},
1130        rama_net::{http::TargetHttpVersion, tls::ApplicationProtocol},
1131        rama_tls::{
1132            ProtocolVersion,
1133            client::{NegotiatedTlsParameters, TlsServerAuthentication},
1134        },
1135        std::convert::Infallible,
1136    };
1137
1138    fn assert_future_budget(connector: &impl Service<Request>, name: &str) {
1139        let request = Request::builder()
1140            .uri("https://example.com/")
1141            .body(Body::empty())
1142            .unwrap();
1143        let future = connector.serve(request);
1144        let size = std::mem::size_of_val(&future);
1145        assert!(
1146            size <= kib(64),
1147            "{name} connector future is {size} bytes; connector error adapters must not duplicate nested future storage"
1148        );
1149    }
1150
1151    #[tokio::test]
1152    async fn default_connector_futures_stay_within_stack_budget() {
1153        let builder = || {
1154            EasyHttpConnectorBuilder::new()
1155                .with_default_transport_connector()
1156                .without_dns_connector()
1157                .without_tls_proxy_support()
1158                .without_proxy_support()
1159        };
1160        let plain = builder()
1161            .without_tls_support()
1162            .with_default_http_connector::<Body>(Executor::default())
1163            .with_default_connection_pool()
1164            .build_connector();
1165        assert_future_budget(&plain, "plain pooled");
1166        #[cfg(feature = "rustls")]
1167        {
1168            let pooled = builder()
1169                .with_tls_support_using_rustls(TlsClientConfig::new())
1170                .with_default_http_connector::<Body>(Executor::default())
1171                .with_default_connection_pool()
1172                .build_connector();
1173            assert_future_budget(&pooled, "rustls pooled");
1174            let unpooled = builder()
1175                .with_tls_support_using_rustls(TlsClientConfig::new())
1176                .with_default_http_connector::<Body>(Executor::default())
1177                .without_connection_pool()
1178                .build_connector();
1179            assert_future_budget(&unpooled, "rustls unpooled");
1180        }
1181    }
1182
1183    #[cfg(feature = "tls")]
1184    #[tokio::test]
1185    async fn custom_http_connector_discovers_h3_without_capability_configuration() {
1186        let cache = AltSvcCache::default();
1187        let origin =
1188            HttpOrigin::new(Protocol::HTTPS, HostWithPort::example_domain_https()).unwrap();
1189        let mut headers = HeaderMap::new();
1190        headers.insert(ALT_SVC, "h3=\":8443\"".parse().unwrap());
1191        cache.record(&origin, &headers, Duration::ZERO);
1192
1193        let builder = EasyHttpConnectorBuilder::new()
1194            .with_custom_transport_connector(())
1195            .without_dns_connector()
1196            .without_tls_proxy_support()
1197            .without_proxy_support()
1198            .without_tls_support()
1199            .with_custom_http_connector(layer_fn(|()| {
1200                service_fn(|input: ConnectRequest| async move {
1201                    let version = input
1202                        .extensions()
1203                        .get_ref::<TargetHttpVersion>()
1204                        .map_or(Version::HTTP_2, |version| version.0);
1205                    let conn = ServiceInput::new(());
1206                    conn.extensions.insert(TargetHttpVersion(version));
1207                    conn.extensions
1208                        .insert(TlsServerAuthentication(Some(input.authority.host.clone())));
1209                    conn.extensions.insert(NegotiatedTlsParameters {
1210                        protocol_version: ProtocolVersion::TLSv1_3,
1211                        application_layer_protocol: Some(if version == Version::HTTP_3 {
1212                            ApplicationProtocol::HTTP_3
1213                        } else {
1214                            ApplicationProtocol::HTTP_2
1215                        }),
1216                        peer_certificate_chain: None,
1217                        server_name: None,
1218                        resumed: None,
1219                    });
1220                    Ok::<_, Infallible>(EstablishedClientConnection { input, conn })
1221                })
1222            }))
1223            .without_connection_pool()
1224            .with_alt_svc_cache(cache.clone());
1225        let connector = builder.build_connector();
1226        for _ in 0..2 {
1227            let established = connector
1228                .serve(
1229                    Request::builder()
1230                        .uri("https://example.com/")
1231                        .body(Body::empty())
1232                        .unwrap(),
1233                )
1234                .await
1235                .unwrap();
1236            assert!(
1237                established
1238                    .input
1239                    .extensions()
1240                    .contains::<SelectedHttpService>()
1241            );
1242            assert_eq!(established.input.version(), Version::HTTP_3);
1243            assert_eq!(
1244                established
1245                    .input
1246                    .extensions()
1247                    .get_ref::<SelectedHttpService>()
1248                    .unwrap()
1249                    .candidate
1250                    .target
1251                    .port,
1252                8443
1253            );
1254        }
1255    }
1256
1257    #[test]
1258    fn custom_tls_proxy_layer_requires_no_builtin_provider() {
1259        EasyHttpConnectorBuilder::new()
1260            .with_custom_transport_connector(())
1261            .without_dns_connector()
1262            .with_custom_tls_proxy_connector(layer_fn(|inner| inner))
1263            .build_connector();
1264    }
1265
1266    #[test]
1267    fn custom_fixed_policy_pool_respects_explicit_override_identity() {
1268        let identifier = HttpConnIdentifier::default();
1269        let input = ConnectRequest::new(HostWithPort::example_domain_https());
1270        let fixed = identifier.id(&input).unwrap();
1271        assert!(fixed.is_reusable());
1272        let policy = TlsPoolId::builder()
1273            .with_verify(&TlsServerVerify(ServerVerifyMode::Disable))
1274            .build()
1275            .unwrap();
1276        input.extensions.insert(policy);
1277        let overridden = identifier.id(&input).unwrap();
1278        assert!(overridden.is_reusable());
1279        assert_ne!(fixed, overridden);
1280        assert_eq!(overridden, identifier.id(&input).unwrap());
1281        input.extensions.insert(TlsPoolId::non_reusable());
1282        assert!(!identifier.id(&input).unwrap().is_reusable());
1283    }
1284
1285    #[cfg(any(
1286        feature = "boring",
1287        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1288    ))]
1289    #[derive(Clone)]
1290    struct RecordingResolver {
1291        calls: Arc<AtomicUsize>,
1292        loopback: bool,
1293    }
1294
1295    #[cfg(any(
1296        feature = "boring",
1297        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1298    ))]
1299    impl DnsAddressResolver for RecordingResolver {
1300        type Error = Infallible;
1301
1302        fn lookup_ipv4(
1303            &self,
1304            domain: Domain,
1305        ) -> impl Stream<Item = Result<Ipv4Addr, Self::Error>> + Send + '_ {
1306            assert_eq!(domain.as_str(), "private.invalid");
1307            self.calls.fetch_add(1, Ordering::Relaxed);
1308            stream::iter(self.loopback.then_some(Ok(Ipv4Addr::LOCALHOST)))
1309        }
1310
1311        fn lookup_ipv6(
1312            &self,
1313            domain: Domain,
1314        ) -> impl Stream<Item = Result<Ipv6Addr, Self::Error>> + Send + '_ {
1315            assert_eq!(domain.as_str(), "private.invalid");
1316            self.calls.fetch_add(1, Ordering::Relaxed);
1317            stream::iter(self.loopback.then_some(Ok(Ipv6Addr::LOCALHOST)))
1318        }
1319    }
1320
1321    #[cfg(any(
1322        feature = "boring",
1323        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1324    ))]
1325    struct RecordingDnsLayer {
1326        resolver: RecordingResolver,
1327        applied: Arc<AtomicUsize>,
1328    }
1329
1330    #[cfg(any(
1331        feature = "boring",
1332        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1333    ))]
1334    impl<S> Layer<S> for RecordingDnsLayer {
1335        type Service = DnsConnector<S, RecordingResolver>;
1336
1337        fn layer(&self, inner: S) -> Self::Service {
1338            self.applied.fetch_add(1, Ordering::Relaxed);
1339            DnsConnector::with_resolver(inner, self.resolver.clone())
1340        }
1341    }
1342
1343    #[cfg(any(
1344        feature = "boring",
1345        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1346    ))]
1347    #[tokio::test]
1348    async fn built_in_quic_uses_the_configured_dns_resolver() {
1349        let calls = Arc::new(AtomicUsize::new(0));
1350        let applied = Arc::new(AtomicUsize::new(0));
1351        let executor = Executor::default();
1352        let h3 = Http3Connector::builder(executor.clone())
1353            .build()
1354            .await
1355            .unwrap();
1356        let connector = EasyHttpConnectorBuilder::new()
1357            .with_default_transport_connector()
1358            .with_dns_connector(RecordingDnsLayer {
1359                resolver: RecordingResolver {
1360                    calls: calls.clone(),
1361                    loopback: false,
1362                },
1363                applied: applied.clone(),
1364            })
1365            .without_tls_proxy_support()
1366            .without_proxy_support()
1367            .without_tls_support()
1368            .with_default_http_connector::<Body>(executor)
1369            .with_http3_support(h3)
1370            .build_connector();
1371        let input = ConnectRequest::new("private.invalid:443".parse().unwrap())
1372            .with_application_protocol(Protocol::HTTPS);
1373        input.extensions.insert(RequestedVersion(Version::HTTP_3));
1374        let error = connector.serve(input).await.err().unwrap();
1375        assert_eq!(error.kind(), ConnectionErrorKind::Unavailable);
1376        assert!(calls.load(Ordering::Relaxed) > 0);
1377        assert_eq!(
1378            applied.load(Ordering::Relaxed),
1379            2,
1380            "the same DNS layer must wrap both transports"
1381        );
1382    }
1383
1384    #[cfg(any(
1385        feature = "boring",
1386        all(feature = "rustls", any(feature = "ring", feature = "aws-lc"))
1387    ))]
1388    #[tokio::test]
1389    async fn ipv4_quic_endpoint_skips_ipv6_candidates_without_terminal_failure() {
1390        let executor = Executor::default();
1391        let endpoint = Endpoint::bind_client(executor.clone(), SocketAddress::local_ipv4(0))
1392            .await
1393            .unwrap();
1394        let blackhole = tokio::net::UdpSocket::bind(SocketAddr::from(SocketAddress::local_ipv4(0)))
1395            .await
1396            .unwrap();
1397        let connector = Http3Connector::builder(executor)
1398            .with_endpoint(endpoint.clone())
1399            .build()
1400            .await
1401            .unwrap();
1402        let calls = Arc::new(AtomicUsize::new(0));
1403        let connector = DnsConnector::with_resolver(
1404            connector,
1405            RecordingResolver {
1406                calls: calls.clone(),
1407                loopback: true,
1408            },
1409        );
1410        let input = ConnectRequest::new(
1411            format!("private.invalid:{}", blackhole.local_addr().unwrap().port())
1412                .parse()
1413                .unwrap(),
1414        )
1415        .with_application_protocol(Protocol::HTTPS);
1416        let attempt = Arc::new(HttpServiceAttempt::default());
1417        input.extensions.insert_arc(attempt.clone());
1418        tokio::time::timeout(Duration::from_millis(500), connector.serve(input))
1419            .await
1420            .unwrap_err();
1421        assert!(calls.load(Ordering::Relaxed) >= 2);
1422        assert!(
1423            !attempt.failed(),
1424            "a mismatched candidate family must not turn the IPv4 timeout into an authentication failure"
1425        );
1426        endpoint.close(0u32, b"test complete");
1427    }
1428}