AWS SDK

AWS SDK

rev. 174400987dccd7e137fefa96b1143d21c7ddfb78 (ignoring whitespace)

Files changed:

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

@@ -492,492 +551,579 @@
  512    512   
                // Invalid URI format, return as-is
  513    513   
                uri_str
  514    514   
            }
  515    515   
        } else {
  516    516   
            // No authentication, return URI as-is
  517    517   
            uri_str
  518    518   
        }
  519    519   
    }
  520    520   
}
  521    521   
         522  +
/// Inject `Proxy-Authorization` for an HTTP-through-proxy request when
         523  +
/// `matcher` carries credentials for the request URI. HTTPS-through-proxy
         524  +
/// uses CONNECT tunneling and authenticates during tunnel setup, so this
         525  +
/// is a no-op for HTTPS URIs. Existing `Proxy-Authorization` headers are
         526  +
/// preserved.
         527  +
pub(crate) fn add_proxy_auth_header(
         528  +
    request: &mut http_1x::Request<aws_smithy_types::body::SdkBody>,
         529  +
    matcher: &Matcher,
         530  +
) {
         531  +
    if request.uri().scheme() != Some(&http_1x::uri::Scheme::HTTP) {
         532  +
        return;
         533  +
    }
         534  +
    if request
         535  +
        .headers()
         536  +
        .contains_key(http_1x::header::PROXY_AUTHORIZATION)
         537  +
    {
         538  +
        return;
         539  +
    }
         540  +
    if let Some(intercept) = matcher.intercept(request.uri()) {
         541  +
        if let Some(auth_header) = intercept.basic_auth() {
         542  +
            request
         543  +
                .headers_mut()
         544  +
                .insert(http_1x::header::PROXY_AUTHORIZATION, auth_header.clone());
         545  +
            tracing::debug!(uri = %request.uri(), "added proxy authentication header");
         546  +
        }
         547  +
    }
         548  +
}
         549  +
  522    550   
#[cfg(test)]
  523    551   
mod tests {
  524    552   
    use super::*;
  525    553   
    use std::env;
  526    554   
  527    555   
    #[test]
  528    556   
    fn test_proxy_config_http() {
  529    557   
        let config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
  530    558   
        assert!(!config.is_disabled());
  531    559   
        assert!(!config.is_from_env());

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

@@ -178,178 +237,282 @@
  198    198   
                    duration: *duration,
  199    199   
                }
  200    200   
            }
  201    201   
            None => MaybeTimeoutFuture::NoTimeout {
  202    202   
                future: self.inner.call(req),
  203    203   
            },
  204    204   
        }
  205    205   
    }
  206    206   
}
  207    207   
         208  +
/// Which timeout label a wrapping future should carry.
         209  +
///
         210  +
/// The label surfaces in the `HttpTimeoutError` produced when the timeout
         211  +
/// fires; it is what users see in the error message and what the retry
         212  +
/// classifier receives via `TimedOutError` downcasting.
         213  +
#[derive(Clone, Copy, Debug)]
         214  +
pub(crate) enum TimeoutKind {
         215  +
    Connect,
         216  +
    Read,
         217  +
}
         218  +
         219  +
impl TimeoutKind {
         220  +
    fn label(self) -> &'static str {
         221  +
        match self {
         222  +
            TimeoutKind::Connect => "HTTP connect",
         223  +
            TimeoutKind::Read => "HTTP read",
         224  +
        }
         225  +
    }
         226  +
}
         227  +
         228  +
/// Wrap `fut` in a `MaybeTimeoutFuture` if `timeout` is set.
         229  +
///
         230  +
/// Applies per-operation `connect_timeout` / `read_timeout` without
         231  +
/// requiring a dedicated Tower service per request. Takes `sleep_impl` by
         232  +
/// reference to avoid cloning it into a dedicated service.
         233  +
pub(crate) fn maybe_timeout_future<F, T, E>(
         234  +
    fut: F,
         235  +
    timeout: Option<Duration>,
         236  +
    sleep_impl: Option<&SharedAsyncSleep>,
         237  +
    kind: TimeoutKind,
         238  +
) -> MaybeTimeoutFuture<F>
         239  +
where
         240  +
    F: Future<Output = Result<T, E>>,
         241  +
    E: Into<BoxError>,
         242  +
{
         243  +
    match (timeout, sleep_impl) {
         244  +
        (Some(duration), Some(sleep)) => MaybeTimeoutFuture::Timeout {
         245  +
            timeout: Timeout::new(fut, sleep.sleep(duration)),
         246  +
            error_type: kind.label(),
         247  +
            duration,
         248  +
        },
         249  +
        _ => MaybeTimeoutFuture::NoTimeout { future: fut },
         250  +
    }
         251  +
}
         252  +
  208    253   
#[cfg(test)]
  209    254   
pub(crate) mod test {
  210    255   
    use hyper::rt::ReadBufCursor;
  211    256   
    use hyper_util::client::legacy::connect::{Connected, Connection};
  212    257   
    use hyper_util::rt::TokioIo;
  213    258   
    use tokio::net::TcpStream;
  214    259   
  215    260   
    use aws_smithy_async::future::never::Never;
  216    261   
  217    262   
    use aws_smithy_runtime_api::box_error::BoxError;

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

@@ -175,175 +279,280 @@
  195    195   
    }
  196    196   
  197    197   
    pub(crate) fn wrap_connector<R>(
  198    198   
        mut conn: HttpConnector<R>,
  199    199   
        crypto_mode: CryptoMode,
  200    200   
        tls_context: &TlsContext,
  201    201   
        proxy_config: crate::client::proxy::ProxyConfig,
  202    202   
    ) -> super::connect::RustTlsConnector<R> {
  203    203   
        let client_config = create_rustls_client_config(crypto_mode, tls_context);
  204    204   
        conn.enforce_http(false);
         205  +
        let timed = crate::client::pool::connection::TimingConnector::new(conn);
  205    206   
        let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
  206    207   
            .with_tls_config(client_config.clone())
  207    208   
            .https_or_http()
  208    209   
            .enable_http1()
  209    210   
            .enable_http2()
  210         -
            .wrap_connector(conn);
         211  +
            .wrap_connector(timed);
  211    212   
  212    213   
        super::connect::RustTlsConnector::new(https_connector, client_config, proxy_config)
  213    214   
    }
  214    215   
}
  215    216   
  216    217   
pub(crate) mod connect {
  217    218   
    use crate::client::connect::{Conn, Connecting};
  218    219   
    use crate::client::proxy::ProxyConfig;
  219    220   
    use aws_smithy_runtime_api::box_error::BoxError;
  220    221   
    use http_1x::uri::Scheme;
  221    222   
    use http_1x::Uri;
  222    223   
    use hyper::rt::{Read, ReadBufCursor, Write};
  223    224   
    use hyper_rustls::MaybeHttpsStream;
  224    225   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
  225    226   
    use hyper_util::client::proxy::matcher::Matcher;
  226    227   
    use hyper_util::rt::TokioIo;
  227    228   
    use pin_project_lite::pin_project;
  228    229   
    use std::error::Error;
  229    230   
    use std::sync::Arc;
  230    231   
    use std::{
  231    232   
        io::{self, IoSlice},
  232    233   
        pin::Pin,
  233    234   
        task::{Context, Poll},
  234    235   
    };
  235    236   
    use tokio::io::{AsyncRead, AsyncWrite};
  236    237   
    use tokio::net::TcpStream;
  237    238   
    use tokio_rustls::client::TlsStream;
  238    239   
    use tower::Service;
  239    240   
  240    241   
    #[derive(Debug, Clone)]
  241    242   
    pub(crate) struct RustTlsConnector<R> {
  242         -
        https: hyper_rustls::HttpsConnector<HttpConnector<R>>,
         243  +
        https: hyper_rustls::HttpsConnector<crate::client::pool::connection::TimingConnector<HttpConnector<R>>>,
  243    244   
        tls_config: Arc<rustls::ClientConfig>,
  244    245   
        proxy_matcher: Option<Arc<Matcher>>, // Pre-computed for performance
  245    246   
    }
  246    247   
  247    248   
    impl<R> RustTlsConnector<R> {
  248    249   
        pub(super) fn new(
  249         -
            https: hyper_rustls::HttpsConnector<HttpConnector<R>>,
         250  +
            https: hyper_rustls::HttpsConnector<crate::client::pool::connection::TimingConnector<HttpConnector<R>>>,
  250    251   
            tls_config: rustls::ClientConfig,
  251    252   
            proxy_config: ProxyConfig,
  252    253   
        ) -> Self {
  253    254   
            // Pre-compute the proxy matcher once during construction
  254    255   
            let proxy_matcher = if proxy_config.is_disabled() {
  255    256   
                None
  256    257   
            } else {
  257    258   
                Some(Arc::new(proxy_config.into_hyper_util_matcher()))
  258    259   
            };
  259    260   
@@ -406,407 +465,490 @@
  426    427   
                    .get_ref()
  427    428   
                    .0
  428    429   
                    .inner()
  429    430   
                    .connected()
  430    431   
                    .negotiated_h2()
  431    432   
            } else {
  432    433   
                self.inner.inner().get_ref().0.inner().connected()
  433    434   
            }
  434    435   
        }
  435    436   
    }
         437  +
         438  +
    impl Connection
         439  +
        for RustTlsConn<
         440  +
            TokioIo<
         441  +
                MaybeHttpsStream<
         442  +
                    crate::client::pool::connection::TransportIo<TokioIo<TcpStream>>,
         443  +
                >,
         444  +
            >,
         445  +
        >
         446  +
    {
         447  +
        fn connected(&self) -> Connected {
         448  +
            if self.inner.inner().get_ref().1.alpn_protocol() == Some(b"h2") {
         449  +
                self.inner
         450  +
                    .inner()
         451  +
                    .get_ref()
         452  +
                    .0
         453  +
                    .inner()
         454  +
                    .connected()
         455  +
                    .negotiated_h2()
         456  +
            } else {
         457  +
                self.inner.inner().get_ref().0.inner().connected()
         458  +
            }
         459  +
        }
         460  +
    }
  436    461   
    impl<T: AsyncRead + AsyncWrite + Unpin> Read for RustTlsConn<T> {
  437    462   
        fn poll_read(
  438    463   
            self: Pin<&mut Self>,
  439    464   
            cx: &mut Context<'_>,
  440    465   
            buf: ReadBufCursor<'_>,
  441    466   
        ) -> Poll<tokio::io::Result<()>> {
  442    467   
            let this = self.project();
  443    468   
            Read::poll_read(this.inner, cx, buf)
  444    469   
        }
  445    470   
    }

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

