Skip to main content

hey_sdk/http/
reqwest.rs

1use std::time::Duration;
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use futures_util::stream;
6use reqwest::redirect::Policy;
7
8use crate::client::DEFAULT_TIMEOUT;
9use crate::error::Error;
10use crate::http::{Body, HttpClient, Request, Response};
11
12/// The [`HttpClient`] the SDK ships, over [`reqwest`] with rustls and HTTP/2. This is what a
13/// client gets when nothing else is supplied.
14///
15/// It never follows a redirect, whatever it was built from: the SDK does that itself.
16#[derive(Debug, Clone)]
17pub struct ReqwestClient {
18    http: reqwest::Client,
19}
20
21impl ReqwestClient {
22    /// A client that gives an answer `timeout` to arrive.
23    pub fn with_timeout(timeout: Duration) -> Result<ReqwestClient, Error> {
24        ReqwestClient::from_builder(reqwest::Client::builder().timeout(timeout))
25    }
26
27    /// A client built from settings of the caller's own — a proxy, a root certificate, a set
28    /// of default headers. Whatever redirect policy the builder carries is replaced with
29    /// none, since following one here would hide it from the SDK.
30    pub fn from_builder(builder: reqwest::ClientBuilder) -> Result<ReqwestClient, Error> {
31        let http = builder
32            .redirect(Policy::none())
33            .build()
34            .map_err(|error| Error::usage(format!("HTTP client: {error}")))?;
35        Ok(ReqwestClient { http })
36    }
37}
38
39/// The shipped client at its default timeout.
40///
41/// # Panics
42///
43/// When reqwest cannot build a client at all — a TLS backend that fails to initialize is the
44/// one way. `Default` has no way to say so; a caller that would rather have the error uses
45/// [`ReqwestClient::with_timeout`] or [`ReqwestClient::from_builder`].
46impl Default for ReqwestClient {
47    #[allow(clippy::expect_used)] // `Default` cannot return the error; the fallible constructors can, see above
48    fn default() -> ReqwestClient {
49        ReqwestClient::with_timeout(DEFAULT_TIMEOUT)
50            .expect("reqwest builds a client from its defaults")
51    }
52}
53
54#[async_trait]
55impl HttpClient for ReqwestClient {
56    async fn send(&self, request: Request<Bytes>) -> Result<Response<Body>, Error> {
57        let request = reqwest::Request::try_from(request).map_err(network)?;
58        let answered = self.http.execute(request).await.map_err(network)?;
59
60        let status = answered.status();
61        let version = answered.version();
62        let headers = answered.headers().clone();
63        let content_length = answered.content_length();
64
65        let mut response = Response::new(Body::from_stream(chunks(answered), content_length));
66        *response.status_mut() = status;
67        *response.version_mut() = version;
68        *response.headers_mut() = headers;
69        Ok(response)
70    }
71}
72
73/// The body a chunk at a time: reqwest hands it out with `chunk()`, so the stream is that
74/// call repeated until it answers `None`.
75fn chunks(response: reqwest::Response) -> impl stream::Stream<Item = Result<Bytes, Error>> + Send {
76    stream::try_unfold(response, |mut response| async move {
77        match response.chunk().await {
78            Ok(Some(chunk)) => Ok(Some((chunk, response))),
79            Ok(None) => Ok(None),
80            Err(error) => Err(network(error)),
81        }
82    })
83}
84
85/// A transport failure as the SDK's network error, less the URL reqwest writes into its
86/// own message: the failure ends up in logs and hints, and the URL carries the query.
87fn network(error: reqwest::Error) -> Error {
88    Error::network(error.without_url())
89}