char/dns-ez

cloudflare dns management via plaintext zonefiles

git clone https://git.t4t.associates/char/dns-ez

Charlotte Somadd support for wix provider :(c6d896d

main
2.4 KiB74 linesraw
1use std::time::Duration;
2
3use anyhow::{Context, Result, anyhow};
4use serde::Serialize;
5use ureq::SendBody;
6use ureq::http::{Method, Request};
7
8const ATTEMPTS: u32 = 4;
9
10/// A JSON HTTP API that authenticates every request with the same headers.
11pub struct Api {
12    agent: ureq::Agent,
13    base: &'static str,
14    headers: Vec<(&'static str, String)>,
15}
16
17impl Api {
18    pub fn new(base: &'static str, headers: Vec<(&'static str, String)>) -> Self {
19        let config = ureq::Agent::config_builder()
20            .timeout_global(Some(Duration::from_secs(30)))
21            .http_status_as_error(false)
22            .build();
23        Api {
24            agent: ureq::Agent::new_with_config(config),
25            base,
26            headers,
27        }
28    }
29
30    /// Send a request, retrying transport errors, 429s and 5xxs with backoff.
31    /// Returns the final status and body: every API words its errors
32    /// differently, so judging them is left to the caller.
33    pub fn call(
34        &self,
35        method: Method,
36        path: &str,
37        body: Option<&impl Serialize>,
38    ) -> Result<(u16, String)> {
39        let mut last_err = String::new();
40        for attempt in 0..ATTEMPTS {
41            if attempt > 0 {
42                std::thread::sleep(Duration::from_millis(500 << (attempt - 1)));
43            }
44            let mut request = Request::builder()
45                .method(method.clone())
46                .uri(format!("{}{path}", self.base));
47            for (name, value) in &self.headers {
48                request = request.header(*name, value);
49            }
50            let body = match body {
51                Some(b) => SendBody::from_json(b)?,
52                None => SendBody::none(),
53            };
54            let mut response = match self.agent.run(request.body(body)?) {
55                Ok(r) => r,
56                Err(e) => {
57                    last_err = format!("{method} {path}: {e}");
58                    continue;
59                }
60            };
61            let status = response.status().as_u16();
62            if (status == 429 || status >= 500) && attempt + 1 < ATTEMPTS {
63                last_err = format!("{method} {path}: HTTP {status}");
64                continue;
65            }
66            let text = response
67                .body_mut()
68                .read_to_string()
69                .with_context(|| format!("{method} {path}: unreadable response"))?;
70            return Ok((status, text));
71        }
72        Err(anyhow!(last_err))
73    }
74}