@@ -33,33 +131,134 @@
   53     53   
        }
   54     54   
    }
   55     55   
   56     56   
    pub(crate) fn wrap_connector<R>(
   57     57   
        mut http_connector: HttpConnector<R>,
   58     58   
        tls_context: &TlsContext,
   59     59   
        proxy_config: crate::client::proxy::ProxyConfig,
   60     60   
    ) -> super::connect::S2nTlsConnector<R> {
   61     61   
        let config = tls_context.s2n_config();
   62     62   
        http_connector.enforce_http(false);
   63         -
        let mut builder = s2n_tls_hyper::connector::HttpsConnector::builder_with_http(
   64         -
            http_connector,
   65         -
            config.clone(),
   66         -
        );
          63  +
        let timed = crate::client::pool::connection::TimingConnector::new(http_connector);
          64  +
        let mut builder =
          65  +
            s2n_tls_hyper::connector::HttpsConnector::builder_with_http(timed, config.clone());
   67     66   
        builder.with_plaintext_http(true);
   68     67   
        let https_connector = builder.build();
   69     68   
   70     69   
        super::connect::S2nTlsConnector::new(https_connector, config, proxy_config)
   71     70   
    }
   72     71   
}
   73     72   
   74     73   
pub(crate) mod connect {
   75     74   
    use crate::client::connect::{Conn, Connecting};
   76     75   
    use crate::client::proxy::ProxyConfig;
   77     76   
    use aws_smithy_runtime_api::box_error::BoxError;
   78     77   
    use http_1x::uri::Scheme;
   79     78   
    use http_1x::Uri;
   80     79   
    use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
   81     80   
    use hyper_util::client::proxy::matcher::Matcher;
   82     81   
    use hyper_util::rt::TokioIo;
   83     82   
    use std::error::Error;
   84     83   
    use std::sync::Arc;
   85     84   
    use std::{
   86     85   
        io::IoSlice,
   87     86   
        pin::Pin,
   88     87   
        task::{Context, Poll},
   89     88   
    };
   90     89   
    use tower::Service;
   91     90   
          91  +
    type S2nHttpsConnector<R> = s2n_tls_hyper::connector::HttpsConnector<
          92  +
        crate::client::pool::connection::TimingConnector<HttpConnector<R>>,
          93  +
    >;
          94  +
   92     95   
    #[derive(Clone)]
   93     96   
    pub(crate) struct S2nTlsConnector<R> {
   94         -
        https: s2n_tls_hyper::connector::HttpsConnector<HttpConnector<R>>,
          97  +
        https: S2nHttpsConnector<R>,
   95     98   
        tls_config: s2n_tls::config::Config,
   96     99   
        proxy_matcher: Option<Arc<Matcher>>, // Pre-computed for performance
   97    100   
    }
   98    101   
   99    102   
    impl<R> S2nTlsConnector<R> {
  100    103   
        pub(super) fn new(
  101         -
            https: s2n_tls_hyper::connector::HttpsConnector<HttpConnector<R>>,
         104  +
            https: S2nHttpsConnector<R>,
  102    105   
            tls_config: s2n_tls::config::Config,
  103    106   
            proxy_config: ProxyConfig,
  104    107   
        ) -> Self {
  105    108   
            // Pre-compute the proxy matcher once during construction
  106    109   
            let proxy_matcher = if proxy_config.is_disabled() {
  107    110   
                None
  108    111   
            } else {
  109    112   
                Some(Arc::new(proxy_config.into_hyper_util_matcher()))
  110    113   
            };
  111    114   
@@ -232,235 +294,299 @@
  252    255   
        T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
  253    256   
    {
  254    257   
        inner: TokioIo<s2n_tls_tokio::TlsStream<T>>,
  255    258   
    }
  256    259   
  257    260   
    impl<T> Connection for S2nTlsConn<T>
  258    261   
    where
  259    262   
        T: Connection + tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
  260    263   
    {
  261    264   
        fn connected(&self) -> Connected {
  262         -
            // For tunneled connections, we can't easily access the underlying connection info
  263         -
            // from s2n-tls, so we'll return a basic Connected instance
  264         -
            Connected::new()
         265  +
            let inner_connected = self.inner.inner().get_ref().connected();
         266  +
            match self.inner.inner().as_ref().application_protocol() {
         267  +
                Some(b"h2") => inner_connected.negotiated_h2(),
         268  +
                _ => inner_connected,
         269  +
            }
  265    270   
        }
  266    271   
    }
  267    272   
  268    273   
    impl<T> hyper::rt::Read for S2nTlsConn<T>
  269    274   
    where
  270    275   
        T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
  271    276   
    {
  272    277   
        fn poll_read(
  273    278   
            self: Pin<&mut Self>,
  274    279   
            cx: &mut Context<'_>,

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

@@ -17,17 +76,80 @@
   37     37   
pub mod hyper_014 {
   38     38   
    pub use crate::hyper_legacy::*;
   39     39   
}
   40     40   
   41     41   
/// Default HTTP and TLS connectors
   42     42   
#[cfg(feature = "default-client")]
   43     43   
pub(crate) mod client;
   44     44   
#[cfg(feature = "default-client")]
   45     45   
pub use client::{default_connector, proxy, tls, Builder, Connector, ConnectorBuilder};
   46     46   
          47  +
/// HTTP client backed by composable connection pools.
          48  +
#[cfg(feature = "default-client")]
          49  +
pub use client::pool;
          50  +
   47     51   
#[cfg(feature = "test-util")]
   48     52   
pub mod test_util;
   49     53   
   50     54   
mod error;
   51     55   
pub use error::HttpClientError;
   52     56   
   53     57   
#[allow(unused_macros, unused_imports)]
   54     58   
#[macro_use]
   55     59   
pub(crate) mod cfg {
   56     60   
    /// Any TLS provider enabled

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

@@ -12,12 +71,73 @@
   32     32   
//! // ... do something with <client>
   33     33   
//! # */
   34     34   
//!
   35     35   
//! // assert that you got the events you expected
   36     36   
//! match_events!(ev!(dns), ev!(connect), ev!(http(200)))(&mock.events());
   37     37   
//! # }
   38     38   
//! ```
   39     39   
   40     40   
#![allow(missing_docs)]
   41     41   
          42  +
pub mod connection;
          43  +
   42     44   
use aws_smithy_async::future::never::Never;
   43     45   
use aws_smithy_async::future::BoxFuture;
   44     46   
use aws_smithy_runtime_api::client::http::SharedHttpClient;
   45     47   
use bytes::Bytes;
   46     48   
use http_body_util::Full;
   47     49   
use hyper::service::service_fn;
   48     50   
use hyper_util::client::legacy::connect::dns::Name;
   49     51   
use hyper_util::rt::{TokioExecutor, TokioIo};
   50     52   
use hyper_util::server::graceful::{GracefulConnection, GracefulShutdown};
   51     53   
use std::collections::HashSet;
@@ -382,384 +427,445 @@
  402    404   
        Box::pin(async move {
  403    405   
            println!("looking up {req:?}, replying with {socket_addr:?}");
  404    406   
            log.lock()
  405    407   
                .unwrap()
  406    408   
                .push(RecordedEvent::DnsLookup(req.to_string()));
  407    409   
            Ok(std::iter::once(socket_addr))
  408    410   
        })
  409    411   
    }
  410    412   
}
  411    413   
         414  +
impl aws_smithy_runtime_api::client::dns::ResolveDns for LoggingDnsResolver {
         415  +
    fn resolve_dns<'a>(
         416  +
        &'a self,
         417  +
        name: &'a str,
         418  +
    ) -> aws_smithy_runtime_api::client::dns::DnsFuture<'a> {
         419  +
        let socket_addr = self.0.socket_addr;
         420  +
        let log = self.0.log.clone();
         421  +
        aws_smithy_runtime_api::client::dns::DnsFuture::new(async move {
         422  +
            log.lock()
         423  +
                .unwrap()
         424  +
                .push(RecordedEvent::DnsLookup(name.to_string()));
         425  +
            Ok(vec![socket_addr.ip()])
         426  +
        })
         427  +
    }
         428  +
}
         429  +
  412    430   
#[cfg(all(feature = "legacy-test-util", feature = "hyper-014"))]
  413    431   
impl hyper_0_14::service::Service<hyper_0_14::client::connect::dns::Name> for LoggingDnsResolver {
  414    432   
    type Response = Once<SocketAddr>;
  415    433   
    type Error = Infallible;
  416    434   
    type Future = BoxFuture<'static, Self::Response, Self::Error>;
  417    435   
  418    436   
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
  419    437   
        self.0.poll_ready(cx)
  420    438   
    }
  421    439   

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

@@ -0,1 +0,451 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! Test harness for connection-level behavior testing.
           7  +
//!
           8  +
//! Simulates multiple IPs via TCP listeners on different loopback addresses
           9  +
//! (`127.0.0.1`, `127.0.0.2`, etc.) sharing the same port.
          10  +
          11  +
use aws_smithy_runtime_api::client::dns::{DnsFuture, ResolveDns};
          12  +
use std::collections::{HashMap, VecDeque};
          13  +
use std::net::{IpAddr, SocketAddr};
          14  +
use std::sync::{Arc, Mutex};
          15  +
use tokio::io::{AsyncReadExt, AsyncWriteExt};
          16  +
use tokio::net::TcpListener;
          17  +
use tokio::task::JoinHandle;
          18  +
          19  +
/// Programmable behavior for a test endpoint per accepted connection.
          20  +
#[derive(Debug, Clone)]
          21  +
pub enum ConnectionBehavior {
          22  +
    /// Accept TCP, send HTTP/1.1 response with `Connection: keep-alive`, keep
          23  +
    /// connection open for subsequent requests on the same TCP stream.
          24  +
    RespondKeepAlive {
          25  +
        /// HTTP status code to return.
          26  +
        status: u16,
          27  +
        /// Response body bytes.
          28  +
        body: &'static [u8],
          29  +
    },
          30  +
    /// Accept TCP, immediately reset the connection (RST).
          31  +
    ResetOnConnect,
          32  +
    /// Accept TCP, send HTTP/1.1 response, then close the connection.
          33  +
    /// Simulates a server that does not support keep-alive.
          34  +
    RespondThenClose {
          35  +
        /// HTTP status code to return.
          36  +
        status: u16,
          37  +
        /// Response body bytes.
          38  +
        body: &'static [u8],
          39  +
    },
          40  +
    /// Accept TCP, hold open for duration, then close.
          41  +
    HoldThenClose(std::time::Duration),
          42  +
    /// Accept TCP, send HTTP/1.1 response with `Connection: keep-alive`,
          43  +
    /// wait for the specified duration, then close the connection.
          44  +
    /// Simulates a server-side idle timeout (e.g. S3 closing after ~20s).
          45  +
    /// The connection appears reusable to the client until the server closes it.
          46  +
    RespondThenIdleClose {
          47  +
        /// HTTP status code to return.
          48  +
        status: u16,
          49  +
        /// Response body bytes.
          50  +
        body: &'static [u8],
          51  +
        /// How long to wait after responding before closing.
          52  +
        idle: std::time::Duration,
          53  +
    },
          54  +
}
          55  +
          56  +
/// Recorded event from the test harness.
          57  +
#[derive(Debug, Clone)]
          58  +
pub enum ConnectionEvent {
          59  +
    /// TCP connection accepted at an endpoint.
          60  +
    TcpAccepted {
          61  +
        /// The address of the endpoint that accepted the connection.
          62  +
        endpoint_addr: SocketAddr,
          63  +
    },
          64  +
    /// HTTP request received by an endpoint.
          65  +
    HttpRequestReceived {
          66  +
        /// The request-target from the request line (e.g. "/path?query").
          67  +
        uri: String,
          68  +
        /// The Host header value, if present.
          69  +
        host: Option<String>,
          70  +
    },
          71  +
    /// DNS lookup performed.
          72  +
    DnsLookup {
          73  +
        /// The hostname that was looked up.
          74  +
        hostname: String,
          75  +
    },
          76  +
}
          77  +
          78  +
/// A TCP endpoint bound to a specific address that executes programmed behaviors.
          79  +
pub struct TestEndpoint {
          80  +
    addr: SocketAddr,
          81  +
    _task: JoinHandle<()>,
          82  +
}
          83  +
          84  +
impl TestEndpoint {
          85  +
    async fn bind(
          86  +
        addr: &str,
          87  +
        behaviors: Vec<ConnectionBehavior>,
          88  +
        events: Arc<Mutex<Vec<ConnectionEvent>>>,
          89  +
    ) -> Self {
          90  +
        let listener = TcpListener::bind(addr)
          91  +
            .await
          92  +
            .unwrap_or_else(|e| panic!("failed to bind TCP listener to {addr}: {e}"));
          93  +
        let addr = listener
          94  +
            .local_addr()
          95  +
            .expect("failed to get local address from listener");
          96  +
        let behaviors = Arc::new(Mutex::new(VecDeque::from(behaviors)));
          97  +
          98  +
        let task = tokio::spawn(async move {
          99  +
            loop {
         100  +
                let (stream, _) = match listener.accept().await {
         101  +
                    Ok(conn) => conn,
         102  +
                    Err(_) => break,
         103  +
                };
         104  +
                events
         105  +
                    .lock()
         106  +
                    .expect("event lock poisoned")
         107  +
                    .push(ConnectionEvent::TcpAccepted {
         108  +
                        endpoint_addr: addr,
         109  +
                    });
         110  +
                let behavior = behaviors
         111  +
                    .lock()
         112  +
                    .expect("behavior lock poisoned")
         113  +
                    .pop_front();
         114  +
                tokio::spawn(handle_connection(
         115  +
                    stream,
         116  +
                    behavior,
         117  +
                    behaviors.clone(),
         118  +
                    events.clone(),
         119  +
                ));
         120  +
            }
         121  +
        });
         122  +
         123  +
        Self { addr, _task: task }
         124  +
    }
         125  +
         126  +
    /// The port this endpoint is listening on.
         127  +
    pub fn port(&self) -> u16 {
         128  +
        self.addr.port()
         129  +
    }
         130  +
         131  +
    /// The IP address this endpoint is listening on.
         132  +
    pub fn ip(&self) -> IpAddr {
         133  +
        self.addr.ip()
         134  +
    }
         135  +
         136  +
    /// The full socket address this endpoint is listening on.
         137  +
    pub fn addr(&self) -> SocketAddr {
         138  +
        self.addr
         139  +
    }
         140  +
}
         141  +
         142  +
async fn handle_connection(
         143  +
    mut stream: tokio::net::TcpStream,
         144  +
    first_behavior: Option<ConnectionBehavior>,
         145  +
    remaining: Arc<Mutex<VecDeque<ConnectionBehavior>>>,
         146  +
    events: Arc<Mutex<Vec<ConnectionEvent>>>,
         147  +
) {
         148  +
    let Some(behavior) = first_behavior else {
         149  +
        drop(stream);
         150  +
        return;
         151  +
    };
         152  +
         153  +
    match behavior {
         154  +
        ConnectionBehavior::ResetOnConnect => {
         155  +
            // Set SO_LINGER to 0 to send TCP RST on close
         156  +
            let sock = socket2::SockRef::from(&stream);
         157  +
            sock.set_linger(Some(std::time::Duration::ZERO))
         158  +
                .expect("failed to set SO_LINGER");
         159  +
            drop(stream);
         160  +
        }
         161  +
        ConnectionBehavior::RespondThenClose { status, body } => {
         162  +
            match read_request(&mut stream).await {
         163  +
                Ok(req_info) => {
         164  +
                    events.lock().expect("event lock poisoned").push(req_info);
         165  +
                }
         166  +
                Err(_) => return,
         167  +
            }
         168  +
            let _ = write_response(&mut stream, status, body, false).await;
         169  +
            // Close immediately after responding — no keep-alive.
         170  +
            drop(stream);
         171  +
        }
         172  +
        ConnectionBehavior::HoldThenClose(duration) => {
         173  +
            tokio::time::sleep(duration).await;
         174  +
            drop(stream);
         175  +
        }
         176  +
        ConnectionBehavior::RespondThenIdleClose { status, body, idle } => {
         177  +
            match read_request(&mut stream).await {
         178  +
                Ok(req_info) => {
         179  +
                    events.lock().expect("event lock poisoned").push(req_info);
         180  +
                }
         181  +
                Err(_) => return,
         182  +
            }
         183  +
            if write_response(&mut stream, status, body, true)
         184  +
                .await
         185  +
                .is_err()
         186  +
            {
         187  +
                return;
         188  +
            }
         189  +
            // Wait, then close — simulates server-side idle timeout.
         190  +
            tokio::time::sleep(idle).await;
         191  +
            drop(stream);
         192  +
        }
         193  +
        ConnectionBehavior::RespondKeepAlive { status, body } => {
         194  +
            // Read the first request before responding.
         195  +
            match read_request(&mut stream).await {
         196  +
                Ok(req_info) => {
         197  +
                    events.lock().expect("event lock poisoned").push(req_info);
         198  +
                }
         199  +
                Err(_) => return,
         200  +
            }
         201  +
            if write_response(&mut stream, status, body, true)
         202  +
                .await
         203  +
                .is_err()
         204  +
            {
         205  +
                return;
         206  +
            }
         207  +
            // Keep-alive loop: read next request, send next behavior's response
         208  +
            loop {
         209  +
                match read_request(&mut stream).await {
         210  +
                    Ok(req_info) => {
         211  +
                        events.lock().expect("event lock poisoned").push(req_info);
         212  +
                    }
         213  +
                    Err(_) => return,
         214  +
                }
         215  +
                let next = remaining
         216  +
                    .lock()
         217  +
                    .expect("behavior lock poisoned")
         218  +
                    .pop_front();
         219  +
                match next {
         220  +
                    Some(ConnectionBehavior::RespondKeepAlive { status, body }) => {
         221  +
                        if write_response(&mut stream, status, body, true)
         222  +
                            .await
         223  +
                            .is_err()
         224  +
                        {
         225  +
                            return;
         226  +
                        }
         227  +
                    }
         228  +
                    Some(ConnectionBehavior::RespondThenClose { status, body }) => {
         229  +
                        let _ = write_response(&mut stream, status, body, false).await;
         230  +
                        return; // close after responding
         231  +
                    }
         232  +
                    _ => return, // No more behaviors or non-Respond behavior: close
         233  +
                }
         234  +
            }
         235  +
        }
         236  +
    }
         237  +
}
         238  +
         239  +
async fn write_response(
         240  +
    stream: &mut tokio::net::TcpStream,
         241  +
    status: u16,
         242  +
    body: &[u8],
         243  +
    keep_alive: bool,
         244  +
) -> Result<(), std::io::Error> {
         245  +
    let conn_header = if keep_alive { "keep-alive" } else { "close" };
         246  +
    let mut response = format!(
         247  +
        "HTTP/1.1 {status} OK\r\nContent-Length: {}\r\nConnection: {conn_header}\r\n\r\n",
         248  +
        body.len()
         249  +
    )
         250  +
    .into_bytes();
         251  +
    response.extend_from_slice(body);
         252  +
    stream.write_all(&response).await?;
         253  +
    stream.flush().await
         254  +
}
         255  +
         256  +
async fn read_request(
         257  +
    stream: &mut tokio::net::TcpStream,
         258  +
) -> Result<ConnectionEvent, std::io::Error> {
         259  +
    // Read until we see \r\n\r\n (end of HTTP headers)
         260  +
    let mut buf = vec![0u8; 4096];
         261  +
    let mut total = 0;
         262  +
    loop {
         263  +
        let n = stream.read(&mut buf[total..]).await?;
         264  +
        if n == 0 {
         265  +
            return Err(std::io::Error::new(
         266  +
                std::io::ErrorKind::ConnectionReset,
         267  +
                "client closed connection",
         268  +
            ));
         269  +
        }
         270  +
        total += n;
         271  +
        if total >= 4 && buf[..total].windows(4).any(|w| w == b"\r\n\r\n") {
         272  +
            break;
         273  +
        }
         274  +
    }
         275  +
    let raw = String::from_utf8_lossy(&buf[..total]);
         276  +
    let mut lines = raw.lines();
         277  +
    // Parse request-target from "GET /path HTTP/1.1"
         278  +
    let uri = lines
         279  +
        .next()
         280  +
        .and_then(|line| line.split_whitespace().nth(1))
         281  +
        .unwrap_or("/")
         282  +
        .to_string();
         283  +
    // Find Host header
         284  +
    let host = lines.find_map(|line| {
         285  +
        let lower = line.to_ascii_lowercase();
         286  +
        if lower.starts_with("host:") {
         287  +
            Some(line[5..].trim().to_string())
         288  +
        } else {
         289  +
            None
         290  +
        }
         291  +
    });
         292  +
    Ok(ConnectionEvent::HttpRequestReceived { uri, host })
         293  +
}
         294  +
         295  +
/// Mock DNS resolver that returns configured IPs and logs lookups.
         296  +
#[derive(Debug, Clone)]
         297  +
pub struct MockDnsResolver {
         298  +
    responses: HashMap<String, Vec<IpAddr>>,
         299  +
    events: Arc<Mutex<Vec<ConnectionEvent>>>,
         300  +
}
         301  +
         302  +
impl MockDnsResolver {
         303  +
    fn new(events: Arc<Mutex<Vec<ConnectionEvent>>>) -> Self {
         304  +
        Self {
         305  +
            responses: HashMap::new(),
         306  +
            events,
         307  +
        }
         308  +
    }
         309  +
         310  +
    /// Add a DNS entry mapping hostname to IPs.
         311  +
    fn with(mut self, hostname: &str, ips: Vec<IpAddr>) -> Self {
         312  +
        self.responses.insert(hostname.to_string(), ips);
         313  +
        self
         314  +
    }
         315  +
}
         316  +
         317  +
impl ResolveDns for MockDnsResolver {
         318  +
    fn resolve_dns<'a>(&'a self, name: &'a str) -> DnsFuture<'a> {
         319  +
        let ips = self.responses.get(name).cloned().unwrap_or_default();
         320  +
        self.events
         321  +
            .lock()
         322  +
            .expect("event lock poisoned")
         323  +
            .push(ConnectionEvent::DnsLookup {
         324  +
                hostname: name.to_string(),
         325  +
            });
         326  +
        DnsFuture::ready(Ok(ips))
         327  +
    }
         328  +
}
         329  +
         330  +
/// Test harness for connection-level behavior testing.
         331  +
pub struct ConnectionTestHarness {
         332  +
    /// The test endpoints managed by this harness.
         333  +
    pub endpoints: Vec<TestEndpoint>,
         334  +
    events: Arc<Mutex<Vec<ConnectionEvent>>>,
         335  +
    dns_resolver: MockDnsResolver,
         336  +
}
         337  +
         338  +
impl ConnectionTestHarness {
         339  +
    /// Create a new builder for the test harness.
         340  +
    pub fn builder() -> HarnessBuilder {
         341  +
        HarnessBuilder {
         342  +
            endpoint_configs: Vec::new(),
         343  +
            dns_entries: Vec::new(),
         344  +
        }
         345  +
    }
         346  +
         347  +
    /// Clone all recorded events.
         348  +
    pub fn events(&self) -> Vec<ConnectionEvent> {
         349  +
        self.events.lock().expect("event lock poisoned").clone()
         350  +
    }
         351  +
         352  +
    /// Count of TCP accepted events.
         353  +
    pub fn tcp_accepted_count(&self) -> usize {
         354  +
        self.events()
         355  +
            .iter()
         356  +
            .filter(|e| matches!(e, ConnectionEvent::TcpAccepted { .. }))
         357  +
            .count()
         358  +
    }
         359  +
         360  +
    /// Count of TCP accepted events for a specific IP.
         361  +
    pub fn tcp_accepted_by(&self, ip: IpAddr) -> usize {
         362  +
        self.events()
         363  +
            .iter()
         364  +
            .filter(|e| matches!(e, ConnectionEvent::TcpAccepted { endpoint_addr } if endpoint_addr.ip() == ip))
         365  +
            .count()
         366  +
    }
         367  +
         368  +
    /// Count of DNS lookup events.
         369  +
    pub fn dns_lookup_count(&self) -> usize {
         370  +
        self.events()
         371  +
            .iter()
         372  +
            .filter(|e| matches!(e, ConnectionEvent::DnsLookup { .. }))
         373  +
            .count()
         374  +
    }
         375  +
         376  +
    /// Collected HTTP request events (uri + host header).
         377  +
    pub fn http_requests(&self) -> Vec<(String, Option<String>)> {
         378  +
        self.events()
         379  +
            .iter()
         380  +
            .filter_map(|e| match e {
         381  +
                ConnectionEvent::HttpRequestReceived { uri, host } => {
         382  +
                    Some((uri.clone(), host.clone()))
         383  +
                }
         384  +
                _ => None,
         385  +
            })
         386  +
            .collect()
         387  +
    }
         388  +
         389  +
    /// Get a clone of the mock DNS resolver.
         390  +
    pub fn dns_resolver(&self) -> MockDnsResolver {
         391  +
        self.dns_resolver.clone()
         392  +
    }
         393  +
}
         394  +
         395  +
/// Builder for [`ConnectionTestHarness`].
         396  +
pub struct HarnessBuilder {
         397  +
    endpoint_configs: Vec<(IpAddr, Vec<ConnectionBehavior>)>,
         398  +
    dns_entries: Vec<(String, Vec<IpAddr>)>,
         399  +
}
         400  +
         401  +
impl HarnessBuilder {
         402  +
    /// Add an endpoint with the given IP and behaviors.
         403  +
    pub fn endpoint(mut self, ip: IpAddr, behaviors: Vec<ConnectionBehavior>) -> Self {
         404  +
        self.endpoint_configs.push((ip, behaviors));
         405  +
        self
         406  +
    }
         407  +
         408  +
    /// Add a DNS entry mapping hostname to IPs.
         409  +
    pub fn dns(mut self, hostname: &str, ips: Vec<IpAddr>) -> Self {
         410  +
        self.dns_entries.push((hostname.to_string(), ips));
         411  +
        self
         412  +
    }
         413  +
         414  +
    /// Add a DNS entry mapping hostname to all configured endpoint IPs.
         415  +
    pub fn dns_all(mut self, hostname: &str) -> Self {
         416  +
        let ips: Vec<IpAddr> = self.endpoint_configs.iter().map(|(ip, _)| *ip).collect();
         417  +
        self.dns_entries.push((hostname.to_string(), ips));
         418  +
        self
         419  +
    }
         420  +
         421  +
    /// Build the test harness, binding all endpoints.
         422  +
    pub async fn build(self) -> ConnectionTestHarness {
         423  +
        let events: Arc<Mutex<Vec<ConnectionEvent>>> = Arc::new(Mutex::new(Vec::new()));
         424  +
        let mut endpoints = Vec::new();
         425  +
         426  +
        let mut port = 0u16;
         427  +
        for (i, (ip, behaviors)) in self.endpoint_configs.into_iter().enumerate() {
         428  +
            let bind_addr = if i == 0 {
         429  +
                format!("{ip}:0")
         430  +
            } else {
         431  +
                format!("{ip}:{port}")
         432  +
            };
         433  +
            let ep = TestEndpoint::bind(&bind_addr, behaviors, events.clone()).await;
         434  +
            if i == 0 {
         435  +
                port = ep.port();
         436  +
            }
         437  +
            endpoints.push(ep);
         438  +
        }
         439  +
         440  +
        let mut resolver = MockDnsResolver::new(events.clone());
         441  +
        for (hostname, ips) in self.dns_entries {
         442  +
            resolver = resolver.with(&hostname, ips);
         443  +
        }
         444  +
         445  +
        ConnectionTestHarness {
         446  +
            endpoints,
         447  +
            events,
         448  +
            dns_resolver: resolver,
         449  +
        }
         450  +
    }
         451  +
}

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

@@ -0,1 +0,279 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
#![cfg(all(feature = "wire-mock", feature = "default-client"))]
           7  +
           8  +
use aws_smithy_http_client::test_util::wire::connection::{
           9  +
    ConnectionBehavior, ConnectionTestHarness,
          10  +
};
          11  +
use std::net::{IpAddr, Ipv4Addr};
          12  +
          13  +
const IP1: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
          14  +
const IP2: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
          15  +
          16  +
/// Check if a loopback address is bindable on this system.
          17  +
/// On macOS, addresses other than 127.0.0.1 require explicit loopback aliases.
          18  +
async fn is_bindable(ip: IpAddr) -> bool {
          19  +
    tokio::net::TcpListener::bind((ip, 0u16)).await.is_ok()
          20  +
}
          21  +
          22  +
#[tokio::test]
          23  +
async fn test_harness_multi_ip_endpoints() {
          24  +
    if !is_bindable(IP2).await {
          25  +
        eprintln!("skipping test: 127.0.0.2 not bindable (loopback alias not configured)");
          26  +
        return;
          27  +
    }
          28  +
          29  +
    let harness = ConnectionTestHarness::builder()
          30  +
        .endpoint(
          31  +
            IP1,
          32  +
            vec![ConnectionBehavior::RespondKeepAlive {
          33  +
                status: 200,
          34  +
                body: b"hello",
          35  +
            }],
          36  +
        )
          37  +
        .endpoint(
          38  +
            IP2,
          39  +
            vec![ConnectionBehavior::RespondKeepAlive {
          40  +
                status: 200,
          41  +
                body: b"world",
          42  +
            }],
          43  +
        )
          44  +
        .dns_all("test.example.com")
          45  +
        .build()
          46  +
        .await;
          47  +
          48  +
    // Verify endpoints are on different IPs but same port
          49  +
    let eps = &harness.endpoints;
          50  +
    assert_ne!(eps[0].ip(), eps[1].ip());
          51  +
    assert_eq!(eps[0].port(), eps[1].port());
          52  +
          53  +
    // Connect to each endpoint directly to verify they work
          54  +
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
          55  +
    use tokio::net::TcpStream;
          56  +
          57  +
    for ep in &harness.endpoints {
          58  +
        let mut stream = TcpStream::connect(ep.addr()).await.unwrap();
          59  +
        stream
          60  +
            .write_all(b"GET / HTTP/1.1\r\nHost: test\r\n\r\n")
          61  +
            .await
          62  +
            .unwrap();
          63  +
        let mut buf = vec![0u8; 1024];
          64  +
        let n = stream.read(&mut buf).await.unwrap();
          65  +
        let response = String::from_utf8_lossy(&buf[..n]);
          66  +
        assert!(
          67  +
            response.contains("200"),
          68  +
            "Expected 200 response, got: {}",
          69  +
            response
          70  +
        );
          71  +
    }
          72  +
          73  +
    assert_eq!(harness.tcp_accepted_count(), 2);
          74  +
    assert_eq!(harness.tcp_accepted_by(IP1), 1);
          75  +
    assert_eq!(harness.tcp_accepted_by(IP2), 1);
          76  +
}
          77  +
          78  +
#[tokio::test]
          79  +
async fn test_harness_reset_on_connect() {
          80  +
    let harness = ConnectionTestHarness::builder()
          81  +
        .endpoint(IP1, vec![ConnectionBehavior::ResetOnConnect])
          82  +
        .build()
          83  +
        .await;
          84  +
          85  +
    use tokio::io::AsyncReadExt;
          86  +
    use tokio::net::TcpStream;
          87  +
          88  +
    let mut stream = TcpStream::connect(harness.endpoints[0].addr())
          89  +
        .await
          90  +
        .unwrap();
          91  +
    // Try to read — should get connection reset or EOF
          92  +
    let mut buf = vec![0u8; 1024];
          93  +
    let result = stream.read(&mut buf).await;
          94  +
    assert!(
          95  +
        result.is_err() || result.unwrap() == 0,
          96  +
        "Expected connection reset or EOF"
          97  +
    );
          98  +
          99  +
    assert_eq!(harness.tcp_accepted_count(), 1);
         100  +
}
         101  +
         102  +
#[tokio::test]
         103  +
async fn test_mock_dns_resolver() {
         104  +
    if !is_bindable(IP2).await {
         105  +
        eprintln!("skipping test: 127.0.0.2 not bindable (loopback alias not configured)");
         106  +
        return;
         107  +
    }
         108  +
         109  +
    let harness = ConnectionTestHarness::builder()
         110  +
        .endpoint(
         111  +
            IP1,
         112  +
            vec![ConnectionBehavior::RespondKeepAlive {
         113  +
                status: 200,
         114  +
                body: b"ok",
         115  +
            }],
         116  +
        )
         117  +
        .endpoint(
         118  +
            IP2,
         119  +
            vec![ConnectionBehavior::RespondKeepAlive {
         120  +
                status: 200,
         121  +
                body: b"ok",
         122  +
            }],
         123  +
        )
         124  +
        .dns("s3.amazonaws.com", vec![IP1, IP2])
         125  +
        .build()
         126  +
        .await;
         127  +
         128  +
    use aws_smithy_runtime_api::client::dns::ResolveDns;
         129  +
    let resolver = harness.dns_resolver();
         130  +
    let ips = resolver.resolve_dns("s3.amazonaws.com").await.unwrap();
         131  +
    assert_eq!(ips.len(), 2);
         132  +
    assert!(ips.contains(&IP1));
         133  +
    assert!(ips.contains(&IP2));
         134  +
    assert_eq!(harness.dns_lookup_count(), 1);
         135  +
}
         136  +
         137  +
#[tokio::test]
         138  +
async fn test_harness_keep_alive_reuse() {
         139  +
    // Multiple responses on a single connection (keep-alive)
         140  +
    let harness = ConnectionTestHarness::builder()
         141  +
        .endpoint(
         142  +
            IP1,
         143  +
            vec![
         144  +
                ConnectionBehavior::RespondKeepAlive {
         145  +
                    status: 200,
         146  +
                    body: b"first",
         147  +
                },
         148  +
                ConnectionBehavior::RespondKeepAlive {
         149  +
                    status: 201,
         150  +
                    body: b"second",
         151  +
                },
         152  +
                ConnectionBehavior::RespondKeepAlive {
         153  +
                    status: 202,
         154  +
                    body: b"third",
         155  +
                },
         156  +
            ],
         157  +
        )
         158  +
        .build()
         159  +
        .await;
         160  +
         161  +
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
         162  +
    use tokio::net::TcpStream;
         163  +
         164  +
    let mut stream = TcpStream::connect(harness.endpoints[0].addr())
         165  +
        .await
         166  +
        .unwrap();
         167  +
         168  +
    for (i, expected_status) in ["200", "201", "202"].iter().enumerate() {
         169  +
        stream
         170  +
            .write_all(b"GET / HTTP/1.1\r\nHost: test\r\n\r\n")
         171  +
            .await
         172  +
            .unwrap();
         173  +
        let mut buf = vec![0u8; 1024];
         174  +
        let n = stream.read(&mut buf).await.unwrap();
         175  +
        let response = String::from_utf8_lossy(&buf[..n]);
         176  +
        assert!(
         177  +
            response.contains(expected_status),
         178  +
            "Request {i}: expected {expected_status}, got: {response}"
         179  +
        );
         180  +
    }
         181  +
         182  +
    // Only 1 TCP connection was accepted — all 3 requests reused it
         183  +
    assert_eq!(harness.tcp_accepted_count(), 1);
         184  +
}
         185  +
         186  +
#[tokio::test]
         187  +
async fn test_harness_hold_then_close() {
         188  +
    use std::time::{Duration, Instant};
         189  +
         190  +
    let hold_duration = Duration::from_millis(100);
         191  +
    let harness = ConnectionTestHarness::builder()
         192  +
        .endpoint(IP1, vec![ConnectionBehavior::HoldThenClose(hold_duration)])
         193  +
        .build()
         194  +
        .await;
         195  +
         196  +
    use tokio::io::AsyncReadExt;
         197  +
    use tokio::net::TcpStream;
         198  +
         199  +
    let start = Instant::now();
         200  +
    let mut stream = TcpStream::connect(harness.endpoints[0].addr())
         201  +
        .await
         202  +
        .unwrap();
         203  +
    let mut buf = vec![0u8; 1024];
         204  +
    let n = stream.read(&mut buf).await.unwrap();
         205  +
    let elapsed = start.elapsed();
         206  +
         207  +
    // Connection held open then closed — read returns 0 (EOF)
         208  +
    assert_eq!(n, 0, "Expected EOF after hold-then-close");
         209  +
    assert!(
         210  +
        elapsed >= hold_duration,
         211  +
        "Expected at least {hold_duration:?} hold, got {elapsed:?}"
         212  +
    );
         213  +
    assert_eq!(harness.tcp_accepted_count(), 1);
         214  +
}
         215  +
         216  +
#[tokio::test]
         217  +
async fn test_harness_dns_all_includes_all_endpoints() {
         218  +
    if !is_bindable(IP2).await {
         219  +
        eprintln!("skipping test: 127.0.0.2 not bindable (loopback alias not configured)");
         220  +
        return;
         221  +
    }
         222  +
         223  +
    let harness = ConnectionTestHarness::builder()
         224  +
        .endpoint(
         225  +
            IP1,
         226  +
            vec![ConnectionBehavior::RespondKeepAlive {
         227  +
                status: 200,
         228  +
                body: b"a",
         229  +
            }],
         230  +
        )
         231  +
        .endpoint(
         232  +
            IP2,
         233  +
            vec![ConnectionBehavior::RespondKeepAlive {
         234  +
                status: 200,
         235  +
                body: b"b",
         236  +
            }],
         237  +
        )
         238  +
        .dns_all("example.com")
         239  +
        .build()
         240  +
        .await;
         241  +
         242  +
    use aws_smithy_runtime_api::client::dns::ResolveDns;
         243  +
    let ips = harness
         244  +
        .dns_resolver()
         245  +
        .resolve_dns("example.com")
         246  +
        .await
         247  +
        .unwrap();
         248  +
    assert_eq!(ips.len(), 2);
         249  +
    assert!(ips.contains(&IP1));
         250  +
    assert!(ips.contains(&IP2));
         251  +
}
         252  +
         253  +
#[tokio::test]
         254  +
async fn test_mock_dns_unknown_host_returns_empty() {
         255  +
    let harness = ConnectionTestHarness::builder()
         256  +
        .endpoint(
         257  +
            IP1,
         258  +
            vec![ConnectionBehavior::RespondKeepAlive {
         259  +
                status: 200,
         260  +
                body: b"ok",
         261  +
            }],
         262  +
        )
         263  +
        .dns("known.com", vec![IP1])
         264  +
        .build()
         265  +
        .await;
         266  +
         267  +
    use aws_smithy_runtime_api::client::dns::ResolveDns;
         268  +
    let ips = harness
         269  +
        .dns_resolver()
         270  +
        .resolve_dns("unknown.com")
         271  +
        .await
         272  +
        .unwrap();
         273  +
    assert!(ips.is_empty(), "Unknown host should return empty IP list");
         274  +
    assert_eq!(
         275  +
        harness.dns_lookup_count(),
         276  +
        1,
         277  +
        "Lookup should still be recorded"
         278  +
    );
         279  +
}

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

@@ -0,1 +0,687 @@
           1  +
/*
           2  +
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
           3  +
 * SPDX-License-Identifier: Apache-2.0
           4  +
 */
           5  +
           6  +
//! H2 connection pool behavior tests.
           7  +
//!
           8  +
//! Tests the v2 pool's HTTP/2 path: multiplexing, GOAWAY handling,
           9  +
//! connection poisoning, and stream limits.
          10  +
//!
          11  +
//! Uses plain TCP with a fake ALPN signal (`Connected::new().negotiated_h2()`)
          12  +
//! so no TLS infrastructure is needed. The pool's Negotiate layer trusts
          13  +
//! the `Connected` metadata to route to the H2 path.
          14  +
          15  +
#![cfg(all(
          16  +
    feature = "wire-mock",
          17  +
    feature = "default-client",
          18  +
    feature = "test-util",
          19  +
    aws_sdk_unstable
          20  +
))]
          21  +
          22  +
