hyperi_rustlib/http_client/
mod.rs1pub mod config;
31
32pub use config::HttpClientConfig;
33
34use reqwest::Response;
35use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
36use reqwest_retry::RetryTransientMiddleware;
37use reqwest_retry::policies::ExponentialBackoff;
38
39#[derive(Debug, thiserror::Error)]
41pub enum HttpClientError {
42 #[error("failed to build HTTP client: {0}")]
44 BuildError(#[from] reqwest::Error),
45}
46
47pub struct HttpClient {
49 inner: ClientWithMiddleware,
50 config: HttpClientConfig,
51}
52
53impl HttpClient {
54 pub fn new(config: HttpClientConfig) -> Result<Self, HttpClientError> {
61 let retry_policy = ExponentialBackoff::builder()
62 .retry_bounds(
63 std::time::Duration::from_millis(config.min_retry_interval_ms),
64 std::time::Duration::from_millis(config.max_retry_interval_ms),
65 )
66 .build_with_max_retries(config.max_retries);
67
68 let mut builder = reqwest::Client::builder()
69 .timeout(std::time::Duration::from_secs(config.timeout_secs))
70 .connect_timeout(std::time::Duration::from_secs(config.connect_timeout_secs));
71
72 if let Some(ref ua) = config.user_agent {
73 builder = builder.user_agent(ua.clone());
74 }
75
76 let reqwest_client = builder.build()?;
77
78 let client = ClientBuilder::new(reqwest_client)
79 .with(RetryTransientMiddleware::new_with_policy(retry_policy))
80 .build();
81
82 Ok(Self {
83 inner: client,
84 config,
85 })
86 }
87
88 pub fn from_cascade() -> Result<Self, HttpClientError> {
95 Self::new(HttpClientConfig::from_cascade())
96 }
97
98 pub async fn get(&self, url: &str) -> Result<Response, reqwest_middleware::Error> {
100 #[cfg(feature = "metrics")]
101 let start = std::time::Instant::now();
102
103 let result = self.inner.get(url).send().await;
104
105 #[cfg(feature = "metrics")]
106 {
107 let status = if result.is_ok() { "success" } else { "error" };
108 metrics::counter!("dfe_http_client_requests_total", "method" => "GET", "status" => status).increment(1);
109 metrics::histogram!("dfe_http_client_duration_seconds", "method" => "GET")
110 .record(start.elapsed().as_secs_f64());
111 }
112
113 result
114 }
115
116 pub async fn post_json<T: serde::Serialize + ?Sized>(
126 &self,
127 url: &str,
128 body: &T,
129 ) -> Result<Response, reqwest_middleware::Error> {
130 #[cfg(feature = "metrics")]
131 let start = std::time::Instant::now();
132
133 let body_bytes = serde_json::to_vec(body).map_err(|e| {
134 reqwest_middleware::Error::Middleware(anyhow::anyhow!(
135 "POST {url}: JSON serialise failed: {e}"
136 ))
137 })?;
138
139 let result = self
140 .inner
141 .post(url)
142 .header("content-type", "application/json")
143 .body(body_bytes)
144 .send()
145 .await;
146
147 #[cfg(feature = "metrics")]
148 {
149 let status = if result.is_ok() { "success" } else { "error" };
150 metrics::counter!("dfe_http_client_requests_total", "method" => "POST", "status" => status).increment(1);
151 metrics::histogram!("dfe_http_client_duration_seconds", "method" => "POST")
152 .record(start.elapsed().as_secs_f64());
153 }
154
155 result
156 }
157
158 pub async fn put_json<T: serde::Serialize + ?Sized>(
164 &self,
165 url: &str,
166 body: &T,
167 ) -> Result<Response, reqwest_middleware::Error> {
168 #[cfg(feature = "metrics")]
169 let start = std::time::Instant::now();
170
171 let body_bytes = serde_json::to_vec(body).map_err(|e| {
172 reqwest_middleware::Error::Middleware(anyhow::anyhow!(
173 "PUT {url}: JSON serialise failed: {e}"
174 ))
175 })?;
176
177 let result = self
178 .inner
179 .put(url)
180 .header("content-type", "application/json")
181 .body(body_bytes)
182 .send()
183 .await;
184
185 #[cfg(feature = "metrics")]
186 {
187 let status = if result.is_ok() { "success" } else { "error" };
188 metrics::counter!("dfe_http_client_requests_total", "method" => "PUT", "status" => status).increment(1);
189 metrics::histogram!("dfe_http_client_duration_seconds", "method" => "PUT")
190 .record(start.elapsed().as_secs_f64());
191 }
192
193 result
194 }
195
196 pub async fn delete(&self, url: &str) -> Result<Response, reqwest_middleware::Error> {
198 #[cfg(feature = "metrics")]
199 let start = std::time::Instant::now();
200
201 let result = self.inner.delete(url).send().await;
202
203 #[cfg(feature = "metrics")]
204 {
205 let status = if result.is_ok() { "success" } else { "error" };
206 metrics::counter!("dfe_http_client_requests_total", "method" => "DELETE", "status" => status).increment(1);
207 metrics::histogram!("dfe_http_client_duration_seconds", "method" => "DELETE")
208 .record(start.elapsed().as_secs_f64());
209 }
210
211 result
212 }
213
214 #[must_use]
216 pub fn client(&self) -> &ClientWithMiddleware {
217 &self.inner
218 }
219
220 #[must_use]
222 pub fn config(&self) -> &HttpClientConfig {
223 &self.config
224 }
225}