AWS SDK

AWS SDK

rev. 5268f3bc6a0eb20fb020dfdb4b4f92f6181f2eed (ignoring whitespace)

Files changed:

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/examples/custom-dns.rs

@@ -1,1 +26,35 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
use aws_smithy_http_client::{
           7  +
    pool::{Client as PoolClient, ConnectionPool},
    7      8   
    tls::{self, rustls_provider::CryptoMode},
    8      9   
    Builder,
    9     10   
};
   10     11   
use aws_smithy_runtime_api::client::dns::{DnsFuture, ResolveDns};
   11     12   
use std::net::{IpAddr, Ipv4Addr};
   12     13   
   13     14   
#[derive(Debug, Clone)]
   14     15   
struct StaticResolver;
   15     16   
   16     17   
impl ResolveDns for StaticResolver {
   17     18   
    fn resolve_dns<'a>(&'a self, _name: &'a str) -> DnsFuture<'a> {
   18     19   
        DnsFuture::ready(Ok(vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]))
   19     20   
    }
   20     21   
}
   21     22   
   22     23   
fn main() {
   23         -
    let _client = Builder::new()
          24  +
    let _legacy_client = Builder::new()
   24     25   
        .tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
   25     26   
        .build_with_resolver(StaticResolver);
          27  +
          28  +
    let pool = ConnectionPool::builder()
          29  +
        .dns_resolver(StaticResolver)
          30  +
        .tls_provider(tls::Provider::Rustls(CryptoMode::Ring))
          31  +
        .build_https()
          32  +
        .expect("valid connection pool");
          33  +
    let _partitioned_client =
          34  +
        PoolClient::new(&pool).expect("pool contains the anonymous partition");
   26     35   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client.rs

@@ -1,1 +53,55 @@
    6      6   
mod dns;
    7      7   
pub mod pool;
    8      8   
/// Proxy configuration
    9      9   
pub mod proxy;
   10     10   
mod timeout;
   11     11   
/// TLS connector(s)
   12     12   
pub mod tls;
   13     13   
   14     14   
pub(crate) mod connect;
   15     15   
          16  +
use self::connect::ConnectPath;
   16     17   
use crate::cfg::cfg_tls;
   17     18   
use crate::tls::TlsContext;
   18     19   
use aws_smithy_async::future::timeout::TimedOutError;
   19     20   
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
   20     21   
use aws_smithy_runtime_api::box_error::BoxError;
   21     22   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   22     23   
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
   23     24   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
          25  +
use aws_smithy_runtime_api::client::dns::ResolveDnsError;
   24     26   
use aws_smithy_runtime_api::client::http::{
   25     27   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpClient,
   26     28   
    SharedHttpConnector,
   27     29   
};
   28     30   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   29     31   
use aws_smithy_runtime_api::client::result::ConnectorError;
   30     32   
use aws_smithy_runtime_api::client::runtime_components::{
   31     33   
    RuntimeComponents, RuntimeComponentsBuilder,
   32     34   
};
   33     35   
use aws_smithy_runtime_api::shared::IntoShared;
@@ -489,491 +550,553 @@
  509    511   
    }
  510    512   
}
  511    513   
  512    514   
/// Extract a smithy connection from a hyper CaptureConnection
  513    515   
fn extract_smithy_connection(capture_conn: &CaptureConnection) -> Option<ConnectionMetadata> {
  514    516   
    let capture_conn = capture_conn.clone();
  515    517   
    if let Some(conn) = capture_conn.clone().connection_metadata().as_ref() {
  516    518   
        let mut extensions = Extensions::new();
  517    519   
        conn.get_extras(&mut extensions);
  518    520   
        let http_info = extensions.get::<HttpInfo>();
         521  +
        let connect_path = ConnectPath::from_connected(conn, &extensions);
  519    522   
        let mut builder = ConnectionMetadata::builder()
  520         -
            .proxied(conn.is_proxied())
         523  +
            .proxied(connect_path.is_proxied())
  521    524   
            .poison_fn(move || match capture_conn.connection_metadata().as_ref() {
  522    525   
                Some(conn) => conn.poison(),
  523    526   
                None => tracing::trace!("no connection existed to poison"),
  524    527   
            });
  525    528   
  526    529   
        builder
  527    530   
            .set_local_addr(http_info.map(|info| info.local_addr()))
  528    531   
            .set_remote_addr(http_info.map(|info| info.remote_addr()));
  529    532   
  530    533   
        let smithy_connection = builder.build();
@@ -610,613 +669,676 @@
  630    633   
fn downcast_error(err: BoxError) -> ConnectorError {
  631    634   
    // is a `TimedOutError` (from aws_smithy_async::timeout) in the chain? if it is, this is a timeout
  632    635   
    if find_source::<TimedOutError>(err.as_ref()).is_some() {
  633    636   
        return ConnectorError::timeout(err);
  634    637   
    }
  635    638   
    // is the top of chain error actually already a `ConnectorError`? return that directly
  636    639   
    let err = match err.downcast::<ConnectorError>() {
  637    640   
        Ok(connector_error) => return *connector_error,
  638    641   
        Err(box_error) => box_error,
  639    642   
    };
         643  +
    // DNS resolution is part of connection establishment and is retryable as I/O.
         644  +
    if find_source::<ResolveDnsError>(err.as_ref()).is_some() {
         645  +
        return ConnectorError::io(err);
         646  +
    }
  640    647   
    // generally, the top of chain will probably be a hyper error. Go through a set of hyper specific
  641    648   
    // error classifications
  642    649   
    let err = match find_source::<hyper::Error>(err.as_ref()) {
  643    650   
        Some(hyper_error) => return to_connector_error(hyper_error)(err),
  644    651   
        None => match find_source::<hyper_util::client::legacy::Error>(err.as_ref()) {
  645    652   
            Some(hyper_util_err) => {
  646    653   
                if hyper_util_err.is_connect()
  647    654   
                    || find_source::<std::io::Error>(hyper_util_err).is_some()
  648    655   
                {
  649    656   
                    return ConnectorError::io(err);
@@ -1398,1405 +1449,1453 @@
 1418   1425   
 1419   1426   
        // Ensure that s2n-tls is used as the underlying TLS provider when selected.
 1420   1427   
        //
 1421   1428   
        // s2n-tls-hyper will error when given an invalid scheme. Ensure that this error is produced
 1422   1429   
        // from s2n-tls-hyper, and not another TLS provider.
 1423   1430   
        let error = connector
 1424   1431   
            .call(HttpRequest::get("notascheme://amazon.com").unwrap())
 1425   1432   
            .await
 1426   1433   
            .unwrap_err();
 1427   1434   
        let error = error.into_source();
 1428         -
        let s2n_error = error
 1429         -
            .source()
 1430         -
            .unwrap()
 1431         -
            .downcast_ref::<s2n_tls_hyper::error::Error>()
 1432         -
            .unwrap();
        1435  +
        let s2n_error = find_source::<s2n_tls_hyper::error::Error>(error.as_ref())
        1436  +
            .expect("s2n-tls-hyper error is present in the source chain");
 1433   1437   
        assert!(matches!(
 1434   1438   
            s2n_error,
 1435   1439   
            s2n_tls_hyper::error::Error::InvalidScheme
 1436   1440   
        ));
 1437   1441   
    }
 1438   1442   
 1439   1443   
    /// The default HTTP client reports "hyper" / "1.x" as its connector metadata.
 1440   1444   
    #[test]
 1441   1445   
    fn connector_metadata_identifies_hyper_1x() {
 1442   1446   
        let client = crate::Builder::new().build_http();

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/connect.rs

@@ -1,1 +164,249 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
use crate::client::connect;
    6      6   
use crate::proxy;
    7      7   
use aws_smithy_runtime_api::box_error::BoxError;
    8         -
use http_1x::Uri;
           8  +
use http_1x::{Extensions, HeaderMap, HeaderValue, Uri};
    9      9   
use hyper::rt::{Read, ReadBufCursor, Write};
   10     10   
use hyper_util::client::legacy::connect::{Connected, Connection};
          11  +
use hyper_util::client::proxy::matcher::Matcher;
   11     12   
use pin_project_lite::pin_project;
          13  +
use std::fmt;
   12     14   
use std::future::Future;
   13     15   
use std::io;
   14     16   
use std::io::IoSlice;
   15     17   
use std::pin::Pin;
          18  +
use std::sync::Arc;
   16     19   
use std::task::{Context, Poll};
   17     20   
   18     21   
pub(crate) trait AsyncConn:
   19     22   
    Read + Write + Connection + Send + Sync + Unpin + 'static
   20     23   
{
   21     24   
}
   22     25   
   23     26   
impl<T: Read + Write + Connection + Send + Sync + Unpin + 'static> AsyncConn for T {}
   24     27   
   25     28   
pub(crate) type BoxConn = Box<dyn AsyncConn>;
   26     29   
   27     30   
// Future for connecting
   28     31   
pub(crate) type Connecting = Pin<Box<dyn Future<Output = Result<Conn, BoxError>> + Send>>;
   29     32   
          33  +
/// Authorization selected for cleartext HTTP requests sent through a proxy.
          34  +
#[derive(Clone)]
          35  +
pub(crate) struct ProxyAuthorization(HeaderValue);
          36  +
          37  +
impl fmt::Debug for ProxyAuthorization {
          38  +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
          39  +
        f.write_str("ProxyAuthorization(** redacted **)")
          40  +
    }
          41  +
}
          42  +
          43  +
impl ProxyAuthorization {
          44  +
    pub(crate) fn new(value: HeaderValue) -> Self {
          45  +
        Self(value)
          46  +
    }
          47  +
          48  +
    /// Applies the selected authorization unless the caller supplied one.
          49  +
    pub(crate) fn apply(&self, headers: &mut HeaderMap) {
          50  +
        headers
          51  +
            .entry(http_1x::header::PROXY_AUTHORIZATION)
          52  +
            .or_insert_with(|| self.0.clone());
          53  +
    }
          54  +
}
          55  +
          56  +
/// How an established transport reaches its origin.
          57  +
#[derive(Clone, Debug)]
          58  +
pub(crate) enum ConnectPath {
          59  +
    /// The transport reaches the origin without a proxy.
          60  +
    Direct,
          61  +
    /// Requests are sent to a forward proxy; HTTP/1 uses absolute-form targets.
          62  +
    ForwardProxy {
          63  +
        authorization: Option<ProxyAuthorization>,
          64  +
    },
          65  +
    /// The transport reaches the origin through an established CONNECT tunnel.
          66  +
    #[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
          67  +
    ProxyTunnel,
          68  +
}
          69  +
          70  +
impl ConnectPath {
          71  +
    pub(crate) fn forward_proxy(authorization: Option<HeaderValue>) -> Self {
          72  +
        Self::ForwardProxy {
          73  +
            authorization: authorization.map(ProxyAuthorization::new),
          74  +
        }
          75  +
    }
          76  +
          77  +
    /// Recovers the full path when available and preserves custom connector behavior.
          78  +
    pub(crate) fn from_connected(connected: &Connected, extras: &Extensions) -> Self {
          79  +
        extras.get::<Self>().cloned().unwrap_or_else(|| {
          80  +
            if connected.is_proxied() {
          81  +
                Self::forward_proxy(None)
          82  +
            } else {
          83  +
                Self::Direct
          84  +
            }
          85  +
        })
          86  +
    }
          87  +
          88  +
    /// Returns whether a proxy participates in the established transport.
          89  +
    pub(crate) fn is_proxied(&self) -> bool {
          90  +
        !matches!(self, Self::Direct)
          91  +
    }
          92  +
          93  +
    /// Returns whether HTTP/1 requests require an absolute-form target.
          94  +
    pub(crate) fn uses_absolute_form(&self) -> bool {
          95  +
        matches!(self, Self::ForwardProxy { .. })
          96  +
    }
          97  +
          98  +
    /// Applies configured forwarding credentials unless the request supplied them.
          99  +
    pub(crate) fn apply_proxy_authorization(&self, headers: &mut HeaderMap) {
         100  +
        if let Self::ForwardProxy {
         101  +
            authorization: Some(authorization),
         102  +
        } = self
         103  +
        {
         104  +
            authorization.apply(headers);
         105  +
        }
         106  +
    }
         107  +
}
         108  +
   30    109   
pin_project! {
   31    110   
    pub(crate) struct Conn {
   32    111   
        #[pin]
   33    112   
        pub(super)inner: BoxConn,
   34         -
        pub(super) is_proxy: bool,
         113  +
        pub(super) connect_path: ConnectPath,
   35    114   
    }
   36    115   
}
   37    116   
   38    117   
impl Connection for Conn {
   39    118   
    fn connected(&self) -> Connected {
   40         -
        self.inner.connected().proxy(self.is_proxy)
         119  +
        self.inner
         120  +
            .connected()
         121  +
            .proxy(self.connect_path.uses_absolute_form())
         122  +
            .extra(self.connect_path.clone())
   41    123   
    }
   42    124   
}
   43    125   
   44    126   
impl Read for Conn {
   45    127   
    fn poll_read(
   46    128   
        self: Pin<&mut Self>,
   47    129   
        cx: &mut Context<'_>,
   48    130   
        buf: ReadBufCursor<'_>,
   49    131   
    ) -> Poll<io::Result<()>> {
   50    132   
        let this = self.project();
   51    133   
        Read::poll_read(this.inner, cx, buf)
   52    134   
    }
   53    135   
}
   54    136   
   55    137   
impl Write for Conn {
   56    138   
    fn poll_write(
   57    139   
        self: Pin<&mut Self>,
   58    140   
        cx: &mut Context<'_>,
   59    141   
        buf: &[u8],
   60    142   
    ) -> Poll<Result<usize, io::Error>> {
   61    143   
        let this = self.project();
   62    144   
        Write::poll_write(this.inner, cx, buf)
   63    145   
    }
   64    146   
   65    147   
    fn poll_write_vectored(
   66    148   
        self: Pin<&mut Self>,
   67    149   
        cx: &mut Context<'_>,
   68    150   
        bufs: &[IoSlice<'_>],
   69    151   
    ) -> Poll<Result<usize, io::Error>> {
   70    152   
        let this = self.project();
   71    153   
        Write::poll_write_vectored(this.inner, cx, bufs)
   72    154   
    }
   73    155   
   74    156   
    fn is_write_vectored(&self) -> bool {
   75    157   
        self.inner.is_write_vectored()
   76    158   
    }
   77    159   
   78    160   
    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
   79    161   
        let this = self.project();
   80    162   
        Write::poll_flush(this.inner, cx)
   81    163   
    }
   82    164   
   83    165   
    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
   84    166   
        let this = self.project();
   85    167   
        Write::poll_shutdown(this.inner, cx)
   86    168   
    }
   87    169   
}
   88    170   
   89    171   
/// HTTP-only proxy connector for handling HTTP requests through HTTP proxies
   90    172   
///
   91    173   
/// This connector handles the HTTP proxy logic when no TLS provider is selected,
   92    174   
/// including request URL modification and proxy authentication.
   93    175   
#[derive(Debug, Clone)]
   94    176   
pub(crate) struct HttpProxyConnector<C> {
   95    177   
    inner: C,
   96         -
    proxy_config: proxy::ProxyConfig,
         178  +
    proxy_matcher: Option<Arc<Matcher>>,
   97    179   
}
   98    180   
   99    181   
impl<C> HttpProxyConnector<C> {
  100    182   
    pub(crate) fn new(inner: C, proxy_config: proxy::ProxyConfig) -> Self {
         183  +
        let proxy_matcher = if proxy_config.is_disabled() {
         184  +
            None
         185  +
        } else {
         186  +
            Some(Arc::new(proxy_config.into_hyper_util_matcher()))
         187  +
        };
  101    188   
        Self {
  102    189   
            inner,
  103         -
            proxy_config,
         190  +
            proxy_matcher,
  104    191   
        }
  105    192   
    }
  106    193   
}
  107    194   
  108    195   
impl<C> tower::Service<Uri> for HttpProxyConnector<C>
  109    196   
where
  110    197   
    C: tower::Service<Uri> + Clone + Send + 'static,
  111    198   
    C::Response: hyper::rt::Read
  112    199   
        + hyper::rt::Write
  113    200   
        + hyper_util::client::legacy::connect::Connection
  114    201   
        + Send
  115    202   
        + Sync
  116    203   
        + Unpin
  117    204   
        + 'static,
  118    205   
    C::Future: Send + 'static,
  119    206   
    C::Error: Into<BoxError>,
  120    207   
{
  121    208   
    type Response = connect::Conn;
  122    209   
    type Error = BoxError;
  123    210   
    type Future = connect::Connecting;
  124    211   
  125    212   
    fn poll_ready(
  126    213   
        &mut self,
  127    214   
        cx: &mut std::task::Context<'_>,
  128    215   
    ) -> std::task::Poll<Result<(), Self::Error>> {
  129    216   
        self.inner.poll_ready(cx).map_err(Into::into)
  130    217   
    }
  131    218   
  132    219   
    fn call(&mut self, dst: Uri) -> Self::Future {
  133         -
        // Check if this request should be proxied
  134         -
        let proxy_intercept = if !self.proxy_config.is_disabled() {
  135         -
            let matcher = self.proxy_config.clone().into_hyper_util_matcher();
  136         -
            matcher.intercept(&dst)
  137         -
        } else {
  138         -
            None
  139         -
        };
         220  +
        let proxy_intercept = self
         221  +
            .proxy_matcher
         222  +
            .as_ref()
         223  +
            .and_then(|matcher| matcher.intercept(&dst));
  140    224   
  141    225   
        if let Some(intercept) = proxy_intercept {
  142    226   
            // HTTP through proxy: Connect to proxy server
  143    227   
            let proxy_uri = intercept.uri().clone();
         228  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  144    229   
            let fut = self.inner.call(proxy_uri);
  145    230   
            Box::pin(async move {
  146    231   
                let conn = fut.await.map_err(Into::into)?;
  147    232   
                Ok(connect::Conn {
  148    233   
                    inner: Box::new(conn),
  149         -
                    is_proxy: true,
         234  +
                    connect_path,
  150    235   
                })
  151    236   
            })
  152    237   
        } else {
  153    238   
            // Direct connection
  154    239   
            let fut = self.inner.call(dst);
  155    240   
            Box::pin(async move {
  156    241   
                let conn = fut.await.map_err(Into::into)?;
  157    242   
                Ok(connect::Conn {
  158    243   
                    inner: Box::new(conn),
  159         -
                    is_proxy: false,
         244  +
                    connect_path: ConnectPath::Direct,
  160    245   
                })
  161    246   
            })
  162    247   
        }
  163    248   
    }
  164    249   
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/builder.rs

@@ -1,1 +60,67 @@
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Construction and validation for [`ConnectionPool`].
    7      7   
    8      8   
use super::establish::{self, TransportFactory};
    9      9   
use super::maintenance::MaintenanceConfig;
   10     10   
use super::registry::{AdmissionPolicy, PartitionRegistry, PartitionRegistryError};
   11     11   
use super::{ConnectionPool, ConnectionReuseScope, Partition, PoolConfig, PoolInner};
          12  +
use crate::client::connect::HttpProxyConnector;
          13  +
use crate::client::dns::HyperUtilResolver;
   12     14   
use crate::client::{TlsProviderSelected, TlsUnset};
          15  +
use crate::proxy::ProxyConfig;
   13     16   
use crate::sync::Arc;
   14     17   
use crate::tls::{self, TlsContext};
   15     18   
use aws_smithy_async::rt::sleep::{default_async_sleep, AsyncSleep, SharedAsyncSleep};
   16     19   
use aws_smithy_async::time::{SharedTimeSource, TimeSource};
   17     20   
#[cfg(any(
   18     21   
    all(test, feature = "rt-tokio"),
   19     22   
    all(feature = "test-util", aws_sdk_unstable)
   20     23   
))]
   21     24   
use aws_smithy_runtime_api::box_error::BoxError;
          25  +
use aws_smithy_runtime_api::client::dns::{ResolveDns, SharedDnsResolver};
          26  +
use aws_smithy_runtime_api::shared::IntoShared;
   22     27   
#[cfg(any(
   23     28   
    all(test, feature = "rt-tokio"),
   24     29   
    all(feature = "test-util", aws_sdk_unstable)
   25     30   
))]
   26     31   