use aws_smithy_http_client::pool::{Client, SharedPool};
          23  +
use aws_smithy_runtime_api::client::http::{
          24  +
    HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient,
          25  +
};
          26  +
use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
          27  +
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
          28  +
use aws_smithy_runtime_api::shared::IntoShared;
          29  +
use bytes::Bytes;
          30  +
use h2::server::SendResponse;
          31  +
use h2::RecvStream;
          32  +
use http_body_util::BodyExt;
          33  +
use hyper_util::client::legacy::connect::Connected;
          34  +
use std::future::Future;
          35  +
use std::net::SocketAddr;
          36  +
use std::pin::Pin;
          37  +
use std::sync::atomic::{AtomicUsize, Ordering};
          38  +
use std::sync::Arc;
          39  +
use std::task::{Context, Poll};
          40  +
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
          41  +
use tokio::net::{TcpListener, TcpStream};
          42  +
use tower::Service;
          43  +
          44  +
// ---------------------------------------------------------------------------
          45  +
// H2MockServer — plain TCP server speaking H2 via the h2 crate directly
          46  +
// ---------------------------------------------------------------------------
          47  +
          48  +
/// Handler function type for H2 requests.
          49  +
type H2Handler =
          50  +
    Arc<dyn Fn(http_1x::Request<RecvStream>, SendResponse<Bytes>) + Send + Sync + 'static>;
          51  +
          52  +
