Skip to main content

dnslib/vendors/
http.rs

1//! Shared HTTP scaffolding for vendor API clients.
2//!
3//! Holds the `reqwest::Client`, base URL, and bearer token; provides the
4//! per-request tracing/auth/error-mapping boilerplate. Vendor clients embed an
5//! [`HttpClient`], build their own request bodies/queries, then call
6//! [`HttpClient::send`] to dispatch and apply the standard instrumentation.
7//! Envelope parsing stays in the vendor module since each API's response shape
8//! differs.
9
10use std::time::Duration;
11
12use reqwest::{Client, RequestBuilder, Response};
13use tracing::Instrument;
14
15use crate::core::error::{Error, Result};
16use crate::core::secret::ApiToken;
17
18#[derive(Clone, Debug)]
19pub struct HttpClient {
20    http: Client,
21    pub base_url: String,
22    token: ApiToken,
23}
24
25impl HttpClient {
26    /// Build a new client. `no_proxy` matches Technitium's existing behaviour
27    /// — it disables proxy detection so requests to LAN-hosted DNS servers
28    /// don't get routed through an HTTP_PROXY env var.
29    pub fn new(base_url: String, token: ApiToken, no_proxy: bool) -> Result<Self> {
30        let mut builder = Client::builder().timeout(Duration::from_secs(30));
31        if no_proxy {
32            builder = builder.no_proxy();
33        }
34        let http = builder.build().map_err(Error::Network)?;
35        Ok(Self {
36            http,
37            base_url,
38            token,
39        })
40    }
41
42    fn url(&self, path: &str) -> String {
43        format!("{}{}", self.base_url, path)
44    }
45
46    pub fn get(&self, path: &str) -> RequestBuilder {
47        self.http.get(self.url(path))
48    }
49
50    pub fn post(&self, path: &str) -> RequestBuilder {
51        self.http.post(self.url(path))
52    }
53
54    pub fn delete(&self, path: &str) -> RequestBuilder {
55        self.http.delete(self.url(path))
56    }
57
58    /// Attach bearer auth, dispatch the request inside an `http.{method}` tracing
59    /// span, map transport errors to `Error::Network`, and record the response
60    /// status on the span. Vendor envelope parsing is the caller's job.
61    pub async fn send(
62        &self,
63        method: &'static str,
64        path: &str,
65        builder: RequestBuilder,
66    ) -> Result<Response> {
67        let span =
68            tracing::debug_span!("http.request", method, path, http.status = tracing::field::Empty);
69        async {
70            tracing::debug!("sending request");
71            let resp = builder
72                .bearer_auth(self.token.expose_for_auth())
73                .send()
74                .await
75                .map_err(|e| {
76                    tracing::warn!(error = %e, "request failed");
77                    Error::Network(e)
78                })?;
79            tracing::Span::current().record("http.status", resp.status().as_u16());
80            tracing::debug!("received response");
81            Ok(resp)
82        }
83        .instrument(span)
84        .await
85    }
86}