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 connector_metadata(&self) -> Option<ConnectorMetadata> {
205        self.selector.connector_metadata()
206    }
207}
208
209impl ValidateConfig for SharedHttpClient {
210    fn validate_base_client_config(
211        &self,
212        runtime_components: &RuntimeComponentsBuilder,
213        cfg: &ConfigBag,
214    ) -> Result<(), BoxError> {
215        self.selector
216            .validate_base_client_config(runtime_components, cfg)
217    }
218
219    fn validate_final_config(
220        &self,
221        runtime_components: &RuntimeComponents,
222        cfg: &ConfigBag,
223    ) -> Result<(), BoxError> {
224        self.selector.validate_final_config(runtime_components, cfg)
225    }
226}
227
228impl_shared_conversions!(convert SharedHttpClient from HttpClient using SharedHttpClient::new);
229
230/// Builder for [`HttpConnectorSettings`].
231#[non_exhaustive]
232#[derive(Default, Debug)]
233pub struct HttpConnectorSettingsBuilder {
234    connect_timeout: Option<Duration>,
235    read_timeout: Option<Duration>,
236}
237
238impl HttpConnectorSettingsBuilder {
239    /// Creates a new builder.
240    pub fn new() -> Self {
241        Default::default()
242    }
243
244    /// Sets the connect timeout that should be used.
245    ///
246    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
247    pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
248        self.connect_timeout = Some(connect_timeout);
249        self
250    }
251
252    /// Sets the connect timeout that should be used.
253    ///
254    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
255    pub fn set_connect_timeout(&mut self, connect_timeout: Option<Duration>) -> &mut Self {
256        self.connect_timeout = connect_timeout;
257        self
258    }
259
260    /// Sets the read timeout that should be used.
261    ///
262    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
263    /// from the time the request is initiated.
264    pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
265        self.read_timeout = Some(read_timeout);
266        self
267    }
268
269    /// Sets the read timeout that should be used.
270    ///
271    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
272    /// from the time the request is initiated.
273    pub fn set_read_timeout(&mut self, read_timeout: Option<Duration>) -> &mut Self {
274        self.read_timeout = read_timeout;
275        self
276    }
277
278    /// Builds the [`HttpConnectorSettings`].
279    pub fn build(self) -> HttpConnectorSettings {
280        HttpConnectorSettings {
281            connect_timeout: self.connect_timeout,
282            read_timeout: self.read_timeout,
283        }
284    }
285}
286
287/// Settings for HTTP Connectors
288#[non_exhaustive]
289#[derive(Clone, Default, Debug)]
290pub struct HttpConnectorSettings {
291    connect_timeout: Option<Duration>,
292    read_timeout: Option<Duration>,
293}
294
295impl HttpConnectorSettings {
296    /// Returns a builder for `HttpConnectorSettings`.
297    pub fn builder() -> HttpConnectorSettingsBuilder {
298        Default::default()
299    }
300
301    /// Returns the connect timeout that should be used.
302    ///
303    /// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
304    pub fn connect_timeout(&self) -> Option<Duration> {
305        self.connect_timeout
306    }
307
308    /// Returns the read timeout that should be used.
309    ///
310    /// The read timeout is the limit on the amount of time it takes to read the first byte of a response
311    /// from the time the request is initiated.
312    pub fn read_timeout(&self) -> Option<Duration> {
313        self.read_timeout
314    }
315}