Skip to main content

cratefield_runtime_native/ports/
http.rs

1//! `HttpClient` over `reqwest` (rustls, ring provider, webpki roots —
2//! no OpenSSL, no native-tls): the native counterpart of
3//! `worker::Fetch`. Adapters (Resend, Turnstile) run unchanged over
4//! either.
5
6use async_trait::async_trait;
7use bytes::Bytes;
8use cratefield_core::{HttpClient, HttpError};
9
10/// A shared reqwest client. Built once per process (the connection pool
11/// is the `Client`); the port is cheap to `Arc` and clone-free on the
12/// hot path.
13///
14/// No default timeout — the port contract leaves timeouts to the caller
15/// (the adapters bound their own calls through the `Clock` port), the
16/// same trade `worker::Fetch` makes.
17pub struct ReqwestClient(reqwest::Client);
18
19impl Default for ReqwestClient {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl ReqwestClient {
26    /// # Panics
27    ///
28    /// Only if the rustls backend cannot initialize, which a static
29    /// webpki-roots configuration cannot do at runtime — a startup-time
30    /// invariant, the same class as core's `HmacSigner::new` panic.
31    pub fn new() -> Self {
32        Self(
33            reqwest::Client::builder()
34                .build()
35                .expect("reqwest initializes: rustls with webpki roots is static"),
36        )
37    }
38}
39
40#[async_trait]
41impl HttpClient for ReqwestClient {
42    async fn send(
43        &self,
44        request: http::Request<Bytes>,
45    ) -> Result<http::Response<Bytes>, HttpError> {
46        // reqwest re-exports the same `http` v1 types (Method, header
47        // map, status), so the conversion is structural, not a rebuild.
48        let (parts, body) = request.into_parts();
49        let url = parts
50            .uri
51            .to_string()
52            .parse::<reqwest::Url>()
53            .map_err(|err| HttpError::Transport(format!("invalid request uri: {err}")))?;
54        let mut builder = self.0.request(parts.method, url);
55        for (name, value) in &parts.headers {
56            builder = builder.header(name, value);
57        }
58        let response = builder
59            .body(body)
60            .send()
61            .await
62            .map_err(|err| HttpError::Transport(err.to_string()))?;
63
64        let status = response.status();
65        let headers = response.headers().clone();
66        let bytes = response
67            .bytes()
68            .await
69            .map_err(|err| HttpError::Transport(err.to_string()))?;
70        let mut rebuilt = http::Response::builder().status(status);
71        if let Some(target) = rebuilt.headers_mut() {
72            *target = headers;
73        }
74        rebuilt
75            .body(bytes)
76            .map_err(|err| HttpError::Transport(err.to_string()))
77    }
78}