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, ResolveDnsError, 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::future::Future;
39use std::net::SocketAddr;
40use std::num::NonZeroUsize;
41use std::pin::Pin;
42use std::sync::atomic::AtomicU64;
43use std::task::{Context, Poll};
44use std::time::Duration;
45use tower::Service;
46
47const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
49
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51enum TriStateOption<T> {
52 #[default]
54 NotSet,
55 ExplicitlyUnset,
57 Set(T),
59}
60
61impl<T> TriStateOption<T> {
62 fn from_nested(value: Option<Option<T>>) -> Self {
64 match value {
65 None => Self::NotSet,
66 Some(None) => Self::ExplicitlyUnset,
67 Some(Some(value)) => Self::Set(value),
68 }
69 }
70
71 fn resolve(self, default: Option<T>) -> Option<T> {
73 match self {
74 Self::NotSet => default,
75 Self::ExplicitlyUnset => None,
76 Self::Set(value) => Some(value),
77 }
78 }
79}
80
81#[derive(Clone)]
83pub struct Builder<Tls = TlsUnset> {
84 idle_timeout: TriStateOption<Duration>,
86 time_source: Option<SharedTimeSource>,
88 sleep_impl: Option<SharedAsyncSleep>,
90 tcp_nodelay: bool,
92 tcp_keepalive: TriStateOption<Duration>,
94 max_connections_per_host: Option<usize>,
96 reuse_scope: ConnectionReuseScope,
98 partitions: Option<Vec<Partition>>,
100 proxy_config: ProxyConfig,
102 dns_resolver: Option<SharedDnsResolver>,
104 event_listener: Option<SharedConnectionEventListener>,
106 tls: Tls,
108}
109
110pub type HttpsBuilder = Builder<TlsProviderSelected>;
115
116impl Default for Builder<TlsUnset> {
117 fn default() -> Self {
118 Self {
119 idle_timeout: TriStateOption::NotSet,
120 time_source: None,
121 sleep_impl: None,
122 tcp_nodelay: true,
123 tcp_keepalive: TriStateOption::NotSet,
124 max_connections_per_host: None,
125 reuse_scope: ConnectionReuseScope::default(),
126 partitions: None,
127 proxy_config: ProxyConfig::disabled(),
128 dns_resolver: None,
129 event_listener: None,
130 tls: TlsUnset {},
131 }
132 }
133}
134
135impl<Tls: fmt::Debug> fmt::Debug for Builder<Tls> {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("Builder")
138 .field("idle_timeout", &self.idle_timeout)
139 .field("time_source", &self.time_source)
140 .field("sleep_impl", &self.sleep_impl)
141 .field("tcp_nodelay", &self.tcp_nodelay)
142 .field("tcp_keepalive", &self.tcp_keepalive)
143 .field("max_connections_per_host", &self.max_connections_per_host)
144 .field("reuse_scope", &self.reuse_scope)
145 .field("partitions", &self.partitions.as_ref().map(Vec::len))
146 .field(
147 "proxy_config",
148 &if self.proxy_config.is_disabled() {
149 "disabled"
150 } else if self.proxy_config.is_from_env() {
151 "environment"
152 } else {
153 "configured"
154 },
155 )
156 .field(
157 "dns_resolver",
158 &if self.dns_resolver.is_some() {
159 "custom"
160 } else {
161 "default"
162 },
163 )
164 .field(
165 "event_listener",
166 &if self.event_listener.is_some() {
167 "configured"
168 } else {
169 "disabled"
170 },
171 )
172 .field("tls", &self.tls)
173 .finish()
174 }
175}
176
177impl<Tls> Builder<Tls> {
178 pub fn idle_timeout(mut self, timeout: impl Into<Option<Duration>>) -> Self {
183 self.idle_timeout = match timeout.into() {
184 Some(timeout) => TriStateOption::Set(timeout),
185 None => TriStateOption::ExplicitlyUnset,
186 };
187 self
188 }
189
190 pub fn set_idle_timeout(&mut self, timeout: Option<Option<Duration>>) -> &mut Self {
194 self.idle_timeout = TriStateOption::from_nested(timeout);
195 self
196 }
197
198 pub fn time_source(mut self, source: impl TimeSource + 'static) -> Self {
200 self.time_source = Some(SharedTimeSource::new(source));
201 self
202 }
203
204 pub fn set_time_source(&mut self, source: Option<SharedTimeSource>) -> &mut Self {
206 self.time_source = source;
207 self
208 }
209
210 pub fn sleep_impl(mut self, sleep: impl AsyncSleep + 'static) -> Self {
212 self.sleep_impl = Some(SharedAsyncSleep::new(sleep));
213 self
214 }
215
216 pub fn set_sleep_impl(&mut self, sleep: Option<SharedAsyncSleep>) -> &mut Self {
218 self.sleep_impl = sleep;
219 self
220 }
221
222 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
224 self.tcp_nodelay = enabled;
225 self
226 }
227
228 pub fn set_tcp_nodelay(&mut self, enabled: bool) -> &mut Self {
230 self.tcp_nodelay = enabled;
231 self
232 }
233
234 pub fn tcp_keepalive(mut self, time: impl Into<Option<Duration>>) -> Self {
236 self.tcp_keepalive = match time.into() {
237 Some(time) => TriStateOption::Set(time),
238 None => TriStateOption::ExplicitlyUnset,
239 };
240 self
241 }
242
243 pub fn set_tcp_keepalive(&mut self, time: Option<Option<Duration>>) -> &mut Self {
245 self.tcp_keepalive = TriStateOption::from_nested(time);
246 self
247 }
248
249 pub fn max_connections_per_host(mut self, limit: usize) -> Self {
258 self.max_connections_per_host = Some(limit);
259 self
260 }
261
262 pub fn set_max_connections_per_host(&mut self, limit: Option<usize>) -> &mut Self {
265 self.max_connections_per_host = limit;
266 self
267 }
268
269 pub fn connection_reuse_scope(mut self, scope: ConnectionReuseScope) -> Self {
271 self.reuse_scope = scope;
272 self
273 }
274
275 pub fn set_connection_reuse_scope(&mut self, scope: ConnectionReuseScope) -> &mut Self {
277 self.reuse_scope = scope;
278 self
279 }
280
281 pub fn has_explicit_partitions(&self) -> bool {
287 self.partitions.is_some()
288 }
289
290 pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
292 self.partitions = Some(partitions.into_iter().collect());
293 self
294 }
295
296 pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
298 self.partitions = partitions;
299 self
300 }
301
302 pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
307 self.proxy_config = config;
308 self
309 }
310
311 pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
315 self.proxy_config = config.unwrap_or_else(ProxyConfig::disabled);
316 self
317 }
318
319 pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
324 self.dns_resolver = Some(resolver.into_shared());
325 self
326 }
327
328 pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
332 self.dns_resolver = resolver;
333 self
334 }
335
336 pub fn event_listener(mut self, listener: impl ConnectionEventListener) -> Self {
345 self.event_listener = Some(SharedConnectionEventListener::new(listener));
346 self
347 }
348
349 pub fn set_event_listener(
354 &mut self,
355 listener: Option<SharedConnectionEventListener>,
356 ) -> &mut Self {
357 self.event_listener = listener;
358 self
359 }
360}
361
362impl Builder<TlsUnset> {
363 pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
365 Builder {
366 idle_timeout: self.idle_timeout,
367 time_source: self.time_source,
368 sleep_impl: self.sleep_impl,
369 tcp_nodelay: self.tcp_nodelay,
370 tcp_keepalive: self.tcp_keepalive,
371 max_connections_per_host: self.max_connections_per_host,
372 reuse_scope: self.reuse_scope,
373 partitions: self.partitions,
374 proxy_config: self.proxy_config,
375 dns_resolver: self.dns_resolver,
376 event_listener: self.event_listener,
377 tls: TlsProviderSelected {
378 provider,
379 context: TlsContext::default(),
380 },
381 }
382 }
383}
384
385impl Builder<TlsProviderSelected> {
386 pub fn build_https(self) -> Result<ConnectionPool, BuildError> {
388 #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
389 {
390 self.build_https_enabled()
391 }
392 #[cfg(not(any(feature = "__rustls", feature = "s2n-tls")))]
393 {
394 match self.tls.provider {}
396 }
397 }
398}
399
400crate::cfg::cfg_tls! {
401 impl Builder<TlsProviderSelected> {
402 pub fn tls_context(mut self, context: TlsContext) -> Self {
404 self.tls.context = context;
405 self
406 }
407
408 pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
410 self.tls.context = context;
411 self
412 }
413
414 fn build_https_enabled(self) -> Result<ConnectionPool, BuildError> {
416 let resolver = self.dns_resolver.clone();
417 match resolver {
418 Some(resolver) => {
419 self.build_https_with_resolver(HyperUtilResolver { resolver })
420 }
421 None => self.build_https_with_resolver(DefaultResolver(GaiResolver::new())),
422 }
423 }
424
425 fn build_https_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
427 where
428 R: tower::Service<Name> + Clone + Send + Sync + 'static,
429 R::Response: Iterator<Item = SocketAddr>,
430 R::Future: Send,
431 R::Error: Into<Box<dyn Error + Send + Sync>>,
432 {
433 validate_default_connector_interfaces(self.partitions.as_deref())?;
434 let provider = self.tls.provider.clone();
435 let context = self.tls.context.clone();
436 let proxy_config = self.proxy_config.clone();
437 match provider {
438 #[cfg(feature = "__rustls")]
439 tls::Provider::Rustls(crypto_mode) => {
440 let tcp_nodelay = self.tcp_nodelay;
441 let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
442 let transport = establish::from_cached_interface_connector(
443 move |interface, alpn_protocols| {
444 let mut connector =
445 HttpConnector::new_with_resolver(resolver.clone());
446 connector.set_nodelay(tcp_nodelay);
447 connector.set_keepalive(tcp_keepalive);
448 set_default_connector_interface(&mut connector, interface);
449 tls::rustls_provider::build_connector::wrap_connector_with_alpn(
450 connector,
451 crypto_mode.clone(),
452 &context,
453 proxy_config.clone(),
454 alpn_protocols,
455 )
456 },
457 true,
458 );
459 self.build_with_transport(transport)
460 }
461 #[cfg(feature = "s2n-tls")]
462 tls::Provider::S2nTls => {
463 let tcp_nodelay = self.tcp_nodelay;
464 let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
465 let transport = establish::from_cached_interface_connector(
466 move |interface, _alpn_protocols| {
467 let mut connector =
468 HttpConnector::new_with_resolver(resolver.clone());
469 connector.set_nodelay(tcp_nodelay);
470 connector.set_keepalive(tcp_keepalive);
471 set_default_connector_interface(&mut connector, interface);
472 tls::s2n_tls_provider::build_connector::wrap_connector(
473 connector,
474 &context,
475 proxy_config.clone(),
476 )
477 },
478 false,
479 );
480 self.build_with_transport(transport)
481 }
482 }
483 }
484 }
485}
486
487#[derive(Clone)]
493struct DefaultResolver<R>(R);
494
495impl<R> Service<Name> for DefaultResolver<R>
496where
497 R: Service<Name>,
498 R::Error: Into<Box<dyn Error + Send + Sync>>,
499 R::Future: Send + 'static,
500{
501 type Response = R::Response;
502 type Error = ResolveDnsError;
503 type Future = Pin<Box<dyn Future<Output = Result<R::Response, ResolveDnsError>> + Send>>;
504
505 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
506 self.0.poll_ready(cx).map_err(ResolveDnsError::new)
507 }
508
509 fn call(&mut self, name: Name) -> Self::Future {
510 let resolving = self.0.call(name);
511 Box::pin(async move { resolving.await.map_err(ResolveDnsError::new) })
512 }
513}
514
515impl Builder<TlsUnset> {
516 #[doc(hidden)]
522 pub fn build_http(self) -> Result<ConnectionPool, BuildError> {
523 let resolver = self.dns_resolver.clone();
524 match resolver {
525 Some(resolver) => self.build_http_with_resolver(HyperUtilResolver { resolver }),
526 None => self.build_http_with_resolver(DefaultResolver(GaiResolver::new())),
527 }
528 }
529
530 fn build_http_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
532 where
533 R: tower::Service<Name> + Clone + Send + Sync + 'static,
534 R::Response: Iterator<Item = SocketAddr>,
535 R::Future: Send,
536 R::Error: Into<Box<dyn Error + Send + Sync>>,
537 {
538 validate_default_connector_interfaces(self.partitions.as_deref())?;
539 let tcp_nodelay = self.tcp_nodelay;
540 let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
541 let proxy_config = self.proxy_config.clone();
542 let transport = if proxy_config.is_disabled() {
543 establish::from_interface_connector(move |interface| {
544 let mut connector = HttpConnector::new_with_resolver(resolver.clone());
545 connector.set_nodelay(tcp_nodelay);
546 connector.set_keepalive(tcp_keepalive);
547 set_default_connector_interface(&mut connector, interface);
548 connector
549 })
550 } else {
551 establish::from_interface_connector(move |interface| {
552 let mut connector = HttpConnector::new_with_resolver(resolver.clone());
553 connector.set_nodelay(tcp_nodelay);
554 connector.set_keepalive(tcp_keepalive);
555 set_default_connector_interface(&mut connector, interface);
556 HttpProxyConnector::new(connector, proxy_config.clone())
557 })
558 };
559 self.build_with_transport(transport)
560 }
561
562 #[cfg(all(feature = "test-util", aws_sdk_unstable))]
564 #[doc(hidden)]
565 pub fn build_http_with_tcp_connector<C, IO>(
566 self,
567 connector: C,
568 ) -> Result<ConnectionPool, BuildError>
569 where
570 C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
571 C::Error: Into<BoxError>,
572 C::Future: Send + 'static,
573 IO: hyper::rt::Read
574 + hyper::rt::Write
575 + hyper_util::client::legacy::connect::Connection
576 + Send
577 + Sync
578 + Unpin
579 + 'static,
580 {
581 self.build_with_transport(establish::from_connector(connector))
582 }
583
584 #[cfg(all(test, feature = "rt-tokio"))]
585 pub(super) fn build_with_connector<C, IO>(
586 self,
587 connector: C,
588 ) -> Result<ConnectionPool, BuildError>
589 where
590 C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
591 C::Error: Into<BoxError>,
592 C::Future: Send + 'static,
593 IO: hyper::rt::Read
594 + hyper::rt::Write
595 + hyper_util::client::legacy::connect::Connection
596 + Send
597 + Sync
598 + Unpin
599 + 'static,
600 {
601 self.build_with_transport(establish::from_connector(connector))
602 }
603}
604
605impl<Tls> Builder<Tls> {
606 #[cfg(test)]
608 pub(super) fn build_with_transport_for_test(
609 self,
610 transport: std::sync::Arc<dyn TransportFactory>,
611 ) -> Result<ConnectionPool, BuildError> {
612 self.build_with_transport(transport)
613 }
614
615 fn build_with_transport(
617 self,
618 transport: std::sync::Arc<dyn TransportFactory>,
619 ) -> Result<ConnectionPool, BuildError> {
620 let max_connections_per_host = match self.max_connections_per_host {
621 Some(0) => {
622 return Err(BuildError::new(
623 "max_connections_per_host must be greater than zero",
624 ))
625 }
626 Some(limit) => NonZeroUsize::new(limit),
627 None => None,
628 };
629 let idle_timeout = self.idle_timeout.resolve(Some(DEFAULT_IDLE_TIMEOUT));
630 let time_source = self.time_source.unwrap_or_default();
631 let sleep_impl = self.sleep_impl.or_else(default_async_sleep);
632 if idle_timeout.is_some() && sleep_impl.is_none() {
633 return Err(BuildError::new(
634 "idle_timeout requires an async sleep implementation",
635 ));
636 }
637 let maintenance = MaintenanceConfig {
638 idle_timeout,
639 time_source: time_source.clone(),
640 sleep: sleep_impl.clone(),
641 };
642 let admission_policy = max_connections_per_host
643 .map(|limit| AdmissionPolicy::new(limit, transport.can_guarantee_http1()));
644 let registry = PartitionRegistry::new(
645 self.partitions,
646 self.reuse_scope,
647 admission_policy,
648 maintenance,
649 )
650 .map_err(BuildError::from)?;
651 let config = PoolConfig {
652 idle_timeout,
653 max_connections_per_host,
654 reuse_scope: self.reuse_scope,
655 };
656 Ok(ConnectionPool {
657 inner: Arc::new(PoolInner {
658 config,
659 registry,
660 transport,
661 connection_events: ConnectionEvents::new(self.event_listener, time_source),
662 next_connection_id: AtomicU64::new(0),
663 }),
664 })
665 }
666}
667
668#[derive(Debug)]
670pub struct BuildError {
671 message: String,
673}
674
675impl BuildError {
676 fn new(message: impl Into<String>) -> Self {
677 Self {
678 message: message.into(),
679 }
680 }
681}
682
683impl fmt::Display for BuildError {
684 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685 self.message.fmt(f)
686 }
687}
688
689impl Error for BuildError {}
690
691impl From<PartitionRegistryError> for BuildError {
692 fn from(error: PartitionRegistryError) -> Self {
693 Self::new(error.to_string())
694 }
695}
696
697#[cfg(any(
700 target_os = "android",
701 target_os = "fuchsia",
702 target_os = "illumos",
703 target_os = "ios",
704 target_os = "linux",
705 target_os = "macos",
706 target_os = "solaris",
707 target_os = "tvos",
708 target_os = "visionos",
709 target_os = "watchos",
710))]
711fn validate_default_connector_interfaces(
712 partitions: Option<&[Partition]>,
713) -> Result<(), BuildError> {
714 if let Some(partition) = partitions.into_iter().flatten().find(|partition| {
715 partition
716 .interface_name()
717 .is_some_and(|interface| interface.as_bytes().contains(&0))
718 }) {
719 return Err(BuildError::new(format!(
720 "partition {:?} has an invalid network-interface name",
721 partition.id()
722 )));
723 }
724 Ok(())
725}
726
727#[cfg(not(any(
729 target_os = "android",
730 target_os = "fuchsia",
731 target_os = "illumos",
732 target_os = "ios",
733 target_os = "linux",
734 target_os = "macos",
735 target_os = "solaris",
736 target_os = "tvos",
737 target_os = "visionos",
738 target_os = "watchos",
739)))]
740fn validate_default_connector_interfaces(
741 _partitions: Option<&[Partition]>,
742) -> Result<(), BuildError> {
743 Ok(())
744}
745
746#[cfg(any(
748 target_os = "android",
749 target_os = "fuchsia",
750 target_os = "illumos",
751 target_os = "ios",
752 target_os = "linux",
753 target_os = "macos",
754 target_os = "solaris",
755 target_os = "tvos",
756 target_os = "visionos",
757 target_os = "watchos",
758))]
759fn set_default_connector_interface<R>(connector: &mut HttpConnector<R>, interface: Option<&str>) {
760 if let Some(interface) = interface {
761 connector.set_interface(interface);
762 }
763}
764
765#[cfg(not(any(
767 target_os = "android",
768 target_os = "fuchsia",
769 target_os = "illumos",
770 target_os = "ios",
771 target_os = "linux",
772 target_os = "macos",
773 target_os = "solaris",
774 target_os = "tvos",
775 target_os = "visionos",
776 target_os = "watchos",
777)))]
778fn set_default_connector_interface<R>(_connector: &mut HttpConnector<R>, _interface: Option<&str>) {
779}
780
781#[cfg(all(test, not(smithy_http_client_loom)))]
782mod tests {
783 use super::*;
784 use crate::client::pool::partition::Spawn;
785 use crate::client::pool::{DriverSpawner, PartitionId};
786 use std::future::Future;
787 use std::net::IpAddr;
788 use std::pin::Pin;
789
790 #[derive(Debug)]
791 struct PendingSleep;
792
793 impl AsyncSleep for PendingSleep {
794 fn sleep(&self, _duration: Duration) -> aws_smithy_async::rt::sleep::Sleep {
795 aws_smithy_async::rt::sleep::Sleep::new(std::future::pending())
796 }
797 }
798
799 #[derive(Debug)]
800 struct TestSpawner;
801
802 impl Spawn for TestSpawner {
803 fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
804 drop(driver);
805 }
806 }
807
808 #[derive(Debug)]
809 struct EmptyDnsResolver;
810
811 impl ResolveDns for EmptyDnsResolver {
812 fn resolve_dns<'a>(
813 &'a self,
814 _name: &'a str,
815 ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
816 aws_smithy_runtime_api::client::dns::DnsFuture::ready(Ok(Vec::<IpAddr>::new()))
817 }
818 }
819
820 fn partition(index: usize) -> Partition {
821 Partition::new(
822 PartitionId::from_index(index),
823 DriverSpawner::new(TestSpawner),
824 )
825 }
826
827 #[test]
828 fn builder_preserves_unset_disabled_and_configured_settings() {
829 let mut builder = Builder::default();
830 assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
831 assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
832 assert!(builder.tcp_nodelay);
833 assert_eq!(None, builder.max_connections_per_host);
834 assert!(builder.partitions.is_none());
835 assert!(builder.proxy_config.is_disabled());
836 assert!(builder.dns_resolver.is_none());
837 assert!(builder.event_listener.is_none());
838
839 builder.set_idle_timeout(Some(None));
840 builder.set_tcp_keepalive(Some(None));
841 assert_eq!(TriStateOption::ExplicitlyUnset, builder.idle_timeout);
842 assert_eq!(TriStateOption::ExplicitlyUnset, builder.tcp_keepalive);
843
844 builder.set_idle_timeout(Some(Some(Duration::from_secs(5))));
845 builder.set_tcp_keepalive(Some(Some(Duration::from_secs(10))));
846 builder.set_tcp_nodelay(false);
847 builder.set_max_connections_per_host(Some(3));
848 builder.set_partitions(Some(vec![partition(7)]));
849 assert_eq!(
850 TriStateOption::Set(Duration::from_secs(5)),
851 builder.idle_timeout
852 );
853 assert_eq!(
854 TriStateOption::Set(Duration::from_secs(10)),
855 builder.tcp_keepalive
856 );
857 assert!(!builder.tcp_nodelay);
858 assert_eq!(Some(3), builder.max_connections_per_host);
859 assert_eq!(1, builder.partitions.as_ref().unwrap().len());
860
861 builder.set_idle_timeout(None);
862 builder.set_tcp_keepalive(None);
863 builder.set_max_connections_per_host(None);
864 builder.set_partitions(None);
865 assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
866 assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
867 assert_eq!(None, builder.max_connections_per_host);
868 assert!(builder.partitions.is_none());
869 }
870
871 #[test]
872 fn proxy_and_dns_settings_set_reset_and_redact() {
873 let mut builder = Builder::default();
874 builder.set_proxy_config(Some(
875 ProxyConfig::http("http://proxy.test:8080")
876 .unwrap()
877 .with_basic_auth("secret-user", "secret-password"),
878 ));
879 builder.set_dns_resolver(Some(SharedDnsResolver::new(EmptyDnsResolver)));
880
881 assert!(!builder.proxy_config.is_disabled());
882 assert!(builder.dns_resolver.is_some());
883 let debug = format!("{builder:?}");
884 assert!(debug.contains("configured"));
885 assert!(debug.contains("custom"));
886 assert!(!debug.contains("secret-user"));
887 assert!(!debug.contains("secret-password"));
888
889 builder.set_proxy_config(None);
890 builder.set_dns_resolver(None);
891 assert!(builder.proxy_config.is_disabled());
892 assert!(builder.dns_resolver.is_none());
893 }
894
895 #[test]
896 fn event_listener_sets_resets_and_survives_tls_selection() {
897 let observed = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
898 let mut builder = Builder::default().event_listener({
899 let observed = observed.clone();
900 move |_: &super::super::ConnectionEvent<'_>| {
901 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
902 }
903 });
904 assert!(builder.event_listener.is_some());
905
906 let shared = builder.event_listener.clone();
907 builder.set_event_listener(None);
908 assert!(builder.event_listener.is_none());
909 builder.set_event_listener(shared);
910 assert!(builder.event_listener.is_some());
911
912 #[cfg(feature = "rustls-ring")]
913 let builder = builder.tls_provider(tls::Provider::rustls(
914 tls::rustls_provider::CryptoMode::Ring,
915 ));
916 assert!(builder.event_listener.is_some());
917 assert_eq!(0, observed.load(std::sync::atomic::Ordering::Relaxed));
918 }
919
920 #[cfg(feature = "rustls-ring")]
921 #[test]
922 fn tls_selection_preserves_proxy_and_dns_settings() {
923 let builder = Builder::default()
924 .proxy_config(
925 ProxyConfig::http("http://proxy.test:8080")
926 .unwrap()
927 .with_basic_auth("secret-user", "secret-password"),
928 )
929 .dns_resolver(EmptyDnsResolver)
930 .tls_provider(tls::Provider::rustls(
931 tls::rustls_provider::CryptoMode::Ring,
932 ));
933
934 assert!(!builder.proxy_config.is_disabled());
935 assert!(builder.dns_resolver.is_some());
936 let debug = format!("{builder:?}");
937 assert!(debug.contains("configured"));
938 assert!(debug.contains("custom"));
939 assert!(!debug.contains("secret-user"));
940 assert!(!debug.contains("secret-password"));
941 }
942
943 #[test]
944 fn terminal_build_resolves_the_default_idle_timeout() {
945 let pool = Builder::default()
946 .sleep_impl(PendingSleep)
947 .build_http()
948 .unwrap();
949
950 assert_eq!(
951 Some(Duration::from_secs(90)),
952 pool.inner.config.idle_timeout
953 );
954 }
955
956 #[test]
957 fn terminal_build_preserves_explicitly_disabled_idle_timeout() {
958 let pool = Builder::default().idle_timeout(None).build_http().unwrap();
959
960 assert_eq!(None, pool.inner.config.idle_timeout);
961 }
962
963 #[test]
964 #[cfg(any(
965 target_os = "android",
966 target_os = "fuchsia",
967 target_os = "illumos",
968 target_os = "ios",
969 target_os = "linux",
970 target_os = "macos",
971 target_os = "solaris",
972 target_os = "tvos",
973 target_os = "visionos",
974 target_os = "watchos",
975 ))]
976 fn terminal_build_rejects_interface_names_with_nul_bytes() {
977 let error = Builder::default()
978 .partitions([partition(7).interface("eth\0invalid")])
979 .build_http()
980 .unwrap_err();
981 assert_eq!(
982 "partition PartitionId(7) has an invalid network-interface name",
983 error.to_string()
984 );
985 }
986
987 #[test]
988 fn terminal_build_rejects_zero_connection_limit() {
989 let error = Builder::default()
990 .max_connections_per_host(0)
991 .build_http()
992 .unwrap_err();
993 assert_eq!(
994 "max_connections_per_host must be greater than zero",
995 error.to_string()
996 );
997 }
998
999 #[test]
1000 fn terminal_build_rejects_empty_explicit_partition_set() {
1001 let error = Builder::default().partitions([]).build_http().unwrap_err();
1002 assert_eq!(
1003 "explicit partition set must not be empty",
1004 error.to_string()
1005 );
1006 }
1007
1008 #[test]
1009 fn terminal_build_rejects_duplicate_partition_ids() {
1010 let error = Builder::default()
1011 .partitions([partition(7), partition(7)])
1012 .build_http()
1013 .unwrap_err();
1014 assert_eq!(
1015 "duplicate partition identifier: PartitionId(7)",
1016 error.to_string()
1017 );
1018 }
1019
1020 #[test]
1021 fn terminal_build_rejects_reserved_anonymous_partition_id() {
1022 let error = Builder::default()
1023 .partitions([Partition::new(
1024 PartitionId::ANONYMOUS,
1025 DriverSpawner::new(TestSpawner),
1026 )])
1027 .build_http()
1028 .unwrap_err();
1029 assert_eq!(
1030 "the anonymous partition identifier is reserved",
1031 error.to_string()
1032 );
1033 }
1034
1035 #[derive(Clone)]
1037 struct FailingResolver;
1038
1039 impl Service<Name> for FailingResolver {
1040 type Response = std::vec::IntoIter<SocketAddr>;
1041 type Error = std::io::Error;
1042 type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
1043
1044 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1045 Poll::Ready(Ok(()))
1046 }
1047
1048 fn call(&mut self, name: Name) -> Self::Future {
1049 std::future::ready(Err(std::io::Error::other(format!(
1050 "no addresses for {name}"
1051 ))))
1052 }
1053 }
1054
1055 #[tokio::test]
1056 async fn default_resolver_failure_through_http_connector_is_io() {
1057 let mut connector = HttpConnector::new_with_resolver(DefaultResolver(FailingResolver));
1058
1059 let error = connector
1060 .call(http_1x::Uri::from_static("http://example.com/"))
1061 .await
1062 .expect_err("DNS resolution fails");
1063
1064 let error = crate::client::downcast_error(error.into());
1065 assert!(error.is_io(), "expected an I/O error, got {error:?}");
1066 }
1067}