use http_1x::Uri;
   27     32   
use hyper_util::client::legacy::connect::dns::GaiResolver;
          33  +
use hyper_util::client::legacy::connect::dns::Name;
   28     34   
use hyper_util::client::legacy::connect::HttpConnector;
   29     35   
use std::error::Error;
   30     36   
use std::fmt;
          37  +
use std::net::SocketAddr;
   31     38   
use std::num::NonZeroUsize;
   32     39   
use std::sync::atomic::AtomicU64;
   33     40   
use std::time::Duration;
   34     41   
#[cfg(any(
   35     42   
    all(test, feature = "rt-tokio"),
   36     43   
    all(feature = "test-util", aws_sdk_unstable)
   37     44   
))]
   38     45   
use tower::Service;
   39     46   
   40     47   
/// Default duration for retaining an idle reusable connection.
@@ -63,70 +153,184 @@
   83     90   
    /// Whether newly created TCP sockets disable Nagle's algorithm.
   84     91   
    tcp_nodelay: bool,
   85     92   
    /// TCP keepalive setting, including explicit disablement.
   86     93   
    tcp_keepalive: TriStateOption<Duration>,
   87     94   
    /// Optional live-connection limit per scheme, host, and port.
   88     95   
    max_connections_per_host: Option<usize>,
   89     96   
    /// Partitions allowed to reuse each other's connections.
   90     97   
    reuse_scope: ConnectionReuseScope,
   91     98   
    /// Complete explicit partition set, or `None` for anonymous topology.
   92     99   
    partitions: Option<Vec<Partition>>,
         100  +
    /// Pool-wide proxy selection, authentication, and bypass policy.
         101  +
    proxy_config: ProxyConfig,
         102  +
    /// Caller-supplied DNS resolver, or `None` for the client default.
         103  +
    dns_resolver: Option<SharedDnsResolver>,
   93    104   
    /// TLS typestate carried into protocol-specific terminal builders.
   94    105   
    tls: Tls,
   95    106   
}
   96    107   
   97    108   
impl Default for Builder<TlsUnset> {
   98    109   
    fn default() -> Self {
   99    110   
        Self {
  100    111   
            idle_timeout: TriStateOption::NotSet,
  101    112   
            time_source: None,
  102    113   
            sleep_impl: None,
  103    114   
            tcp_nodelay: true,
  104    115   
            tcp_keepalive: TriStateOption::NotSet,
  105    116   
            max_connections_per_host: None,
  106    117   
            reuse_scope: ConnectionReuseScope::default(),
  107    118   
            partitions: None,
         119  +
            proxy_config: ProxyConfig::disabled(),
         120  +
            dns_resolver: None,
  108    121   
            tls: TlsUnset {},
  109    122   
        }
  110    123   
    }
  111    124   
}
  112    125   
  113    126   
impl<Tls: fmt::Debug> fmt::Debug for Builder<Tls> {
  114    127   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  115    128   
        f.debug_struct("Builder")
  116    129   
            .field("idle_timeout", &self.idle_timeout)
  117    130   
            .field("time_source", &self.time_source)
  118    131   
            .field("sleep_impl", &self.sleep_impl)
  119    132   
            .field("tcp_nodelay", &self.tcp_nodelay)
  120    133   
            .field("tcp_keepalive", &self.tcp_keepalive)
  121    134   
            .field("max_connections_per_host", &self.max_connections_per_host)
  122    135   
            .field("reuse_scope", &self.reuse_scope)
  123    136   
            .field("partitions", &self.partitions.as_ref().map(Vec::len))
         137  +
            .field(
         138  +
                "proxy_config",
         139  +
                &if self.proxy_config.is_disabled() {
         140  +
                    "disabled"
         141  +
                } else if self.proxy_config.is_from_env() {
         142  +
                    "environment"
         143  +
                } else {
         144  +
                    "configured"
         145  +
                },
         146  +
            )
         147  +
            .field(
         148  +
                "dns_resolver",
         149  +
                &if self.dns_resolver.is_some() {
         150  +
                    "custom"
         151  +
                } else {
         152  +
                    "default"
         153  +
                },
         154  +
            )
  124    155   
            .field("tls", &self.tls)
  125    156   
            .finish()
  126    157   
    }
  127    158   
}
  128    159   
  129    160   
impl<Tls> Builder<Tls> {
  130    161   
    /// Sets how long a reusable idle connection remains in the pool.
  131    162   
    ///
  132    163   
    /// Passing `None` disables idle expiration. When unset, the pool uses a
  133    164   
    /// 90-second timeout.
@@ -212,243 +429,547 @@
  232    263   
    pub fn partitions(mut self, partitions: impl IntoIterator<Item = Partition>) -> Self {
  233    264   
        self.partitions = Some(partitions.into_iter().collect());
  234    265   
        self
  235    266   
    }
  236    267   
  237    268   
    /// Mutably declares or clears the explicit partition set.
  238    269   
    pub fn set_partitions(&mut self, partitions: Option<Vec<Partition>>) -> &mut Self {
  239    270   
        self.partitions = partitions;
  240    271   
        self
  241    272   
    }
         273  +
         274  +
    /// Configures proxy selection for every connection established by this pool.
         275  +
    ///
         276  +
    /// The configuration applies across every partition. Proxying is disabled
         277  +
    /// unless this method is called.
         278  +
    pub fn proxy_config(mut self, config: ProxyConfig) -> Self {
         279  +
        self.proxy_config = config;
         280  +
        self
         281  +
    }
         282  +
         283  +
    /// Mutably configures pool-wide proxy selection.
         284  +
    ///
         285  +
    /// Passing `None` disables proxying.
         286  +
    pub fn set_proxy_config(&mut self, config: Option<ProxyConfig>) -> &mut Self {
         287  +
        self.proxy_config = config.unwrap_or_else(ProxyConfig::disabled);
         288  +
        self
         289  +
    }
         290  +
         291  +
    /// Configures the DNS resolver used by connections established by this pool.
         292  +
    ///
         293  +
    /// The resolver applies across every partition. When unset, the client uses
         294  +
    /// its default resolver, which currently delegates to the operating system.
         295  +
    pub fn dns_resolver(mut self, resolver: impl ResolveDns + 'static) -> Self {
         296  +
        self.dns_resolver = Some(resolver.into_shared());
         297  +
        self
         298  +
    }
         299  +
         300  +
    /// Mutably configures the pool-wide DNS resolver.
         301  +
    ///
         302  +
    /// Passing `None` restores the client default.
         303  +
    pub fn set_dns_resolver(&mut self, resolver: Option<SharedDnsResolver>) -> &mut Self {
         304  +
        self.dns_resolver = resolver;
         305  +
        self
         306  +
    }
  242    307   
}
  243    308   
  244    309   
impl Builder<TlsUnset> {
  245    310   
    /// Selects the TLS provider used by HTTPS pool connections.
  246    311   
    pub fn tls_provider(self, provider: tls::Provider) -> Builder<TlsProviderSelected> {
  247    312   
        Builder {
  248    313   
            idle_timeout: self.idle_timeout,
  249    314   
            time_source: self.time_source,
  250    315   
            sleep_impl: self.sleep_impl,
  251    316   
            tcp_nodelay: self.tcp_nodelay,
  252    317   
            tcp_keepalive: self.tcp_keepalive,
  253    318   
            max_connections_per_host: self.max_connections_per_host,
  254    319   
            reuse_scope: self.reuse_scope,
  255    320   
            partitions: self.partitions,
         321  +
            proxy_config: self.proxy_config,
         322  +
            dns_resolver: self.dns_resolver,
  256    323   
            tls: TlsProviderSelected {
  257    324   
                provider,
  258    325   
                context: TlsContext::default(),
  259    326   
            },
  260    327   
        }
  261    328   
    }
  262    329   
}
  263    330   
  264    331   
