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