cadmus_core/
http.rs

1//! Reusable HTTP client with pre-configured TLS, timeouts, and user agent.
2//!
3//! This module provides [`Client`] as the recommended base HTTP client for all
4//! network requests in the application. It is pre-configured with:
5//!
6//! - TLS using `webpki-roots` certificates (no system cert store required)
7//! - 30 second request timeout
8//! - User agent identifying the application
9//!
10//! # Example
11//!
12//! ```no_run
13//! use cadmus_core::http::Client;
14//!
15//! fn main() -> Result<(), Box<dyn std::error::Error>> {
16//!     let client = Client::new()?;
17//!     client.get("https://example.com").send()?;
18//!     Ok(())
19//! }
20//! ```
21
22use reqwest::blocking::{Client as ReqwestClient, RequestBuilder};
23use rustls::RootCertStore;
24use std::time::Duration;
25use thiserror::Error;
26
27pub const CLIENT_TIMEOUT_SECS: u64 = 30;
28
29const USER_AGENT: &str = concat!("github.com/OGKevin/cadmus/", env!("GIT_VERSION"));
30
31#[derive(Error, Debug)]
32pub enum HttpError {
33    #[error("Failed to build HTTP client: {0}")]
34    Build(#[from] reqwest::Error),
35}
36
37/// Pre-configured HTTP client for making network requests.
38///
39/// This client should be used as the base for all HTTP requests rather than
40/// constructing raw `reqwest` clients. It comes with:
41/// - TLS using `webpki-roots` certificates (works on Kobo devices without system cert store)
42/// - 30 second request timeout
43/// - User agent header set
44///
45/// # Example
46///
47/// ```no_run
48/// use cadmus_core::http::Client;
49///
50/// fn main() -> Result<(), Box<dyn std::error::Error>> {
51///     let client = Client::new()?;
52///     client.get("https://api.github.com").send()?;
53///     Ok(())
54/// }
55/// ```
56pub struct Client {
57    client: ReqwestClient,
58}
59
60impl Client {
61    pub fn new() -> Result<Self, HttpError> {
62        let root_store = build_root_store();
63
64        let tls_config = rustls::ClientConfig::builder()
65            .with_root_certificates(root_store)
66            .with_no_client_auth();
67
68        let client = ReqwestClient::builder()
69            .use_preconfigured_tls(tls_config)
70            .user_agent(USER_AGENT)
71            .timeout(Duration::from_secs(CLIENT_TIMEOUT_SECS))
72            .build()
73            .map_err(HttpError::Build)?;
74
75        tracing::debug!("HTTP client built successfully");
76        Ok(Self { client })
77    }
78
79    pub fn get(&self, url: &str) -> RequestBuilder {
80        self.client.get(url)
81    }
82
83    pub fn post(&self, url: &str) -> RequestBuilder {
84        self.client.post(url)
85    }
86}
87
88impl Clone for Client {
89    fn clone(&self) -> Self {
90        Self {
91            client: self.client.clone(),
92        }
93    }
94}
95
96fn build_root_store() -> RootCertStore {
97    let mut store = RootCertStore::empty();
98    store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
99    store
100}