Skip to main content

aws_smithy_http_client_reqwest/
lib.rs

1#![warn(missing_docs)]
2//! `reqwest`-backed HTTP client integration for Smithy runtimes.
3//!
4//! This crate provides [`ReqwestHttpClient`], an implementation of
5//! [`aws_smithy_runtime_api::client::http::HttpClient`] backed by
6//! [`reqwest::Client`].
7//!
8//! It is useful when you want to reuse an existing `reqwest` client configuration
9//! such as custom headers, proxies, TLS settings, or connection pools with
10//! Smithy-based clients.
11//! 
12//! This helps remove dependencies such as `rustls` and `aws-lc-rs` that are forced
13//! onto the AWS SDK; you will need to build these yourself via reqwest.
14//!
15//! # Examples
16//!
17//! ```rust
18//! use aws_config::BehaviorVersion;
19//! use aws_smithy_http_client_reqwest::ReqwestHttpClient;
20//!
21//! let reqwest_client = reqwest::Client::builder()
22//!     .user_agent("my-app/1.0")
23//!     .build()?;
24//!
25//! let config = aws_config::defaults(BehaviorVersion::latest())
26//!   .http_client(ReqwestHttpClient::new(reqwest_client))
27//!   .load();
28//! # Ok::<(), reqwest::Error>(())
29//! ```
30//! 
31//! # Limitations
32//! 
33//! 1. Connect timeout is not supported
34
35use std::time::Duration;
36
37use aws_smithy_runtime_api::client::{
38    http::{
39        HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector,
40    },
41    orchestrator::{HttpRequest, HttpResponse},
42    result::ConnectorError,
43    runtime_components::RuntimeComponents,
44};
45use aws_smithy_types::body::SdkBody;
46use http_body_util::BodyExt;
47
48#[derive(Debug)]
49/// A Smithy [`HttpClient`] implementation backed by [`reqwest::Client`].
50///
51/// This type lets Smithy-based SDK clients send requests through a preconfigured
52/// `reqwest` client, allowing you to share connection pools and transport-level
53/// settings across your application.
54///
55/// Use [`ReqwestHttpClient::new`] when you already have a customized
56/// [`reqwest::Client`]. If you just need a default configuration, use
57/// [`ReqwestHttpClient::default`].
58pub struct ReqwestHttpClient {
59    client: reqwest::Client,
60}
61
62impl ReqwestHttpClient {
63    /// Creates a Smithy HTTP client from an existing [`reqwest::Client`].
64    ///
65    /// The provided `reqwest` client is cloned as needed when Smithy creates
66    /// connectors, so it can be shared safely across multiple service clients.
67    ///
68    /// # Examples
69    ///
70    /// ```rust
71    /// use aws_smithy_http_client_reqwest::ReqwestHttpClient;
72    ///
73    /// let reqwest_client = reqwest::Client::new();
74    /// let http_client = ReqwestHttpClient::new(reqwest_client);
75    /// # let _ = http_client;
76    /// ```
77    #[must_use]
78    pub fn new(client: reqwest::Client) -> Self {
79        Self { client }
80    }
81}
82
83impl Default for ReqwestHttpClient {
84    fn default() -> Self {
85        Self::new(reqwest::Client::new())
86    }
87}
88
89impl HttpClient for ReqwestHttpClient {
90    fn http_connector(
91        &self,
92        settings: &HttpConnectorSettings,
93        _components: &RuntimeComponents,
94    ) -> SharedHttpConnector {
95        SharedHttpConnector::new(ReqwestHttpConnector {
96            client: self.client.clone(),
97            settings: settings.clone(),
98        })
99    }
100}
101
102enum CustomConnectorError {
103    ReqwestError(reqwest::Error),
104    HttpError(aws_smithy_runtime_api::http::HttpError),
105}
106
107#[derive(Debug)]
108struct ReqwestHttpConnector {
109    client: reqwest::Client,
110    settings: HttpConnectorSettings,
111}
112
113impl ReqwestHttpConnector {
114    async fn convert_request(
115        req: HttpRequest,
116        timeout: Option<Duration>,
117    ) -> Result<reqwest::Request, CustomConnectorError> {
118        let req = req
119            .try_into_http1x()
120            .map_err(|err| CustomConnectorError::HttpError(err))?;
121        let (parts, body) = req.into_parts();
122
123        let mut req = reqwest::Request::new(
124            parts.method.clone(),
125            parts.uri.to_string().parse().expect("known valid"),
126        );
127
128        *req.headers_mut() = parts.headers;
129        req.body_mut()
130            .replace(reqwest::Body::wrap_stream(body.into_data_stream()));
131
132        if let Some(timeout) = timeout {
133            req.timeout_mut().replace(timeout);
134        }
135
136        Ok(req)
137    }
138
139    async fn convert_response(
140        resp: reqwest::Response,
141    ) -> Result<HttpResponse, CustomConnectorError> {
142        let headers = resp.headers().clone();
143
144        let mut resp = HttpResponse::new(
145            aws_smithy_runtime_api::http::StatusCode::from(resp.status()),
146            SdkBody::from(
147                resp.bytes()
148                    .await
149                    .map_err(|err| CustomConnectorError::ReqwestError(err))?,
150            ),
151        );
152
153        *resp.headers_mut() = aws_smithy_runtime_api::http::Headers::try_from(headers)
154            .map_err(|err| CustomConnectorError::HttpError(err))?;
155
156        Ok(resp)
157    }
158}
159
160impl HttpConnector for ReqwestHttpConnector {
161    fn call(&self, req: HttpRequest) -> HttpConnectorFuture {
162        let client = self.client.clone();
163        let timeout = self.settings.read_timeout();
164        HttpConnectorFuture::new(async move {
165            let req = Self::convert_request(req, timeout)
166                .await
167                .map_err(|err| match err {
168                    CustomConnectorError::HttpError(err) => {
169                        ConnectorError::user(Box::new(err)).never_connected()
170                    }
171                    CustomConnectorError::ReqwestError(err) => {
172                        ConnectorError::other(Box::new(err), None).never_connected()
173                    }
174                })?;
175
176            let resp = client
177                .execute(req)
178                .await
179                .map_err(|err| ConnectorError::other(Box::new(err), None))?;
180
181            let resp = Self::convert_response(resp)
182                .await
183                .map_err(|err| match err {
184                    CustomConnectorError::HttpError(err) => ConnectorError::user(Box::new(err)),
185                    CustomConnectorError::ReqwestError(err) => {
186                        ConnectorError::other(Box::new(err), None)
187                    }
188                })?;
189
190            Ok(resp)
191        })
192    }
193}