armature_http_client/
client.rs1use http::Method;
4use reqwest::Request;
5use std::sync::Arc;
6use tracing::debug;
7
8use crate::{
9 CircuitBreaker, HttpClientConfig, HttpClientError, RequestBuilder, Response, Result,
10 RetryStrategy,
11};
12
13#[derive(Clone)]
15pub struct HttpClient {
16 inner: reqwest::Client,
17 config: Arc<HttpClientConfig>,
18 circuit_breaker: Option<Arc<CircuitBreaker>>,
19}
20
21impl HttpClient {
22 pub fn new(config: HttpClientConfig) -> Self {
24 let mut builder = reqwest::Client::builder()
25 .timeout(config.timeout)
26 .connect_timeout(config.connect_timeout)
27 .pool_idle_timeout(config.pool_idle_timeout)
28 .pool_max_idle_per_host(config.pool_max_idle_per_host)
29 .user_agent(&config.user_agent);
30
31 if config.gzip {
32 builder = builder.gzip(true);
33 }
34 if config.brotli {
35 builder = builder.brotli(true);
36 }
37 if config.follow_redirects {
38 builder = builder.redirect(reqwest::redirect::Policy::limited(config.max_redirects));
39 } else {
40 builder = builder.redirect(reqwest::redirect::Policy::none());
41 }
42
43 let inner = builder.build().expect("Failed to build HTTP client");
44
45 let circuit_breaker = config
46 .circuit_breaker
47 .as_ref()
48 .map(|cb_config| Arc::new(CircuitBreaker::new(cb_config.clone())));
49
50 Self {
51 inner,
52 config: Arc::new(config),
53 circuit_breaker,
54 }
55 }
56
57 pub fn default_client() -> Self {
59 Self::new(HttpClientConfig::default())
60 }
61
62 pub fn inner(&self) -> &reqwest::Client {
64 &self.inner
65 }
66
67 pub fn config(&self) -> &HttpClientConfig {
69 &self.config
70 }
71
72 pub fn get(&self, url: impl Into<String>) -> RequestBuilder<'_> {
74 RequestBuilder::new(self, Method::GET, url.into())
75 }
76
77 pub fn post(&self, url: impl Into<String>) -> RequestBuilder<'_> {
79 RequestBuilder::new(self, Method::POST, url.into())
80 }
81
82 pub fn put(&self, url: impl Into<String>) -> RequestBuilder<'_> {
84 RequestBuilder::new(self, Method::PUT, url.into())
85 }
86
87 pub fn patch(&self, url: impl Into<String>) -> RequestBuilder<'_> {
89 RequestBuilder::new(self, Method::PATCH, url.into())
90 }
91
92 pub fn delete(&self, url: impl Into<String>) -> RequestBuilder<'_> {
94 RequestBuilder::new(self, Method::DELETE, url.into())
95 }
96
97 pub fn head(&self, url: impl Into<String>) -> RequestBuilder<'_> {
99 RequestBuilder::new(self, Method::HEAD, url.into())
100 }
101
102 pub fn request(&self, method: Method, url: impl Into<String>) -> RequestBuilder<'_> {
104 RequestBuilder::new(self, method, url.into())
105 }
106
107 pub(crate) async fn execute(&self, request: Request) -> Result<Response> {
109 if let Some(cb) = &self.circuit_breaker {
111 if !cb.is_allowed() {
112 return Err(HttpClientError::CircuitOpen);
113 }
114 }
115
116 if let Some(retry_config) = &self.config.retry {
118 self.execute_with_retry(request, retry_config).await
119 } else {
120 self.execute_once(request).await
121 }
122 }
123
124 async fn execute_with_retry(
126 &self,
127 request: Request,
128 retry_config: &crate::RetryConfig,
129 ) -> Result<Response> {
130 let mut attempt = 0;
131 let mut last_error: Option<HttpClientError> = None;
132 let start = std::time::Instant::now();
133
134 loop {
135 if let Some(max_time) = retry_config.max_retry_time {
137 if start.elapsed() > max_time {
138 break;
139 }
140 }
141
142 let request_clone = clone_request(&request);
144
145 match self.execute_once(request_clone).await {
146 Ok(response) => {
147 if let Some(cb) = &self.circuit_breaker {
149 cb.record_success();
150 }
151
152 if retry_config.should_retry_status(response.status().as_u16())
154 && attempt < retry_config.max_attempts - 1
155 {
156 debug!(
157 attempt = attempt + 1,
158 status = %response.status(),
159 "Retrying request due to status code"
160 );
161 last_error = Some(HttpClientError::Response {
162 status: response.status().as_u16(),
163 message: "Retriable status code".to_string(),
164 });
165 attempt += 1;
166 let delay = retry_config.delay_for_attempt(attempt);
167 tokio::time::sleep(delay).await;
168 continue;
169 }
170
171 return Ok(response);
172 }
173 Err(e) => {
174 if let Some(cb) = &self.circuit_breaker {
176 cb.record_failure();
177 }
178
179 if retry_config.should_retry(attempt, &e)
181 && attempt < retry_config.max_attempts - 1
182 {
183 debug!(
184 attempt = attempt + 1,
185 error = %e,
186 "Retrying request due to error"
187 );
188 last_error = Some(e);
189 attempt += 1;
190 let delay = retry_config.delay_for_attempt(attempt);
191 tokio::time::sleep(delay).await;
192 continue;
193 }
194
195 return Err(e);
196 }
197 }
198 }
199
200 Err(HttpClientError::RetryExhausted {
201 attempts: attempt + 1,
202 message: last_error
203 .map(|e| e.to_string())
204 .unwrap_or_else(|| "Unknown error".to_string()),
205 })
206 }
207
208 async fn execute_once(&self, request: Request) -> Result<Response> {
210 let response = self.inner.execute(request).await?;
211 Ok(Response::from_reqwest(response).await)
212 }
213}
214
215fn clone_request(request: &Request) -> Request {
217 let mut builder = reqwest::Request::new(request.method().clone(), request.url().clone());
218 *builder.headers_mut() = request.headers().clone();
219 builder
220}
221
222impl Default for HttpClient {
223 fn default() -> Self {
224 Self::default_client()
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use std::time::Duration;
232
233 #[test]
234 fn test_client_creation() {
235 let client = HttpClient::default();
236 assert!(client.config().gzip);
237 assert!(client.config().brotli);
238 }
239
240 #[test]
241 fn test_client_with_config() {
242 let config = HttpClientConfig::builder()
243 .timeout(Duration::from_secs(60))
244 .base_url("https://api.example.com")
245 .build();
246
247 let client = HttpClient::new(config);
248 assert_eq!(client.config().timeout, Duration::from_secs(60));
249 assert_eq!(
250 client.config().base_url.as_deref(),
251 Some("https://api.example.com")
252 );
253 }
254}