crate::cfg::cfg_tls! {
  265    332   
    impl Builder<TlsProviderSelected> {
  266    333   
        /// Sets provider-specific TLS configuration for HTTPS connections.
  267    334   
        pub fn tls_context(mut self, context: TlsContext) -> Self {
  268    335   
            self.tls.context = context;
  269    336   
            self
  270    337   
        }
  271    338   
  272    339   
        /// Mutably sets provider-specific TLS configuration.
  273    340   
        pub fn set_tls_context(&mut self, context: TlsContext) -> &mut Self {
  274    341   
            self.tls.context = context;
  275    342   
            self
  276    343   
        }
  277    344   
  278    345   
        /// Builds a pool whose connector performs TLS and ALPN negotiation.
  279         -
        ///
  280         -
        /// This builder currently creates direct connections. Proxy routing
  281         -
        /// remains available through the existing `Connector` API.
  282    346   
        pub fn build_https(self) -> Result<ConnectionPool, BuildError> {
         347  +
            let resolver = self.dns_resolver.clone();
         348  +
            match resolver {
         349  +
                Some(resolver) => {
         350  +
                    self.build_https_with_resolver(HyperUtilResolver { resolver })
         351  +
                }
         352  +
                None => self.build_https_with_resolver(GaiResolver::new()),
         353  +
            }
         354  +
        }
         355  +
         356  +
        /// Builds an HTTPS transport around one concrete Hyper resolver.
         357  +
        fn build_https_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
         358  +
        where
         359  +
            R: tower::Service<Name> + Clone + Send + Sync + 'static,
         360  +
            R::Response: Iterator<Item = SocketAddr>,
         361  +
            R::Future: Send,
         362  +
            R::Error: Into<Box<dyn Error + Send + Sync>>,
         363  +
        {
  283    364   
            validate_default_connector_interfaces(self.partitions.as_deref())?;
  284    365   
            let provider = self.tls.provider.clone();
  285    366   
            let context = self.tls.context.clone();
         367  +
            let proxy_config = self.proxy_config.clone();
  286    368   
            match provider {
  287    369   
                #[cfg(feature = "__rustls")]
  288    370   
                tls::Provider::Rustls(crypto_mode) => {
  289    371   
                    let tcp_nodelay = self.tcp_nodelay;
  290    372   
                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
  291    373   
                    let transport = establish::from_cached_interface_connector(
  292    374   
                        move |interface, alpn_protocols| {
  293    375   
                            let mut connector =
  294         -
                                HttpConnector::new_with_resolver(GaiResolver::new());
         376  +
                                HttpConnector::new_with_resolver(resolver.clone());
  295    377   
                            connector.set_nodelay(tcp_nodelay);
  296    378   
                            connector.set_keepalive(tcp_keepalive);
  297    379   
                            set_default_connector_interface(&mut connector, interface);
  298    380   
                            tls::rustls_provider::build_connector::wrap_connector_with_alpn(
  299    381   
                                connector,
  300    382   
                                crypto_mode.clone(),
  301    383   
                                &context,
  302         -
                                crate::proxy::ProxyConfig::disabled(),
         384  +
                                proxy_config.clone(),
  303    385   
                                alpn_protocols,
  304    386   
                            )
  305    387   
                        },
  306    388   
                        true,
  307    389   
                    );
  308    390   
                    self.build_with_transport(transport)
  309    391   
                }
  310    392   
                #[cfg(feature = "s2n-tls")]
  311    393   
                tls::Provider::S2nTls => {
  312    394   
                    let tcp_nodelay = self.tcp_nodelay;
  313    395   
                    let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
  314    396   
                    let transport = establish::from_cached_interface_connector(
  315    397   
                        move |interface, _alpn_protocols| {
  316    398   
                            let mut connector =
  317         -
                                HttpConnector::new_with_resolver(GaiResolver::new());
         399  +
                                HttpConnector::new_with_resolver(resolver.clone());
  318    400   
                            connector.set_nodelay(tcp_nodelay);
  319    401   
                            connector.set_keepalive(tcp_keepalive);
  320    402   
                            set_default_connector_interface(&mut connector, interface);
  321    403   
                            tls::s2n_tls_provider::build_connector::wrap_connector(
  322    404   
                                connector,
  323    405   
                                &context,
  324         -
                                crate::proxy::ProxyConfig::disabled(),
         406  +
                                proxy_config.clone(),
  325    407   
                            )
  326    408   
                        },
  327    409   
                        false,
  328    410   
                    );
  329    411   
                    self.build_with_transport(transport)
  330    412   
                }
  331    413   
            }
  332    414   
        }
  333    415   
    }
  334    416   
}
  335    417   
  336    418   
impl Builder<TlsUnset> {
  337    419   
    /// Builds a pool for cleartext HTTP connections.
  338    420   
    ///
  339    421   
    /// This method remains public for explicit cleartext endpoints and tests,
  340    422   
    /// but is hidden from generated documentation because HTTPS with automatic
  341    423   
    /// ALPN negotiation is the intended production entry point.
  342    424   
    #[doc(hidden)]
  343    425   
    pub fn build_http(self) -> Result<ConnectionPool, BuildError> {
         426  +
        let resolver = self.dns_resolver.clone();
         427  +
        match resolver {
         428  +
            Some(resolver) => self.build_http_with_resolver(HyperUtilResolver { resolver }),
         429  +
            None => self.build_http_with_resolver(GaiResolver::new()),
         430  +
        }
         431  +
    }
         432  +
         433  +
    /// Builds a cleartext transport around one concrete Hyper resolver.
         434  +
    fn build_http_with_resolver<R>(self, resolver: R) -> Result<ConnectionPool, BuildError>
         435  +
    where
         436  +
        R: tower::Service<Name> + Clone + Send + Sync + 'static,
         437  +
        R::Response: Iterator<Item = SocketAddr>,
         438  +
        R::Future: Send,
         439  +
        R::Error: Into<Box<dyn Error + Send + Sync>>,
         440  +
    {
  344    441   
        validate_default_connector_interfaces(self.partitions.as_deref())?;
  345         -
        let mut connector = HttpConnector::new_with_resolver(GaiResolver::new());
  346         -
        connector.set_nodelay(self.tcp_nodelay);
  347         -
        connector.set_keepalive(self.tcp_keepalive.clone().resolve(None));
  348         -
        let transport = establish::from_interface_connector(move |interface| {
  349         -
            let mut connector = connector.clone();
         442  +
        let tcp_nodelay = self.tcp_nodelay;
         443  +
        let tcp_keepalive = self.tcp_keepalive.clone().resolve(None);
         444  +
        let proxy_config = self.proxy_config.clone();
         445  +
        let transport = if proxy_config.is_disabled() {
         446  +
            establish::from_interface_connector(move |interface| {
         447  +
                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
         448  +
                connector.set_nodelay(tcp_nodelay);
         449  +
                connector.set_keepalive(tcp_keepalive);
  350    450   
                set_default_connector_interface(&mut connector, interface);
  351    451   
                connector
  352         -
        });
         452  +
            })
         453  +
        } else {
         454  +
            establish::from_interface_connector(move |interface| {
         455  +
                let mut connector = HttpConnector::new_with_resolver(resolver.clone());
         456  +
                connector.set_nodelay(tcp_nodelay);
         457  +
                connector.set_keepalive(tcp_keepalive);
         458  +
                set_default_connector_interface(&mut connector, interface);
         459  +
                HttpProxyConnector::new(connector, proxy_config.clone())
         460  +
            })
         461  +
        };
  353    462   
        self.build_with_transport(transport)
  354    463   
    }
  355    464   
  356    465   
    /// Builds a pool around a test transport connector.
  357    466   
    #[cfg(all(feature = "test-util", aws_sdk_unstable))]
  358    467   
    #[doc(hidden)]
  359    468   
    pub fn build_http_with_tcp_connector<C, IO>(
  360    469   
        self,
  361    470   
        connector: C,
  362    471   
    ) -> Result<ConnectionPool, BuildError>
  363    472   
    where
  364    473   
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  365    474   
        C::Error: Into<BoxError>,
  366    475   
        C::Future: Send + 'static,
  367    476   
        IO: hyper::rt::Read
  368    477   
            + hyper::rt::Write
  369    478   
            + hyper_util::client::legacy::connect::Connection
  370    479   
            + Send
  371    480   
            + Sync
  372    481   
            + Unpin
  373    482   
            + 'static,
  374    483   
    {
  375    484   
        self.build_with_transport(establish::from_connector(connector))
  376    485   
    }
  377    486   
  378    487   
    #[cfg(all(test, feature = "rt-tokio"))]
  379    488   
    pub(super) fn build_with_connector<C, IO>(
  380    489   
        self,
  381    490   
        connector: C,
  382    491   
    ) -> Result<ConnectionPool, BuildError>
  383    492   
    where
  384    493   
        C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  385    494   
        C::Error: Into<BoxError>,
  386    495   
        C::Future: Send + 'static,
  387    496   
        IO: hyper::rt::Read
  388    497   
            + hyper::rt::Write
  389    498   
            + hyper_util::client::legacy::connect::Connection
  390    499   
            + Send
  391    500   
            + Sync
  392    501   
            + Unpin
  393    502   
            + 'static,
  394    503   
    {
  395    504   
        self.build_with_transport(establish::from_connector(connector))
  396    505   
    }
  397    506   
}
  398    507   
  399    508   
impl<Tls> Builder<Tls> {
         509  +
    /// Builds a pool with a transport supplied by a pool-internal test.
         510  +
    #[cfg(test)]
         511  +
    pub(super) fn build_with_transport_for_test(
         512  +
        self,
         513  +
        transport: std::sync::Arc<dyn TransportFactory>,
         514  +
    ) -> Result<ConnectionPool, BuildError> {
         515  +
        self.build_with_transport(transport)
         516  +
    }
         517  +
  400    518   
    /// Validates pool policy and installs the type-erased transport factory.
  401    519   
    fn build_with_transport(
  402    520   
        self,
  403    521   
        transport: std::sync::Arc<dyn TransportFactory>,
  404    522   
    ) -> Result<ConnectionPool, BuildError> {
  405    523   
        let max_connections_per_host = match self.max_connections_per_host {
  406    524   
            Some(0) => {
  407    525   
                return Err(BuildError::new(
  408    526   
                    "max_connections_per_host must be greater than zero",
  409    527   
                ))
@@ -513,631 +670,844 @@
  533    651   
    target_os = "fuchsia",
  534    652   
    target_os = "illumos",
  535    653   
    target_os = "ios",
  536    654   
    target_os = "linux",
  537    655   
    target_os = "macos",
  538    656   
    target_os = "solaris",
  539    657   
    target_os = "tvos",
  540    658   
    target_os = "visionos",
  541    659   
    target_os = "watchos",
  542    660   
))]
  543         -
fn set_default_connector_interface(
  544         -
    connector: &mut HttpConnector<GaiResolver>,
  545         -
    interface: Option<&str>,
  546         -
) {
         661  +
fn set_default_connector_interface<R>(connector: &mut HttpConnector<R>, interface: Option<&str>) {
  547    662   
    if let Some(interface) = interface {
  548    663   
        connector.set_interface(interface);
  549    664   
    }
  550    665   
}
  551    666   
  552    667   
/// Leaves the default connector unchanged on unsupported targets.
  553    668   
#[cfg(not(any(
  554    669   
    target_os = "android",
  555    670   
    target_os = "fuchsia",
  556    671   
    target_os = "illumos",
  557    672   
    target_os = "ios",
  558    673   
    target_os = "linux",
  559    674   
    target_os = "macos",
  560    675   
    target_os = "solaris",
  561    676   
    target_os = "tvos",
  562    677   
    target_os = "visionos",
  563    678   
    target_os = "watchos",
  564    679   
)))]
  565         -
