Skip to main content

aws_smithy_runtime_api/client/
http.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! HTTP clients and connectors
7//!
8//! # What is a connector?
9//!
10//! When we talk about connectors, we are referring to the [`HttpConnector`] trait, and implementations of
11//! that trait. This trait simply takes a HTTP request, and returns a future with the response for that
12//! request.
13//!
14//! This is slightly different from what a connector is in other libraries such as
15//! [`hyper`]. In hyper 0.x, the connector is a [`tower`] `Service` that takes a `Uri` and returns
16//! a future with something that implements `AsyncRead + AsyncWrite`.
17//!
18//! The [`HttpConnector`] is designed to be a layer on top of
19//! whole HTTP libraries, such as hyper, which allows Smithy clients to be agnostic to the underlying HTTP
20//! transport layer. This also makes it easy to write tests with a fake HTTP connector, and several
21//! such test connector implementations are available in [`aws-smithy-runtime`]
22//! with the `test-util` feature enabled.
23//!
24//! # Responsibilities of a connector
25//!
26//! A connector primarily makes HTTP requests, but is also the place where connect and read timeouts are
27//! implemented. The `HyperConnector` in [`aws-smithy-runtime`] is an example where timeouts are implemented
28//! as part of the connector.
29//!
30//! Connectors are also responsible for DNS lookup, TLS, connection reuse, pooling, and eviction.
31//! The Smithy clients have no knowledge of such concepts.
32//!
33//! # The [`HttpClient`] trait
34//!
35//! Connectors allow us to make requests, but we need a layer on top of connectors so that we can handle
36//! varying connector settings. For example, say we configure some default HTTP connect/read timeouts on
37//! Client, and then configure some override connect/read timeouts for a specific operation. These timeouts
38//! ultimately are part of the connector, so the same connector can't be reused for the two different sets
39//! of timeouts. Thus, the [`HttpClient`] implementation is responsible for managing multiple connectors
40//! with varying config. Some example configs that can impact which connector is used:
41//!
42//! - HTTP protocol versions
43//! - TLS settings
44//! - Timeouts
45//!
46//! Some of these aren't implemented yet, but they will appear in the [`HttpConnectorSettings`] struct
47//! once they are.
48//!
49//! [`hyper`]: https://crates.io/crates/hyper
50//! [`tower`]: https://crates.io/crates/tower
51//! [`aws-smithy-runtime`]: https://crates.io/crates/aws-smithy-runtime
52
53use crate::box_error::BoxError;
54use crate::client::connector_metadata::ConnectorMetadata;
55use crate::client::orchestrator::{HttpRequest, HttpResponse};
56use crate::client::result::ConnectorError;
57use crate::client::runtime_components::sealed::ValidateConfig;
58use crate::client::runtime_components::{RuntimeComponents, RuntimeComponentsBuilder};
59use crate::impl_shared_conversions;
60use aws_smithy_types::config_bag::ConfigBag;
61use std::fmt;
62use std::sync::Arc;
63use std::time::Duration;
64
65new_type_future! {
66    #[doc = "Future for [`HttpConnector::call`]."]
67    pub struct HttpConnectorFuture<'static, HttpResponse, ConnectorError>;
68}
69
70/// Trait with a `call` function that asynchronously converts a request into a response.
71///
72/// Ordinarily, a connector would use an underlying HTTP library such as [hyper](https://crates.io/crates/hyper),
73/// and any associated HTTPS implementation alongside it to service requests.
74///
75/// However, it can also be useful to create fake/mock connectors implementing this trait
76/// for testing.
77pub trait HttpConnector: Send + Sync + fmt::Debug {
78    /// Asynchronously converts a request into a response.
79    fn call(&self, request: HttpRequest) -> HttpConnectorFuture;
80}
81
82/// A shared [`HttpConnector`] implementation.
83#[derive(Clone, Debug)]
84pub struct SharedHttpConnector(Arc<dyn HttpConnector>);
85
86impl SharedHttpConnector {
87    /// Returns a new [`SharedHttpConnector`].
88    pub fn new(connection: impl HttpConnector + 'static) -> Self {
89        Self(Arc::new(connection))
90    }
91}
92
93impl HttpConnector for SharedHttpConnector {
94    fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
95        (*self.0).call(request)
96    }
97}
98
99impl_shared_conversions!(convert SharedHttpConnector from HttpConnector using SharedHttpConnector::new);
100
101/// Returns a [`SharedHttpClient`] that calls the given `connector` function to select a HTTP connector.
102pub fn http_client_fn<F>(connector: F) -> SharedHttpClient
103where
104    F: Fn(&HttpConnectorSettings, &RuntimeComponents) -> SharedHttpConnector
105        + Send
106        + Sync
107        + 'static,
108{
109    struct ConnectorFn<T>(T);
110    impl<T> fmt::Debug for ConnectorFn<T> {
111        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112            f.write_str("ConnectorFn")
113        }
114    }
115    impl<T> HttpClient for ConnectorFn<T>
116    where
117        T: (Fn(&HttpConnectorSettings, &RuntimeComponents) -> SharedHttpConnector) + Send + Sync,
118    {
119        fn http_connector(
120            &self,
121            settings: &HttpConnectorSettings,
122            components: &RuntimeComponents,
123        ) -> SharedHttpConnector {
124            (self.0)(settings, components)
125        }
126    }
127
128    SharedHttpClient::new(ConnectorFn(connector))
129}
130
131/// HTTP client abstraction.
132///
133/// A HTTP client implementation must apply connect/read timeout settings,
134/// and must maintain a connection pool.
135pub trait HttpClient: Send + Sync + fmt::Debug {
136    /// Returns a HTTP connector based on the requested connector settings.
137    ///
138    /// The settings include connector timeouts, which should be incorporated
139    /// into the connector. The `HttpClient` is responsible for caching
140    /// the connector across requests.
141    ///
142    /// In the future, the settings may have additional parameters added,
143    /// such as HTTP version, or TLS certificate paths.
144    fn http_connector(
145        &self,
146        settings: &HttpConnectorSettings,
147        components: &RuntimeComponents,
148    ) -> SharedHttpConnector;
149
150    #[doc = include_str!("../../rustdoc/validate_base_client_config.md")]
151    fn validate_base_client_config(
152        &self,
153        runtime_components: &RuntimeComponentsBuilder,
154        cfg: &ConfigBag,
155    ) -> Result<(), BoxError> {
156        let _ = (runtime_components, cfg);
157        Ok(())
158    }
159
160    #[doc = include_str!("../../rustdoc/validate_final_config.md")]
161    fn validate_final_config(
162        &self,
163        runtime_components: &RuntimeComponents,
164        cfg: &ConfigBag,
165    ) -> Result<(), BoxError> {
166        let _ = (runtime_components, cfg);
167        Ok(())
168    }
169
170    /// Provide metadata about the crate that this HttpClient uses to make connectors.
171    ///
172    /// If this is implemented and returns metadata, interceptors may inspect it
173    /// for the purpose of inserting that data into the user agent string when
174    /// making a request with this client.
175    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
176        None
177    }
178}
179
180/// Shared HTTP client for use across multiple clients and requests.
181#[derive(Clone, Debug)]
182pub struct SharedHttpClient {
183    selector: Arc<dyn HttpClient>,
184}
185
186impl SharedHttpClient {
187    /// Creates a new `SharedHttpClient`
188    pub fn new(selector: impl HttpClient + 'static) -> Self {
189        Self {
190            selector: Arc::new(selector),
191        }
192    }
193}
194
195impl HttpClient for SharedHttpClient {
196    fn http_connector(
197        &self,
198        settings: &HttpConnectorSettings,
199        components: &RuntimeComponents,
200    ) -> SharedHttpConnector {
201        self.selector.http_connector(settings, components)
202    }
203
204    fn validate_base_client_config(
205        &self,
206        runtime_components: &RuntimeComponentsBuilder,
207        cfg: &ConfigBag,
208    ) -> Result<(), BoxError> {
209        self.selector
210            .validate_base_client_config(runtime_components, cfg)
211    }
212
213    fn validate_final_config(
214        &self,
215        runtime_components: &RuntimeComponents,
216        cfg: &ConfigBag,
217    ) -> Result<(), BoxError> {
218        self.selector.validate_final_config(runtime_components, cfg)
219    }
220
221    fn connector_metadata(&self) -> Option<ConnectorMetadata> {
222        self.selector.connector_metadata()
223    }
224}
225
226impl ValidateConfig for SharedHttpClient {
227    fn validate_base_client_config(
228        &self,
229        runtime_components: &RuntimeComponentsBuilder,
230        cfg: &ConfigBag,
231    ) -> Result<(), BoxError> {
232        HttpClient::validate_base_client_config(self, runtime_components, cfg)
233    }
234
235    fn validate_final_config(
236        &self,
237        runtime_components: &RuntimeComponents,
238        cfg: &ConfigBag,
239    ) -> Result<(), BoxError> {
240        HttpClient::validate_final_config(self, runtime_components, cfg)
241    }
242}
243
244impl_shared_conversions!(convert SharedHttpClient from HttpClient using SharedHttpClient::new);
245
246/// Builder for [`HttpConnectorSettings`].
247#[non_exhaustive]
248#[derive(Default, Debug)]
249pub struct HttpConnectorSettingsBuilder {
250    connect_timeout: Option<Duration>,
251    read_timeout: Option<Duration>,
252}
253
254impl HttpConnectorSettingsBuilder {
255    /// Creates a new builder.
256    pub fn new() -> Self {
257        Default::default()
258    }
259
260    /// Sets the connect timeout that should be used.
261    ///
262    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
263    pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
264        self.connect_timeout = Some(connect_timeout);
265        self
266    }
267
268    /// Sets the connect timeout that should be used.
269    ///
270    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
271    pub fn set_connect_timeout(&mut self, connect_timeout: Option<Duration>) -> &mut Self {
272        self.connect_timeout = connect_timeout;
273        self
274    }
275
276    /// Sets the read timeout that should be used.
277    ///
278    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
279    /// from the time the request is initiated.
280    pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
281        self.read_timeout = Some(read_timeout);
282        self
283    }
284
285    /// Sets the read timeout that should be used.
286    ///
287    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
288    /// from the time the request is initiated.
289    pub fn set_read_timeout(&mut self, read_timeout: Option<Duration>) -> &mut Self {
290        self.read_timeout = read_timeout;
291        self
292    }
293
294    /// Builds the [`HttpConnectorSettings`].
295    pub fn build(self) -> HttpConnectorSettings {
296        HttpConnectorSettings {
297            connect_timeout: self.connect_timeout,
298            read_timeout: self.read_timeout,
299        }
300    }
301}
302
303/// Settings for HTTP Connectors
304#[non_exhaustive]
305#[derive(Clone, Default, Debug)]
306pub struct HttpConnectorSettings {
307    connect_timeout: Option<Duration>,
308    read_timeout: Option<Duration>,
309}
310
311impl HttpConnectorSettings {
312    /// Returns a builder for `HttpConnectorSettings`.
313    pub fn builder() -> HttpConnectorSettingsBuilder {
314        Default::default()
315    }
316
317    /// Returns the connect timeout that should be used.
318    ///
319    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
320    pub fn connect_timeout(&self) -> Option<Duration> {
321        self.connect_timeout
322    }
323
324    /// Returns the read timeout that should be used.
325    ///
326    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
327    /// from the time the request is initiated.
328    pub fn read_timeout(&self) -> Option<Duration> {
329        self.read_timeout
330    }
331}
332
333#[cfg(all(test, feature = "test-util"))]
334mod tests {
335    use super::*;
336    use crate::client::runtime_components::RuntimeComponentsBuilder;
337    use aws_smithy_types::config_bag::ConfigBag;
338    use std::sync::atomic::{AtomicUsize, Ordering};
339
340    /// Selector that records how many times each validation hook was invoked.
341    #[derive(Debug, Default)]
342    struct CountingSelector {
343        base: AtomicUsize,
344        final_: AtomicUsize,
345    }
346
347    impl HttpClient for CountingSelector {
348        fn http_connector(
349            &self,
350            _settings: &HttpConnectorSettings,
351            _components: &RuntimeComponents,
352        ) -> SharedHttpConnector {
353            unreachable!("http_connector is not exercised by these tests")
354        }
355
356        fn validate_base_client_config(
357            &self,
358            _runtime_components: &RuntimeComponentsBuilder,
359            _cfg: &ConfigBag,
360        ) -> Result<(), BoxError> {
361            self.base.fetch_add(1, Ordering::SeqCst);
362            Ok(())
363        }
364
365        fn validate_final_config(
366            &self,
367            _runtime_components: &RuntimeComponents,
368            _cfg: &ConfigBag,
369        ) -> Result<(), BoxError> {
370            self.final_.fetch_add(1, Ordering::SeqCst);
371            Ok(())
372        }
373    }
374
375    /// External decorator that owns an inner [`SharedHttpClient`] and forwards the
376    /// public [`HttpClient`] validation hooks through it. This models a downstream
377    /// wrapper that cannot reach the sealed `ValidateConfig` trait and must rely on
378    /// the public forwarding methods.
379    #[derive(Debug)]
380    struct DecoratingClient {
381        inner: SharedHttpClient,
382    }
383
384    impl HttpClient for DecoratingClient {
385        fn http_connector(
386            &self,
387            settings: &HttpConnectorSettings,
388            components: &RuntimeComponents,
389        ) -> SharedHttpConnector {
390            self.inner.http_connector(settings, components)
391        }
392
393        fn validate_base_client_config(
394            &self,
395            runtime_components: &RuntimeComponentsBuilder,
396            cfg: &ConfigBag,
397        ) -> Result<(), BoxError> {
398            HttpClient::validate_base_client_config(&self.inner, runtime_components, cfg)
399        }
400
401        fn validate_final_config(
402            &self,
403            runtime_components: &RuntimeComponents,
404            cfg: &ConfigBag,
405        ) -> Result<(), BoxError> {
406            HttpClient::validate_final_config(&self.inner, runtime_components, cfg)
407        }
408    }
409
410    #[test]
411    fn shared_http_client_forwards_validation_to_selector() {
412        let selector = Arc::new(CountingSelector::default());
413        let shared = SharedHttpClient {
414            selector: selector.clone(),
415        };
416        let cfg = ConfigBag::base();
417        let builder = RuntimeComponentsBuilder::for_tests();
418        let components = RuntimeComponentsBuilder::for_tests().build().unwrap();
419
420        // Public `HttpClient` path: what external decorators call.
421        HttpClient::validate_base_client_config(&shared, &builder, &cfg).unwrap();
422        HttpClient::validate_final_config(&shared, &components, &cfg).unwrap();
423
424        // Sealed `ValidateConfig` path: what the runtime's component validation calls.
425        ValidateConfig::validate_base_client_config(&shared, &builder, &cfg).unwrap();
426        ValidateConfig::validate_final_config(&shared, &components, &cfg).unwrap();
427
428        // Each hook ran exactly once per path (2 paths), so 2 invocations each. A
429        // higher count would signal recursion; a lower count divergence between the
430        // two forwarding paths.
431        assert_eq!(2, selector.base.load(Ordering::SeqCst));
432        assert_eq!(2, selector.final_.load(Ordering::SeqCst));
433    }
434
435    #[test]
436    fn external_decorator_reaches_selector_through_shared_client() {
437        let selector = Arc::new(CountingSelector::default());
438        let decorator = DecoratingClient {
439            inner: SharedHttpClient {
440                selector: selector.clone(),
441            },
442        };
443        let cfg = ConfigBag::base();
444        let builder = RuntimeComponentsBuilder::for_tests();
445        let components = RuntimeComponentsBuilder::for_tests().build().unwrap();
446
447        decorator
448            .validate_base_client_config(&builder, &cfg)
449            .unwrap();
450        decorator.validate_final_config(&components, &cfg).unwrap();
451
452        assert_eq!(1, selector.base.load(Ordering::SeqCst));
453        assert_eq!(1, selector.final_.load(Ordering::SeqCst));
454    }
455}