struct H2MockServer {
          53  +
    addr: SocketAddr,
          54  +
    /// Total H2 connections accepted (each connection can multiplex many streams).
          55  +
    connections: Arc<AtomicUsize>,
          56  +
    /// Total streams (requests) handled across all connections.
          57  +
    streams: Arc<AtomicUsize>,
          58  +
    _shutdown: tokio::sync::oneshot::Sender<()>,
          59  +
}
          60  +
          61  +
impl H2MockServer {
          62  +
    /// Start an H2 server that responds 200 with the given body to every request.
          63  +
    async fn start(body: &'static str) -> Self {
          64  +
        Self::start_with_handler(Arc::new(move |_req, mut respond| {
          65  +
            let response = http_1x::Response::builder().status(200).body(()).unwrap();
          66  +
            let mut send_stream = respond.send_response(response, false).unwrap();
          67  +
            send_stream.send_data(Bytes::from(body), true).unwrap();
          68  +
        }))
          69  +
        .await
          70  +
    }
          71  +
          72  +
    /// Start with a custom handler for each stream.
          73  +
    async fn start_with_handler(handler: H2Handler) -> Self {
          74  +
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
          75  +
        let addr = listener.local_addr().unwrap();
          76  +
        let connections = Arc::new(AtomicUsize::new(0));
          77  +
        let streams = Arc::new(AtomicUsize::new(0));
          78  +
        let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
          79  +
          80  +
        let conns = connections.clone();
          81  +
        let strms = streams.clone();
          82  +
          83  +
        tokio::spawn(async move {
          84  +
            loop {
          85  +
                tokio::select! {
          86  +
                    accept = listener.accept() => {
          87  +
                        let (stream, _) = match accept {
          88  +
                            Ok(v) => v,
          89  +
                            Err(_) => break,
          90  +
                        };
          91  +
                        conns.fetch_add(1, Ordering::SeqCst);
          92  +
                        let handler = handler.clone();
          93  +
                        let strms = strms.clone();
          94  +
                        tokio::spawn(async move {
          95  +
                            let mut conn = h2::server::Builder::new()
          96  +
                                .handshake(stream)
          97  +
                                .await
          98  +
                                .unwrap();
          99  +
                            while let Some(result) = conn.accept().await {
         100  +
                                let (req, respond) = result.unwrap();
         101  +
                                strms.fetch_add(1, Ordering::SeqCst);
         102  +
                                handler(req, respond);
         103  +
                            }
         104  +
                        });
         105  +
                    }
         106  +
                    _ = &mut shutdown_rx => break,
         107  +
                }
         108  +
            }
         109  +
        });
         110  +
         111  +
        Self {
         112  +
            addr,
         113  +
            connections,
         114  +
            streams,
         115  +
            _shutdown: shutdown_tx,
         116  +
        }
         117  +
    }
         118  +
         119  +
    /// Start a server that sends GOAWAY after `n` streams on each connection.
         120  +
    async fn start_goaway_after(n: usize, body: &'static str) -> Self {
         121  +
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
         122  +
        let addr = listener.local_addr().unwrap();
         123  +
        let connections = Arc::new(AtomicUsize::new(0));
         124  +
        let streams = Arc::new(AtomicUsize::new(0));
         125  +
        let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
         126  +
         127  +
        let conns = connections.clone();
         128  +
        let strms = streams.clone();
         129  +
         130  +
        tokio::spawn(async move {
         131  +
            loop {
         132  +
                tokio::select! {
         133  +
                    accept = listener.accept() => {
         134  +
                        let (stream, _) = match accept {
         135  +
                            Ok(v) => v,
         136  +
                            Err(_) => break,
         137  +
                        };
         138  +
                        conns.fetch_add(1, Ordering::SeqCst);
         139  +
                        let strms = strms.clone();
         140  +
                        let per_conn_count = Arc::new(AtomicUsize::new(0));
         141  +
                        tokio::spawn(async move {
         142  +
                            let mut conn = h2::server::Builder::new()
         143  +
                                .handshake(stream)
         144  +
                                .await
         145  +
                                .unwrap();
         146  +
                            while let Some(result) = conn.accept().await {
         147  +
                                let (req, mut respond) = result.unwrap();
         148  +
                                strms.fetch_add(1, Ordering::SeqCst);
         149  +
                                let count = per_conn_count.fetch_add(1, Ordering::SeqCst) + 1;
         150  +
         151  +
                                // Respond normally
         152  +
                                let response = http_1x::Response::builder()
         153  +
                                    .status(200)
         154  +
                                    .body(())
         155  +
                                    .unwrap();
         156  +
                                let mut send_stream = respond.send_response(response, false).unwrap();
         157  +
                                send_stream.send_data(Bytes::from(body), true).unwrap();
         158  +
                                drop(req);
         159  +
         160  +
                                // After n streams, send GOAWAY
         161  +
                                if count >= n {
         162  +
                                    conn.graceful_shutdown();
         163  +
                                }
         164  +
                            }
         165  +
                        });
         166  +
                    }
         167  +
                    _ = &mut shutdown_rx => break,
         168  +
                }
         169  +
            }
         170  +
        });
         171  +
         172  +
        Self {
         173  +
            addr,
         174  +
            connections,
         175  +
            streams,
         176  +
            _shutdown: shutdown_tx,
         177  +
        }
         178  +
    }
         179  +
         180  +
    fn connection_count(&self) -> usize {
         181  +
        self.connections.load(Ordering::SeqCst)
         182  +
    }
         183  +
         184  +
    fn stream_count(&self) -> usize {
         185  +
        self.streams.load(Ordering::SeqCst)
         186  +
    }
         187  +
         188  +
    fn url(&self) -> String {
         189  +
        format!("http://127.0.0.1:{}/", self.addr.port())
         190  +
    }
         191  +
}
         192  +
         193  +
