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}