fn set_default_connector_interface(
  566         -
    _connector: &mut HttpConnector<GaiResolver>,
  567         -
    _interface: Option<&str>,
  568         -
) {
         680  +
fn set_default_connector_interface<R>(_connector: &mut HttpConnector<R>, _interface: Option<&str>) {
  569    681   
}
  570    682   
  571    683   
#[cfg(all(test, not(smithy_http_client_loom)))]
  572    684   
mod tests {
  573    685   
    use super::*;
  574    686   
    use crate::client::pool::{DriverSpawner, PartitionId};
  575    687   
    use std::future::Future;
         688  +
    use std::net::IpAddr;
  576    689   
    use std::pin::Pin;
  577    690   
  578    691   
    #[derive(Debug)]
  579    692   
    struct PendingSleep;
  580    693   
  581    694   
    impl AsyncSleep for PendingSleep {
  582    695   
        fn sleep(&self, _duration: Duration) -> aws_smithy_async::rt::sleep::Sleep {
  583    696   
            aws_smithy_async::rt::sleep::Sleep::new(std::future::pending())
  584    697   
        }
  585    698   
    }
  586    699   
  587    700   
    #[derive(Debug)]
  588    701   
    struct TestSpawner;
  589    702   
  590    703   
    impl DriverSpawner for TestSpawner {
  591    704   
        fn spawn(&self, driver: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {
  592    705   
            drop(driver);
  593    706   
        }
  594    707   
    }
  595    708   
         709  +
    #[derive(Debug)]
         710  +
    struct EmptyDnsResolver;
         711  +
         712  +
    impl ResolveDns for EmptyDnsResolver {
         713  +
        fn resolve_dns<'a>(
         714  +
            &'a self,
         715  +
            _name: &'a str,
         716  +
        ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
         717  +
            aws_smithy_runtime_api::client::dns::DnsFuture::ready(Ok(Vec::<IpAddr>::new()))
         718  +
        }
         719  +
    }
         720  +
  596    721   
    fn partition(index: usize) -> Partition {
  597    722   
        Partition::new(PartitionId::from_index(index), TestSpawner)
  598    723   
    }
  599    724   
  600    725   
    #[test]
  601    726   
    fn builder_preserves_unset_disabled_and_configured_settings() {
  602    727   
        let mut builder = Builder::default();
  603    728   
        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
  604    729   
        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
  605    730   
        assert!(builder.tcp_nodelay);
  606    731   
        assert_eq!(None, builder.max_connections_per_host);
  607    732   
        assert!(builder.partitions.is_none());
         733  +
        assert!(builder.proxy_config.is_disabled());
         734  +
        assert!(builder.dns_resolver.is_none());
  608    735   
  609    736   
        builder.set_idle_timeout(Some(None));
  610    737   
        builder.set_tcp_keepalive(Some(None));
  611    738   
        assert_eq!(TriStateOption::ExplicitlyUnset, builder.idle_timeout);
  612    739   
        assert_eq!(TriStateOption::ExplicitlyUnset, builder.tcp_keepalive);
  613    740   
  614    741   
        builder.set_idle_timeout(Some(Some(Duration::from_secs(5))));
  615    742   
        builder.set_tcp_keepalive(Some(Some(Duration::from_secs(10))));
  616    743   
        builder.set_tcp_nodelay(false);
  617    744   
        builder.set_max_connections_per_host(Some(3));
  618    745   
        builder.set_partitions(Some(vec![partition(7)]));
  619    746   
        assert_eq!(
  620    747   
            TriStateOption::Set(Duration::from_secs(5)),
  621    748   
            builder.idle_timeout
  622    749   
        );
  623    750   
        assert_eq!(
  624    751   
            TriStateOption::Set(Duration::from_secs(10)),
  625    752   
            builder.tcp_keepalive
  626    753   
        );
  627    754   
        assert!(!builder.tcp_nodelay);
  628    755   
        assert_eq!(Some(3), builder.max_connections_per_host);
  629    756   
        assert_eq!(1, builder.partitions.as_ref().unwrap().len());
  630    757   
  631    758   
        builder.set_idle_timeout(None);
  632    759   
        builder.set_tcp_keepalive(None);
  633    760   
        builder.set_max_connections_per_host(None);
  634    761   
        builder.set_partitions(None);
  635    762   
        assert_eq!(TriStateOption::NotSet, builder.idle_timeout);
  636    763   
        assert_eq!(TriStateOption::NotSet, builder.tcp_keepalive);
  637    764   
        assert_eq!(None, builder.max_connections_per_host);
  638    765   
        assert!(builder.partitions.is_none());
  639    766   
    }
  640    767   
         768  +
    #[test]
         769  +
    fn proxy_and_dns_settings_set_reset_and_redact() {
         770  +
        let mut builder = Builder::default();
         771  +
        builder.set_proxy_config(Some(
         772  +
            ProxyConfig::http("http://proxy.test:8080")
         773  +
                .unwrap()
         774  +
                .with_basic_auth("secret-user", "secret-password"),
         775  +
        ));
         776  +
        builder.set_dns_resolver(Some(SharedDnsResolver::new(EmptyDnsResolver)));
         777  +
         778  +
        assert!(!builder.proxy_config.is_disabled());
         779  +
        assert!(builder.dns_resolver.is_some());
         780  +
        let debug = format!("{builder:?}");
         781  +
        assert!(debug.contains("configured"));
         782  +
        assert!(debug.contains("custom"));
         783  +
        assert!(!debug.contains("secret-user"));
         784  +
        assert!(!debug.contains("secret-password"));
         785  +
         786  +
        builder.set_proxy_config(None);
         787  +
        builder.set_dns_resolver(None);
         788  +
        assert!(builder.proxy_config.is_disabled());
         789  +
        assert!(builder.dns_resolver.is_none());
         790  +
    }
         791  +
         792  +
    #[cfg(feature = "rustls-ring")]
         793  +
    #[test]
         794  +
    fn tls_selection_preserves_proxy_and_dns_settings() {
         795  +
        let builder = Builder::default()
         796  +
            .proxy_config(
         797  +
                ProxyConfig::http("http://proxy.test:8080")
         798  +
                    .unwrap()
         799  +
                    .with_basic_auth("secret-user", "secret-password"),
         800  +
            )
         801  +
            .dns_resolver(EmptyDnsResolver)
         802  +
            .tls_provider(tls::Provider::rustls(
         803  +
                tls::rustls_provider::CryptoMode::Ring,
         804  +
            ));
         805  +
         806  +
        assert!(!builder.proxy_config.is_disabled());
         807  +
        assert!(builder.dns_resolver.is_some());
         808  +
        let debug = format!("{builder:?}");
         809  +
        assert!(debug.contains("configured"));
         810  +
        assert!(debug.contains("custom"));
         811  +
        assert!(!debug.contains("secret-user"));
         812  +
        assert!(!debug.contains("secret-password"));
         813  +
    }
         814  +
  641    815   
    #[test]
  642    816   
    fn terminal_build_resolves_the_default_idle_timeout() {
  643    817   
        let pool = Builder::default()
  644    818   
            .sleep_impl(PendingSleep)
  645    819   
            .build_http()
  646    820   
            .unwrap();
  647    821   
  648    822   
        assert_eq!(
  649    823   
            Some(Duration::from_secs(90)),
  650    824   
            pool.inner.config.idle_timeout

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/client.rs

@@ -1,1 +51,55 @@
    5      5   
    6      6   
//! Partition-bound handles for Smithy HTTP operations.
    7      7   
    8      8   
use super::partition::PartitionId;
    9      9   
use super::registry::PartitionState;
   10     10   
use super::ConnectionPool;
   11     11   
use crate::client::downcast_error;
   12     12   
use crate::client::timeout::{self, TimeoutKind};
   13     13   
use crate::sync::Arc;
   14     14   
use aws_smithy_async::rt::sleep::{default_async_sleep, SharedAsyncSleep};
          15  +
use aws_smithy_runtime_api::box_error::BoxError;
   15     16   
use aws_smithy_runtime_api::client::connector_metadata::ConnectorMetadata;
   16     17   
use aws_smithy_runtime_api::client::http::{
   17     18   
    HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
   18     19   
};
   19     20   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   20     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   21         -
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
          22  +
use aws_smithy_runtime_api::client::runtime_components::{
          23  +
    RuntimeComponents, RuntimeComponentsBuilder,
          24  +
};
          25  +
use aws_smithy_types::config_bag::ConfigBag;
   22     26   
use std::borrow::Cow;
   23     27   
use std::error::Error;
   24     28   
use std::fmt;
   25     29   
use std::time::Duration;
   26     30   
   27     31   
/// Smithy HTTP client bound to one connection-pool partition.
   28     32   
///
   29     33   
/// Construction resolves the partition once. Every request uses that partition
   30     34   
/// for establishment placement and local reuse, while the configured reuse
   31     35   
/// scope may permit dispatch through a peer partition's connection. Cloning a
@@ -56,60 +115,130 @@
   76     80   
impl fmt::Debug for Client {
   77     81   
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
   78     82   
        f.debug_struct("Client")
   79     83   
            .field("partition", &self.partition.id())
   80     84   
            .field("pool", &self.pool)
   81     85   
            .finish_non_exhaustive()
   82     86   
    }
   83     87   
}
   84     88   
   85     89   
impl HttpClient for Client {
          90  +
    fn validate_base_client_config(
          91  +
        &self,
          92  +
        _: &RuntimeComponentsBuilder,
          93  +
        _: &ConfigBag,
          94  +
    ) -> Result<(), BoxError> {
          95  +
        self.pool
          96  +
            .inner
          97  +
            .transport
          98  +
            .initialize_for_partition(&self.partition)
          99  +
    }
         100  +
   86    101   
    fn http_connector(
   87    102   
        &self,
   88    103   
        settings: &HttpConnectorSettings,
   89    104   
        components: &RuntimeComponents,
   90    105   
    ) -> SharedHttpConnector {
   91    106   
        let connect_timeout = settings.connect_timeout();
   92    107   
        let read_timeout = settings.read_timeout();
   93    108   
        let sleep = components.sleep_impl().or_else(default_async_sleep);
   94    109   
   95    110   
        SharedHttpConnector::new(PoolConnector {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/connection.rs

@@ -1,1 +47,48 @@
    8      8   
//! [`ConnectionState`] serializes dispatch commitment with logical close.
    9      9   
//! [`DispatchGuard`] accounts for accepted requests.
   10     10   
//! [`PhysicalConnectionGuard`] follows root I/O until the client releases its
   11     11   
//! physical connection handle, including transfer through a protocol upgrade.
   12     12   
//! The operating system may continue TCP teardown afterward. Logical close
   13     13   
//! returns bounded capacity without waiting for either lifetime to finish.
   14     14   
   15     15   
use super::admission::CapacityLease;
   16     16   
use super::origin::OriginKey;
   17     17   
use super::partition::PartitionId;
          18  +
use crate::client::connect::ConnectPath;
   18     19   
use crate::sync::{Arc, Mutex};
   19     20   
pub use aws_smithy_runtime_api::client::connection::ConnectionId;
   20     21   
use aws_smithy_runtime_api::client::connection::ConnectionMetadata;
   21     22   
use http_1x::Extensions;
   22     23   
use hyper::rt::{Read, ReadBufCursor, Write};
   23     24   
use hyper_util::client::legacy::connect::{Connected, Connection, HttpInfo};
   24     25   
use pin_project_lite::pin_project;
   25     26   
use std::fmt;
   26     27   
use std::io::{self, IoSlice};
   27     28   
use std::net::SocketAddr;
@@ -51,52 +200,202 @@
   71     72   
    /// Canonical origin this connection may serve.
   72     73   
    origin: OriginKey,
   73     74   
    /// Partition that retains the transport and protocol driver.
   74     75   
    owner_partition: PartitionId,
   75     76   
    /// Protocol selected by configuration or ALPN.
   76     77   
    protocol: NegotiatedProtocol,
   77     78   
    /// Local socket address reported by the connector, when available.
   78     79   
    local_addr: Option<SocketAddr>,
   79     80   
    /// Remote socket address reported by the connector, when available.
   80     81   
    remote_addr: Option<SocketAddr>,
   81         -
    /// Whether the transport reaches the origin through a proxy.
   82         -
    proxied: bool,
          82  +
    /// How the established transport reaches its origin.
          83  +
    connect_path: ConnectPath,
   83     84   
    /// Connector metadata copied into every response on this connection.
   84     85   
    connected: Connected,
   85     86   
}
   86     87   
   87     88   
impl ConnectionInfo {
   88     89   
    /// Captures immutable facts after transport negotiation selects a protocol.
   89     90   
    pub(super) fn new(
   90     91   
        id: ConnectionId,
   91     92   
        origin: OriginKey,
   92     93   
        owner_partition: PartitionId,
   93     94   
        protocol: NegotiatedProtocol,
   94     95   
        connected: Connected,
   95     96   
    ) -> Arc<Self> {
   96     97   
        let mut extras = Extensions::new();
   97     98   
        connected.get_extras(&mut extras);
   98     99   
        let http_info = extras.get::<HttpInfo>();
         100  +
        let connect_path = ConnectPath::from_connected(&connected, &extras);
   99    101   
        Arc::new(Self {
  100    102   
            id,
  101    103   
            origin,
  102    104   
            owner_partition,
  103    105   
            protocol,
  104    106   
            local_addr: http_info.map(HttpInfo::local_addr),
  105    107   
            remote_addr: http_info.map(HttpInfo::remote_addr),
  106         -
            proxied: connected.is_proxied(),
         108  +
            connect_path,
  107    109   
            connected,
  108    110   
        })
  109    111   
    }
  110    112   
  111    113   
    /// Returns the pool-assigned physical connection identity.
  112    114   
    pub(super) fn id(&self) -> ConnectionId {
  113    115   
        self.id
  114    116   
    }
  115    117   
  116    118   
    /// Returns the canonical origin this connection may serve.
  117    119   
    pub(super) fn origin(&self) -> &OriginKey {
  118    120   
        &self.origin
  119    121   
    }
  120    122   
  121    123   
    /// Returns the partition that owns this connection's I/O and driver.
  122    124   
    pub(super) fn owner_partition(&self) -> PartitionId {
  123    125   
        self.owner_partition
  124    126   
    }
  125    127   
  126    128   
    /// Returns the established HTTP protocol.
  127    129   
    pub(super) fn protocol(&self) -> NegotiatedProtocol {
  128    130   
        self.protocol
  129    131   
    }
  130    132   
  131    133   
    /// Returns the connector-reported local socket address.
  132    134   
    #[cfg(test)]
  133    135   
    pub(super) fn local_addr(&self) -> Option<SocketAddr> {
  134    136   
        self.local_addr
  135    137   
    }
  136    138   
  137    139   
    /// Returns the connector-reported remote socket address.
  138    140   
    #[cfg(test)]
  139    141   
    pub(super) fn remote_addr(&self) -> Option<SocketAddr> {
  140    142   
        self.remote_addr
  141    143   
    }
  142    144   
  143         -
    /// Returns whether the origin is reached through a proxy.
  144         -
    pub(super) fn is_proxied(&self) -> bool {
  145         -
        self.proxied
         145  +
    /// Returns how this connection reaches its origin.
         146  +
    pub(super) fn connect_path(&self) -> &ConnectPath {
         147  +
        &self.connect_path
  146    148   
    }
  147    149   
  148    150   
    /// Copies connector-provided values into a response extension map.
  149    151   
    pub(super) fn apply_connector_extras(&self, extensions: &mut Extensions) {
  150    152   
        self.connected.get_extras(extensions);
  151    153   
    }
  152    154   
  153    155   
    /// Builds Smithy metadata with close authority for this H1 record.
  154    156   
    pub(super) fn metadata(&self, close: super::cell::h1::H1CloseHandle) -> ConnectionMetadata {
  155    157   
        let mut builder = ConnectionMetadata::builder()
  156         -
            .proxied(self.proxied)
         158  +
            .proxied(self.connect_path.is_proxied())
  157    159   
            .connection_id(self.id)
  158    160   
            .poison_fn(move || {
  159    161   
                close.close(CloseReason::Poisoned);
  160    162   
            });
  161    163   
        builder
  162    164   
            .set_local_addr(self.local_addr)
  163    165   
            .set_remote_addr(self.remote_addr);
  164    166   
        builder.build()
  165    167   
    }
  166    168   
  167    169   
    /// Builds Smithy metadata with close authority for this H2 generation.
  168    170   
    pub(super) fn h2_metadata(&self, close: super::cell::h2::H2CloseHandle) -> ConnectionMetadata {
  169    171   
        let mut builder = ConnectionMetadata::builder()
  170         -
            .proxied(self.proxied)
         172  +
            .proxied(self.connect_path.is_proxied())
  171    173   
            .connection_id(self.id)
  172    174   
            .poison_fn(move || {
  173    175   
                close.close(CloseReason::Poisoned);
  174    176   
            });
  175    177   
        builder
  176    178   
            .set_local_addr(self.local_addr)
  177    179   
            .set_remote_addr(self.remote_addr);
  178    180   
        builder.build()
  179    181   
    }
  180    182   
@@ -724,726 +784,786 @@
  744    746   
                .extra(ConnectorMarker("connector-extra")),
  745    747   
        );
  746    748   
        let (connection, _physical) = ConnectionState::unbounded(info);
  747    749   
  748    750   
        assert_eq!(ConnectionId::new(7), connection.info().id());
  749    751   
        assert_eq!(&origin, connection.info().origin());
  750    752   
        assert_eq!(PartitionId::from_index(2), connection.owner_partition());
  751    753   
        assert_eq!(NegotiatedProtocol::Http1, connection.info().protocol());
  752    754   
        assert_eq!(None, connection.info().local_addr());
  753    755   
        assert_eq!(None, connection.info().remote_addr());
  754         -
        assert!(connection.info().is_proxied());
         756  +
        assert!(connection.info().connect_path().is_proxied());
  755    757   
        let mut extensions = Extensions::new();
  756    758   
        connection.info().apply_connector_extras(&mut extensions);
  757    759   
        assert_eq!(
  758    760   
            Some(&ConnectorMarker("connector-extra")),
  759    761   
            extensions.get::<ConnectorMarker>()
  760    762   
        );
  761    763   
    }
  762    764   
  763    765   
    #[test]
  764    766   
    fn dropping_connection_io_completes_physical_connection_ownership() {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/dispatch/h1.rs

@@ -44,44 +104,106 @@
   64     64   
        let metadata =
   65     65   
            captured_metadata.unwrap_or_else(|| connection.info().metadata(close_handle));
   66     66   
        return Err(ConnectorError::user(
   67     67   
            "an HTTP/2 request cannot use an HTTP/1 connection".into(),
   68     68   
        )
   69     69   
        .with_connection(metadata));
   70     70   
    }
   71     71   
   72     72   
    add_host_header(&mut request, &context.absolute_uri)
   73     73   
        .map_err(|error| ConnectorError::user(error.into()))?;
   74         -
    rewrite_h1_request_target(&mut request, connection.info().is_proxied());
          74  +
    let connect_path = connection.info().connect_path();
          75  +
    connect_path.apply_proxy_authorization(request.headers_mut());
          76  +
    rewrite_h1_request_target(&mut request, connect_path.uses_absolute_form());
   75     77   
   76     78   
    // Commit against logical close before transferring the request to
   77     79   
    // Hyper. A close that wins this race leaves the request untouched.
   78     80   
    let Some(dispatch) = ConnectionState::try_commit_dispatch(&connection) else {
   79     81   
        tracing::trace!(
   80     82   
            connection_id = %connection.id(),
   81     83   
            request_partition = ?context.partition.id(),
   82     84   
            connection_partition = ?connection.owner_partition(),
   83     85   
            origin_scheme = %connection.info().origin().scheme(),
   84     86   
            origin_host = connection.info().origin().host(),
@@ -396,398 +455,458 @@
  416    418   
        return;
  417    419   
    }
  418    420   
    let mut parts = Parts::default();
  419    421   
    parts.path_and_query = request.uri().path_and_query().cloned();
  420    422   
    *request.uri_mut() = Uri::from_parts(parts).expect("request path and query are a valid URI");
  421    423   
}
  422    424   
  423    425   
#[cfg(all(test, not(smithy_http_client_loom), feature = "rt-tokio"))]
  424    426   
mod tests {
  425    427   
    use super::*;
         428  +
    use crate::client::connect::ConnectPath;
  426    429   
    use crate::client::pool::cell::h1::H1Sender;
  427    430   
    use crate::client::pool::cell::OriginCell;
  428    431   
    use crate::client::pool::connection::ConnectionInfo;
  429    432   
    use crate::client::pool::dispatch::RequestOptions;
  430    433   
    use crate::client::pool::registry::PartitionState;
  431    434   
    use crate::client::pool::{
  432    435   
        Client, ConnectionId, ConnectionPool, ConnectionReuseScope, Partition, PartitionId,
  433    436   
        TokioDriverSpawner,
  434    437   
    };
  435    438   
    use crate::client::timeout::test::{NeverConnects, NeverReplies};
@@ -799,802 +858,887 @@
  819    822   
            add_host_header(&mut request, &absolute_uri).unwrap();
  820    823   
  821    824   
            assert_eq!(
  822    825   
                expected,
  823    826   
                request.headers()[http_1x::header::HOST].to_str().unwrap(),
  824    827   
                "{name}"
  825    828   
            );
  826    829   
        }
  827    830   
    }
  828    831   
         832  +
    #[test]
         833  +
    fn forward_proxy_authorization_preserves_a_caller_value() {
         834  +
        let connect_path =
         835  +
            ConnectPath::forward_proxy(Some(http_1x::HeaderValue::from_static("Basic connector")));
         836  +
        let caller_value = http_1x::HeaderValue::from_static("Basic caller");
         837  +
         838  +
        let mut request = Request::get("http://example.com/")
         839  +
            .body(SdkBody::empty())
         840  +
            .unwrap();
         841  +
        connect_path.apply_proxy_authorization(request.headers_mut());
         842  +
        assert_eq!(
         843  +
            request.headers()[http_1x::header::PROXY_AUTHORIZATION],
         844  +
            "Basic connector"
         845  +
        );
         846  +
         847  +
        let mut request = Request::get("http://example.com/")
         848  +
            .header(http_1x::header::PROXY_AUTHORIZATION, caller_value.clone())
         849  +
            .body(SdkBody::empty())
         850  +
            .unwrap();
         851  +
        connect_path.apply_proxy_authorization(request.headers_mut());
         852  +
        assert_eq!(
         853  +
            request.headers().get(http_1x::header::PROXY_AUTHORIZATION),
         854  +
            Some(&caller_value)
         855  +
        );
         856  +
    }
         857  +
  829    858   
    #[test]
  830    859   
    fn h1_request_target_uses_the_required_wire_form() {
  831    860   
        let cases = [
  832    861   
            (
  833    862   
                "CONNECT",
  834    863   
                Method::CONNECT,
  835    864   
                "http://example.com:8443/ignored",
  836    865   
                false,
  837    866   
                "example.com:8443",
  838    867   
            ),

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/dispatch/h2.rs

@@ -1,1 +53,54 @@
   14     14   
//! [`H2RequestBody`] owns the upload guard and [`H2ResponseBody`] owns the
   15     15   
//! response guard. Either may finish first; the generation request count is
   16     16   
//! released only after both finish. A returned request re-arms the existing
   17     17   
//! body wrapper rather than nesting wrappers around the original body.
   18     18   
   19     19   
use super::super::cell::h2::{
   20     20   
    H2Activation, H2CloseHandle, H2DispatchParts, H2ResponseGuard, H2UploadGuard,
   21     21   
};
   22     22   
use super::super::connection::{CloseReason, ConnectionState};
   23     23   
use super::{AcquisitionContext, H1HostHeaderInserted};
          24  +
use crate::client::connect::ConnectPath;
   24     25   
use crate::client::downcast_error;
   25     26   
use crate::sync::{Arc, Mutex};
   26     27   
use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
   27     28   
use aws_smithy_runtime_api::client::result::ConnectorError;
   28     29   
use aws_smithy_types::body::SdkBody;
   29     30   
use http_1x::{Request, Response};
   30     31   
use hyper::body::Body;
   31     32   
use std::future::{poll_fn, Future};
   32     33   
use std::pin::Pin;
   33     34   
use std::task::{Context, Poll};
@@ -59,60 +120,125 @@
   79     80   
   80     81   
/// Dispatches one request through prospective H2 request authority.
   81     82   
///
   82     83   
/// Pool-side staleness reacquires before Hyper sees the request. Once Hyper
   83     84   
/// returns an envelope, only a reused generation may reacquire.
   84     85   
pub(super) async fn dispatch(
   85     86   
    context: &AcquisitionContext,
   86     87   
    mut request: Request<SdkBody>,
   87     88   
    mut activation: H2Activation,
   88     89   
) -> Result<H2DispatchOutcome, ConnectorError> {
   89         -
    prepare_h2_request(&mut request, &context.absolute_uri);
   90     90   
    let connection = activation.connection().clone();
          91  +
    prepare_h2_request(
          92  +
        &mut request,
          93  +
        &context.absolute_uri,
          94  +
        connection.info().connect_path(),
          95  +
    );
   91     96   
    let reused = activation.is_reused();
   92     97   
    let close = activation.close_handle();
   93     98   
    let captured_metadata = request
   94     99   
        .extensions()
   95    100   
        .get::<CaptureSmithyConnection>()
   96    101   
        .cloned()
   97    102   
        .map(|capture| {
   98    103   
            let metadata = connection.info().h2_metadata(close.clone());
   99    104   
            let captured = metadata.clone();
  100    105   
            capture.set_connection_retriever(move || Some(captured.clone()));
@@ -220,225 +289,299 @@
  240    245   
                close.close(CloseReason::ProtocolClosed);
  241    246   
            }
  242    247   
            drop(response_guard);
  243    248   
            let metadata =
  244    249   
                captured_metadata.unwrap_or_else(|| connection.info().h2_metadata(close));
  245    250   
            Err(downcast_error(Box::new(error.into_error())).with_connection(metadata))
  246    251   
        }
  247    252   
    }
  248    253   
}
  249    254   
  250         -
/// Restores the absolute URI and removes only a pool-synthesized H1 `Host`.
  251         -
fn prepare_h2_request(request: &mut Request<SdkBody>, absolute_uri: &http_1x::Uri) {
         255  +
/// Restores the absolute URI and applies connection-path request metadata.
         256  +
fn prepare_h2_request(
         257  +
    request: &mut Request<SdkBody>,
         258  +
    absolute_uri: &http_1x::Uri,
         259  +
    connect_path: &ConnectPath,
         260  +
) {
  252    261   
    *request.uri_mut() = absolute_uri.clone();
  253    262   
    if request
  254    263   
        .extensions_mut()
  255    264   
        .remove::<H1HostHeaderInserted>()
  256    265   
        .is_some()
  257    266   
    {
  258    267   
        request.headers_mut().remove(http_1x::header::HOST);
  259    268   
    }
         269  +
    connect_path.apply_proxy_authorization(request.headers_mut());
  260    270   
}
  261    271   
  262    272   
/// Point at which the pool learned that Hyper had not accepted the request.
  263    273   
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  264    274   
enum UnacceptedStage {
  265    275   
    /// The pool rejected a stale generation before calling Hyper.
  266    276   
    BeforeHyper,
  267    277   
    /// Hyper returned the original request envelope.
  268    278   
    ReturnedByHyper,
  269    279   
}
@@ -563,573 +636,674 @@
  583    593   
  584    594   
    #[test]
  585    595   
    fn h2_preparation_restores_uri_and_removes_only_synthesized_host() {
  586    596   
        let absolute: http_1x::Uri = "https://example.com/resource".parse().unwrap();
  587    597   
        let mut request = Request::get("/resource").body(SdkBody::empty()).unwrap();
  588    598   
        request
  589    599   
            .headers_mut()
  590    600   
            .insert(http_1x::header::HOST, "example.com".parse().unwrap());
  591    601   
        request.extensions_mut().insert(H1HostHeaderInserted);
  592    602   
  593         -
        prepare_h2_request(&mut request, &absolute);
         603  +
        prepare_h2_request(&mut request, &absolute, &ConnectPath::Direct);
  594    604   
  595    605   
        assert_eq!(&absolute, request.uri());
  596    606   
        assert!(!request.headers().contains_key(http_1x::header::HOST));
  597    607   
        assert!(request.extensions().get::<H1HostHeaderInserted>().is_none());
  598    608   
  599    609   
        let mut user_host = Request::get("/resource").body(SdkBody::empty()).unwrap();
  600    610   
        user_host
  601    611   
            .headers_mut()
  602    612   
            .insert(http_1x::header::HOST, "signed.example".parse().unwrap());
  603         -
        prepare_h2_request(&mut user_host, &absolute);
         613  +
        prepare_h2_request(&mut user_host, &absolute, &ConnectPath::Direct);
  604    614   
        assert_eq!("signed.example", user_host.headers()[http_1x::header::HOST]);
  605    615   
    }
  606    616   
         617  +
    #[test]
         618  +
    fn h2_preparation_applies_forward_proxy_auth_without_overwriting_caller() {
         619  +
        let absolute: http_1x::Uri = "http://example.com/resource".parse().unwrap();
         620  +
        let connect_path =
         621  +
            ConnectPath::forward_proxy(Some(http_1x::HeaderValue::from_static("Basic connector")));
         622  +
        let mut request = Request::get("/resource").body(SdkBody::empty()).unwrap();
         623  +
         624  +
        prepare_h2_request(&mut request, &absolute, &connect_path);
         625  +
         626  +
        assert_eq!(
         627  +
            "Basic connector",
         628  +
            request.headers()[http_1x::header::PROXY_AUTHORIZATION]
         629  +
        );
         630  +
         631  +
        let caller_value = http_1x::HeaderValue::from_static("Basic caller");
         632  +
        let mut request = Request::get("/resource")
         633  +
            .header(http_1x::header::PROXY_AUTHORIZATION, caller_value.clone())
         634  +
            .body(SdkBody::empty())
         635  +
            .unwrap();
         636  +
         637  +
        prepare_h2_request(&mut request, &absolute, &connect_path);
         638  +
         639  +
        assert_eq!(
         640  +
            Some(&caller_value),
         641  +
            request.headers().get(http_1x::header::PROXY_AUTHORIZATION)
         642  +
        );
         643  +
    }
         644  +
  607    645   
    #[test]
  608    646   
    fn retry_authority_distinguishes_pool_checks_from_hyper_returns() {
  609    647   
        let checked_before_hyper = resolve_unaccepted_request(
  610    648   
            Request::new(SdkBody::empty()),
  611    649   
            false,
  612    650   
            UnacceptedStage::BeforeHyper,
  613    651   
            ConnectorError::user("pre-Hyper failure".into()),
  614    652   
        );
  615    653   
        assert!(matches!(
  616    654   
            checked_before_hyper,

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/pool/establish/transport.rs

@@ -1,1 +46,47 @@
    5      5   
    6      6   
//! Transport connection policy below HTTP protocol establishment.
    7      7   
//!
    8      8   
//! This module owns the boundary from a pool connection attempt to connected
    9      9   
//! I/O with a negotiated protocol. [`TransportConnectContext`] keeps placement,
   10     10   
//! timeout, and ALPN inputs together. A factory may delegate DNS, proxy, TLS,
   11     11   
//! and ALPN to an injected connector, but it must report whether an HTTP/1-only
   12     12   
//! attempt is enforceable before admission may reclaim an idle H2 connection.
   13     13   
//!
   14     14   
//! Cached factories retain configured connector services, not connections or
   15         -
//! DNS results. Network I/O begins only when [`TransportFactory::connect`] is
   16         -
//! called.
          15  +
//! DNS results. Base client validation initializes those services for the
          16  +
//! selected partition without network I/O. Network I/O begins only when
          17  +
//! [`TransportFactory::connect`] is called.
   17     18   
   18     19   
use super::super::admission::ProtocolRequirement;
   19     20   
use super::super::registry::PartitionState;
   20     21   
use crate::client::connect::{AsyncConn, BoxConn};
   21     22   
use crate::client::timeout::{self, TimeoutKind};
   22     23   
use aws_smithy_async::rt::sleep::SharedAsyncSleep;
   23     24   
use aws_smithy_runtime_api::box_error::BoxError;
   24     25   
use http_1x::Uri;
   25     26   
use std::future::{poll_fn, Future};
   26     27   
use std::pin::Pin;
@@ -74,75 +133,144 @@
   94     95   
type TransportFuture = Pin<Box<dyn Future<Output = Result<BoxConn, BoxError>> + Send + 'static>>;
   95     96   
   96     97   
/// Type-erased transport construction below HTTP protocol establishment.
   97     98   
///
   98     99   
/// A pool retains one factory. The concrete connector remains responsible for
   99    100   
/// the transport stages it implements and for returning connector metadata.
  100    101   
/// Protocol establishment consumes the resulting [`BoxConn`].
  101    102   
pub(in crate::client::pool) trait TransportFactory:
  102    103   
    Send + Sync + 'static
  103    104   
{
         105  +
    /// Initializes connector services used by one partition.
         106  +
    ///
         107  +
    /// Initialization must be idempotent and must not perform DNS resolution
         108  +
    /// or network I/O. Factories without retained initialization state may use
         109  +
    /// the default no-op implementation.
         110  +
    fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         111  +
        let _ = partition;
         112  +
        Ok(())
         113  +
    }
         114  +
  104    115   
    /// Returns whether an H1-required attempt is guaranteed to negotiate H1.
  105    116   
    fn can_guarantee_http1(&self) -> bool;
  106    117   
  107    118   
    /// Creates one partition-bound transport.
  108    119   
    ///
  109    120   
    /// Connector readiness completes before the connection timeout starts.
  110    121   
    fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture;
  111    122   
}
  112    123   
  113    124   
/// Connector factory whose concrete service is selected by interface.
@@ -180,191 +0,569 @@
  200    211   
    C::Error: Into<BoxError>,
  201    212   
    C::Future: Send + 'static,
  202    213   
    IO: AsyncConn,
  203    214   
{
  204    215   
    StdArc::new(ServiceTransportFactory {
  205    216   
        connector_for_interface,
  206    217   
        can_guarantee_http1,
  207    218   
    })
  208    219   
}
  209    220   
  210         -
/// Erases and caches configured TLS connector services.
         221  +
/// Identifies one configured connector service in the TLS transport cache.
  211    222   
///
  212         -
/// Interface placement is immutable for a partition. The first request for
  213         -
/// each `(interface, ALPN offer)` constructs its connector while holding the
  214         -
/// cache lock; later requests clone that retained connector. Provider
  215         -
/// configuration and certificate loading therefore occur at most once for
  216         -
/// each placement and offer rather than once per connection.
         223  +
/// Partition identity is deliberately absent. Partitions with equal interface
         224  +
/// bindings use the same connector configuration and therefore share entries.
  217    225   
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
  218         -
pub(in crate::client::pool) fn from_cached_interface_connector<F, C, IO>(
  219         -
    connector_for_interface: F,
         226  +
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
         227  +
struct ConnectorCacheKey {
         228  +
    /// Network-interface binding applied before connect.
         229  +
    interface: Option<StdArc<str>>,
         230  +
    /// Protocol offer installed in the TLS connector.
         231  +
    alpn_protocols: AlpnProtocols,
         232  +
}
         233  +
         234  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         235  +
impl ConnectorCacheKey {
         236  +
    /// Derives the connector configuration used by one partition and offer.
         237  +
    fn for_partition(partition: &PartitionState, alpn_protocols: AlpnProtocols) -> Self {
         238  +
        Self {
         239  +
            interface: partition.interface().cloned(),
         240  +
            alpn_protocols,
         241  +
        }
         242  +
    }
         243  +
         244  +
    /// Returns the borrowed interface name expected by connector builders.
         245  +
    fn interface(&self) -> Option<&str> {
         246  +
        self.interface.as_deref()
         247  +
    }
         248  +
}
         249  +
         250  +
/// Retains configured TLS connector services by placement and ALPN offer.
         251  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         252  +
struct CachedTransportFactory<F, C> {
         253  +
    /// Constructs a connector service for one exact cache key.
         254  +
    factory: F,
         255  +
    /// Whether every H1-required connection is guaranteed to negotiate H1.
  220    256   
    can_guarantee_http1: bool,
  221         -
) -> StdArc<dyn TransportFactory>
         257  +
    /// Connector services constructed during client validation or first use.
         258  +
    connectors: crate::sync::Mutex<std::collections::HashMap<ConnectorCacheKey, C>>,
         259  +
}
         260  +
         261  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         262  +
impl<F, C> CachedTransportFactory<F, C>
  222    263   
where
  223         -
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  224         -
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  225         -
    C::Error: Into<BoxError>,
  226         -
    C::Future: Send + 'static,
  227         -
    IO: AsyncConn,
         264  +
    F: Fn(Option<&str>, AlpnProtocols) -> C,
         265  +
    C: Clone,
  228    266   
{
  229         -
    struct CachedTransportFactory<F, C> {
  230         -
        factory: F,
  231         -
        can_guarantee_http1: bool,
  232         -
        connectors:
  233         -
            crate::sync::Mutex<std::collections::HashMap<(Option<String>, AlpnProtocols), C>>,
         267  +
    /// Returns the retained connector for one partition and protocol offer.
         268  +
    fn connector(&self, partition: &PartitionState, alpn_protocols: AlpnProtocols) -> C {
         269  +
        let key = ConnectorCacheKey::for_partition(partition, alpn_protocols);
         270  +
        match self.connectors.lock().entry(key) {
         271  +
            std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
         272  +
            std::collections::hash_map::Entry::Vacant(entry) => {
         273  +
                let connector = (self.factory)(entry.key().interface(), entry.key().alpn_protocols);
         274  +
                entry.insert(connector).clone()
         275  +
            }
  234    276   
        }
         277  +
    }
         278  +
         279  +
    /// Materializes every connector variant this partition may select.
         280  +
    fn initialize_connectors_for_partition(&self, partition: &PartitionState) {
         281  +
        drop(self.connector(partition, HTTP_ALPN_PROTOCOLS));
         282  +
        drop(self.connector(partition, HTTP1_ALPN_PROTOCOLS));
         283  +
    }
         284  +
}
  235    285   
  236         -
    impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
  237         -
    where
         286  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         287  +
impl<F, C, IO> TransportFactory for CachedTransportFactory<F, C>
         288  +
where
  238    289   
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
  239    290   
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
  240    291   
    C::Error: Into<BoxError>,
  241    292   
    C::Future: Send + 'static,
  242    293   
    IO: AsyncConn,
  243         -
    {
         294  +
{
         295  +
    fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         296  +
        self.initialize_connectors_for_partition(partition);
         297  +
        Ok(())
         298  +
    }
         299  +
  244    300   
    fn can_guarantee_http1(&self) -> bool {
  245    301   
        self.can_guarantee_http1
  246    302   
    }
  247    303   
  248    304   
    fn connect(&self, context: TransportConnectContext<'_>) -> TransportFuture {
  249    305   
        let TransportConnectContext {
  250    306   
            partition,
  251    307   
            uri,
  252    308   
            timeout,
  253    309   
            alpn_protocols,
  254    310   
        } = context;
  255         -
            let interface = partition.interface().map(|value| value.to_string());
  256         -
            let mut connector = self
  257         -
                .connectors
  258         -
                .lock()
  259         -
                .entry((interface.clone(), alpn_protocols))
  260         -
                .or_insert_with(|| (self.factory)(interface.as_deref(), alpn_protocols))
  261         -
                .clone();
         311  +
        let mut connector = self.connector(partition, alpn_protocols);
  262    312   
        Box::pin(async move {
  263    313   
            poll_fn(|cx| connector.poll_ready(cx))
  264    314   
                .await
  265    315   
                .map_err(Into::into)?;
  266    316   
            let connect = connector.call(uri);
  267    317   
            let io = timeout::maybe_timeout_future(
  268    318   
                connect,
  269    319   
                timeout.as_ref().map(|timeout| timeout.duration),
  270    320   
                timeout.as_ref().map(|timeout| &timeout.sleep),
  271    321   
                TimeoutKind::Connect,
  272    322   
            )
  273    323   
            .await?;
  274    324   
            Ok(Box::new(io) as BoxConn)
  275    325   
        })
  276    326   
    }
  277         -
    }
         327  +
}
  278    328   
         329  +
/// Erases and caches configured TLS connector services.
         330  +
///
         331  +
/// Interface placement is immutable for a partition. Client validation
         332  +
/// constructs both ALPN variants for the selected partition while holding the
         333  +
/// cache lock; later requests clone the retained connector. Provider
         334  +
/// configuration and certificate loading therefore occur at most once for
         335  +
/// each placement and offer rather than once per connection.
         336  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         337  +
pub(in crate::client::pool) fn from_cached_interface_connector<F, C, IO>(
         338  +
    connector_for_interface: F,
         339  +
    can_guarantee_http1: bool,
         340  +
) -> StdArc<dyn TransportFactory>
         341  +
where
         342  +
    F: Fn(Option<&str>, AlpnProtocols) -> C + Send + Sync + 'static,
         343  +
    C: Service<Uri, Response = IO> + Clone + Send + Sync + 'static,
         344  +
    C::Error: Into<BoxError>,
         345  +
    C::Future: Send + 'static,
         346  +
    IO: AsyncConn,
         347  +
{
  279    348   
    StdArc::new(CachedTransportFactory {
  280    349   
        factory: connector_for_interface,
  281    350   
        can_guarantee_http1,
  282    351   
        connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
  283    352   
    })
  284    353   
}
  285    354   
  286    355   
/// Selects the ALPN offer that preserves the request's wire semantics.
  287    356   
fn alpn_protocols(requirement: ProtocolRequirement) -> AlpnProtocols {
  288    357   
    match requirement {
  289    358   
        ProtocolRequirement::H1Required => HTTP1_ALPN_PROTOCOLS,
  290    359   
        ProtocolRequirement::H1Compatible | ProtocolRequirement::H2Required => HTTP_ALPN_PROTOCOLS,
  291    360   
    }
  292    361   
}
         362  +
         363  +
#[cfg(all(
         364  +
    test,
         365  +
    any(feature = "__rustls", feature = "s2n-tls"),
         366  +
    any(
         367  +
        target_os = "android",
         368  +
        target_os = "fuchsia",
         369  +
        target_os = "illumos",
         370  +
        target_os = "ios",
         371  +
        target_os = "linux",
         372  +
        target_os = "macos",
         373  +
        target_os = "solaris",
         374  +
        target_os = "tvos",
         375  +
        target_os = "visionos",
         376  +
        target_os = "watchos",
         377  +
    )
         378  +
))]
         379  +
mod tests {
         380  +
    use super::*;
         381  +
    use crate::client::pool::maintenance::MaintenanceConfig;
         382  +
    use crate::client::pool::partition::{
         383  +
        ConnectionReuseScope, DriverSpawner, Partition, PartitionId,
         384  +
    };
         385  +
    use crate::client::pool::registry::PartitionRegistry;
         386  +
    use aws_smithy_runtime_api::client::http::HttpClient;
         387  +
    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
         388  +
    use aws_smithy_types::config_bag::ConfigBag;
         389  +
         390  +
    use std::sync::{Arc, Mutex};
         391  +
         392  +
    #[derive(Debug)]
         393  +
    struct TestSpawner;
         394  +
         395  +
    impl DriverSpawner for TestSpawner {
         396  +
        fn spawn(&self, _: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) {}
         397  +
    }
         398  +
         399  +
    struct InitializationRecorder {
         400  +
        partitions: Arc<Mutex<Vec<PartitionId>>>,
         401  +
    }
         402  +
         403  +
    impl TransportFactory for InitializationRecorder {
         404  +
        fn initialize_for_partition(&self, partition: &PartitionState) -> Result<(), BoxError> {
         405  +
            self.partitions
         406  +
                .lock()
         407  +
                .expect("initialization log is not poisoned")
         408  +
                .push(partition.id());
         409  +
            Ok(())
         410  +
        }
         411  +
         412  +
        fn can_guarantee_http1(&self) -> bool {
         413  +
            true
         414  +
        }
         415  +
         416  +
        fn connect(&self, _: TransportConnectContext<'_>) -> TransportFuture {
         417  +
            Box::pin(async { panic!("client validation must not start a connection") })
         418  +
        }
         419  +
    }
         420  +
         421  +
    type Construction = (Option<String>, AlpnProtocols);
         422  +
         423  +
    struct CachedFactoryFixture<F> {
         424  +
        factory: CachedTransportFactory<F, usize>,
         425  +
        constructions: Arc<Mutex<Vec<Construction>>>,
         426  +
    }
         427  +
         428  +
    fn recording_factory() -> CachedFactoryFixture<impl Fn(Option<&str>, AlpnProtocols) -> usize> {
         429  +
        let constructions = Arc::new(Mutex::new(Vec::new()));
         430  +
        let observed = constructions.clone();
         431  +
        let factory = move |interface: Option<&str>, alpn_protocols: AlpnProtocols| {
         432  +
            let mut constructions = observed.lock().expect("construction log is not poisoned");
         433  +
            constructions.push((interface.map(str::to_owned), alpn_protocols));
         434  +
            constructions.len()
         435  +
        };
         436  +
        CachedFactoryFixture {
         437  +
            factory: CachedTransportFactory {
         438  +
                factory,
         439  +
                can_guarantee_http1: true,
         440  +
                connectors: crate::sync::Mutex::new(std::collections::HashMap::new()),
         441  +
            },
         442  +
            constructions,
         443  +
        }
         444  +
    }
         445  +
         446  +
    fn registry(partitions: Option<Vec<Partition>>) -> PartitionRegistry {
         447  +
        PartitionRegistry::new(
         448  +
            partitions,
         449  +
            ConnectionReuseScope::Partition,
         450  +
            None,
         451  +
            MaintenanceConfig::default(),
         452  +
        )
         453  +
        .expect("valid partition registry")
         454  +
    }
         455  +
         456  +
    #[test]
         457  +
    fn client_validation_initializes_its_selected_partition() {
         458  +
        let selected = PartitionId::from_index(7);
         459  +
        let initialized = Arc::new(Mutex::new(Vec::new()));
         460  +
        let pool = crate::client::pool::builder::Builder::default()
         461  +
            .partitions([Partition::new(selected, TestSpawner)])
         462  +
            .build_with_transport_for_test(Arc::new(InitializationRecorder {
         463  +
                partitions: initialized.clone(),
         464  +
            }))
         465  +
            .expect("valid test pool");
         466  +
        let client = crate::client::pool::Client::from_partition(&pool, selected)
         467  +
            .expect("selected partition exists");
         468  +
         469  +
        client
         470  +
            .validate_base_client_config(&RuntimeComponentsBuilder::for_tests(), &ConfigBag::base())
         471  +
            .expect("transport initialization succeeds");
         472  +
         473  +
        assert_eq!(
         474  +
            vec![selected],
         475  +
            *initialized
         476  +
                .lock()
         477  +
                .expect("initialization log is not poisoned")
         478  +
        );
         479  +
    }
         480  +
         481  +
    #[test]
         482  +
    fn initialization_constructs_each_alpn_variant_once() {
         483  +
        let partition = registry(None)
         484  +
            .partition(PartitionId::ANONYMOUS)
         485  +
            .expect("anonymous partition exists");
         486  +
        let fixture = recording_factory();
         487  +
         488  +
        fixture
         489  +
            .factory
         490  +
            .initialize_connectors_for_partition(&partition);
         491  +
        fixture
         492  +
            .factory
         493  +
            .initialize_connectors_for_partition(&partition);
         494  +
         495  +
        let constructions = fixture
         496  +
            .constructions
         497  +
            .lock()
         498  +
            .expect("construction log is not poisoned");
         499  +
        assert_eq!(2, constructions.len());
         500  +
        assert_eq!(None, constructions[0].0);
         501  +
        assert_eq!(HTTP_ALPN_PROTOCOLS, constructions[0].1);
         502  +
        assert_eq!(HTTP1_ALPN_PROTOCOLS, constructions[1].1);
         503  +
        assert_eq!(2, fixture.factory.connectors.lock().len());
         504  +
    }
         505  +
         506  +
    #[test]
         507  +
    fn equal_interface_bindings_share_connector_entries() {
         508  +
        let first = PartitionId::from_index(1);
         509  +
        let second = PartitionId::from_index(2);
         510  +
        let registry = registry(Some(vec![
         511  +
            Partition::new(first, TestSpawner).interface("interface-a"),
         512  +
            Partition::new(second, TestSpawner).interface("interface-a"),
         513  +
        ]));
         514  +
        let fixture = recording_factory();
         515  +
         516  +
        fixture.factory.initialize_connectors_for_partition(
         517  +
            &registry.partition(first).expect("first partition exists"),
         518  +
        );
         519  +
        fixture.factory.initialize_connectors_for_partition(
         520  +
            &registry.partition(second).expect("second partition exists"),
         521  +
        );
         522  +
         523  +
        let constructions = fixture
         524  +
            .constructions
         525  +
            .lock()
         526  +
            .expect("construction log is not poisoned");
         527  +
        assert_eq!(
         528  +
            [
         529  +
                (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
         530  +
                (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
         531  +
            ],
         532  +
            constructions.as_slice()
         533  +
        );
         534  +
        assert_eq!(2, fixture.factory.connectors.lock().len());
         535  +
    }
         536  +
         537  +
    #[test]
         538  +
    fn distinct_interface_bindings_use_distinct_connector_entries() {
         539  +
        let first = PartitionId::from_index(1);
         540  +
        let second = PartitionId::from_index(2);
         541  +
        let registry = registry(Some(vec![
         542  +
            Partition::new(first, TestSpawner).interface("interface-a"),
         543  +
            Partition::new(second, TestSpawner).interface("interface-b"),
         544  +
        ]));
         545  +
        let fixture = recording_factory();
         546  +
         547  +
        fixture.factory.initialize_connectors_for_partition(
         548  +
            &registry.partition(first).expect("first partition exists"),
         549  +
        );
         550  +
        fixture.factory.initialize_connectors_for_partition(
         551  +
            &registry.partition(second).expect("second partition exists"),
         552  +
        );
         553  +
         554  +
        let constructions = fixture
         555  +
            .constructions
         556  +
            .lock()
         557  +
            .expect("construction log is not poisoned");
         558  +
        assert_eq!(
         559  +
            [
         560  +
                (Some("interface-a".to_string()), HTTP_ALPN_PROTOCOLS),
         561  +
                (Some("interface-a".to_string()), HTTP1_ALPN_PROTOCOLS),
         562  +
                (Some("interface-b".to_string()), HTTP_ALPN_PROTOCOLS),
         563  +
                (Some("interface-b".to_string()), HTTP1_ALPN_PROTOCOLS),
         564  +
            ],
         565  +
            constructions.as_slice()
         566  +
        );
         567  +
        assert_eq!(4, fixture.factory.connectors.lock().len());
         568  +
    }
         569  +
}

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/tls/rustls_provider.rs

@@ -318,318 +378,378 @@
  338    338   
            .collect();
  339    339   
        conn.enforce_http(false);
  340    340   
        let https_connector =
  341    341   
            hyper_rustls::HttpsConnector::from((conn, client_config.clone()));
  342    342   
  343    343   
        super::connect::RustTlsConnector::new(https_connector, client_config, proxy_config)
  344    344   
    }
  345    345   
}
  346    346   
  347    347   
pub(crate) mod connect {
  348         -
    use crate::client::connect::{Conn, Connecting};
         348  +
    use crate::client::connect::{Conn, ConnectPath, Connecting};
  349    349   
    use crate::client::proxy::ProxyConfig;
  350    350   
    use aws_smithy_runtime_api::box_error::BoxError;
  351    351   
    use http_1x::uri::Scheme;
  352    352   
    use http_1x::Uri;
  353    353   
    use hyper::rt::{Read, ReadBufCursor, Write};
  354    354   
    use hyper_rustls::MaybeHttpsStream;
  355    355   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
  356    356   
    use hyper_util::client::proxy::matcher::Matcher;
  357    357   
    use hyper_util::rt::TokioIo;
  358    358   
    use pin_project_lite::pin_project;
@@ -422,422 +554,552 @@
  442    442   
        R::Response: Iterator<Item = std::net::SocketAddr>,
  443    443   
        R::Future: Send,
  444    444   
        R::Error: Into<Box<dyn Error + Send + Sync>>,
  445    445   
    {
  446    446   
        fn handle_direct_connection(&mut self, dst: Uri) -> Connecting {
  447    447   
            let fut = self.https.call(dst);
  448    448   
            Box::pin(async move {
  449    449   
                let conn = fut.await?;
  450    450   
                Ok(Conn {
  451    451   
                    inner: Box::new(conn),
  452         -
                    is_proxy: false,
         452  +
                    connect_path: ConnectPath::Direct,
  453    453   
                })
  454    454   
            })
  455    455   
        }
  456    456   
  457    457   
        fn handle_http_through_proxy(
  458    458   
            &mut self,
  459    459   
            _dst: Uri,
  460    460   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  461    461   
        ) -> Connecting {
  462    462   
            // For HTTP through proxy, connect to the proxy and let it handle the request
  463    463   
            let proxy_uri = intercept.uri().clone();
         464  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  464    465   
            let fut = self.https.call(proxy_uri);
  465    466   
            Box::pin(async move {
  466    467   
                let conn = fut.await?;
  467    468   
                Ok(Conn {
  468    469   
                    inner: Box::new(conn),
  469         -
                    is_proxy: true,
         470  +
                    connect_path,
  470    471   
                })
  471    472   
            })
  472    473   
        }
  473    474   
  474    475   
        fn handle_https_through_proxy(
  475    476   
            &mut self,
  476    477   
            dst: Uri,
  477    478   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  478    479   
        ) -> Connecting {
  479    480   
            use rustls_pki_types::ServerName as RustlsServerName;
  480    481   
            // For HTTPS through HTTP proxy, we need to:
  481    482   
            // 1. Establish CONNECT tunnel using the HTTPS connector
  482    483   
            // 2. Perform manual TLS handshake over the tunneled stream
  483    484   
  484    485   
            let tunnel = hyper_util::client::legacy::connect::proxy::Tunnel::new(
  485    486   
                intercept.uri().clone(),
  486    487   
                self.https.clone(),
  487    488   
            );
  488    489   
  489    490   
            // Configure tunnel with authentication if present
  490    491   
            let mut tunnel = if let Some(auth) = intercept.basic_auth() {
  491    492   
                tunnel.with_auth(auth.clone())
  492    493   
            } else {
  493    494   
                tunnel
  494    495   
            };
  495    496   
  496    497   
            let tls_config = self.tls_config.clone();
  497    498   
            let dst_clone = dst.clone();
  498    499   
  499    500   
            Box::pin(async move {
  500    501   
                // Establish CONNECT tunnel
  501    502   
                tracing::trace!("tunneling HTTPS over proxy");
  502         -
                let tunneled = tunnel
  503         -
                    .call(dst_clone.clone())
  504         -
                    .await
  505         -
                    .map_err(|e| BoxError::from(format!("CONNECT tunnel failed: {e}")))?;
         503  +
                let tunneled = tunnel.call(dst_clone.clone()).await?;
  506    504   
  507    505   
                // Stage 2: Manual TLS handshake over tunneled stream
  508    506   
                let host = dst_clone
  509    507   
                    .host()
  510    508   
                    .ok_or("missing host in URI for TLS handshake")?;
  511    509   
  512    510   
                let server_name = RustlsServerName::try_from(host.to_owned()).map_err(|e| {
  513    511   
                    BoxError::from(format!("invalid server name for TLS handshake: {e}"))
  514    512   
                })?;
  515    513   
  516    514   
                let tls_connector = tokio_rustls::TlsConnector::from(tls_config)
  517    515   
                    .connect(server_name, TokioIo::new(tunneled))
  518    516   
                    .await?;
  519    517   
  520    518   
                Ok(Conn {
  521    519   
                    inner: Box::new(RustTlsConn {
  522    520   
                        inner: TokioIo::new(tls_connector),
  523    521   
                    }),
  524         -
                    is_proxy: true,
         522  +
                    connect_path: ConnectPath::ProxyTunnel,
  525    523   
                })
  526    524   
            })
  527    525   
        }
  528    526   
    }
  529    527   
  530    528   
    pin_project! {
  531    529   
        pub(crate) struct RustTlsConn<T> {
  532    530   
            #[pin] pub(super) inner: TokioIo<TlsStream<T>>
  533    531   
        }
  534    532   
    }

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/src/client/tls/s2n_tls_provider.rs

@@ -209,209 +437,491 @@
  229    229   
            assert!(!matches_host_name("baz1.example.com", "baz*.example.com"));
  230    230   
            assert!(!matches_host_name(
  231    231   
                "foo.bar.example.com",
  232    232   
                "foo.*.example.com"
  233    233   
            ));
  234    234   
        }
  235    235   
    }
  236    236   
}
  237    237   
  238    238   
pub(crate) mod connect {
  239         -
    use crate::client::connect::{Conn, Connecting};
         239  +
    use crate::client::connect::{Conn, ConnectPath, Connecting};
  240    240   
    use crate::client::proxy::ProxyConfig;
  241    241   
    use aws_smithy_runtime_api::box_error::BoxError;
  242    242   
    use http_1x::uri::Scheme;
  243    243   
    use http_1x::Uri;
  244    244   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
  245    245   
    use hyper_util::client::proxy::matcher::Matcher;
  246    246   
    use hyper_util::rt::TokioIo;
  247    247   
    use std::error::Error;
  248    248   
    use std::sync::Arc;
  249    249   
    use std::{
         250  +
        future::Future,
  250    251   
        io::IoSlice,
  251    252   
        pin::Pin,
  252    253   
        task::{Context, Poll},
  253    254   
    };
  254    255   
    use tower::Service;
  255    256   
         257  +
    /// Restores the source chain omitted by `s2n-tls-hyper`'s error wrapper.
         258  +
    #[derive(Debug)]
         259  +
    struct S2nConnectorError(s2n_tls_hyper::error::Error);
         260  +
         261  +
    impl std::fmt::Display for S2nConnectorError {
         262  +
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         263  +
            self.0.fmt(f)
         264  +
        }
         265  +
    }
         266  +
         267  +
    impl Error for S2nConnectorError {
         268  +
        fn source(&self) -> Option<&(dyn Error + 'static)> {
         269  +
            match &self.0 {
         270  +
                s2n_tls_hyper::error::Error::HttpError(error) => Some(error.as_ref()),
         271  +
                s2n_tls_hyper::error::Error::TlsError(error) => Some(error),
         272  +
                _ => Some(&self.0),
         273  +
            }
         274  +
        }
         275  +
    }
         276  +
         277  +
    fn box_s2n_connector_error(error: s2n_tls_hyper::error::Error) -> BoxError {
         278  +
        Box::new(S2nConnectorError(error))
         279  +
    }
         280  +
         281  +
    /// Adapts s2n connector errors before another connector wraps them.
         282  +
    #[derive(Clone)]
         283  +
    struct S2nErrorSourceConnector<C>(C);
         284  +
         285  +
    impl<C> Service<Uri> for S2nErrorSourceConnector<C>
         286  +
    where
         287  +
        C: Service<Uri, Error = s2n_tls_hyper::error::Error>,
         288  +
        C::Future: Send + 'static,
         289  +
        C::Response: 'static,
         290  +
    {
         291  +
        type Response = C::Response;
         292  +
        type Error = S2nConnectorError;
         293  +
        type Future = Pin<
         294  +
            Box<
         295  +
                dyn Future<Output = Result<Self::Response, Self::Error>>
         296  +
                    + Send
         297  +
                    + 'static,
         298  +
            >,
         299  +
        >;
         300  +
         301  +
        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
         302  +
            self.0.poll_ready(cx).map_err(S2nConnectorError)
         303  +
        }
         304  +
         305  +
        fn call(&mut self, request: Uri) -> Self::Future {
         306  +
            let future = self.0.call(request);
         307  +
            Box::pin(async move { future.await.map_err(S2nConnectorError) })
         308  +
        }
         309  +
    }
         310  +
  256    311   
    #[derive(Clone)]
  257    312   
    pub(crate) struct S2nTlsConnector<R> {
  258    313   
        https: s2n_tls_hyper::connector::HttpsConnector<HttpConnector<R>>,
  259    314   
        tls_config: s2n_tls::config::Config,
  260    315   
        proxy_matcher: Option<Arc<Matcher>>, // Pre-computed for performance
  261    316   
    }
  262    317   
  263    318   
    impl<R> S2nTlsConnector<R> {
  264    319   
        pub(super) fn new(
  265    320   
            https: s2n_tls_hyper::connector::HttpsConnector<HttpConnector<R>>,
  266    321   
            tls_config: s2n_tls::config::Config,
  267    322   
            proxy_config: ProxyConfig,
  268    323   
        ) -> Self {
  269    324   
            // Pre-compute the proxy matcher once during construction
  270    325   
            let proxy_matcher = if proxy_config.is_disabled() {
  271    326   
                None
  272    327   
            } else {
  273    328   
                Some(Arc::new(proxy_config.into_hyper_util_matcher()))
  274    329   
            };
  275    330   
  276    331   
            Self {
  277    332   
                https,
  278    333   
                tls_config,
  279    334   
                proxy_matcher,
  280    335   
            }
  281    336   
        }
  282    337   
    }
  283    338   
  284    339   
    impl<R> Service<Uri> for S2nTlsConnector<R>
  285    340   
    where
  286    341   
        R: Clone + Send + Sync + 'static,
  287    342   
        R: Service<hyper_util::client::legacy::connect::dns::Name>,
  288    343   
        R::Response: Iterator<Item = std::net::SocketAddr>,
  289    344   
        R::Future: Send,
  290    345   
        R::Error: Into<Box<dyn Error + Send + Sync>>,
  291    346   
    {
  292    347   
        type Response = Conn;
  293    348   
        type Error = BoxError;
  294    349   
        type Future = Connecting;
  295    350   
  296    351   
        fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
  297         -
            self.https.poll_ready(cx).map_err(Into::into)
         352  +
            self.https.poll_ready(cx).map_err(box_s2n_connector_error)
  298    353   
        }
  299    354   
  300    355   
        fn call(&mut self, dst: Uri) -> Self::Future {
  301    356   
            // Check if this request should be proxied using pre-computed matcher
  302    357   
            let proxy_intercept = if let Some(ref matcher) = self.proxy_matcher {
  303    358   
                matcher.intercept(&dst)
  304    359   
            } else {
  305    360   
                None
  306    361   
            };
  307    362   
  308    363   
            if let Some(intercept) = proxy_intercept {
  309    364   
                if dst.scheme() == Some(&Scheme::HTTPS) {
  310    365   
                    // HTTPS through HTTP proxy: Use CONNECT tunneling + manual TLS
  311    366   
                    self.handle_https_through_proxy(dst, intercept)
  312    367   
                } else {
  313    368   
                    // HTTP through proxy: Direct connection to proxy
  314    369   
                    self.handle_http_through_proxy(dst, intercept)
  315    370   
                }
  316    371   
            } else {
  317    372   
                // Direct connection: Use the existing HTTPS connector
  318    373   
                self.handle_direct_connection(dst)
  319    374   
            }
  320    375   
        }
  321    376   
    }
  322    377   
  323    378   
    impl<R> S2nTlsConnector<R>
  324    379   
    where
  325    380   
        R: Clone + Send + Sync + 'static,
  326    381   
        R: Service<hyper_util::client::legacy::connect::dns::Name>,
  327    382   
        R::Response: Iterator<Item = std::net::SocketAddr>,
  328    383   
        R::Future: Send,
  329    384   
        R::Error: Into<Box<dyn Error + Send + Sync>>,
  330    385   
    {
  331    386   
        fn handle_direct_connection(&mut self, dst: Uri) -> Connecting {
  332    387   
            let fut = self.https.call(dst);
  333    388   
            Box::pin(async move {
  334         -
                let conn = fut.await?;
         389  +
                let conn = fut.await.map_err(box_s2n_connector_error)?;
  335    390   
                Ok(Conn {
  336    391   
                    inner: Box::new(conn),
  337         -
                    is_proxy: false,
         392  +
                    connect_path: ConnectPath::Direct,
  338    393   
                })
  339    394   
            })
  340    395   
        }
  341    396   
  342    397   
        fn handle_http_through_proxy(
  343    398   
            &mut self,
  344    399   
            _dst: Uri,
  345    400   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  346    401   
        ) -> Connecting {
  347    402   
            // For HTTP through proxy, connect to the proxy and let it handle the request
  348    403   
            let proxy_uri = intercept.uri().clone();
         404  +
            let connect_path = ConnectPath::forward_proxy(intercept.basic_auth().cloned());
  349    405   
            let fut = self.https.call(proxy_uri);
  350    406   
            Box::pin(async move {
  351         -
                let conn = fut.await?;
         407  +
                let conn = fut.await.map_err(box_s2n_connector_error)?;
  352    408   
                Ok(Conn {
  353    409   
                    inner: Box::new(conn),
  354         -
                    is_proxy: true,
         410  +
                    connect_path,
  355    411   
                })
  356    412   
            })
  357    413   
        }
  358    414   
  359    415   
        fn handle_https_through_proxy(
  360    416   
            &mut self,
  361    417   
            dst: Uri,
  362    418   
            intercept: hyper_util::client::proxy::matcher::Intercept,
  363    419   
        ) -> Connecting {
  364    420   
            // For HTTPS through HTTP proxy, we need to:
  365    421   
            // 1. Establish CONNECT tunnel using the HTTPS connector
  366    422   
            // 2. Perform manual TLS handshake over the tunneled stream
  367    423   
         424  +
            let connector = S2nErrorSourceConnector(self.https.clone());
  368    425   
            let tunnel = hyper_util::client::legacy::connect::proxy::Tunnel::new(
  369    426   
                intercept.uri().clone(),
  370         -
                self.https.clone(),
         427  +
                connector,
  371    428   
            );
  372    429   
  373    430   
            // Configure tunnel with authentication if present
  374    431   
            let mut tunnel = if let Some(auth) = intercept.basic_auth() {
  375    432   
                tunnel.with_auth(auth.clone())
  376    433   
            } else {
  377    434   
                tunnel
  378    435   
            };
  379    436   
  380    437   
            let tls_config = self.tls_config.clone();
  381    438   
            let dst_clone = dst.clone();
  382    439   
  383    440   
            Box::pin(async move {
  384    441   
                // Stage 1: Establish CONNECT tunnel
  385    442   
                tracing::trace!("tunneling HTTPS over proxy using s2n-tls");
  386         -
                let tunneled = tunnel
  387         -
                    .call(dst_clone.clone())
  388         -
                    .await
  389         -
                    .map_err(|e| BoxError::from(format!("CONNECT tunnel failed: {e}")))?;
         443  +
                let tunneled = tunnel.call(dst_clone.clone()).await?;
  390    444   
  391    445   
                // Stage 2: Manual TLS handshake over tunneled stream
  392    446   
                let host = dst_clone
  393    447   
                    .host()
  394    448   
                    .ok_or("missing host in URI for TLS handshake")?;
  395    449   
  396    450   
                // s2n-tls uses string server names (simpler than rustls ServerName)
  397    451   
                let tls_connector = s2n_tls_tokio::TlsConnector::new(tls_config);
  398    452   
                let tls_stream = tls_connector
  399    453   
                    .connect(host, TokioIo::new(tunneled))
  400    454   
                    .await
  401    455   
                    .map_err(|e| BoxError::from(format!("s2n-tls handshake failed: {e}")))?;
  402    456   
  403    457   
                Ok(Conn {
  404    458   
                    inner: Box::new(S2nTlsConn {
  405    459   
                        inner: TokioIo::new(tls_stream),
  406    460   
                    }),
  407         -
                    is_proxy: true,
         461  +
                    connect_path: ConnectPath::ProxyTunnel,
  408    462   
                })
  409    463   
            })
  410    464   
        }
  411    465   
    }
  412    466   
  413    467   
    // Simple wrapper that implements Connection for s2n-tls streams
  414    468   
    struct S2nTlsConn<T>
  415    469   
    where
  416    470   
        T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
  417    471   
    {

tmp-codegen-diff/aws-sdk/sdk/aws-smithy-http-client/tests/common/client.rs

@@ -1,1 +64,270 @@
    1      1   
/*
    2      2   
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
    3      3   
 * SPDX-License-Identifier: Apache-2.0
    4      4   
 */
    5      5   
    6      6   
//! Client configuration and request helpers shared by connection behavior tests.
    7      7   
    8      8   
use aws_smithy_async::rt::sleep::{SharedAsyncSleep, TokioSleep};
    9      9   
use aws_smithy_async::time::SystemTimeSource;
          10  +
use aws_smithy_http_client::pool::{Client as PoolClient, ConnectionPool};
          11  +
use aws_smithy_http_client::proxy::ProxyConfig;
          12  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
          13  +
use aws_smithy_http_client::tls;
          14  +
use aws_smithy_http_client::{Builder, Connector};
          15  +
use aws_smithy_runtime_api::client::dns::SharedDnsResolver;
   10     16   
use aws_smithy_runtime_api::client::http::{
   11         -
    HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient, SharedHttpConnector,
          17  +
    http_client_fn, HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient,
          18  +
    SharedHttpConnector,
   12     19   
};
   13     20   
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse};
   14     21   
use aws_smithy_runtime_api::client::result::ConnectorError;
   15     22   
use aws_smithy_runtime_api::client::runtime_components::{
   16     23   
    RuntimeComponents, RuntimeComponentsBuilder,
   17     24   
};
   18     25   
use http_body_util::BodyExt;
   19     26   
use std::time::Duration;
   20     27   
   21     28   
/// Default timeout for test waits and deadline assertions.
   22     29   
pub(crate) const WAIT: Duration = Duration::from_secs(5);
   23     30   
   24         -
#[derive(Clone, Copy, Debug, Default)]
          31  +
/// Settings applied uniformly by backend-neutral connection contracts.
          32  +
#[derive(Clone, Debug, Default)]
   25     33   
pub(crate) struct BackendConfig {
   26     34   
    pub(crate) pool_idle_timeout: Option<Duration>,
          35  +
    pub(crate) proxy_config: Option<ProxyConfig>,
          36  +
    pub(crate) dns_resolver: Option<SharedDnsResolver>,
   27     37   
}
   28     38   
   29     39   
/// Hyper 1.x through `hyper_util::client::legacy::Client`.
   30     40   
///
   31     41   
/// "Legacy" is Hyper Util's module name and does not refer to smithy-rs's `hyper-014` feature.
   32     42   
#[derive(Clone, Copy, Debug)]
   33     43   
pub(crate) struct HyperUtilLegacyPool;
   34     44   
          45  +
/// The partition-aware Smithy connection pool.
          46  +
#[derive(Clone, Copy, Debug)]
          47  +
pub(crate) struct PartitionedConnectionPool;
          48  +
          49  +
/// Builds a cleartext client for one connection-pool implementation.
          50  +
#[allow(dead_code)]
          51  +
pub(crate) trait HttpClientBackend {
          52  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient;
          53  +
}
          54  +
          55  +
impl HttpClientBackend for HyperUtilLegacyPool {
          56  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
          57  +
        let BackendConfig {
          58  +
            pool_idle_timeout,
          59  +
            proxy_config,
          60  +
            dns_resolver,
          61  +
        } = config;
          62  +
        if proxy_config.is_none() && dns_resolver.is_none() {
          63  +
            let mut builder = Builder::new();
          64  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
          65  +
                builder = builder.pool_idle_timeout(pool_idle_timeout);
          66  +
            }
          67  +
            return builder.build_http();
          68  +
        }
          69  +
          70  +
        http_client_fn(move |settings, _| {
          71  +
            let mut builder = Connector::builder().connector_settings(settings.clone());
          72  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
          73  +
                builder = builder.pool_idle_timeout(Some(pool_idle_timeout));
          74  +
            }
          75  +
            if let Some(proxy_config) = &proxy_config {
          76  +
                builder = builder.proxy_config(proxy_config.clone());
          77  +
            }
          78  +
            if let Some(resolver) = &dns_resolver {
          79  +
                #[cfg(any(
          80  +
                    feature = "rustls-aws-lc",
          81  +
                    feature = "rustls-aws-lc-fips",
          82  +
                    feature = "rustls-ring",
          83  +
                    feature = "s2n-tls"
          84  +
                ))]
          85  +
                return SharedHttpConnector::new(
          86  +
                    builder
          87  +
                        .tls_provider(custom_dns_test_provider())
          88  +
                        .build_with_resolver(resolver.clone()),
          89  +
                );
          90  +
                #[cfg(not(any(
          91  +
                    feature = "rustls-aws-lc",
          92  +
                    feature = "rustls-aws-lc-fips",
          93  +
                    feature = "rustls-ring",
          94  +
                    feature = "s2n-tls"
          95  +
                )))]
          96  +
                {
          97  +
                    let _ = resolver;
          98  +
                    panic!("the legacy custom-DNS test backend requires an enabled TLS provider");
          99  +
                }
         100  +
            }
         101  +
            SharedHttpConnector::new(builder.build_http())
         102  +
        })
         103  +
    }
         104  +
}
         105  +
         106  +
