Skip to main content

rama/http/client/
builder.rs

1#[cfg(any(feature = "rustls", feature = "boring"))]
2use rama_core::layer::AddInputExtension;
3use rama_core::rt::Executor;
4
5use super::{
6    HttpConnectRequestAdapter, HttpConnector, HttpPooledConnector, HttpPooledConnectorConfig,
7};
8#[cfg(any(feature = "rustls", feature = "boring"))]
9use crate::http::conn::FallbackHttpVersion;
10use crate::{
11    Layer, Service,
12    dns::client::{DnsConnectorLayer, resolver::DnsAddressResolver},
13    error::BoxError,
14    extensions::ExtensionsRef,
15    http::{
16        Request, StreamingBody, client::proxy::layer::HttpProxyConnector,
17        layer::version_adapter::RequestVersionAdapter,
18    },
19    net::client::{
20        ConnectRequest, ConnectionError, ConnectorService, EstablishedClientConnection,
21        ProxyRouteFailureCache, ProxyRouteFailureCacheConnector, ProxyRoutesConnector,
22        pool::PooledConnector,
23    },
24    service::BoxService,
25    tcp::client::service::TcpConnector,
26};
27use std::{marker::PhantomData, time::Duration};
28
29#[cfg(feature = "boring")]
30use crate::tls::boring::client as boring_client;
31
32#[cfg(any(feature = "rustls", feature = "boring"))]
33use crate::tls::client::TlsClientConfig;
34#[cfg(feature = "rustls")]
35use crate::tls::rustls::client as rustls_client;
36
37#[cfg(feature = "socks5")]
38use crate::{http::client::proxy_connector::ProxyConnector, proxy::socks5::Socks5ProxyConnector};
39
40/// Builder that is designed to easily create a connector for [`super::EasyHttpWebClient`] from most basic use cases
41#[derive(Default)]
42pub struct EasyHttpConnectorBuilder<C = (), S = ()> {
43    connector: C,
44    _phantom: PhantomData<S>,
45}
46
47#[non_exhaustive]
48#[derive(Debug)]
49pub struct TransportStage;
50#[non_exhaustive]
51#[derive(Debug)]
52pub struct DnsStage;
53#[non_exhaustive]
54#[derive(Debug)]
55pub struct ProxyTunnelStage<const TLS_PROXY: bool = true>;
56#[non_exhaustive]
57#[derive(Debug)]
58pub struct ProxyStage<const PROXY: bool = true>;
59#[non_exhaustive]
60#[derive(Debug)]
61pub struct TlsStage<const PROXY: bool = true>;
62#[non_exhaustive]
63#[derive(Debug)]
64pub struct HttpStage<const PROXY: bool = true>;
65#[non_exhaustive]
66#[derive(Debug)]
67pub struct ProxyRouteFailureCacheStage;
68#[non_exhaustive]
69#[derive(Debug)]
70pub struct PoolStage;
71
72impl EasyHttpConnectorBuilder {
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    #[must_use]
79    pub fn with_default_transport_connector(
80        self,
81    ) -> EasyHttpConnectorBuilder<TcpConnector, TransportStage> {
82        let connector = TcpConnector::default();
83        EasyHttpConnectorBuilder {
84            connector,
85            _phantom: PhantomData,
86        }
87    }
88
89    /// Add a custom transport connector that will be used by this client for the transport layer
90    pub fn with_custom_transport_connector<C>(
91        self,
92        connector: C,
93    ) -> EasyHttpConnectorBuilder<C, TransportStage> {
94        EasyHttpConnectorBuilder {
95            connector,
96            _phantom: PhantomData,
97        }
98    }
99}
100
101impl<T, Stage> EasyHttpConnectorBuilder<T, Stage> {
102    /// Add a custom connector to this Stage.
103    ///
104    /// Adding a custom connector to a stage will not change the state
105    /// so this can be used to modify behaviour at a specific stage.
106    pub fn with_custom_connector<L>(
107        self,
108        connector_layer: L,
109    ) -> EasyHttpConnectorBuilder<L::Service, Stage>
110    where
111        L: Layer<T>,
112    {
113        self.map_connector(|c| connector_layer.into_layer(c))
114    }
115
116    /// Map the current connector using the given fn.
117    ///
118    /// Mapping a connector to a stage will not change the state
119    /// so this can be used to modify behaviour at a specific stage.
120    pub fn map_connector<T2>(
121        self,
122        map_fn: impl FnOnce(T) -> T2,
123    ) -> EasyHttpConnectorBuilder<T2, Stage> {
124        let connector = map_fn(self.connector);
125        EasyHttpConnectorBuilder {
126            connector,
127            _phantom: PhantomData,
128        }
129    }
130}
131
132impl<T> EasyHttpConnectorBuilder<T, TransportStage> {
133    /// Add the default DNS connector layer using the global DNS resolver.
134    pub fn with_default_dns_connector(
135        self,
136    ) -> EasyHttpConnectorBuilder<crate::dns::client::DnsConnector<T>, DnsStage> {
137        self.with_dns_connector(DnsConnectorLayer::new())
138    }
139
140    /// Add a DNS connector layer using a custom [`DnsAddressResolver`].
141    pub fn with_dns_address_resolver<R: DnsAddressResolver + Clone>(
142        self,
143        resolver: R,
144    ) -> EasyHttpConnectorBuilder<crate::dns::client::DnsConnector<T, R>, DnsStage> {
145        self.with_dns_connector(DnsConnectorLayer::with_resolver(resolver))
146    }
147
148    /// Don't add a DNS connector
149    ///
150    /// Warning: this means the transport connector will only work if the configured target
151    /// is using an IP address and not a DNS address
152    pub fn without_dns_connector(
153        self,
154    ) -> EasyHttpConnectorBuilder<crate::dns::client::DnsConnector<T>, DnsStage> {
155        self.with_dns_connector(DnsConnectorLayer::new())
156    }
157
158    /// Add a custom DNS connector layer.
159    pub fn with_dns_connector<L>(
160        self,
161        connector_layer: L,
162    ) -> EasyHttpConnectorBuilder<L::Service, DnsStage>
163    where
164        L: Layer<T>,
165    {
166        let connector = connector_layer.into_layer(self.connector);
167        EasyHttpConnectorBuilder {
168            connector,
169            _phantom: PhantomData,
170        }
171    }
172}
173
174impl<T> EasyHttpConnectorBuilder<T, DnsStage> {
175    #[cfg(any(feature = "rustls", feature = "boring"))]
176    /// Add a custom proxy TLS connector used to establish TLS to an HTTPS proxy.
177    ///
178    /// The layer must attach `rama_tls::client::NegotiatedTlsParameters` to
179    /// the established connection. Rama uses this as positive proof that TLS
180    /// was negotiated and to select the proxy-side HTTP version; missing
181    /// evidence fails closed before proxy HTTP is sent.
182    pub fn with_custom_tls_proxy_connector<L>(
183        self,
184        connector_layer: L,
185    ) -> EasyHttpConnectorBuilder<L::Service, ProxyTunnelStage<true>>
186    where
187        L: Layer<T>,
188    {
189        let connector = connector_layer.into_layer(self.connector);
190        EasyHttpConnectorBuilder {
191            connector,
192            _phantom: PhantomData,
193        }
194    }
195
196    #[cfg(feature = "boring")]
197    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
198    /// Support a tls tunnel to the proxy itself using boringssl
199    ///
200    /// Note that a tls proxy is not needed to make a https connection
201    /// to the final target. It only has an influence on the initial connection
202    /// to the proxy itself
203    pub fn with_tls_proxy_support_using_boringssl(
204        self,
205    ) -> EasyHttpConnectorBuilder<
206        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
207        ProxyTunnelStage<true>,
208    > {
209        let connector = boring_client::TlsConnector::tunnel(self.connector, None);
210        EasyHttpConnectorBuilder {
211            connector,
212            _phantom: PhantomData,
213        }
214    }
215
216    #[cfg(feature = "boring")]
217    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
218    /// Support a tls tunnel to the proxy itself using boringssl and the provided config
219    ///
220    /// Note that a tls proxy is not needed to make a https connection
221    /// to the final target. It only has an influence on the initial connection
222    /// to the proxy itself
223    pub fn with_tls_proxy_support_using_boringssl_config(
224        self,
225        config: TlsClientConfig,
226    ) -> EasyHttpConnectorBuilder<
227        boring_client::TlsConnector<T, boring_client::ConnectorKindTunnel>,
228        ProxyTunnelStage<true>,
229    > {
230        let connector =
231            boring_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
232        EasyHttpConnectorBuilder {
233            connector,
234            _phantom: PhantomData,
235        }
236    }
237
238    #[cfg(feature = "rustls")]
239    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
240    /// Support a tls tunnel to the proxy itself using rustls
241    ///
242    /// Note that a tls proxy is not needed to make a https connection
243    /// to the final target. It only has an influence on the initial connection
244    /// to the proxy itself
245    pub fn with_tls_proxy_support_using_rustls(
246        self,
247    ) -> EasyHttpConnectorBuilder<
248        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
249        ProxyTunnelStage<true>,
250    > {
251        let connector = rustls_client::TlsConnector::tunnel(self.connector, None);
252
253        EasyHttpConnectorBuilder {
254            connector,
255            _phantom: PhantomData,
256        }
257    }
258
259    #[cfg(feature = "rustls")]
260    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
261    /// Support a tls tunnel to the proxy itself using rustls and the provided config
262    ///
263    /// Note that a tls proxy is not needed to make a https connection
264    /// to the final target. It only has an influence on the initial connection
265    /// to the proxy itself
266    pub fn with_tls_proxy_support_using_rustls_config(
267        self,
268        config: TlsClientConfig,
269    ) -> EasyHttpConnectorBuilder<
270        rustls_client::TlsConnector<T, rustls_client::ConnectorKindTunnel>,
271        ProxyTunnelStage<true>,
272    > {
273        let connector =
274            rustls_client::TlsConnector::tunnel(self.connector, None).with_base_config(config);
275
276        EasyHttpConnectorBuilder {
277            connector,
278            _phantom: PhantomData,
279        }
280    }
281
282    /// Don't support a tls tunnel to the proxy itself
283    ///
284    /// Note that a tls proxy is not needed to make a https connection
285    /// to the final target. It only has an influence on the initial connection
286    /// to the proxy itself
287    pub fn without_tls_proxy_support(self) -> EasyHttpConnectorBuilder<T, ProxyTunnelStage<false>> {
288        EasyHttpConnectorBuilder {
289            connector: self.connector,
290            _phantom: PhantomData,
291        }
292    }
293}
294
295impl<T, const TLS_PROXY: bool> EasyHttpConnectorBuilder<T, ProxyTunnelStage<TLS_PROXY>> {
296    /// Add a custom proxy connector that will be used by this client
297    pub fn with_custom_proxy_connector<L>(
298        self,
299        connector_layer: L,
300    ) -> EasyHttpConnectorBuilder<L::Service, ProxyStage<true>>
301    where
302        L: Layer<T>,
303    {
304        let connector = connector_layer.into_layer(self.connector);
305        EasyHttpConnectorBuilder {
306            connector,
307            _phantom: PhantomData,
308        }
309    }
310
311    #[cfg(not(feature = "socks5"))]
312    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
313    ///
314    /// Note that a tls proxy is not needed to make a https connection
315    /// to the final target. It only has an influence on the initial connection
316    /// to the proxy itself
317    ///
318    /// Note to also enable socks proxy support enable feature `socks5`
319    ///
320    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
321    pub fn with_proxy_support(
322        self,
323    ) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage<true>> {
324        self.with_http_proxy_support()
325    }
326
327    /// Add support for usage of a http(s) [`ProxyAddress`] to this client
328    ///
329    /// Note that a tls proxy is not needed to make a https connection
330    /// to the final target. It only has an influence on the initial connection
331    /// to the proxy itself
332    ///
333    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
334    pub fn with_http_proxy_support(
335        self,
336    ) -> EasyHttpConnectorBuilder<HttpProxyConnector<T>, ProxyStage<true>> {
337        let connector =
338            HttpProxyConnector::optional(self.connector).with_tls_proxy_support(TLS_PROXY);
339
340        EasyHttpConnectorBuilder {
341            connector,
342            _phantom: PhantomData,
343        }
344    }
345
346    #[cfg(feature = "socks5")]
347    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
348    /// Add support for usage of a socks5(h) [`ProxyAddress`] to this client
349    ///
350    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
351    pub fn with_socks5_proxy_support(
352        self,
353    ) -> EasyHttpConnectorBuilder<Socks5ProxyConnector<T>, ProxyStage<true>> {
354        let connector = Socks5ProxyConnector::optional(self.connector);
355
356        EasyHttpConnectorBuilder {
357            connector,
358            _phantom: PhantomData,
359        }
360    }
361
362    /// Make a client without proxy support
363    pub fn without_proxy_support(self) -> EasyHttpConnectorBuilder<T, ProxyStage<false>> {
364        EasyHttpConnectorBuilder {
365            connector: self.connector,
366            _phantom: PhantomData,
367        }
368    }
369}
370
371impl<T: Clone, const TLS_PROXY: bool> EasyHttpConnectorBuilder<T, ProxyTunnelStage<TLS_PROXY>> {
372    #[cfg(feature = "socks5")]
373    #[cfg_attr(docsrs, doc(cfg(feature = "socks5")))]
374    /// Add support for usage of a http(s) and socks5(h) [`ProxyAddress`] to this client
375    ///
376    /// Note that a tls proxy is not needed to make a https connection
377    /// to the final target. It only has an influence on the initial connection
378    /// to the proxy itself
379    ///
380    /// [`ProxyAddress`]: rama_net::address::ProxyAddress
381    pub fn with_proxy_support(
382        self,
383    ) -> EasyHttpConnectorBuilder<ProxyConnector<T>, ProxyStage<true>> {
384        use rama_http_backend::client::proxy::layer::HttpProxyConnectorLayer;
385        use rama_socks5::Socks5ProxyConnectorLayer;
386
387        let connector = ProxyConnector::optional(
388            self.connector,
389            Socks5ProxyConnectorLayer::required(),
390            HttpProxyConnectorLayer::required().with_tls_proxy_support(TLS_PROXY),
391        );
392
393        EasyHttpConnectorBuilder {
394            connector,
395            _phantom: PhantomData,
396        }
397    }
398}
399
400impl<T, const PROXY: bool> EasyHttpConnectorBuilder<T, ProxyStage<PROXY>> {
401    #[cfg(any(feature = "rustls", feature = "boring"))]
402    /// Add a custom tls connector that will be used by the client
403    ///
404    /// The final HTTP transition applies a [`RequestVersionAdapter`] outside
405    /// the complete connection attempt so it can apply the negotiated version
406    /// to the original HTTP request.
407    pub fn with_custom_tls_connector<L>(
408        self,
409        connector_layer: L,
410    ) -> EasyHttpConnectorBuilder<L::Service, TlsStage<PROXY>>
411    where
412        L: Layer<T>,
413    {
414        let connector = connector_layer.into_layer(self.connector);
415
416        EasyHttpConnectorBuilder {
417            connector,
418            _phantom: PhantomData,
419        }
420    }
421
422    #[cfg(feature = "boring")]
423    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
424    /// Support https connections by using boringssl for tls
425    ///
426    /// The final HTTP transition automatically applies the HTTP version
427    /// negotiated through TLS to the original request.
428    pub fn with_tls_support_using_boringssl(
429        self,
430        config: TlsClientConfig,
431    ) -> EasyHttpConnectorBuilder<boring_client::TlsConnector<T>, TlsStage<PROXY>> {
432        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
433
434        EasyHttpConnectorBuilder {
435            connector,
436            _phantom: PhantomData,
437        }
438    }
439
440    #[cfg(feature = "boring")]
441    #[cfg_attr(docsrs, doc(cfg(feature = "boring")))]
442    /// Same as [`Self::with_tls_support_using_boringssl`] but also
443    /// setting a fallback HTTP version in case no ALPN is negotiated.
444    /// The fallback does not constrain the ALPN protocols offered by TLS.
445    ///
446    /// This is a fairly important detail for proxy purposes given otherwise
447    /// you might come in situations where the ingress traffic is negotiated to `h2`,
448    /// but the egress traffic has no negotiation which would without a default
449    /// http version remain on h2... In such a case you can get failed
450    /// requests if the egress server does not handle multiple http versions.
451    pub fn with_tls_support_using_boringssl_and_default_http_version(
452        self,
453        config: TlsClientConfig,
454        default_http_version: rama_http::Version,
455    ) -> EasyHttpConnectorBuilder<
456        AddInputExtension<boring_client::TlsConnector<T>, FallbackHttpVersion>,
457        TlsStage<PROXY>,
458    > {
459        let connector = boring_client::TlsConnector::auto(self.connector).with_base_config(config);
460        let connector =
461            AddInputExtension::new(connector, FallbackHttpVersion(default_http_version))
462                .with_overwrite(false);
463
464        EasyHttpConnectorBuilder {
465            connector,
466            _phantom: PhantomData,
467        }
468    }
469
470    #[cfg(feature = "rustls")]
471    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
472    /// Support https connections by using ruslts for tls
473    ///
474    /// The final HTTP transition automatically applies the HTTP version
475    /// negotiated through TLS to the original request.
476    pub fn with_tls_support_using_rustls(
477        self,
478        config: TlsClientConfig,
479    ) -> EasyHttpConnectorBuilder<rustls_client::TlsConnector<T>, TlsStage<PROXY>> {
480        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
481
482        EasyHttpConnectorBuilder {
483            connector,
484            _phantom: PhantomData,
485        }
486    }
487
488    #[cfg(feature = "rustls")]
489    #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
490    /// Same as [`Self::with_tls_support_using_rustls`] but also
491    /// setting a fallback HTTP version in case no ALPN is negotiated.
492    /// The fallback does not constrain the ALPN protocols offered by TLS.
493    ///
494    /// This is a fairly important detail for proxy purposes given otherwise
495    /// you might come in situations where the ingress traffic is negotiated to `h2`,
496    /// but the egress traffic has no negotiation which would without a default
497    /// http version remain on h2... In such a case you can get failed
498    /// requests if the egress server does not handle multiple http versions.
499    pub fn with_tls_support_using_rustls_and_default_http_version(
500        self,
501        config: TlsClientConfig,
502        default_http_version: rama_http::Version,
503    ) -> EasyHttpConnectorBuilder<
504        AddInputExtension<rustls_client::TlsConnector<T>, FallbackHttpVersion>,
505        TlsStage<PROXY>,
506    > {
507        let connector = rustls_client::TlsConnector::auto(self.connector).with_base_config(config);
508        let connector =
509            AddInputExtension::new(connector, FallbackHttpVersion(default_http_version))
510                .with_overwrite(false);
511
512        EasyHttpConnectorBuilder {
513            connector,
514            _phantom: PhantomData,
515        }
516    }
517
518    /// Don't support https on this connector
519    pub fn without_tls_support(self) -> EasyHttpConnectorBuilder<T, TlsStage<PROXY>> {
520        EasyHttpConnectorBuilder {
521            connector: self.connector,
522            _phantom: PhantomData,
523        }
524    }
525}
526
527impl<T, const PROXY: bool> EasyHttpConnectorBuilder<T, TlsStage<PROXY>> {
528    /// Add http support to this connector
529    pub fn with_default_http_connector<Body>(
530        self,
531        exec: Executor,
532    ) -> EasyHttpConnectorBuilder<HttpConnector<T, Body>, HttpStage<PROXY>> {
533        let connector = HttpConnector::new(self.connector, exec);
534
535        EasyHttpConnectorBuilder {
536            connector,
537            _phantom: PhantomData,
538        }
539    }
540
541    /// Add a custom http connector that will be run just after tls
542    pub fn with_custom_http_connector<L>(
543        self,
544        connector_layer: L,
545    ) -> EasyHttpConnectorBuilder<L::Service, HttpStage<PROXY>>
546    where
547        L: Layer<T>,
548    {
549        let connector = connector_layer.into_layer(self.connector);
550
551        EasyHttpConnectorBuilder {
552            connector,
553            _phantom: PhantomData,
554        }
555    }
556}
557
558type DefaultHttpConnector<T> =
559    RequestVersionAdapter<HttpConnectRequestAdapter<ProxyRoutesConnector<T>>>;
560
561type ConfiguredConnectionBuilder<T> = EasyHttpConnectorBuilder<DefaultHttpConnector<T>, PoolStage>;
562
563type ConfiguredConnectionPoolBuilder<T> =
564    EasyHttpConnectorBuilder<DefaultHttpConnector<HttpPooledConnector<T>>, PoolStage>;
565
566type ErasedConnector<C> =
567    BoxService<ConnectRequest, EstablishedClientConnection<C, ConnectRequest>, ConnectionError>;
568
569type DefaultConnectionBuilder<C> =
570    ConfiguredConnectionBuilder<ProxyRouteFailureCacheConnector<ErasedConnector<C>>>;
571
572type DefaultConnectionPoolBuilder<C> =
573    ConfiguredConnectionPoolBuilder<ProxyRouteFailureCacheConnector<ErasedConnector<C>>>;
574
575// Keep the configured connector and its future behind one dynamic boundary
576// before adding route caching and fallback. This prevents deeply nested TLS
577// connector futures from overflowing ordinary thread stacks while dispatching
578// only once per new connection (and behind the pool when pooling is enabled).
579struct ConnectorServiceAdapter<T>(T);
580
581impl<T> Service<ConnectRequest> for ConnectorServiceAdapter<T>
582where
583    T: ConnectorService<ConnectRequest>,
584{
585    type Output = EstablishedClientConnection<T::Connection, ConnectRequest>;
586    type Error = ConnectionError;
587
588    fn serve(
589        &self,
590        input: ConnectRequest,
591    ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
592        self.0.connect(input)
593    }
594}
595
596fn erase_connector<T>(connector: T) -> ErasedConnector<T::Connection>
597where
598    T: ConnectorService<ConnectRequest>,
599{
600    ConnectorServiceAdapter(connector).boxed()
601}
602
603fn finalize_http_connector<T>(connector: T) -> DefaultHttpConnector<T> {
604    let connector = ProxyRoutesConnector::new(connector);
605    let connector = HttpConnectRequestAdapter::new(connector);
606    RequestVersionAdapter::new(connector)
607}
608
609fn finish_without_connection_pool<T, Stage>(
610    builder: EasyHttpConnectorBuilder<T, Stage>,
611) -> ConfiguredConnectionBuilder<T>
612where
613    T: ConnectorService<ConnectRequest>,
614{
615    EasyHttpConnectorBuilder {
616        connector: finalize_http_connector(builder.connector),
617        _phantom: PhantomData,
618    }
619}
620
621fn finish_with_connection_pool<T, Stage>(
622    builder: EasyHttpConnectorBuilder<T, Stage>,
623    config: HttpPooledConnectorConfig,
624) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
625where
626    T: ConnectorService<ConnectRequest>,
627{
628    let connector = config.try_build_connector(builder.connector)?;
629    Ok(EasyHttpConnectorBuilder {
630        connector: finalize_http_connector(connector),
631        _phantom: PhantomData,
632    })
633}
634
635fn finish_with_default_connection_pool<T, Stage>(
636    builder: EasyHttpConnectorBuilder<T, Stage>,
637) -> ConfiguredConnectionPoolBuilder<T>
638where
639    T: ConnectorService<ConnectRequest>,
640{
641    let connector = HttpPooledConnectorConfig::build_default_connector(builder.connector);
642    EasyHttpConnectorBuilder {
643        connector: finalize_http_connector(connector),
644        _phantom: PhantomData,
645    }
646}
647
648fn finish_with_custom_connection_pool<T, Stage, P, R>(
649    builder: EasyHttpConnectorBuilder<T, Stage>,
650    pool: P,
651    req_to_conn_id: R,
652    wait_for_pool_timeout: Option<Duration>,
653) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
654    let connector = PooledConnector::new(builder.connector, pool, req_to_conn_id)
655        .maybe_with_wait_for_pool_timeout(wait_for_pool_timeout);
656    EasyHttpConnectorBuilder {
657        connector,
658        _phantom: PhantomData,
659    }
660}
661
662impl<T, const PROXY: bool> EasyHttpConnectorBuilder<T, HttpStage<PROXY>> {
663    /// Explicitly use the given shared proxy route failure cache.
664    ///
665    /// This selects the failure-cache policy for the final connection stage.
666    /// The configured connector is type-erased at this boundary to keep the
667    /// combined connector future stack-safe.
668    #[must_use]
669    pub fn with_proxy_route_failure_cache(
670        self,
671        cache: ProxyRouteFailureCache,
672    ) -> EasyHttpConnectorBuilder<
673        ProxyRouteFailureCacheConnector<ErasedConnector<T::Connection>>,
674        ProxyRouteFailureCacheStage,
675    >
676    where
677        T: ConnectorService<ConnectRequest>,
678    {
679        EasyHttpConnectorBuilder {
680            connector: ProxyRouteFailureCacheConnector::new(erase_connector(self.connector), cache),
681            _phantom: PhantomData,
682        }
683    }
684
685    /// Disable negative caching of temporarily failing proxy routes.
686    #[must_use]
687    pub fn without_proxy_route_failure_cache(
688        self,
689    ) -> EasyHttpConnectorBuilder<T, ProxyRouteFailureCacheStage> {
690        EasyHttpConnectorBuilder {
691            connector: self.connector,
692            _phantom: PhantomData,
693        }
694    }
695}
696
697impl<T> EasyHttpConnectorBuilder<T, HttpStage<true>> {
698    /// Finish the default HTTP connector stack without adding a connection pool.
699    ///
700    /// This still installs HTTP request adaptation and ordered proxy-route
701    /// fallback. It also installs the default proxy-route failure cache. The
702    /// only omitted component is the pool itself.
703    pub fn without_connection_pool(self) -> DefaultConnectionBuilder<T::Connection>
704    where
705        T: ConnectorService<ConnectRequest>,
706    {
707        finish_without_connection_pool(
708            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
709        )
710    }
711
712    /// Use the default connection pool for this [`super::EasyHttpWebClient`]
713    ///
714    /// This will create a [`MultiplexPool`](crate::net::client::pool::MultiplexPool)
715    /// using the provided limits and will use
716    /// [`HttpConnIdentifier`](super::HttpConnIdentifier) to group connections on
717    /// protocol, authority, selected route, physical transport, any HTTP
718    /// version requirement, and the selected plaintext HTTP proxy mode. This
719    /// keeps forward-proxy connections separate from CONNECT tunnels to the
720    /// same proxy. The default proxy-route failure cache is installed behind
721    /// the pool, so reusable connections bypass negative-cache checks.
722    ///
723    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
724    ///
725    /// If you need a different pool or custom way to group connection you can
726    /// use [`EasyHttpConnectorBuilder::with_custom_connection_pool()`] to provide
727    /// you own.
728    ///
729    /// This also applies a [`RequestVersionAdapter`] layer to make sure that request versions
730    /// are adapted when pooled connections are used, which you almost always need, but in case
731    /// that is unwanted, you can use [`Self::with_custom_connection_pool`] instead.
732    pub fn try_with_connection_pool(
733        self,
734        config: HttpPooledConnectorConfig,
735    ) -> Result<DefaultConnectionPoolBuilder<T::Connection>, BoxError>
736    where
737        T: ConnectorService<ConnectRequest>,
738    {
739        finish_with_connection_pool(
740            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
741            config,
742        )
743    }
744
745    /// Use Rama's default connection pool and default proxy-route failure
746    /// cache.
747    ///
748    /// This operation is infallible because Rama's built-in pool limits are
749    /// known to be valid and non-zero.
750    pub fn with_default_connection_pool(self) -> DefaultConnectionPoolBuilder<T::Connection>
751    where
752        T: ConnectorService<ConnectRequest>,
753    {
754        finish_with_default_connection_pool(
755            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
756        )
757    }
758
759    /// Configure this client to use the provided [`Pool`] and [`ReqToConnId`]
760    ///
761    /// Use `wait_for_pool_timeout` to limit how long we wait for the pool to give us a connection
762    ///
763    /// Warning: this does not apply a [`RequestVersionAdapter`] layer to make sure that request versions
764    /// are adapted when pooled connections are used, which you almost always. This should be manually added
765    /// by using [`Self::with_custom_connector`] after configuring this pool and providing a [`RequestVersionAdapter`] there.
766    /// Unlike [`Self::try_with_connection_pool`], this fully generic method also does not install the HTTP
767    /// connect-request adapter or proxy-route connector. It installs the default proxy-route failure cache behind
768    /// the custom pool. Callers that want route-aware fallback around a custom pool can compose those layers
769    /// explicitly around their [`PooledConnector`].
770    ///
771    /// When the connector supports plaintext HTTP through an HTTP proxy, the
772    /// custom [`ReqToConnId`] must keep ordinary forward-proxy connections
773    /// separate from CONNECT tunnels to the same proxy. Rama's
774    /// [`HttpConnIdentifier`](super::HttpConnIdentifier) includes this
775    /// distinction automatically.
776    ///
777    /// [`Pool`]: rama_net::client::pool::Pool
778    /// [`ReqToConnId`]: rama_net::client::pool::ReqToConnID
779    pub fn with_custom_connection_pool<P, R>(
780        self,
781        pool: P,
782        req_to_conn_id: R,
783        wait_for_pool_timeout: Option<Duration>,
784    ) -> EasyHttpConnectorBuilder<
785        PooledConnector<ProxyRouteFailureCacheConnector<ErasedConnector<T::Connection>>, P, R>,
786        PoolStage,
787    >
788    where
789        T: ConnectorService<ConnectRequest>,
790    {
791        finish_with_custom_connection_pool(
792            self.with_proxy_route_failure_cache(ProxyRouteFailureCache::default()),
793            pool,
794            req_to_conn_id,
795            wait_for_pool_timeout,
796        )
797    }
798}
799
800impl<T> EasyHttpConnectorBuilder<T, HttpStage<false>> {
801    /// Finish the proxy-free HTTP connector stack without a connection pool.
802    ///
803    /// No proxy-route failure cache is installed. Call
804    /// [`Self::with_proxy_route_failure_cache`] before this method to
805    /// explicitly add one for a custom transport.
806    pub fn without_connection_pool(self) -> ConfiguredConnectionBuilder<T>
807    where
808        T: ConnectorService<ConnectRequest>,
809    {
810        finish_without_connection_pool(self)
811    }
812
813    /// Use the default connection pool without a proxy-route failure cache.
814    pub fn try_with_connection_pool(
815        self,
816        config: HttpPooledConnectorConfig,
817    ) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
818    where
819        T: ConnectorService<ConnectRequest>,
820    {
821        finish_with_connection_pool(self, config)
822    }
823
824    /// Use Rama's known-valid default connection pool configuration without a
825    /// proxy-route failure cache.
826    pub fn with_default_connection_pool(self) -> ConfiguredConnectionPoolBuilder<T>
827    where
828        T: ConnectorService<ConnectRequest>,
829    {
830        finish_with_default_connection_pool(self)
831    }
832
833    /// Use a custom connection pool without a proxy-route failure cache.
834    pub fn with_custom_connection_pool<P, R>(
835        self,
836        pool: P,
837        req_to_conn_id: R,
838        wait_for_pool_timeout: Option<Duration>,
839    ) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
840        finish_with_custom_connection_pool(self, pool, req_to_conn_id, wait_for_pool_timeout)
841    }
842}
843
844impl<T> EasyHttpConnectorBuilder<T, ProxyRouteFailureCacheStage> {
845    /// Finish the default HTTP connector stack without a connection pool.
846    pub fn without_connection_pool(self) -> ConfiguredConnectionBuilder<T>
847    where
848        T: ConnectorService<ConnectRequest>,
849    {
850        finish_without_connection_pool(self)
851    }
852
853    /// Use the default connection pool with the selected failure-cache policy.
854    pub fn try_with_connection_pool(
855        self,
856        config: HttpPooledConnectorConfig,
857    ) -> Result<ConfiguredConnectionPoolBuilder<T>, BoxError>
858    where
859        T: ConnectorService<ConnectRequest>,
860    {
861        finish_with_connection_pool(self, config)
862    }
863
864    /// Use Rama's known-valid default connection pool configuration with the
865    /// selected failure-cache policy.
866    pub fn with_default_connection_pool(self) -> ConfiguredConnectionPoolBuilder<T>
867    where
868        T: ConnectorService<ConnectRequest>,
869    {
870        finish_with_default_connection_pool(self)
871    }
872
873    /// Use a custom connection pool with the selected failure-cache policy.
874    ///
875    /// For a proxy-capable connector, the custom
876    /// [`ReqToConnID`](rama_net::client::pool::ReqToConnID) must partition
877    /// plaintext HTTP forward-proxy connections from CONNECT tunnels to the
878    /// same proxy. [`HttpConnIdentifier`](super::HttpConnIdentifier) does so by
879    /// default.
880    pub fn with_custom_connection_pool<P, R>(
881        self,
882        pool: P,
883        req_to_conn_id: R,
884        wait_for_pool_timeout: Option<Duration>,
885    ) -> EasyHttpConnectorBuilder<PooledConnector<T, P, R>, PoolStage> {
886        finish_with_custom_connection_pool(self, pool, req_to_conn_id, wait_for_pool_timeout)
887    }
888}
889
890impl<T> EasyHttpConnectorBuilder<T, PoolStage> {
891    /// Build a [`super::EasyHttpWebClient`] using the currently configured connector
892    pub fn build_client<Body, ModifiedBody, ConnResponse>(
893        self,
894    ) -> super::EasyHttpWebClient<Body, T::Output, ()>
895    where
896        Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
897        ModifiedBody:
898            StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Unpin + Send + 'static,
899        T: Service<
900                Request<Body>,
901                Output = EstablishedClientConnection<ConnResponse, Request<ModifiedBody>>,
902                Error: Into<BoxError>,
903            >,
904        ConnResponse: ExtensionsRef,
905    {
906        super::EasyHttpWebClient::new(self.connector)
907    }
908}
909
910impl<T, S> EasyHttpConnectorBuilder<T, S> {
911    /// Build a connector from the currently configured setup
912    pub fn build_connector(self) -> T {
913        self.connector
914    }
915}