secrets_core/
http.rs

1use anyhow::{Context, Result};
2use reqwest::{
3    Client, Method, Response,
4    header::{HeaderMap, HeaderName, HeaderValue},
5};
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8use std::time::Duration;
9use url::Url;
10
11use crate::rt;
12
13/// Builder for [`Http`] clients that wraps `reqwest::ClientBuilder` options we
14/// commonly surface to providers.
15#[derive(Clone, Debug, Default)]
16pub struct HttpBuilder {
17    timeout: Option<Duration>,
18    danger_accept_invalid_certs: bool,
19    danger_accept_invalid_hostnames: bool,
20    proxy: Option<Url>,
21    default_headers: Option<HeaderMap>,
22}
23
24impl HttpBuilder {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn timeout(mut self, duration: Duration) -> Self {
30        self.timeout = Some(duration);
31        self
32    }
33
34    pub fn danger_accept_invalid_certs(mut self, on: bool) -> Self {
35        self.danger_accept_invalid_certs = on;
36        self
37    }
38
39    pub fn danger_accept_invalid_hostnames(mut self, on: bool) -> Self {
40        self.danger_accept_invalid_hostnames = on;
41        self
42    }
43
44    /// Convenience toggle that opts into both certificate + hostname bypass.
45    pub fn insecure_tls(mut self, on: bool) -> Self {
46        self.danger_accept_invalid_certs = on;
47        self.danger_accept_invalid_hostnames = on;
48        self
49    }
50
51    pub fn proxy(mut self, url: Option<Url>) -> Self {
52        self.proxy = url;
53        self
54    }
55
56    pub fn default_headers(mut self, headers: HeaderMap) -> Self {
57        self.default_headers = Some(headers);
58        self
59    }
60
61    pub fn build(self) -> Result<Http> {
62        let mut builder = Client::builder().use_rustls_tls();
63        if let Some(timeout) = self.timeout {
64            builder = builder.timeout(timeout);
65        }
66        builder = builder
67            .danger_accept_invalid_certs(self.danger_accept_invalid_certs)
68            .danger_accept_invalid_hostnames(self.danger_accept_invalid_hostnames);
69        if let Some(proxy_url) = self.proxy {
70            let proxy = reqwest::Proxy::all(proxy_url.as_str())
71                .with_context(|| format!("invalid proxy url: {proxy_url}"))?;
72            builder = builder.proxy(proxy);
73        }
74        if let Some(headers) = self.default_headers {
75            builder = builder.default_headers(headers);
76        }
77        Http::from_builder(builder)
78    }
79}
80
81/// Thin synchronous facade over the async reqwest client.
82#[derive(Clone)]
83pub struct Http {
84    client: Client,
85}
86
87impl Http {
88    /// Builds a client with the provided timeout and rustls TLS stack.
89    pub fn new(timeout: Duration) -> Result<Self> {
90        Self::builder().timeout(timeout).build()
91    }
92
93    /// Builds a client from a custom reqwest builder.
94    pub fn from_builder(builder: reqwest::ClientBuilder) -> Result<Self> {
95        Ok(Self {
96            client: builder.build().context("failed to build HTTP client")?,
97        })
98    }
99
100    /// Starts building an HTTP client with custom provider options.
101    pub fn builder() -> HttpBuilder {
102        HttpBuilder::new()
103    }
104
105    /// Creates a new request with the provided method and URL.
106    pub fn request(&self, method: Method, url: impl AsRef<str>) -> HttpRequest {
107        let url = url.as_ref();
108        let builder = self.client.request(method, url);
109        HttpRequest { builder }
110    }
111
112    pub fn get(&self, url: impl AsRef<str>) -> HttpRequest {
113        self.request(Method::GET, url)
114    }
115
116    pub fn post(&self, url: impl AsRef<str>) -> HttpRequest {
117        self.request(Method::POST, url)
118    }
119
120    pub fn put(&self, url: impl AsRef<str>) -> HttpRequest {
121        self.request(Method::PUT, url)
122    }
123
124    pub fn delete(&self, url: impl AsRef<str>) -> HttpRequest {
125        self.request(Method::DELETE, url)
126    }
127
128    pub fn client(&self) -> &Client {
129        &self.client
130    }
131}
132
133pub struct HttpRequest {
134    builder: reqwest::RequestBuilder,
135}
136
137impl HttpRequest {
138    pub fn bearer_auth(mut self, token: impl AsRef<str>) -> Self {
139        self.builder = self.builder.bearer_auth(token.as_ref());
140        self
141    }
142
143    pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
144        self.builder = self.builder.header(name, value);
145        self
146    }
147
148    pub fn headers(mut self, headers: HeaderMap) -> Self {
149        self.builder = self.builder.headers(headers);
150        self
151    }
152
153    pub fn json(mut self, value: &impl Serialize) -> Self {
154        self.builder = self.builder.json(value);
155        self
156    }
157
158    pub fn body(mut self, value: impl Into<reqwest::Body>) -> Self {
159        self.builder = self.builder.body(value);
160        self
161    }
162
163    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
164        self.builder = self.builder.query(query);
165        self
166    }
167
168    pub fn form(mut self, value: &impl Serialize) -> Self {
169        self.builder = self.builder.form(value);
170        self
171    }
172
173    pub fn send(self) -> Result<HttpResponse> {
174        rt::sync_await(async {
175            let response = self.builder.send().await?;
176            Ok(HttpResponse { inner: response })
177        })
178    }
179
180    pub fn send_json<T: DeserializeOwned>(self) -> Result<T> {
181        let response = self.send()?;
182        response.json()
183    }
184
185    pub fn send_text(self) -> Result<String> {
186        let response = self.send()?;
187        response.text()
188    }
189}
190
191pub struct HttpResponse {
192    inner: Response,
193}
194
195impl HttpResponse {
196    pub fn status(&self) -> reqwest::StatusCode {
197        self.inner.status()
198    }
199
200    pub fn headers(&self) -> &HeaderMap {
201        self.inner.headers()
202    }
203
204    pub fn into_inner(self) -> Response {
205        self.inner
206    }
207
208    pub fn json<T: DeserializeOwned>(self) -> Result<T> {
209        rt::sync_await(async {
210            self.inner
211                .json::<T>()
212                .await
213                .context("failed to decode JSON")
214        })
215    }
216
217    pub fn text(self) -> Result<String> {
218        rt::sync_await(async {
219            self.inner
220                .text()
221                .await
222                .context("failed to read body as text")
223        })
224    }
225}