1use 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
48const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
50
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52enum TriStateOption<T> {
53 #[default]
55 NotSet,
56 ExplicitlyUnset,
58 Set(T),
60}
61
62impl<T> TriStateOption<T> {
63 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 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#[derive(Clone)]
84pub struct Builder<Tls = TlsUnset> {
85 idle_timeout: TriStateOption<Duration>,
87 time_source: Option<SharedTimeSource>,
89 sleep_impl: Option<SharedAsyncSleep>,
91 tcp_nodelay: bool,
93 tcp_keepalive: TriStateOption<Duration>,
95 max_connections_per_host: Option<usize>,
97 reuse_scope: ConnectionReuseScope,
99 partitions: Option<Vec<Partition>>,
101 proxy_config: ProxyConfig,
103 dns_resolver: Option<SharedDnsResolver>,
105 event_listener: Option<SharedConnectionEventListener>,
107 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 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 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 pub fn time_source(mut self, source: impl TimeSource + 'static) -> Self {
195 self.time_source = Some(SharedTimeSource::new(source));
196 self
197 }
198
199 pub fn set_time_source(&mut self, source: Option<SharedTimeSource>) -> &mut Self {
201 self.time_source = source;
202 self
203 }
204
205 pub fn sleep_impl(mut self, sleep: impl AsyncSleep + 'static) -> Self {
207 self.sleep_impl = Some(SharedAsyncSleep::new(sleep));
208 self
209 }
210
211 pub fn set_sleep_impl(&mut self, sleep: Option<SharedAsyncSleep>) -> &mut Self {
213 self.sleep_impl = sleep;
214 self
215 }
216
217 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
219 self.tcp_nodelay = enabled;
220 self
221 }
222
223 pub fn set_tcp_nodelay(&mut self, enabled: bool) -> &mut Self {
225 self.tcp_nodelay = enabled;
226 self
227 }
228
229 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 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 pub fn max_connections_per_host(mut self, limit: usize) -> Self {
253 self.max_connections_per_host = Some(limit);
254 self
255 }
256
257 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 pub fn connection_reuse_scope(mut self, scope: ConnectionReuseScope) -> Self {
266 self.reuse_scope = scope;
267 self
268 }
269
270 pub fn set_connection_reuse_scope(&mut self, scope: ConnectionReuseScope) -> &mut Self {
272 self.reuse_scope = scope;
273 self
274 }
275
276 pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
278 self.partitions = Some(partitions.into_iter().collect());
279 self
280 }
281
282 pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
284 self.partitions = partitions;
285 self
286 }
287
288 pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
293 self.proxy_config = config;
294 self
295 }
296
297 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 pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
310 self.dns_resolver = Some(resolver.into_shared());
311 self
312 }
313
314 pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
318 self.dns_resolver = resolver;
319 self
320 }
321
322 pub fn event_listener(mut self, listener: impl ConnectionEventListener) -> Self {
331 self.event_listener = Some(SharedConnectionEventListener::new(listener));
332 self
333 }
334
335 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 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 pub fn tls_context(mut self, context: TlsContext) -> Self {
375 self.tls.context = context;
376 self
377 }
378
379 pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
381 self.tls.context = context;
382 self
383 }
384
385 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 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 #[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 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 #[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 #[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 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#[derive(Debug)]
613pub struct BuildError {
614 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#[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#[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#[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#[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}