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