impl HttpClientBackend for PartitionedConnectionPool {
         107  +
    fn build(&self, config: BackendConfig) -> SharedHttpClient {
         108  +
        let mut builder = ConnectionPool::builder();
         109  +
        if let Some(dns_resolver) = config.dns_resolver {
         110  +
            builder = builder.dns_resolver(dns_resolver);
         111  +
        }
         112  +
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
         113  +
            builder = builder.idle_timeout(pool_idle_timeout);
         114  +
        }
         115  +
        if let Some(proxy_config) = config.proxy_config {
         116  +
            builder = builder.proxy_config(proxy_config);
         117  +
        }
         118  +
        let pool = builder.build_http().expect("valid connection-pool config");
         119  +
        SharedHttpClient::new(PoolClient::new(&pool).expect("anonymous partition exists"))
         120  +
    }
         121  +
}
         122  +
         123  +
#[cfg(any(
         124  +
    feature = "rustls-aws-lc",
         125  +
    feature = "rustls-aws-lc-fips",
         126  +
    feature = "rustls-ring",
         127  +
    feature = "s2n-tls"
         128  +
))]
         129  +
fn custom_dns_test_provider() -> tls::Provider {
         130  +
    #[cfg(feature = "rustls-aws-lc")]
         131  +
    {
         132  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)
         133  +
    }
         134  +
    #[cfg(all(not(feature = "rustls-aws-lc"), feature = "rustls-aws-lc-fips"))]
         135  +
    {
         136  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLcFips)
         137  +
    }
         138  +
    #[cfg(all(
         139  +
        not(feature = "rustls-aws-lc"),
         140  +
        not(feature = "rustls-aws-lc-fips"),
         141  +
        feature = "rustls-ring"
         142  +
    ))]
         143  +
    {
         144  +
        tls::Provider::Rustls(tls::rustls_provider::CryptoMode::Ring)
         145  +
    }
         146  +
    #[cfg(all(
         147  +
        not(feature = "rustls-aws-lc"),
         148  +
        not(feature = "rustls-aws-lc-fips"),
         149  +
        not(feature = "rustls-ring"),
         150  +
        feature = "s2n-tls"
         151  +
    ))]
         152  +
    {
         153  +
        tls::Provider::S2nTls
         154  +
    }
         155  +
}
         156  +
         157  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         158  +
