ddns_a/webhook/client.rs
1//! Production HTTP client implementation using reqwest.
2
3use super::{HttpClient, HttpError, HttpRequest, HttpResponse};
4
5/// Production HTTP client using reqwest.
6///
7/// This is a thin wrapper around `reqwest::Client` that implements
8/// the [`HttpClient`] trait. It inherits reqwest's default configuration
9/// including connection pooling and reasonable timeouts.
10///
11/// # Example
12///
13/// ```no_run
14/// use ddns_a::webhook::{ReqwestClient, HttpClient, HttpRequest};
15/// use url::Url;
16///
17/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18/// let client = ReqwestClient::new();
19/// let url = Url::parse("https://api.example.com/webhook")?;
20/// let request = HttpRequest::post(url).with_body(b"hello".to_vec());
21/// let response = client.request(request).await?;
22/// println!("Status: {}", response.status);
23/// # Ok(())
24/// # }
25/// ```
26#[derive(Debug, Clone)]
27pub struct ReqwestClient {
28 inner: reqwest::Client,
29}
30
31impl ReqwestClient {
32 /// Creates a new HTTP client with default configuration.
33 #[must_use]
34 pub fn new() -> Self {
35 Self {
36 inner: reqwest::Client::new(),
37 }
38 }
39
40 /// Creates an HTTP client from an existing reqwest client.
41 ///
42 /// Useful when you need custom configuration (timeouts, TLS, etc.).
43 #[must_use]
44 pub const fn from_client(client: reqwest::Client) -> Self {
45 Self { inner: client }
46 }
47}
48
49impl Default for ReqwestClient {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl HttpClient for ReqwestClient {
56 async fn request(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> {
57 // Build the reqwest request
58 let mut builder = self.inner.request(req.method, req.url.as_str());
59
60 // Add headers
61 for (name, value) in &req.headers {
62 builder = builder.header(name, value);
63 }
64
65 // Add body if present
66 if let Some(body) = req.body {
67 builder = builder.body(body);
68 }
69
70 // Send the request
71 let response = builder.send().await.map_err(|e| {
72 if e.is_timeout() {
73 HttpError::Timeout
74 } else if e.is_builder() {
75 HttpError::InvalidUrl(e.to_string())
76 } else {
77 HttpError::Connection(Box::new(e))
78 }
79 })?;
80
81 // Extract response parts
82 let status = response.status();
83 let headers = response.headers().clone();
84 let body = response
85 .bytes()
86 .await
87 .map_err(|e| HttpError::Connection(Box::new(e)))?
88 .to_vec();
89
90 Ok(HttpResponse::new(status, headers, body))
91 }
92}