// ---------------------------------------------------------------------------
         194  +
// H2Connector — connects via TCP, signals negotiated_h2()
         195  +
// ---------------------------------------------------------------------------
         196  +
         197  +
/// IO wrapper that signals H2 negotiation to the pool's Negotiate layer.
         198  +
struct H2Io {
         199  +
    inner: TcpStream,
         200  +
}
         201  +
         202  +
impl hyper_util::client::legacy::connect::Connection for H2Io {
         203  +
    fn connected(&self) -> Connected {
         204  +
        Connected::new().negotiated_h2()
         205  +
    }
         206  +
}
         207  +
         208  +
impl AsyncRead for H2Io {
         209  +
    fn poll_read(
         210  +
        mut self: Pin<&mut Self>,
         211  +
        cx: &mut Context<'_>,
         212  +
        buf: &mut ReadBuf<'_>,
         213  +
    ) -> Poll<std::io::Result<()>> {
         214  +
        Pin::new(&mut self.inner).poll_read(cx, buf)
         215  +
    }
         216  +
}
         217  +
         218  +
impl AsyncWrite for H2Io {
         219  +
    fn poll_write(
         220  +
        mut self: Pin<&mut Self>,
         221  +
        cx: &mut Context<'_>,
         222  +
        buf: &[u8],
         223  +
    ) -> Poll<std::io::Result<usize>> {
         224  +
        Pin::new(&mut self.inner).poll_write(cx, buf)
         225  +
    }
         226  +
         227  +
    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
         228  +
        Pin::new(&mut self.inner).poll_flush(cx)
         229  +
    }
         230  +
         231  +
    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
         232  +
        Pin::new(&mut self.inner).poll_shutdown(cx)
         233  +
    }
         234  +
}
         235  +
         236  +
