Skip to main content

aws_smithy_http_client/client/pool/
builder.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Construction and validation for [`ConnectionPool`].
7
8use super::establish::{self, TransportFactory};
9use super::events::{ConnectionEventListener, ConnectionEvents, SharedConnectionEventListener};
10use super::maintenance::MaintenanceConfig;
11use super::registry::{AdmissionPolicy, PartitionRegistry, PartitionRegistryError};
12use super::{ConnectionPool, ConnectionReuseScope, Partition, PoolConfig, PoolInner};
13use crate::client::connect::HttpProxyConnector;
14use crate::client::dns::HyperUtilResolver;
15use crate::client::{TlsProviderSelected, TlsUnset};
16use crate::proxy::ProxyConfig;
17use crate::sync::Arc;
18use crate::tls::{self, TlsContext};
19use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
20use aws_smithy_async::time::{SharedTimeSource, TimeSource};
21#[cfg(any(
22    all(test, feature = "rt-tokio"),
23    all(feature = "test-util", aws_sdk_unstable)
24))]
25use aws_smithy_runtime_api::box_error::BoxError;
26use aws_smithy_runtime_api::client::dns::{ResolveDns, SharedDnsResolver};
27use aws_smithy_runtime_api::shared::IntoShared;
28#[cfg(any(
29    all(test, feature = "rt-tokio"),
30    all(feature = "test-util", aws_sdk_unstable)
31))]
32use http_1x::Uri;
33use hyper_util::client::legacy::connect::dns::GaiResolver;
34use hyper_util::client::legacy::connect::dns::Name;
35use hyper_util::client::legacy::connect::HttpConnector;
36use std::error::Error;
37use std::fmt;
38use std::net::SocketAddr;
39use std::num::NonZeroUsize;
40use std::sync::atomic::AtomicU64;
41use std::time::Duration;
42#[cfg(any(
43    all(test, feature = "rt-tokio"),
44    all(feature = "test-util", aws_sdk_unstable)
45))]
46use tower::Service;
47
48/// Default duration for retaining an idle reusable connection.
49const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
50
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52enum TriStateOption<T> {
53    /// No option was set by the user, so construction applies its default.
54    #[default]
55    NotSet,
56    /// The option was explicitly unset, so construction does not apply a default.
57    ExplicitlyUnset,
58    /// The caller supplied this value.
59    Set(T),
60}
61
62impl<T> TriStateOption<T> {
63    /// Preserves the public nested-option setter contract internally.
64    fn from_nested(value: Option<Option<T>>) -> Self {
65        match value {
66            None => Self::NotSet,
67            Some(None) => Self::ExplicitlyUnset,
68            Some(Some(value)) => Self::Set(value),
69        }
70    }
71
72    /// Resolves the setting, applying `default` only when it was never set.
73    fn resolve(self, default: Option<T>) -> Option<T> {
74        match self {
75            Self::NotSet => default,
76            Self::ExplicitlyUnset => None,
77            Self::Set(value) => Some(value),
78        }
79    }
80}
81
82/// Builds a partition-aware connection pool.
83#[derive(Clone)]
84pub struct Builder<Tls = TlsUnset> {
85    /// Idle retention setting, including explicit disablement.
86    idle_timeout: TriStateOption<Duration>,
87    /// Clock used to assign and evaluate idle deadlines.
88    time_source: Option<SharedTimeSource>,
89    /// Runtime timer used by idle maintenance.
90    sleep_impl: Option<SharedAsyncSleep>,
91    /// Whether newly created TCP sockets disable Nagle's algorithm.
92    tcp_nodelay: bool,
93    /// TCP keepalive setting, including explicit disablement.
94    tcp_keepalive: TriStateOption<Duration>,
95    /// Optional live-connection limit per scheme, host, and port.
96    max_connections_per_host: Option<usize>,
97    /// Partitions allowed to reuse each other's connections.
98    reuse_scope: ConnectionReuseScope,
99    /// Complete explicit partition set, or `None` for anonymous topology.
100    partitions: Option<Vec<Partition>>,
101    /// Pool-wide proxy selection, authentication, and bypass policy.
102    proxy_config: ProxyConfig,
103    /// Caller-supplied DNS resolver, or `None` for the client default.
104    dns_resolver: Option<SharedDnsResolver>,
105    /// Optional listener for every connection lifecycle event from this pool.
106    event_listener: Option<SharedConnectionEventListener>,
107    /// TLS typestate carried into protocol-specific terminal builders.
108    tls: Tls,
109}
110
111impl Default for Builder<TlsUnset> {
112    fn default() -> Self {
113        Self {
114            idle_timeout: TriStateOption::NotSet,
115            time_source: None,
116            sleep_impl: None,
117            tcp_nodelay: true,
118            tcp_keepalive: TriStateOption::NotSet,
119            max_connections_per_host: None,
120            reuse_scope: ConnectionReuseScope::default(),
121            partitions: None,
122            proxy_config: ProxyConfig::disabled(),
123            dns_resolver: None,
124            event_listener: None,
125            tls: TlsUnset {},
126        }
127    }
128}
129
130impl<Tls: fmt::Debug> fmt::Debug for Builder<Tls> {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.debug_struct("Builder")
133            .field("idle_timeout", &self.idle_timeout)
134            .field("time_source", &self.time_source)
135            .field("sleep_impl", &self.sleep_impl)
136            .field("tcp_nodelay", &self.tcp_nodelay)
137            .field("tcp_keepalive", &self.tcp_keepalive)
138            .field("max_connections_per_host", &self.max_connections_per_host)
139            .field("reuse_scope", &self.reuse_scope)
140            .field("partitions", &self.partitions.as_ref().map(Vec::len))
141            .field(
142                "proxy_config",
143                &if self.proxy_config.is_disabled() {
144                    "disabled"
145                } else if self.proxy_config.is_from_env() {
146                    "environment"
147                } else {
148                    "configured"
149                },
150            )
151            .field(
152                "dns_resolver",
153                &if self.dns_resolver.is_some() {
154                    "custom"
155                } else {
156                    "default"
157                },
158            )
159            .field(
160                "event_listener",
161                &if self.event_listener.is_some() {
162                    "configured"
163                } else {
164                    "disabled"
165                },
166            )
167            .field("tls", &self.tls)
168            .finish()
169    }
170}
171
172impl<Tls> Builder<Tls> {
173    /// Sets how long a reusable idle connection remains in the pool.
174    ///
175    /// Passing `None` disables idle expiration. When unset, the pool uses a
176    /// 90-second timeout.
177    pub fn idle_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
178        self.idle_timeout = match timeout.into() {
179            Some(timeout) => TriStateOption::Set(timeout),
180            None => TriStateOption::ExplicitlyUnset,
181        };
182        self
183    }
184
185    /// Mutably sets the idle timeout.
186    ///
187    /// The outer `None` restores the default; `Some(None)` disables expiry.
188    pub fn set_idle_timeout(&mut self, timeout: Option<Option<Duration>>) -> &mut Self {
189        self.idle_timeout = TriStateOption::from_nested(timeout);
190        self
191    }
192
193    /// Sets the clock used by pool maintenance and connection timing.
194    pub fn time_source(mut self, source: impl TimeSource + 'static) -> Self {
195        self.time_source = Some(SharedTimeSource::new(source));
196        self
197    }
198
199    /// Mutably sets the clock used by pool maintenance and connection timing.
200    pub fn set_time_source(&mut self, source: Option<SharedTimeSource>) -> &mut Self {
201        self.time_source = source;
202        self
203    }
204
205    /// Sets the sleep implementation used by pool maintenance.
206    pub fn sleep_impl(mut self, sleep: impl AsyncSleep + 'static) -> Self {
207        self.sleep_impl = Some(SharedAsyncSleep::new(sleep));
208        self
209    }
210
211    /// Mutably sets the sleep implementation used by pool maintenance.
212    pub fn set_sleep_impl(&mut self, sleep: Option<SharedAsyncSleep>) -> &mut Self {
213        self.sleep_impl = sleep;
214        self
215    }
216
217    /// Configures `TCP_NODELAY` for newly created sockets.
218    pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
219        self.tcp_nodelay = enabled;
220        self
221    }
222
223    /// Mutably configures `TCP_NODELAY`.
224    pub fn set_tcp_nodelay(&mut self, enabled: bool) -> &mut Self {
225        self.tcp_nodelay = enabled;
226        self
227    }
228
229    /// Configures TCP keepalive for newly created sockets.
230    pub fn tcp_keepalive(mut self, time: impl Into<Option<Duration>>) -> Self {
231        self.tcp_keepalive = match time.into() {
232            Some(time) => TriStateOption::Set(time),
233            None => TriStateOption::ExplicitlyUnset,
234        };
235        self
236    }
237
238    /// Mutably configures TCP keepalive.
239    pub fn set_tcp_keepalive(&mut self, time: Option<Option<Duration>>) -> &mut Self {
240        self.tcp_keepalive = TriStateOption::from_nested(time);
241        self
242    }
243
244    /// Bounds connection capacity for one origin across every pool partition.
245    ///
246    /// An origin is a scheme, host, and port, so `https://example.com` and
247    /// `http://example.com` are bounded separately, as is each non-default
248    /// port. Establishing, idle, and active connections occupy capacity.
249    /// Logical close normally returns capacity while accepted work drains.
250    /// An HTTP/1 upgrade retains capacity until the caller releases upgraded
251    /// I/O. This is not an idle-connection limit.
252    pub fn max_connections_per_host(mut self, limit: usize) -> Self {
253        self.max_connections_per_host = Some(limit);
254        self
255    }
256
257    /// Mutably sets the connection-capacity bound described by
258    /// [`Builder::max_connections_per_host`].
259    pub fn set_max_connections_per_host(&mut self, limit: Option<usize>) -> &mut Self {
260        self.max_connections_per_host = limit;
261        self
262    }
263
264    /// Selects which partitions may reuse each other's connections.
265    pub fn connection_reuse_scope(mut self, scope: ConnectionReuseScope) -> Self {
266        self.reuse_scope = scope;
267        self
268    }
269
270    /// Mutably selects the connection reuse scope.
271    pub fn set_connection_reuse_scope(&mut self, scope: ConnectionReuseScope) -> &mut Self {
272        self.reuse_scope = scope;
273        self
274    }
275
276    /// Declares the complete explicit partition set.
277    pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
278        self.partitions = Some(partitions.into_iter().collect());
279        self
280    }
281
282    /// Mutably declares or clears the explicit partition set.
283    pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
284        self.partitions = partitions;
285        self
286    }
287
288    /// Configures proxy selection for every connection established by this pool.
289    ///
290    /// The configuration applies across every partition. Proxying is disabled
291    /// unless this method is called.
292    pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
293        self.proxy_config = config;
294        self
295    }
296
297    /// Mutably configures pool-wide proxy selection.
298    ///
299    /// Passing `None` disables proxying.
300    pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
301        self.proxy_config = config.unwrap_or_else(ProxyConfig::disabled);
302        self
303    }
304
305    /// Configures the DNS resolver used by connections established by this pool.
306    ///
307    /// The resolver applies across every partition. When unset, the client uses
308    /// its default resolver, which currently delegates to the operating system.
309    pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
310        self.dns_resolver = Some(resolver.into_shared());
311        self
312    }
313
314    /// Mutably configures the pool-wide DNS resolver.
315    ///
316    /// Passing `None` restores the client default.
317    pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
318        self.dns_resolver = resolver;
319        self
320    }
321
322    /// Observes connection establishment and installed-lifetime transitions.
323    ///
324    /// The listener applies to every partition. Each callback runs
325    /// synchronously on the task that completed the transition and after pool
326    /// locks are released. A listener must not block on work that requires
327    /// progress from that same task.
328    ///
329    /// Observation is disabled unless a listener is configured.
330    pub fn event_listener(mut self, listener: impl ConnectionEventListener) -> Self {
331        self.event_listener = Some(SharedConnectionEventListener::new(listener));
332        self
333    }
334
335    /// Mutably configures pool-wide connection lifecycle observation.
336    ///
337    /// Passing `None` disables observation. The callback contract is the same
338    /// as [`Builder::event_listener`].
339    pub fn set_event_listener(
340        &mut self,
341        listener: Option<SharedConnectionEventListener>,
342    ) -> &mut Self {
343        self.event_listener = listener;
344        self
345    }
346}
347
348impl Builder<TlsUnset> {
349    /// Selects the TLS provider used by HTTPS pool connections.
350    pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
351        Builder {
352            idle_timeout: self.idle_timeout,
353            time_source: self.time_source,
354            sleep_impl: self.sleep_impl,
355            tcp_nodelay: self.tcp_nodelay,
356            tcp_keepalive: self.tcp_keepalive,
357            max_connections_per_host: self.max_connections_per_host,
358            reuse_scope: self.reuse_scope,
359            partitions: self.partitions,
360            proxy_config: self.proxy_config,
361            dns_resolver: self.dns_resolver,
362            event_listener: self.event_listener,
363            tls: TlsProviderSelected {
364                provider,
365                context: TlsContext::default(),
366            },
367        }
368    }
369}
370
371crate::cfg::cfg_tls! {
372    impl Builder<TlsProviderSelected> {
373        /// Sets provider-specific TLS configuration for HTTPS connections.
374        pub fn tls_context(mut self, context: TlsContext) -> Self {
375            self.tls.context = context;
376            self
377        }
378
379        /// Mutably sets provider-specific TLS configuration.
380        pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
381            self.tls.context = context;
382            self
383        }
384
385        /// Builds a pool whose connector performs TLS and ALPN negotiation.
386        pub fn build_https(self) -> Result<ConnectionPool, BuildError> {
387            let resolver = self.dns_resolver.clone();
388            match resolver {
389                Some(resolver) => {
390                    self.build_https_with_resolver(HyperUtilResolver { resolver })
391                }
392                None => self.build_https_with_resolver(GaiResolver::new()),
393            }
394        }
395
396        /// Builds an HTTPS transport around one concrete Hyper resolver.
397        fn build_https_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
398        where
399            R: tower::Service<Name> + Clone + Send + Sync + 'static,
400            R::Response: Iterator<Item = SocketAddr>,
401            R::Future: Send,
402            R::Error: Into<Box<dyn Error + Send + Sync>>,
403        {
404            validate_default_connector_interfaces(self.partitions.as_deref())?;
405            let provider = self.tls.provider.clone();
406            let context = self.tls.context.clone();
407            let proxy_config = self.proxy_config.clone();
408            match provider {
409                #[cfg(feature = "__rustls")]
410                tls::Provider::Rustls(crypto_mode) => {
411                    let tcp_nodelay = self.tcp_nodelay;
412                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
413                    let transport = establish::from_cached_interface_connector(
414                        move |interface, alpn_protocols| {
415                            let mut connector =
416                                HttpConnector::new_with_resolver(resolver.clone());
417                            connector.set_nodelay(tcp_nodelay);
418                            connector.set_keepalive(tcp_keepalive);
419                            set_default_connector_interface(&mut connector, interface);
420                            tls::rustls_provider::build_connector::wrap_connector_with_alpn(
421                                connector,
422                                crypto_mode.clone(),
423                                &context,
424                                proxy_config.clone(),
425                                alpn_protocols,
426                            )
427                        },
428                        true,
429                    );
430                    self.build_with_transport(transport)
431                }
432                #[cfg(feature = "s2n-tls")]
433                tls::Provider::S2nTls => {
434                    let tcp_nodelay = self.tcp_nodelay;
435                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
436                    let transport = establish::from_cached_interface_connector(
437                        move |interface, _alpn_protocols| {
438                            let mut connector =
439                                HttpConnector::new_with_resolver(resolver.clone());
440                            connector.set_nodelay(tcp_nodelay);
441                            connector.set_keepalive(tcp_keepalive);
442                            set_default_connector_interface(&mut connector, interface);
443                            tls::s2n_tls_provider::build_connector::wrap_connector(
444                                connector,
445                                &context,
446                                proxy_config.clone(),
447                            )
448                        },
449                        false,
450                    );
451                    self.build_with_transport(transport)
452                }
453            }
454        }
455    }
456}
457
458impl Builder<TlsUnset> {
459    /// Builds a pool for cleartext HTTP connections.
460    ///
461    /// This method remains public for explicit cleartext endpoints and tests,
462    /// but is hidden from generated documentation because HTTPS with automatic
463    /// ALPN negotiation is the intended production entry point.
464    #[doc(hidden)]
465    pub fn build_http(self) -> Result<ConnectionPool, BuildError> {
466        let resolver = self.dns_resolver.clone();
467        match resolver {
468            Some(resolver) => self.build_http_with_resolver(HyperUtilResolver { resolver }),
469            None => self.build_http_with_resolver(GaiResolver::new()),
470        }
471    }
472
473    /// Builds a cleartext transport around one concrete Hyper resolver.
474    fn build_http_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
475    where
476        R: tower::Service<Name> + Clone + Send + Sync + 'static,
477        R::Response: Iterator<Item = SocketAddr>,
478        R::Future: Send,
479        R::Error: Into<Box<dyn Error + Send + Sync>>,
480    {
481        validate_default_connector_interfaces(self.partitions.as_deref())?;
482        let tcp_nodelay = self.tcp_nodelay;
483        let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
484        let proxy_config = self.proxy_config.clone();
485        let transport = if proxy_config.is_disabled() {
486            establish::from_interface_connector(move |interface| {
487                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
488                connector.set_nodelay(tcp_nodelay);
489                connector.set_keepalive(tcp_keepalive);
490                set_default_connector_interface(&mut connector, interface);
491                connector
492            })
493        } else {
494            establish::from_interface_connector(move |interface| {
495                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
496                connector.set_nodelay(tcp_nodelay);
497                connector.set_keepalive(tcp_keepalive);
498                set_default_connector_interface(&mut connector, interface);
499                HttpProxyConnector::new(connector, proxy_config.clone())
500            })
501        };
502        self.build_with_transport(transport)
503    }
504
505    /// Builds a pool around a test transport connector.
506    #[cfg(all(feature = "test-util", aws_sdk_unstable))]
507    #[doc(hidden)]
508    pub fn build_http_with_tcp_connector<C, IO>(
509        self,
510        connector: C,
511    ) -> Result<ConnectionPool, BuildError>
512    where
513        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
514        C::Error: Into<BoxError>,
515        C::Future: Send + 'static,
516        IO: hyper::rt::Read
517            + hyper::rt::Write
518            + hyper_util::client::legacy::connect::Connection
519            + Send
520            + Sync
521            + Unpin
522            + 'static,
523    {
524        self.build_with_transport(establish::from_connector(connector))
525    }
526
527    #[cfg(all(test, feature = "rt-tokio"))]
528    pub(super) fn build_with_connector<C, IO>(
529        self,
530        connector: C,
531    ) -> Result<ConnectionPool, BuildError>
532    where
533        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
534        C::Error: Into<BoxError>,
535        C::Future: Send + 'static,
536        IO: hyper::rt::Read
537            + hyper::rt::Write
538            + hyper_util::client::legacy::connect::Connection
539            + Send
540            + Sync
541            + Unpin
542            + 'static,
543    {
544        self.build_with_transport(establish::from_connector(connector))
545    }
546}
547
548impl<Tls> Builder<Tls> {
549    /// Builds a pool with a transport supplied by a pool-internal test.
550    #[cfg(test)]
551    pub(super) fn build_with_transport_for_test(
552        self,
553        transport: std::sync::Arc<dyn TransportFactory>,
554    ) -> Result<ConnectionPool, BuildError> {
555        self.build_with_transport(transport)
556    }
557
558    /// Validates pool policy and installs the type-erased transport factory.
559    fn build_with_transport(
560        self,
561        transport: std::sync::Arc<dyn TransportFactory>,
562    ) -> Result<ConnectionPool, BuildError> {
563        let max_connections_per_host = match self.max_connections_per_host {
564            Some(0) => {
565                return Err(BuildError::new(
566                    "max_connections_per_host must be greater than zero",
567                ))
568            }
569            Some(limit) => NonZeroUsize::new(limit),
570            None => None,
571        };
572        let idle_timeout = self.idle_timeout.resolve(Some(DEFAULT_IDLE_TIMEOUT));
573        let time_source = self.time_source.unwrap_or_default();
574        let sleep_impl = self.sleep_impl.or_else(default_async_sleep);
575        if idle_timeout.is_some() && sleep_impl.is_none() {
576            return Err(BuildError::new(
577                "idle_timeout requires an async sleep implementation",
578            ));
579        }
580        let maintenance = MaintenanceConfig {
581            idle_timeout,
582            time_source: time_source.clone(),
583            sleep: sleep_impl.clone(),
584        };
585        let admission_policy = max_connections_per_host
586            .map(|limit| AdmissionPolicy::new(limit, transport.can_guarantee_http1()));
587        let registry = PartitionRegistry::new(
588            self.partitions,
589            self.reuse_scope,
590            admission_policy,
591            maintenance,
592        )
593        .map_err(BuildError::from)?;
594        let config = PoolConfig {
595            idle_timeout,
596            max_connections_per_host,
597            reuse_scope: self.reuse_scope,
598        };
599        Ok(ConnectionPool {
600            inner: Arc::new(PoolInner {
601                config,
602                registry,
603                transport,
604                connection_events: ConnectionEvents::new(self.event_listener, time_source),
605                next_connection_id: AtomicU64::new(0),
606            }),
607        })
608    }
609}
610
611/// Error returned when a pool configuration is internally inconsistent.
612#[derive(Debug)]
613pub struct BuildError {
614    /// Stable configuration diagnostic.
615    message: String,
616}
617
618impl BuildError {
619    fn new(message: impl Into<String>) -> Self {
620        Self {
621            message: message.into(),
622        }
623    }
624}
625
626impl fmt::Display for BuildError {
627    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628        self.message.fmt(f)
629    }
630}
631
632impl Error for BuildError {}
633
634impl From<PartitionRegistryError> for BuildError {
635    fn from(error: PartitionRegistryError) -> Self {
636        Self::new(error.to_string())
637    }
638}
639
640/// Rejects interface values the default connector cannot convert to its
641/// platform socket representation.
642#[cfg(any(
643    target_os = "android",
644    target_os = "fuchsia",
645    target_os = "illumos",
646    target_os = "ios",
647    target_os = "linux",
648    target_os = "macos",
649    target_os = "solaris",
650    target_os = "tvos",
651    target_os = "visionos",
652    target_os = "watchos",
653))]
654fn validate_default_connector_interfaces(
655    partitions: Option<&[Partition]>,
656) -> Result<(), BuildError> {
657    if let Some(partition) = partitions.into_iter().flatten().find(|partition| {
658        partition
659            .interface_name()
660            .is_some_and(|interface| interface.as_bytes().contains(&0))
661    }) {
662        return Err(BuildError::new(format!(
663            "partition {:?} has an invalid network-interface name",
664            partition.id()
665        )));
666    }
667    Ok(())
668}
669
670/// Performs no interface validation on targets without connector support.
671#[cfg(not(any(
672    target_os = "android",
673    target_os = "fuchsia",
674    target_os = "illumos",
675    target_os = "ios",
676    target_os = "linux",
677    target_os = "macos",
678    target_os = "solaris",
679    target_os = "tvos",
680    target_os = "visionos",
681    target_os = "watchos",
682)))]
683fn validate_default_connector_interfaces(
684    _partitions: Option<&[Partition]>,
685) -> Result<(), BuildError> {
686    Ok(())
687}
688
689/// Applies this partition's interface to the default connector.
690#[cfg(any(
691    target_os = "android",
692    target_os = "fuchsia",
693    target_os = "illumos",
694    target_os = "ios",
695    target_os = "linux",
696    target_os = "macos",
697    target_os = "solaris",
698    target_os = "tvos",
699    target_os = "visionos",
700    target_os = "watchos",
701))]
702fn set_default_connector_interface<R>(connector: &mut HttpConnector<R>, interface: Option<&str>) {
703    if let Some(interface) = interface {
704        connector.set_interface(interface);
705    }
706}
707
708/// Leaves the default connector unchanged on unsupported targets.
709#[cfg(not(any(
710    target_os = "android",
711    target_os = "fuchsia",
712    target_os = "illumos",
713    target_os = "ios",
714    target_os = "linux",
715    target_os = "macos",
716    target_os = "solaris",
717    target_os = "tvos",
718    target_os = "visionos",
719    target_os = "watchos",
720)))]
721fn set_default_connector_interface<R>(_connector: &mut HttpConnector<R>, _interface: Option<&str>) {
722}
723
724#[cfg(all(test, not(smithy_http_client_loom)))]
725mod tests {
726    use super::*;
727    use crate::client::pool::partition::Spawn;
728    use crate::client::pool::{DriverSpawner, PartitionId};
729    use std::future::Future;
730    use std::net::IpAddr;
731    use std::pin::Pin;
732
733    #[derive(Debug)]
734    struct PendingSleep;
735
736    impl AsyncSleep for PendingSleep {
737        fn sleep(&self, _duration: Duration) -> aws_smithy_async::rt::sleep::Sleep {
738            aws_smithy_async::rt::sleep::Sleep::new(std::future::pending())
739        }
740    }
741
742    #[derive(Debug)]
743    struct TestSpawner;
744
745    impl Spawn for TestSpawner {
746        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
747            drop(driver);
748        }
749    }
750
751    #[derive(Debug)]
752    struct EmptyDnsResolver;
753
754    impl ResolveDns for EmptyDnsResolver {
755        fn resolve_dns<'a>(
756            &'a self,
757            _name: &'a str,
758        ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
759            aws_smithy_runtime_api::client::dns::DnsFuture::ready(Ok(Vec::<IpAddr>::new()))
760        }
761    }
762
763    fn partition(index: usize) -> Partition {
764        Partition::new(
765            PartitionId::from_index(index),
766            DriverSpawner::new(TestSpawner),
767        )
768    }
769
770    #[test]
771    fn builder_preserves_unset_disabled_and_configured_settings() {
772        let mut builder = Builder::default();
773        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
774        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
775        assert!(builder.tcp_nodelay);
776        assert_eq!(None, builder.max_connections_per_host);
777        assert!(builder.partitions.is_none());
778        assert!(builder.proxy_config.is_disabled());
779        assert!(builder.dns_resolver.is_none());
780        assert!(builder.event_listener.is_none());
781
782        builder.set_idle_timeout(Some(None));
783        builder.set_tcp_keepalive(Some(None));
784        assert_eq!(TriStateOption::ExplicitlyUnset, builder.idle_timeout);
785        assert_eq!(TriStateOption::ExplicitlyUnset, builder.tcp_keepalive);
786
787        builder.set_idle_timeout(Some(Some(Duration::from_secs(5))));
788        builder.set_tcp_keepalive(Some(Some(Duration::from_secs(10))));
789        builder.set_tcp_nodelay(false);
790        builder.set_max_connections_per_host(Some(3));
791        builder.set_partitions(Some(vec![partition(7)]));
792        assert_eq!(
793            TriStateOption::Set(Duration::from_secs(5)),
794            builder.idle_timeout
795        );
796        assert_eq!(
797            TriStateOption::Set(Duration::from_secs(10)),
798            builder.tcp_keepalive
799        );
800        assert!(!builder.tcp_nodelay);
801        assert_eq!(Some(3), builder.max_connections_per_host);
802        assert_eq!(1, builder.partitions.as_ref().unwrap().len());
803
804        builder.set_idle_timeout(None);
805        builder.set_tcp_keepalive(None);
806        builder.set_max_connections_per_host(None);
807        builder.set_partitions(None);
808        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
809        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
810        assert_eq!(None, builder.max_connections_per_host);
811        assert!(builder.partitions.is_none());
812    }
813
814    #[test]
815    fn proxy_and_dns_settings_set_reset_and_redact() {
816        let mut builder = Builder::default();
817        builder.set_proxy_config(Some(
818            ProxyConfig::http("http://proxy.test:8080")
819                .unwrap()
820                .with_basic_auth("secret-user", "secret-password"),
821        ));
822        builder.set_dns_resolver(Some(SharedDnsResolver::new(EmptyDnsResolver)));
823
824        assert!(!builder.proxy_config.is_disabled());
825        assert!(builder.dns_resolver.is_some());
826        let debug = format!("{builder:?}");
827        assert!(debug.contains("configured"));
828        assert!(debug.contains("custom"));
829        assert!(!debug.contains("secret-user"));
830        assert!(!debug.contains("secret-password"));
831
832        builder.set_proxy_config(None);
833        builder.set_dns_resolver(None);
834        assert!(builder.proxy_config.is_disabled());
835        assert!(builder.dns_resolver.is_none());
836    }
837
838    #[test]
839    fn event_listener_sets_resets_and_survives_tls_selection() {
840        let observed = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
841        let mut builder = Builder::default().event_listener({
842            let observed = observed.clone();
843            move |_: &super::super::ConnectionEvent<'_>| {
844                observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
845            }
846        });
847        assert!(builder.event_listener.is_some());
848
849        let shared = builder.event_listener.clone();
850        builder.set_event_listener(None);
851        assert!(builder.event_listener.is_none());
852        builder.set_event_listener(shared);
853        assert!(builder.event_listener.is_some());
854
855        #[cfg(feature = "rustls-ring")]
856        let builder = builder.tls_provider(tls::Provider::rustls(
857            tls::rustls_provider::CryptoMode::Ring,
858        ));
859        assert!(builder.event_listener.is_some());
860        assert_eq!(0, observed.load(std::sync::atomic::Ordering::Relaxed));
861    }
862
863    #[cfg(feature = "rustls-ring")]
864    #[test]
865    fn tls_selection_preserves_proxy_and_dns_settings() {
866        let builder = Builder::default()
867            .proxy_config(
868                ProxyConfig::http("http://proxy.test:8080")
869                    .unwrap()
870                    .with_basic_auth("secret-user", "secret-password"),
871            )
872            .dns_resolver(EmptyDnsResolver)
873            .tls_provider(tls::Provider::rustls(
874                tls::rustls_provider::CryptoMode::Ring,
875            ));
876
877        assert!(!builder.proxy_config.is_disabled());
878        assert!(builder.dns_resolver.is_some());
879        let debug = format!("{builder:?}");
880        assert!(debug.contains("configured"));
881        assert!(debug.contains("custom"));
882        assert!(!debug.contains("secret-user"));
883        assert!(!debug.contains("secret-password"));
884    }
885
886    #[test]
887    fn terminal_build_resolves_the_default_idle_timeout() {
888        let pool = Builder::default()
889            .sleep_impl(PendingSleep)
890            .build_http()
891            .unwrap();
892
893        assert_eq!(
894            Some(Duration::from_secs(90)),
895            pool.inner.config.idle_timeout
896        );
897    }
898
899    #[test]
900    fn terminal_build_preserves_explicitly_disabled_idle_timeout() {
901        let pool = Builder::default().idle_timeout(None).build_http().unwrap();
902
903        assert_eq!(None, pool.inner.config.idle_timeout);
904    }
905
906    #[test]
907    #[cfg(any(
908        target_os = "android",
909        target_os = "fuchsia",
910        target_os = "illumos",
911        target_os = "ios",
912        target_os = "linux",
913        target_os = "macos",
914        target_os = "solaris",
915        target_os = "tvos",
916        target_os = "visionos",
917        target_os = "watchos",
918    ))]
919    fn terminal_build_rejects_interface_names_with_nul_bytes() {
920        let error = Builder::default()
921            .partitions([partition(7).interface("eth\0invalid")])
922            .build_http()
923            .unwrap_err();
924        assert_eq!(
925            "partition PartitionId(7) has an invalid network-interface name",
926            error.to_string()
927        );
928    }
929
930    #[test]
931    fn terminal_build_rejects_zero_connection_limit() {
932        let error = Builder::default()
933            .max_connections_per_host(0)
934            .build_http()
935            .unwrap_err();
936        assert_eq!(
937            "max_connections_per_host must be greater than zero",
938            error.to_string()
939        );
940    }
941
942    #[test]
943    fn terminal_build_rejects_empty_explicit_partition_set() {
944        let error = Builder::default().partitions([]).build_http().unwrap_err();
945        assert_eq!(
946            "explicit partition set must not be empty",
947            error.to_string()
948        );
949    }
950
951    #[test]
952    fn terminal_build_rejects_duplicate_partition_ids() {
953        let error = Builder::default()
954            .partitions([partition(7), partition(7)])
955            .build_http()
956            .unwrap_err();
957        assert_eq!(
958            "duplicate partition identifier: PartitionId(7)",
959            error.to_string()
960        );
961    }
962
963    #[test]
964    fn terminal_build_rejects_reserved_anonymous_partition_id() {
965        let error = Builder::default()
966            .partitions([Partition::new(
967                PartitionId::ANONYMOUS,
968                DriverSpawner::new(TestSpawner),
969            )])
970            .build_http()
971            .unwrap_err();
972        assert_eq!(
973            "the anonymous partition identifier is reserved",
974            error.to_string()
975        );
976    }
977}