Skip to main content

dns_update/
http.rs

1/*
2 * Copyright Stalwart Labs LLC See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use crate::Error;
13use reqwest::{
14    Client, Method,
15    header::{CONTENT_TYPE, HeaderMap, HeaderValue},
16    redirect::Policy,
17};
18use serde::{Serialize, de::DeserializeOwned};
19use std::time::Duration;
20
21#[derive(Debug, Clone)]
22pub struct HttpClientBuilder {
23    timeout: Duration,
24    headers: HeaderMap<HeaderValue>,
25}
26
27#[derive(Debug, Clone)]
28pub struct HttpClient {
29    headers: HeaderMap<HeaderValue>,
30    client: Client,
31}
32
33#[derive(Debug, Clone)]
34pub struct HttpRequest {
35    method: Method,
36    url: String,
37    headers: HeaderMap<HeaderValue>,
38    body: Option<String>,
39    client: Client,
40}
41
42impl Default for HttpClientBuilder {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl HttpClientBuilder {
49    pub fn new() -> Self {
50        let mut headers = HeaderMap::new();
51        headers.append(CONTENT_TYPE, HeaderValue::from_static("application/json"));
52
53        Self {
54            timeout: Duration::from_secs(30),
55            headers,
56        }
57    }
58
59    pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
60        if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
61            self.headers.append(name, value);
62        }
63        self
64    }
65
66    pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
67        if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
68            self.headers.insert(name, value);
69        }
70        self
71    }
72
73    pub fn without_header(mut self, name: &'static str) -> Self {
74        self.headers.remove(name);
75        self
76    }
77
78    pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
79        if let Some(timeout) = timeout {
80            self.timeout = timeout;
81        }
82        self
83    }
84
85    pub fn build(self) -> HttpClient {
86        HttpClient {
87            headers: self.headers,
88            client: build_client(Some(self.timeout)),
89        }
90    }
91}
92
93impl HttpClient {
94    pub fn request(&self, method: Method, url: impl Into<String>) -> HttpRequest {
95        HttpRequest {
96            method,
97            url: url.into(),
98            headers: self.headers.clone(),
99            body: None,
100            client: self.client.clone(),
101        }
102    }
103
104    pub fn get(&self, url: impl Into<String>) -> HttpRequest {
105        self.request(Method::GET, url)
106    }
107
108    pub fn post(&self, url: impl Into<String>) -> HttpRequest {
109        self.request(Method::POST, url)
110    }
111
112    pub fn put(&self, url: impl Into<String>) -> HttpRequest {
113        self.request(Method::PUT, url)
114    }
115
116    pub fn delete(&self, url: impl Into<String>) -> HttpRequest {
117        self.request(Method::DELETE, url)
118    }
119
120    pub fn patch(&self, url: impl Into<String>) -> HttpRequest {
121        self.request(Method::PATCH, url)
122    }
123}
124
125impl HttpRequest {
126    pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
127        if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
128            self.headers.append(name, value);
129        }
130        self
131    }
132
133    pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
134        if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
135            self.headers.insert(name, value);
136        }
137        self
138    }
139
140    pub fn with_body<B: Serialize>(mut self, body: B) -> crate::Result<Self> {
141        match serde_json::to_string(&body) {
142            Ok(body) => {
143                self.body = Some(body);
144                Ok(self)
145            }
146            Err(err) => Err(Error::Serialize(format!(
147                "Failed to serialize request: {err}"
148            ))),
149        }
150    }
151
152    pub fn with_raw_body(mut self, body: String) -> Self {
153        self.body = Some(body);
154        self
155    }
156
157    pub async fn send<T>(self) -> crate::Result<T>
158    where
159        T: DeserializeOwned,
160    {
161        let response = self.send_raw().await?;
162        serde_json::from_slice::<T>(response.as_bytes()).map_err(|err| {
163            Error::Serialize(format!(
164                "Failed to deserialize response: {err} (body: {})",
165                body_snippet(&response)
166            ))
167        })
168    }
169
170    pub async fn send_raw(self) -> crate::Result<String> {
171        self.send_raw_with_headers().await.map(|(body, _)| body)
172    }
173
174    pub async fn send_raw_with_headers(self) -> crate::Result<(String, HeaderMap<HeaderValue>)> {
175        let mut request = self
176            .client
177            .request(self.method, &self.url)
178            .headers(self.headers);
179
180        if let Some(body) = self.body {
181            request = request.body(body);
182        }
183
184        let response = request
185            .send()
186            .await
187            .map_err(|err| Error::Api(format!("Failed to send request to {}: {err}", self.url)))?;
188
189        let code = response.status().as_u16();
190        let headers = response.headers().clone();
191        match code {
192            204 => Ok((String::new(), headers)),
193            200..=299 => response
194                .text()
195                .await
196                .map(|body| (body, headers))
197                .map_err(|err| {
198                    Error::Api(format!("Failed to read response from {}: {err}", self.url))
199                }),
200            401 => Err(Error::Unauthorized),
201            404 => Err(Error::NotFound),
202            _ => {
203                let text = response.text().await.unwrap_or_default();
204                Err(Error::Api(http_status_message(code, &text)))
205            }
206        }
207    }
208
209    pub async fn send_with_retry<T>(self, max_retries: u32) -> crate::Result<T>
210    where
211        T: DeserializeOwned,
212    {
213        let mut attempts = 0;
214        let body = self.body;
215        loop {
216            let mut request = self
217                .client
218                .request(self.method.clone(), &self.url)
219                .headers(self.headers.clone());
220
221            if let Some(body) = body.as_ref() {
222                request = request.body(body.clone());
223            }
224
225            let response = request.send().await.map_err(|err| {
226                Error::Api(format!("Failed to send request to {}: {err}", self.url))
227            })?;
228
229            let code = response.status().as_u16();
230            return match code {
231                204 => serde_json::from_str("{}").map_err(|err| {
232                    Error::Serialize(format!("Failed to create empty response: {err}"))
233                }),
234                200..=299 => {
235                    let text = response.text().await.map_err(|err| {
236                        Error::Api(format!("Failed to read response from {}: {err}", self.url))
237                    })?;
238                    let parse_target = if text.trim().is_empty() { "{}" } else { &text };
239                    serde_json::from_str(parse_target).map_err(|err| {
240                        Error::Serialize(format!(
241                            "Failed to deserialize response from {}: {err} (body: {})",
242                            self.url,
243                            body_snippet(&text)
244                        ))
245                    })
246                }
247                429 | 503 if attempts < max_retries => {
248                    let delay = retry_after(response.headers())
249                        .unwrap_or_else(|| Duration::from_secs(1u64 << attempts.min(6)));
250                    tokio::time::sleep(delay.min(MAX_RETRY_DELAY)).await;
251                    attempts += 1;
252                    continue;
253                }
254                401 => Err(Error::Unauthorized),
255                404 => Err(Error::NotFound),
256                _ => {
257                    let text = response.text().await.unwrap_or_default();
258                    Err(Error::Api(http_status_message(code, &text)))
259                }
260            };
261        }
262    }
263}
264
265pub(crate) fn try_build_client(timeout: Option<Duration>) -> crate::Result<Client> {
266    let mut builder = Client::builder().redirect(Policy::none());
267    if let Some(timeout) = timeout {
268        builder = builder.timeout(timeout);
269    }
270    builder
271        .build()
272        .map_err(|err| Error::Client(format!("Failed to build HTTP client: {err}")))
273}
274
275#[inline(always)]
276pub(crate) fn build_client(timeout: Option<Duration>) -> Client {
277    try_build_client(timeout).unwrap_or_default()
278}
279
280const MAX_RETRY_DELAY: Duration = Duration::from_secs(60);
281pub(crate) const MAX_BODY_SNIPPET: usize = 512;
282
283pub(crate) fn body_snippet(body: &str) -> &str {
284    let trimmed = body.trim();
285    trimmed
286        .get(..trimmed.floor_char_boundary(MAX_BODY_SNIPPET))
287        .unwrap_or(trimmed)
288}
289
290fn retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
291    headers
292        .get("retry-after")?
293        .to_str()
294        .ok()?
295        .parse::<u64>()
296        .ok()
297        .map(Duration::from_secs)
298}
299
300fn http_status_message(code: u16, body: &str) -> String {
301    let trimmed = body_snippet(body);
302    if code == 400 {
303        if trimmed.is_empty() {
304            "BadRequest".to_string()
305        } else {
306            format!("BadRequest {trimmed}")
307        }
308    } else if trimmed.is_empty() {
309        format!("HTTP {code}")
310    } else {
311        format!("HTTP {code}: {trimmed}")
312    }
313}