// hyper::rt::Read and Write are needed by the pool
         237  +
impl hyper::rt::Read for H2Io {
         238  +
    fn poll_read(
         239  +
        self: Pin<&mut Self>,
         240  +
        cx: &mut Context<'_>,
         241  +
        mut buf: hyper::rt::ReadBufCursor<'_>,
         242  +
    ) -> Poll<std::io::Result<()>> {
         243  +
        let n = unsafe {
         244  +
            let mut tbuf = ReadBuf::uninit(buf.as_mut());
         245  +
            match Pin::new(&mut self.get_mut().inner).poll_read(cx, &mut tbuf) {
         246  +
                Poll::Ready(Ok(())) => tbuf.filled().len(),
         247  +
                other => return other,
         248  +
            }
         249  +
        };
         250  +
        unsafe { buf.advance(n) };
         251  +
        Poll::Ready(Ok(()))
         252  +
    }
         253  +
}
         254  +
         255  +
impl hyper::rt::Write for H2Io {
         256  +
    fn poll_write(
         257  +
        mut self: Pin<&mut Self>,
         258  +
        cx: &mut Context<'_>,
         259  +
        buf: &[u8],
         260  +
    ) -> Poll<std::io::Result<usize>> {
         261  +
        Pin::new(&mut self.inner).poll_write(cx, buf)
         262  +
    }
         263  +
         264  +
    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
         265  +
        Pin::new(&mut self.inner).poll_flush(cx)
         266  +
    }
         267  +
         268  +
    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
         269  +
        Pin::new(&mut self.inner).poll_shutdown(cx)
         270  +
    }
         271  +
}
         272  +
         273  +
