typesafe_sdk/transport/
hyper.rs1use std::{
15 error::Error as StdError,
16 fmt,
17 future::Future,
18 io,
19 pin::Pin,
20 sync::Arc,
21 task::{Context, Poll},
22 time::Duration,
23};
24
25use ::hyper::body::Incoming;
26use bytes::Bytes;
27use http::{Request, Response};
28use http_body::{Frame, SizeHint};
29use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
30use hyper_util::{
31 client::legacy::{self, connect::HttpConnector},
32 rt::{TokioExecutor, TokioTimer},
33};
34use rustls::{ClientConfig, pki_types::CertificateDer};
35use rustls_platform_verifier::{BuilderVerifierExt as _, Verifier};
36use tower_service::Service;
37
38use super::{Body, BoxError};
39use crate::{error::Error, text};
40
41const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
43
44const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52#[non_exhaustive]
53pub enum HttpVersion {
54 Http2Only,
60 Auto,
68}
69
70pub(crate) struct TransportSettings {
72 pub(crate) version: HttpVersion,
73 pub(crate) extra_roots: Vec<Vec<u8>>,
75 pub(crate) connect_timeout: Option<Duration>,
76}
77
78#[derive(Clone)]
83pub struct HyperTransport {
84 client: legacy::Client<HttpsConnector<HttpConnector>, Body>,
85 version: HttpVersion,
86 extra_roots: usize,
87 connect_timeout: Option<Duration>,
88}
89
90impl HyperTransport {
91 pub(crate) fn new(settings: TransportSettings) -> Result<Self, Error> {
102 let TransportSettings { version, extra_roots, connect_timeout } = settings;
103 let root_count = extra_roots.len();
104 let tls = tls_config(extra_roots.into_iter().map(CertificateDer::from).collect())?;
105
106 let mut http = HttpConnector::new();
107 http.enforce_http(false);
110 http.set_nodelay(true);
111 http.set_connect_timeout(connect_timeout);
112
113 let https = HttpsConnectorBuilder::new().with_tls_config(tls).https_or_http();
117 let connector = match version {
118 HttpVersion::Http2Only => https.enable_http2().wrap_connector(http),
119 HttpVersion::Auto => https.enable_http1().enable_http2().wrap_connector(http),
120 };
121
122 let mut builder = legacy::Client::builder(TokioExecutor::new());
123 builder
125 .timer(TokioTimer::new())
126 .pool_timer(TokioTimer::new())
127 .pool_idle_timeout(POOL_IDLE_TIMEOUT)
128 .http2_keep_alive_interval(KEEP_ALIVE_INTERVAL)
129 .http2_keep_alive_while_idle(true)
130 .http2_only(version == HttpVersion::Http2Only);
131
132 Ok(Self {
133 client: builder.build(connector),
134 version,
135 extra_roots: root_count,
136 connect_timeout,
137 })
138 }
139}
140
141impl fmt::Debug for HyperTransport {
142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144 formatter
145 .debug_struct("HyperTransport")
146 .field("http_version", &self.version)
147 .field("extra_roots", &self.extra_roots)
148 .field("connect_timeout", &self.connect_timeout)
149 .finish()
150 }
151}
152
153impl Service<Request<Body>> for HyperTransport {
154 type Response = Response<ResponseBody>;
155 type Error = BoxError;
156 type Future = HyperResponseFuture;
157
158 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), BoxError>> {
160 Poll::Ready(Ok(()))
161 }
162
163 fn call(&mut self, request: Request<Body>) -> HyperResponseFuture {
164 HyperResponseFuture {
165 inner: self.client.request(request),
166 connect_timeout: self.connect_timeout,
167 }
168 }
169}
170
171#[must_use = "futures do nothing unless polled"]
173pub struct HyperResponseFuture {
174 inner: legacy::ResponseFuture,
175 connect_timeout: Option<Duration>,
176}
177
178impl fmt::Debug for HyperResponseFuture {
179 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
180 formatter.debug_struct("HyperResponseFuture").finish_non_exhaustive()
181 }
182}
183
184impl Future for HyperResponseFuture {
185 type Output = Result<Response<ResponseBody>, BoxError>;
186
187 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
188 let this = self.get_mut();
191 match Pin::new(&mut this.inner).poll(cx) {
192 Poll::Pending => Poll::Pending,
193 Poll::Ready(Ok(response)) => Poll::Ready(Ok(response.map(ResponseBody))),
194 Poll::Ready(Err(error)) => Poll::Ready(Err(failure(error, this.connect_timeout))),
195 }
196 }
197}
198
199pub struct ResponseBody(Incoming);
210
211impl fmt::Debug for ResponseBody {
212 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213 formatter.debug_struct("ResponseBody").finish_non_exhaustive()
214 }
215}
216
217impl http_body::Body for ResponseBody {
218 type Data = Bytes;
219 type Error = BoxError;
220
221 fn poll_frame(
222 self: Pin<&mut Self>,
223 cx: &mut Context<'_>,
224 ) -> Poll<Option<Result<Frame<Bytes>, BoxError>>> {
225 Pin::new(&mut self.get_mut().0).poll_frame(cx).map_err(Into::into)
229 }
230
231 fn is_end_stream(&self) -> bool {
232 self.0.is_end_stream()
233 }
234
235 fn size_hint(&self) -> SizeHint {
236 self.0.size_hint()
237 }
238}
239
240fn failure(error: legacy::Error, connect_timeout: Option<Duration>) -> BoxError {
243 match connect_timeout {
244 Some(timeout) if error.is_connect() && timed_out(&error) => {
245 Box::new(Error::timeout(timeout))
246 }
247 _ => Box::new(error),
248 }
249}
250
251fn timed_out(error: &(dyn StdError + 'static)) -> bool {
253 let mut link = Some(error);
254 while let Some(current) = link {
255 if current
256 .downcast_ref::<io::Error>()
257 .is_some_and(|io| io.kind() == io::ErrorKind::TimedOut)
258 {
259 return true;
260 }
261 link = current.source();
262 }
263 false
264}
265
266fn tls_config(extra_roots: Vec<CertificateDer<'static>>) -> Result<ClientConfig, Error> {
274 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
275 let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
276 .with_safe_default_protocol_versions()
277 .map_err(verifier_error)?;
278 let config = if extra_roots.is_empty() {
279 builder.with_platform_verifier().map_err(verifier_error)?.with_no_client_auth()
280 } else {
281 let verifier =
282 Verifier::new_with_extra_roots(extra_roots, provider).map_err(verifier_error)?;
283 builder
284 .dangerous()
285 .with_custom_certificate_verifier(Arc::new(verifier))
286 .with_no_client_auth()
287 };
288 Ok(config)
289}
290
291fn verifier_error(error: rustls::Error) -> Error {
297 Error::config(format!(
298 "The TLS certificate verifier could not be built: {}.",
299 text::bounded(&error, text::MAX_MESSAGE_CHARS)
300 ))
301}
302
303#[cfg(test)]
304#[path = "hyper_tests.rs"]
305mod tests;