1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! HTTP clients and connectors
//!
//! # What is a connector?
//!
//! When we talk about connectors, we are referring to the [`HttpConnector`] trait, and implementations of
//! that trait. This trait simply takes a HTTP request, and returns a future with the response for that
//! request.
//!
//! This is slightly different from what a connector is in other libraries such as
//! [`hyper`]. In hyper 0.x, the connector is a [`tower`] `Service` that takes a `Uri` and returns
//! a future with something that implements `AsyncRead + AsyncWrite`.
//!
//! The [`HttpConnector`] is designed to be a layer on top of
//! whole HTTP libraries, such as hyper, which allows Smithy clients to be agnostic to the underlying HTTP
//! transport layer. This also makes it easy to write tests with a fake HTTP connector, and several
//! such test connector implementations are available in [`aws-smithy-runtime`]
//! with the `test-util` feature enabled.
//!
//! # Responsibilities of a connector
//!
//! A connector primarily makes HTTP requests, but is also the place where connect and read timeouts are
//! implemented. The `HyperConnector` in [`aws-smithy-runtime`] is an example where timeouts are implemented
//! as part of the connector.
//!
//! Connectors are also responsible for DNS lookup, TLS, connection reuse, pooling, and eviction.
//! The Smithy clients have no knowledge of such concepts.
//!
//! # The [`HttpClient`] trait
//!
//! Connectors allow us to make requests, but we need a layer on top of connectors so that we can handle
//! varying connector settings. For example, say we configure some default HTTP connect/read timeouts on
//! Client, and then configure some override connect/read timeouts for a specific operation. These timeouts
//! ultimately are part of the connector, so the same connector can't be reused for the two different sets
//! of timeouts. Thus, the [`HttpClient`] implementation is responsible for managing multiple connectors
//! with varying config. Some example configs that can impact which connector is used:
//!
//! - HTTP protocol versions
//! - TLS settings
//! - Timeouts
//!
//! Some of these aren't implemented yet, but they will appear in the [`HttpConnectorSettings`] struct
//! once they are.
//!
//! [`hyper`]: https://crates.io/crates/hyper
//! [`tower`]: https://crates.io/crates/tower
//! [`aws-smithy-runtime`]: https://crates.io/crates/aws-smithy-runtime
use crate::box_error::BoxError;
use crate::client::connector_metadata::ConnectorMetadata;
use crate::client::orchestrator::{HttpRequest, HttpResponse};
use crate::client::result::ConnectorError;
use crate::client::runtime_components::sealed::ValidateConfig;
use crate::client::runtime_components::{RuntimeComponents, RuntimeComponentsBuilder};
use crate::impl_shared_conversions;
use aws_smithy_types::config_bag::ConfigBag;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
new_type_future! {
#[doc = "Future for [`HttpConnector::call`]."]
pub struct HttpConnectorFuture<'static, HttpResponse, ConnectorError>;
}
/// Trait with a `call` function that asynchronously converts a request into a response.
///
/// Ordinarily, a connector would use an underlying HTTP library such as [hyper](https://crates.io/crates/hyper),
/// and any associated HTTPS implementation alongside it to service requests.
///
/// However, it can also be useful to create fake/mock connectors implementing this trait
/// for testing.
pub trait HttpConnector: Send + Sync + fmt::Debug {
/// Asynchronously converts a request into a response.
fn call(&self, request: HttpRequest) -> HttpConnectorFuture;
}
/// A shared [`HttpConnector`] implementation.
#[derive(Clone, Debug)]
pub struct SharedHttpConnector(Arc<dyn HttpConnector>);
impl SharedHttpConnector {
/// Returns a new [`SharedHttpConnector`].
pub fn new(connection: impl HttpConnector + 'static) -> Self {
Self(Arc::new(connection))
}
}
impl HttpConnector for SharedHttpConnector {
fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
(*self.0).call(request)
}
}
impl_shared_conversions!(convert SharedHttpConnector from HttpConnector using SharedHttpConnector::new);
/// Returns a [`SharedHttpClient`] that calls the given `connector` function to select a HTTP connector.
pub fn http_client_fn<F>(connector: F) -> SharedHttpClient
where
F: Fn(&HttpConnectorSettings, &RuntimeComponents) -> SharedHttpConnector
+ Send
+ Sync
+ 'static,
{
struct ConnectorFn<T>(T);
impl<T> fmt::Debug for ConnectorFn<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ConnectorFn")
}
}
impl<T> HttpClient for ConnectorFn<T>
where
T: (Fn(&HttpConnectorSettings, &RuntimeComponents) -> SharedHttpConnector) + Send + Sync,
{
fn http_connector(
&self,
settings: &HttpConnectorSettings,
components: &RuntimeComponents,
) -> SharedHttpConnector {
(self.0)(settings, components)
}
}
SharedHttpClient::new(ConnectorFn(connector))
}
/// HTTP client abstraction.
///
/// A HTTP client implementation must apply connect/read timeout settings,
/// and must maintain a connection pool.
pub trait HttpClient: Send + Sync + fmt::Debug {
/// Returns a HTTP connector based on the requested connector settings.
///
/// The settings include connector timeouts, which should be incorporated
/// into the connector. The `HttpClient` is responsible for caching
/// the connector across requests.
///
/// In the future, the settings may have additional parameters added,
/// such as HTTP version, or TLS certificate paths.
fn http_connector(
&self,
settings: &HttpConnectorSettings,
components: &RuntimeComponents,
) -> SharedHttpConnector;
#[doc = include_str!("../../rustdoc/validate_base_client_config.md")]
fn validate_base_client_config(
&self,
runtime_components: &RuntimeComponentsBuilder,
cfg: &ConfigBag,
) -> Result<(), BoxError> {
let _ = (runtime_components, cfg);
Ok(())
}
#[doc = include_str!("../../rustdoc/validate_final_config.md")]
fn validate_final_config(
&self,
runtime_components: &RuntimeComponents,
cfg: &ConfigBag,
) -> Result<(), BoxError> {
let _ = (runtime_components, cfg);
Ok(())
}
/// Provide metadata about the crate that this HttpClient uses to make connectors.
///
/// If this is implemented and returns metadata, interceptors may inspect it
/// for the purpose of inserting that data into the user agent string when
/// making a request with this client.
fn connector_metadata(&self) -> Option<ConnectorMetadata> {
None
}
}
/// Shared HTTP client for use across multiple clients and requests.
#[derive(Clone, Debug)]
pub struct SharedHttpClient {
selector: Arc<dyn HttpClient>,
}
impl SharedHttpClient {
/// Creates a new `SharedHttpClient`
pub fn new(selector: impl HttpClient + 'static) -> Self {
Self {
selector: Arc::new(selector),
}
}
}
impl HttpClient for SharedHttpClient {
fn http_connector(
&self,
settings: &HttpConnectorSettings,
components: &RuntimeComponents,
) -> SharedHttpConnector {
self.selector.http_connector(settings, components)
}
fn connector_metadata(&self) -> Option<ConnectorMetadata> {
self.selector.connector_metadata()
}
}
impl ValidateConfig for SharedHttpClient {
fn validate_base_client_config(
&self,
runtime_components: &RuntimeComponentsBuilder,
cfg: &ConfigBag,
) -> Result<(), BoxError> {
self.selector
.validate_base_client_config(runtime_components, cfg)
}
fn validate_final_config(
&self,
runtime_components: &RuntimeComponents,
cfg: &ConfigBag,
) -> Result<(), BoxError> {
self.selector.validate_final_config(runtime_components, cfg)
}
}
impl_shared_conversions!(convert SharedHttpClient from HttpClient using SharedHttpClient::new);
/// Builder for [`HttpConnectorSettings`].
#[non_exhaustive]
#[derive(Default, Debug)]
pub struct HttpConnectorSettingsBuilder {
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
}
impl HttpConnectorSettingsBuilder {
/// Creates a new builder.
pub fn new() -> Self {
Default::default()
}
/// Sets the connect timeout that should be used.
///
/// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
pub fn connect_timeout(mut self, connect_timeout: Duration) -> Self {
self.connect_timeout = Some(connect_timeout);
self
}
/// Sets the connect timeout that should be used.
///
/// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
pub fn set_connect_timeout(&mut self, connect_timeout: Option<Duration>) -> &mut Self {
self.connect_timeout = connect_timeout;
self
}
/// Sets the read timeout that should be used.
///
/// The read timeout is the limit on the amount of time it takes to read the first byte of a response
/// from the time the request is initiated.
pub fn read_timeout(mut self, read_timeout: Duration) -> Self {
self.read_timeout = Some(read_timeout);
self
}
/// Sets the read timeout that should be used.
///
/// The read timeout is the limit on the amount of time it takes to read the first byte of a response
/// from the time the request is initiated.
pub fn set_read_timeout(&mut self, read_timeout: Option<Duration>) -> &mut Self {
self.read_timeout = read_timeout;
self
}
/// Builds the [`HttpConnectorSettings`].
pub fn build(self) -> HttpConnectorSettings {
HttpConnectorSettings {
connect_timeout: self.connect_timeout,
read_timeout: self.read_timeout,
}
}
}
/// Settings for HTTP Connectors
#[non_exhaustive]
#[derive(Clone, Default, Debug)]
pub struct HttpConnectorSettings {
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
}
impl HttpConnectorSettings {
/// Returns a builder for `HttpConnectorSettings`.
pub fn builder() -> HttpConnectorSettingsBuilder {
Default::default()
}
/// Returns the connect timeout that should be used.
///
/// The connect timeout is a limit on the amount of time it takes to initiate a socket connection.
pub fn connect_timeout(&self) -> Option<Duration> {
self.connect_timeout
}
/// Returns the read timeout that should be used.
///
/// The read timeout is the limit on the amount of time it takes to read the first byte of a response
/// from the time the request is initiated.
pub fn read_timeout(&self) -> Option<Duration> {
self.read_timeout
}
}