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