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