/// Connector that establishes TCP connections and signals H2 negotiation.
         274  +
#[derive(Clone)]
         275  +
struct H2Connector {
         276  +
    addr: SocketAddr,
         277  +
}
         278  +
         279  +
impl Service<http_1x::Uri> for H2Connector {
         280  +
    type Response = H2Io;
         281  +
    type Error = Box<dyn std::error::Error + Send + Sync>;
         282  +
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
         283  +
         284  +
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
         285  +
        Poll::Ready(Ok(()))
         286  +
    }
         287  +
         288  +
    fn call(&mut self, _req: http_1x::Uri) -> Self::Future {
         289  +
        let addr = self.addr;
         290  +
        Box::pin(async move {
         291  +
            let stream = TcpStream::connect(addr).await?;
         292  +
            Ok(H2Io { inner: stream })
         293  +
        })
         294  +
    }
         295  +
}
         296  +
         297  +
// ---------------------------------------------------------------------------
         298  +
// Helpers
         299  +
// ---------------------------------------------------------------------------
         300  +
         301  +
fn build_h2_client(server: &H2MockServer) -> SharedHttpClient {
         302  +
    let pool =
         303  +
        SharedPool::builder().build_http_with_tcp_connector(H2Connector { addr: server.addr });
         304  +
    Client::new(&pool).into_shared()
         305  +
}
         306  +
         307  +
fn runtime_components() -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents {
         308  +
    RuntimeComponentsBuilder::for_tests()
         309  +
        .with_time_source(Some(aws_smithy_async::time::SystemTimeSource::new()))
         310  +
        .build()
         311  +
        .expect("valid runtime components")
         312  +
}
         313  +
         314  +
async fn send_request(
         315  +
    client: &SharedHttpClient,
         316  +
    url: &str,
         317  +
) -> Result<(u16, Vec<u8>), aws_smithy_runtime_api::client::result::ConnectorError> {
         318  +
    let settings = HttpConnectorSettings::builder().build();
         319  +
    let components = runtime_components();
         320  +
    let connector = client.http_connector(&settings, &components);
         321  +
    let resp = connector
         322  +
        .call(HttpRequest::get(url).expect("valid request"))
         323  +
        .await?;
         324  +
    let status = resp.status().as_u16();
         325  +
    let body = resp
         326  +
        .into_body()
         327  +
        .collect()
         328  +
        .await
         329  +
        .expect("body")
         330  +
        .to_bytes()
         331  +
        .to_vec();
         332  +
    Ok((status, body))
         333  +
}
         334  +
         335  +
// ---------------------------------------------------------------------------
         336  +
// Tests
         337  +
// ---------------------------------------------------------------------------
         338  +
         339  +
/// Multiple concurrent requests multiplex over a single H2 connection.
         340  +
#[tokio::test]
         341  +
async fn h2_multiplexing_shares_one_connection() {
         342  +
    let server = H2MockServer::start("ok").await;
         343  +
    let client = build_h2_client(&server);
         344  +
    let url = server.url();
         345  +
         346  +
    // Warm the connection with one request first so the Singleton is populated
         347  +
    let (status, _) = send_request(&client, &url).await.unwrap();
         348  +
    assert_eq!(status, 200);
         349  +
    assert_eq!(server.connection_count(), 1);
         350  +
         351  +
    // Now send 4 concurrent requests — they should all multiplex on the existing connection
         352  +
    let futs: Vec<_> = (0..4).map(|_| send_request(&client, &url)).collect();
         353  +
    let results = futures_util::future::join_all(futs).await;
         354  +
         355  +
    for (i, r) in results.iter().enumerate() {
         356  +
        let (status, _) = r
         357  +
            .as_ref()
         358  +
            .unwrap_or_else(|e| panic!("request {i} failed: {e}"));
         359  +
        assert_eq!(*status, 200);
         360  +
    }
         361  +
         362  +
    // All 5 requests (1 warm + 4 concurrent) should have used a single connection
         363  +
    assert_eq!(
         364  +
        server.connection_count(),
         365  +
        1,
         366  +
        "H2 should multiplex on one connection"
         367  +
    );
         368  +
    assert_eq!(server.stream_count(), 5, "should have 5 streams");
         369  +
}
         370  +
         371  +
/// After GOAWAY, the pool establishes a new connection for subsequent requests.
         372  +
#[tokio::test]
         373  +
async fn h2_goaway_triggers_new_connection() {
         374  +
    // Server sends GOAWAY after 2 streams per connection
         375  +
    let server = H2MockServer::start_goaway_after(2, "ok").await;
         376  +
    let client = build_h2_client(&server);
         377  +
    let url = server.url();
         378  +
         379  +
    // First 2 requests on connection 1
         380  +
    let (s1, _) = send_request(&client, &url).await.unwrap();
         381  +
    let (s2, _) = send_request(&client, &url).await.unwrap();
         382  +
    assert_eq!(s1, 200);
         383  +
    assert_eq!(s2, 200);
         384  +
         385  +
    // Give the pool time to observe the GOAWAY
         386  +
    tokio::time::sleep(std::time::Duration::from_millis(50)).await;
         387  +
         388  +
    // Next request should go on a new connection
         389  +
    let (s3, _) = send_request(&client, &url).await.unwrap();
         390  +
    assert_eq!(s3, 200);
         391  +
         392  +
    assert!(
         393  +
        server.connection_count() >= 2,
         394  +
        "should have opened a second connection after GOAWAY, got {}",
         395  +
        server.connection_count()
         396  +
    );
         397  +
}
         398  +
         399  +
/// Sequential requests reuse the same H2 connection (no unnecessary reconnects).
         400  +
#[tokio::test]
         401  +
async fn h2_sequential_requests_reuse_connection() {
         402  +
    let server = H2MockServer::start("hello").await;
         403  +
    let client = build_h2_client(&server);
         404  +
    let url = server.url();
         405  +
         406  +
    for i in 0..5 {
         407  +
        let (status, body) = send_request(&client, &url).await.unwrap();
         408  +
        assert_eq!(status, 200, "request {i}");
         409  +
        assert_eq!(body, b"hello", "request {i}");
         410  +
    }
         411  +
         412  +
    assert_eq!(
         413  +
        server.connection_count(),
         414  +
        1,
         415  +
        "should reuse one H2 connection"
         416  +
    );
         417  +
    assert_eq!(server.stream_count(), 5);
         418  +
}
         419  +
         420  +
/// Poisoning an H2 connection forces the pool to establish a new one.
         421  +
#[tokio::test]
         422  +
async fn h2_poisoned_connection_not_reused() {
         423  +
    use aws_smithy_runtime_api::client::connection::CaptureSmithyConnection;
         424  +
         425  +
    let server = H2MockServer::start("ok").await;
         426  +
    let client = build_h2_client(&server);
         427  +
    let url = server.url();
         428  +
         429  +
    // First request: establish H2 connection, capture metadata.
         430  +
    let settings = HttpConnectorSettings::builder().build();
         431  +
    let components = runtime_components();
         432  +
    let connector = client.http_connector(&settings, &components);
         433  +
         434  +
    let capture = CaptureSmithyConnection::new();
         435  +
    let mut request = HttpRequest::get(&url).expect("valid request");
         436  +
    request.add_extension(capture.clone());
         437  +
         438  +
    let resp = connector
         439  +
        .call(request)
         440  +
        .await
         441  +
        .expect("request should succeed");
         442  +
    let _body = resp.into_body().collect().await.expect("body");
         443  +
    assert_eq!(server.connection_count(), 1);
         444  +
         445  +
    let metadata = capture.get().expect("adapter should populate metadata");
         446  +
    metadata.poison();
         447  +
         448  +
    // Give the pool a moment to observe the poison
         449  +
    tokio::time::sleep(std::time::Duration::from_millis(20)).await;
         450  +
         451  +
    // Next request should open a NEW H2 connection
         452  +
    let (status, _) = send_request(&client, &url).await.unwrap();
         453  +
    assert_eq!(status, 200);
         454  +
    assert!(
         455  +
        server.connection_count() >= 2,
         456  +
        "poisoned H2 connection should not be reused, got {} connections",
         457  +
        server.connection_count()
         458  +
    );
         459  +
}
         460  +
         461  +
// ===========================================================================
         462  +
// TLS + ALPN — real certificate negotiation
         463  +