/// Builds a TLS client for one connection-pool implementation.
         159  +
#[allow(dead_code)]
         160  +
pub(crate) trait HttpsClientBackend {
         161  +
    fn build_https(
         162  +
        &self,
         163  +
        config: BackendConfig,
         164  +
        provider: tls::Provider,
         165  +
        tls_context: tls::TlsContext,
         166  +
    ) -> SharedHttpClient;
         167  +
}
         168  +
         169  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         170  +
impl HttpsClientBackend for HyperUtilLegacyPool {
         171  +
    fn build_https(
         172  +
        &self,
         173  +
        config: BackendConfig,
         174  +
        provider: tls::Provider,
         175  +
        tls_context: tls::TlsContext,
         176  +
    ) -> SharedHttpClient {
         177  +
        let BackendConfig {
         178  +
            pool_idle_timeout,
         179  +
            proxy_config,
         180  +
            dns_resolver,
         181  +
        } = config;
         182  +
        if proxy_config.is_none() && dns_resolver.is_none() {
         183  +
            let mut builder = Builder::new();
         184  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
         185  +
                builder = builder.pool_idle_timeout(pool_idle_timeout);
         186  +
            }
         187  +
            return builder
         188  +
                .tls_provider(provider)
         189  +
                .tls_context(tls_context)
         190  +
                .build_https();
         191  +
        };
         192  +
         193  +
        http_client_fn(move |settings, _| {
         194  +
            let mut builder = Connector::builder()
         195  +
                .connector_settings(settings.clone())
         196  +
                .tls_provider(provider.clone())
         197  +
                .tls_context(tls_context.clone());
         198  +
            if let Some(proxy_config) = &proxy_config {
         199  +
                builder = builder.proxy_config(proxy_config.clone());
         200  +
            }
         201  +
            if let Some(pool_idle_timeout) = pool_idle_timeout {
         202  +
                builder = builder.pool_idle_timeout(Some(pool_idle_timeout));
         203  +
            }
         204  +
            match &dns_resolver {
         205  +
                Some(resolver) => {
         206  +
                    SharedHttpConnector::new(builder.build_with_resolver(resolver.clone()))
         207  +
                }
         208  +
                None => SharedHttpConnector::new(builder.build()),
         209  +
            }
         210  +
        })
         211  +
    }
         212  +
}
         213  +
         214  +
