shared_framework/utils/http_client.rs
1//! Outbound JSON HTTP client.
2//!
3//! [`HttpClient`] wraps `reqwest` with Rustls TLS for JSON GET/POST calls.
4//! Responses with non-success status codes surface as errors.
5//!
6//! ```ignore
7//! let client = HttpClient::new();
8//! let dto: MyDto = client.get("https://example.com/api").await?;
9//! ```
10
11use serde::{de::DeserializeOwned, Serialize};
12
13/// Thin `reqwest`-backed client that sends and receives JSON.
14#[derive(Clone)]
15pub struct HttpClient {
16 inner: reqwest::Client,
17}
18
19impl HttpClient {
20 /// Creates a client with Rustls TLS, falling back to a default client on builder failure.
21 pub fn new() -> Self {
22 Self { inner: reqwest::Client::builder().use_rustls_tls().build().unwrap_or_else(|_| reqwest::Client::new()) }
23 }
24
25 /// Sends a GET request and decodes the JSON response as `T`.
26 /// Returns an error on transport failure, non-success status, or invalid JSON.
27 pub async fn get<T: DeserializeOwned>(&self, url: &str) -> anyhow::Result<T> {
28 let resp = self.inner.get(url).send().await?.error_for_status()?;
29 Ok(resp.json::<T>().await?)
30 }
31
32 /// Sends a POST request with a JSON body and decodes the JSON response as `T`.
33 /// Returns an error on transport failure, non-success status, or invalid JSON.
34 pub async fn post<T: DeserializeOwned, B: Serialize>(&self, url: &str, body: &B) -> anyhow::Result<T> {
35 let resp = self.inner.post(url).json(body).send().await?.error_for_status()?;
36 Ok(resp.json::<T>().await?)
37 }
38
39 /// Sends a POST request with a JSON body and ignores the response body.
40 /// Returns an error on transport failure or non-success status.
41 pub async fn post_empty<B: Serialize>(&self, url: &str, body: &B) -> anyhow::Result<()> {
42 self.inner.post(url).json(body).send().await?.error_for_status()?;
43 Ok(())
44 }
45}
46
47impl Default for HttpClient {
48 fn default() -> Self { Self::new() }
49}