1use crate::Error;
13use reqwest::{
14 Method,
15 header::{CONTENT_TYPE, HeaderMap, HeaderValue},
16};
17use serde::{Serialize, de::DeserializeOwned};
18use std::time::Duration;
19
20#[derive(Debug, Clone)]
21pub struct HttpClientBuilder {
22 timeout: Duration,
23 headers: HeaderMap<HeaderValue>,
24}
25
26#[derive(Debug, Clone)]
27pub struct HttpClient {
28 headers: HeaderMap<HeaderValue>,
29 client: reqwest::Client,
30}
31
32#[derive(Debug, Clone)]
33pub struct HttpRequest {
34 method: Method,
35 url: String,
36 headers: HeaderMap<HeaderValue>,
37 body: Option<String>,
38 client: reqwest::Client,
39}
40
41impl Default for HttpClientBuilder {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl HttpClientBuilder {
48 pub fn new() -> Self {
49 let mut headers = HeaderMap::new();
50 headers.append(CONTENT_TYPE, HeaderValue::from_static("application/json"));
51
52 Self {
53 timeout: Duration::from_secs(30),
54 headers,
55 }
56 }
57
58 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
59 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
60 self.headers.append(name, value);
61 }
62 self
63 }
64
65 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
66 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
67 self.headers.insert(name, value);
68 }
69 self
70 }
71
72 pub fn with_timeout(mut self, timeout: Option<Duration>) -> Self {
73 if let Some(timeout) = timeout {
74 self.timeout = timeout;
75 }
76 self
77 }
78
79 pub fn build(self) -> HttpClient {
80 let client = reqwest::Client::builder()
81 .timeout(self.timeout)
82 .build()
83 .unwrap_or_default();
84 HttpClient {
85 headers: self.headers,
86 client,
87 }
88 }
89}
90
91impl HttpClient {
92 pub fn request(&self, method: Method, url: impl Into<String>) -> HttpRequest {
93 HttpRequest {
94 method,
95 url: url.into(),
96 headers: self.headers.clone(),
97 body: None,
98 client: self.client.clone(),
99 }
100 }
101
102 pub fn get(&self, url: impl Into<String>) -> HttpRequest {
103 self.request(Method::GET, url)
104 }
105
106 pub fn post(&self, url: impl Into<String>) -> HttpRequest {
107 self.request(Method::POST, url)
108 }
109
110 pub fn put(&self, url: impl Into<String>) -> HttpRequest {
111 self.request(Method::PUT, url)
112 }
113
114 pub fn delete(&self, url: impl Into<String>) -> HttpRequest {
115 self.request(Method::DELETE, url)
116 }
117
118 pub fn patch(&self, url: impl Into<String>) -> HttpRequest {
119 self.request(Method::PATCH, url)
120 }
121}
122
123impl HttpRequest {
124 pub fn with_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
125 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
126 self.headers.append(name, value);
127 }
128 self
129 }
130
131 pub fn set_header(mut self, name: &'static str, value: impl AsRef<str>) -> Self {
132 if let Ok(value) = HeaderValue::from_str(value.as_ref()) {
133 self.headers.insert(name, value);
134 }
135 self
136 }
137
138 pub fn with_body<B: Serialize>(mut self, body: B) -> crate::Result<Self> {
139 match serde_json::to_string(&body) {
140 Ok(body) => {
141 self.body = Some(body);
142 Ok(self)
143 }
144 Err(err) => Err(Error::Serialize(format!(
145 "Failed to serialize request: {err}"
146 ))),
147 }
148 }
149
150 pub fn with_raw_body(mut self, body: String) -> Self {
151 self.body = Some(body);
152 self
153 }
154
155 pub async fn send<T>(self) -> crate::Result<T>
156 where
157 T: DeserializeOwned,
158 {
159 let response = self.send_raw().await?;
160 serde_json::from_slice::<T>(response.as_bytes()).map_err(|err| {
161 Error::Serialize(format!(
162 "Failed to deserialize response: {err} (body: {})",
163 body_snippet(&response)
164 ))
165 })
166 }
167
168 pub async fn send_raw(self) -> crate::Result<String> {
169 self.send_raw_with_headers().await.map(|(body, _)| body)
170 }
171
172 pub async fn send_raw_with_headers(self) -> crate::Result<(String, HeaderMap<HeaderValue>)> {
173 let mut request = self
174 .client
175 .request(self.method, &self.url)
176 .headers(self.headers);
177
178 if let Some(body) = self.body {
179 request = request.body(body);
180 }
181
182 let response = request
183 .send()
184 .await
185 .map_err(|err| Error::Api(format!("Failed to send request to {}: {err}", self.url)))?;
186
187 let code = response.status().as_u16();
188 let headers = response.headers().clone();
189 match code {
190 204 => Ok((String::new(), headers)),
191 200..=299 => response
192 .text()
193 .await
194 .map(|body| (body, headers))
195 .map_err(|err| {
196 Error::Api(format!("Failed to read response from {}: {err}", self.url))
197 }),
198 401 => Err(Error::Unauthorized),
199 404 => Err(Error::NotFound),
200 _ => {
201 let text = response.text().await.unwrap_or_default();
202 Err(Error::Api(http_status_message(code, &text)))
203 }
204 }
205 }
206
207 pub async fn send_with_retry<T>(self, max_retries: u32) -> crate::Result<T>
208 where
209 T: DeserializeOwned,
210 {
211 let mut attempts = 0;
212 let body = self.body;
213 loop {
214 let mut request = self
215 .client
216 .request(self.method.clone(), &self.url)
217 .headers(self.headers.clone());
218
219 if let Some(body) = body.as_ref() {
220 request = request.body(body.clone());
221 }
222
223 let response = request.send().await.map_err(|err| {
224 Error::Api(format!("Failed to send request to {}: {err}", self.url))
225 })?;
226
227 let code = response.status().as_u16();
228 return match code {
229 204 => serde_json::from_str("{}").map_err(|err| {
230 Error::Serialize(format!("Failed to create empty response: {err}"))
231 }),
232 200..=299 => {
233 let text = response.text().await.map_err(|err| {
234 Error::Api(format!("Failed to read response from {}: {err}", self.url))
235 })?;
236 let parse_target = if text.trim().is_empty() { "{}" } else { &text };
237 serde_json::from_str(parse_target).map_err(|err| {
238 Error::Serialize(format!(
239 "Failed to deserialize response from {}: {err} (body: {})",
240 self.url,
241 body_snippet(&text)
242 ))
243 })
244 }
245 429 | 503 if attempts < max_retries => {
246 let delay = retry_after(response.headers())
247 .unwrap_or_else(|| Duration::from_secs(1u64 << attempts.min(6)));
248 tokio::time::sleep(delay.min(MAX_RETRY_DELAY)).await;
249 attempts += 1;
250 continue;
251 }
252 401 => Err(Error::Unauthorized),
253 404 => Err(Error::NotFound),
254 _ => {
255 let text = response.text().await.unwrap_or_default();
256 Err(Error::Api(http_status_message(code, &text)))
257 }
258 };
259 }
260 }
261}
262
263const MAX_RETRY_DELAY: Duration = Duration::from_secs(60);
264const MAX_BODY_SNIPPET: usize = 512;
265
266fn body_snippet(body: &str) -> &str {
267 let trimmed = body.trim();
268 if trimmed.len() <= MAX_BODY_SNIPPET {
269 trimmed
270 } else {
271 &trimmed[..trimmed.ceil_char_boundary(MAX_BODY_SNIPPET)]
272 }
273}
274
275fn retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
276 headers
277 .get("retry-after")?
278 .to_str()
279 .ok()?
280 .parse::<u64>()
281 .ok()
282 .map(Duration::from_secs)
283}
284
285fn http_status_message(code: u16, body: &str) -> String {
286 let trimmed = body.trim();
287 if code == 400 {
288 if trimmed.is_empty() {
289 "BadRequest".to_string()
290 } else {
291 format!("BadRequest {trimmed}")
292 }
293 } else if trimmed.is_empty() {
294 format!("HTTP {code}")
295 } else {
296 format!("HTTP {code}: {trimmed}")
297 }
298}