#[cfg(any(feature = "__rustls", feature = "s2n-tls"))]
         215  +
impl HttpsClientBackend for PartitionedConnectionPool {
         216  +
    fn build_https(
         217  +
        &self,
         218  +
        config: BackendConfig,
         219  +
        provider: tls::Provider,
         220  +
        tls_context: tls::TlsContext,
         221  +
    ) -> SharedHttpClient {
         222  +
        let mut builder = ConnectionPool::builder();
         223  +
        if let Some(dns_resolver) = config.dns_resolver {
         224  +
            builder = builder.dns_resolver(dns_resolver);
         225  +
        }
         226  +
        if let Some(pool_idle_timeout) = config.pool_idle_timeout {
         227  +
            builder = builder.idle_timeout(pool_idle_timeout);
         228  +
        }
         229  +
        if let Some(proxy_config) = config.proxy_config {
         230  +
            builder = builder.proxy_config(proxy_config);
         231  +
        }
         232  +
        let pool = builder
         233  +
            .tls_provider(provider)
         234  +
            .tls_context(tls_context)
         235  +
            .build_https()
         236  +
            .expect("valid connection-pool config");
         237  +
        SharedHttpClient::new(PoolClient::new(&pool).expect("anonymous partition exists"))
         238  +
    }
         239  +
}
         240  +
   35    241   
pub(crate) fn runtime_components() -> RuntimeComponents {
   36    242   
    RuntimeComponentsBuilder::for_tests()
   37    243   
        .with_time_source(Some(SystemTimeSource::new()))
   38    244   
        .with_sleep_impl(Some(SharedAsyncSleep::new(TokioSleep::new())))
   39    245   
        .build()
   40    246   
        .expect("valid runtime components")
   41    247   
}
   42    248   
   43    249   
pub(crate) fn connector_with_settings(
   44    250   
    client: &SharedHttpClient,