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