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