// ===========================================================================
         464  +
//
         465  +
// These tests use a TLS server with self-signed certs advertising h2 via ALPN.
         466  +
// They verify the v2 pool correctly routes to H2 when ALPN negotiates it
         467  +
// through real TLS.
         468  +
         469  +
#[cfg(feature = "rustls-aws-lc")]
         470  +
mod tls_h2 {
         471  +
    use aws_smithy_http_client::pool::{Client, SharedPool};
         472  +
    use aws_smithy_http_client::tls;
         473  +
    use aws_smithy_http_client::tls::{TlsContext, TrustStore};
         474  +
    use aws_smithy_runtime_api::client::http::{
         475  +
        HttpClient, HttpConnector, HttpConnectorSettings, SharedHttpClient,
         476  +
    };
         477  +
    use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
         478  +
    use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
         479  +
    use aws_smithy_runtime_api::shared::IntoShared;
         480  +
    use http_body_util::BodyExt;
         481  +
    use hyper_util::rt::TokioExecutor;
         482  +
    use std::net::SocketAddr;
         483  +
    use std::sync::atomic::{AtomicUsize, Ordering};
         484  +
    use std::sync::Arc;
         485  +
    use tokio::net::TcpListener;
         486  +
    use tokio_rustls::TlsAcceptor;
         487  +
         488  +
    /// TLS test server that tracks connection count and serves H2 via ALPN.
         489  +
    struct TlsH2Server {
         490  +
        addr: SocketAddr,
         491  +
        connections: Arc<AtomicUsize>,
         492  +
        _shutdown: tokio::sync::oneshot::Sender<()>,
         493  +
    }
         494  +
         495  +
    impl TlsH2Server {
         496  +
        async fn start() -> Self {
         497  +
            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
         498  +
         499  +
            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
         500  +
            let addr = listener.local_addr().unwrap();
         501  +
            let connections = Arc::new(AtomicUsize::new(0));
         502  +
            let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
         503  +
         504  +
            // Load certs
         505  +
            let cert_pem = std::fs::read("tests/server.pem").unwrap();
         506  +
            let key_pem = std::fs::read("tests/server.rsa").unwrap();
         507  +
            let certs: Vec<_> = rustls_pemfile::certs(&mut &cert_pem[..])
         508  +
                .collect::<Result<_, _>>()
         509  +
                .unwrap();
         510  +
            let key = rustls_pemfile::private_key(&mut &key_pem[..])
         511  +
                .unwrap()
         512  +
                .unwrap();
         513  +
         514  +
            let mut server_config = rustls::ServerConfig::builder()
         515  +
                .with_no_client_auth()
         516  +
                .with_single_cert(certs, key)
         517  +
                .unwrap();
         518  +
            // Only advertise h2 — force H2 negotiation
         519  +
            server_config.alpn_protocols = vec![b"h2".to_vec()];
         520  +
            let tls_acceptor = TlsAcceptor::from(Arc::new(server_config));
         521  +
         522  +
            let conns = connections.clone();
         523  +
            tokio::spawn(async move {
         524  +
                loop {
         525  +
                    tokio::select! {
         526  +
                        accept = listener.accept() => {
         527  +
                            let (tcp, _) = match accept {
         528  +
                                Ok(v) => v,
         529  +
                                Err(_) => break,
         530  +
                            };
         531  +
                            conns.fetch_add(1, Ordering::SeqCst);
         532  +
                            let tls_acceptor = tls_acceptor.clone();
         533  +
                            tokio::spawn(async move {
         534  +
                                let tls_stream = match tls_acceptor.accept(tcp).await {
         535  +
                                    Ok(s) => s,
         536  +
                                    Err(e) => {
         537  +
                                        eprintln!("TLS accept failed: {e}");
         538  +
                                        return;
         539  +
                                    }
         540  +
                                };
         541  +
                                let service = hyper::service::service_fn(|_req| async {
         542  +
                                    Ok::<_, hyper::Error>(
         543  +
                                        http_1x::Response::builder()
         544  +
                                            .status(200)
         545  +
                                            .body(http_body_util::Full::new(
         546  +
                                                bytes::Bytes::from("h2-ok"),
         547  +
                                            ))
         548  +
                                            .unwrap(),
         549  +
                                    )
         550  +
                                });
         551  +
                                let io = hyper_util::rt::TokioIo::new(tls_stream);
         552  +
                                // Use http2 only server since we only advertise h2
         553  +
                                let _ = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
         554  +
                                    .serve_connection(io, service)
         555  +
                                    .await;
         556  +
                            });
         557  +
                        }
         558  +
                        _ = &mut shutdown_rx => break,
         559  +
                    }
         560  +
                }
         561  +
            });
         562  +
         563  +
            Self {
         564  +
                addr,
         565  +
                connections,
         566  +
                _shutdown: shutdown_tx,
         567  +
            }
         568  +
        }
         569  +
         570  +
        fn connection_count(&self) -> usize {
         571  +
            self.connections.load(Ordering::SeqCst)
         572  +
        }
         573  +
         574  +
        fn url(&self) -> String {
         575  +
            format!("https://localhost:{}/", self.addr.port())
         576  +
        }
         577  +
    }
         578  +
         579  +
    fn tls_context() -> TlsContext {
         580  +
        let pem = std::fs::read("tests/server.pem").unwrap();
         581  +
        let trust_store = TrustStore::empty().with_pem_certificate(pem);
         582  +
        TlsContext::builder()
         583  +
            .with_trust_store(trust_store)
         584  +
            .build()
         585  +
            .unwrap()
         586  +
    }
         587  +
         588  +
    fn runtime_components() -> aws_smithy_runtime_api::client::runtime_components::RuntimeComponents
         589  +
    {
         590  +
        RuntimeComponentsBuilder::for_tests()
         591  +
            .with_time_source(Some(aws_smithy_async::time::SystemTimeSource::new()))
         592  +
            .build()
         593  +
            .unwrap()
         594  +
    }
         595  +
         596  +
    async fn send_request(
         597  +
        client: &SharedHttpClient,
         598  +
        url: &str,
         599  +
    ) -> Result<(u16, Vec<u8>), aws_smithy_runtime_api::client::result::ConnectorError> {
         600  +
        let settings = HttpConnectorSettings::builder().build();
         601  +
        let components = runtime_components();
         602  +
        let connector = client.http_connector(&settings, &components);
         603  +
        let resp = connector
         604  +
            .call(HttpRequest::get(url).expect("valid request"))
         605  +
            .await?;
         606  +
        let status = resp.status().as_u16();
         607  +
        let body = resp
         608  +
            .into_body()
         609  +
            .collect()
         610  +
            .await
         611  +
            .expect("body")
         612  +
            .to_bytes()
         613  +
            .to_vec();
         614  +
        Ok((status, body))
         615  +
    }
         616  +
         617  +
    /// Rustls + ALPN h2: v2 pool routes to H2, multiple requests multiplex.
         618  +
    #[tokio::test]
         619  +
    async fn rustls_alpn_h2_multiplexing() {
         620  +
        let server = TlsH2Server::start().await;
         621  +
        let pool = SharedPool::builder()
         622  +
            .tls_provider(tls::Provider::Rustls(
         623  +
                tls::rustls_provider::CryptoMode::AwsLc,
         624  +
            ))
         625  +
            .tls_context(tls_context())
         626  +
            .build_https();
         627  +
        let client: SharedHttpClient = Client::new(&pool).into_shared();
         628  +
         629  +
        let url = server.url();
         630  +
         631  +
        // First request establishes connection
         632  +
        let (status, body) = send_request(&client, &url).await.unwrap();
         633  +
        assert_eq!(status, 200);
         634  +
        assert_eq!(body, b"h2-ok");
         635  +
         636  +
        // Second request should multiplex on same connection
         637  +
        let (status, _) = send_request(&client, &url).await.unwrap();
         638  +
        assert_eq!(status, 200);
         639  +
         640  +
        // Third request too
         641  +
        let (status, _) = send_request(&client, &url).await.unwrap();
         642  +
        assert_eq!(status, 200);
         643  +
         644  +
        assert_eq!(
         645  +
            server.connection_count(),
         646  +
            1,
         647  +
            "rustls H2: all requests should multiplex on one connection"
         648  +
        );
         649  +
    }
         650  +
         651  +
    /// s2n-tls + ALPN h2: the v2 pool routes to H2 and multiplexes. Three
         652  +
    /// sequential requests share one connection because `S2nTlsConn::connected()`
         653  +
    /// reports the negotiated protocol from the TLS `application_protocol`, so the
         654  +
    /// Negotiate layer selects the H2 leg.
         655  +
    #[cfg(feature = "s2n-tls")]
         656  +
    #[tokio::test]
         657  +
    async fn s2n_tls_alpn_h2_multiplexing() {
         658  +
        let server = TlsH2Server::start().await;
         659  +
        let pool = SharedPool::builder()
         660  +
            .tls_provider(tls::Provider::S2nTls)
         661  +
            .tls_context(tls_context())
         662  +
            .build_https();
         663  +
        let client: SharedHttpClient = Client::new(&pool).into_shared();
         664  +
         665  +
        let url = server.url();
         666  +
         667  +
        // First request
         668  +
        let (status, body) = send_request(&client, &url).await.unwrap();
         669  +
        assert_eq!(status, 200);
         670  +
        assert_eq!(body, b"h2-ok");
         671  +
         672  +
        // Second request — if H2 works, should multiplex on same connection
         673  +
        let (status, _) = send_request(&client, &url).await.unwrap();
         674  +
        assert_eq!(status, 200);
         675  +
         676  +
        // Third request
         677  +
        let (status, _) = send_request(&client, &url).await.unwrap();
         678  +
        assert_eq!(status, 200);
         679  +
         680  +
        // All three requests multiplex on one H2 connection.
         681  +
        assert_eq!(
         682  +
            server.connection_count(),
         683  +
            1,
         684  +
            "s2n-tls H2: all requests should multiplex on one connection"
         685  +
        );
         686  +
    }
         687  +
}