1use crate::error::{ErrorData, Result};
2use alien_error::{AlienError, Context, IntoAlienError};
3use async_trait::async_trait;
4use backon::{ExponentialBuilder, Retryable};
5use serde::de::DeserializeOwned;
6use std::time::Duration;
7
8fn extract_request_body_string(request: &reqwest::Request) -> Option<String> {
10 request
11 .body()
12 .and_then(|body| body.as_bytes())
13 .map(|bytes| String::from_utf8_lossy(bytes).into_owned())
14}
15
16fn build_and_extract_body(
18 builder: reqwest::RequestBuilder,
19) -> Result<(reqwest::Client, reqwest::Request, Option<String>)> {
20 let (client, req_result) = builder.build_split();
21 let request = req_result
22 .into_alien_error()
23 .context(ErrorData::HttpRequestFailed {
24 message: "Failed to build request".to_string(),
25 })?;
26
27 let body_string = extract_request_body_string(&request);
28 Ok((client, request, body_string))
29}
30
31pub async fn handle_json_response<T: DeserializeOwned>(
33 response: reqwest::Response,
34 request_body: Option<String>,
35) -> Result<T> {
36 let status = response.status();
37 let url = response.url().to_string();
38 let response_text =
39 response
40 .text()
41 .await
42 .into_alien_error()
43 .context(ErrorData::HttpRequestFailed {
44 message: "Failed to read response body".to_string(),
45 })?;
46
47 if !status.is_success() {
48 return Err(AlienError::new(ErrorData::HttpResponseError {
49 message: format!(
50 "Request failed with HTTP {}: {}",
51 status.as_u16(),
52 status.canonical_reason().unwrap_or("Unknown error")
53 ),
54 url,
55 http_status: status.as_u16(),
56 http_request_text: request_body,
57 http_response_text: Some(response_text),
58 }));
59 }
60
61 let jd = &mut serde_json::Deserializer::from_str(&response_text);
63 let parsed_response: T = serde_path_to_error::deserialize(jd).map_err(|err| {
64 AlienError::new(ErrorData::HttpResponseError {
65 message: format!(
66 "Invalid JSON response at field '{}': {}",
67 err.path(),
68 err.inner()
69 ),
70 url,
71 http_status: status.as_u16(),
72 http_request_text: request_body,
73 http_response_text: Some(response_text),
74 })
75 })?;
76
77 Ok(parsed_response)
78}
79
80pub async fn handle_xml_response<T: DeserializeOwned>(
82 response: reqwest::Response,
83 request_body: Option<String>,
84) -> Result<T> {
85 let status = response.status();
86 let url = response.url().to_string();
87 let response_text =
88 response
89 .text()
90 .await
91 .into_alien_error()
92 .context(ErrorData::HttpRequestFailed {
93 message: "Failed to read response body".to_string(),
94 })?;
95
96 if !status.is_success() {
97 return Err(AlienError::new(ErrorData::HttpResponseError {
98 message: format!(
99 "Request failed with HTTP {}: {}",
100 status.as_u16(),
101 status.canonical_reason().unwrap_or("Unknown error")
102 ),
103 url,
104 http_status: status.as_u16(),
105 http_request_text: request_body,
106 http_response_text: Some(response_text),
107 }));
108 }
109
110 let mut xml_deserializer = quick_xml::de::Deserializer::from_str(&response_text);
112 let parsed_response: T =
113 serde_path_to_error::deserialize(&mut xml_deserializer).map_err(|err| {
114 AlienError::new(ErrorData::HttpResponseError {
115 message: format!(
116 "Invalid XML response at field '{}': {}",
117 err.path(),
118 err.inner()
119 ),
120 url,
121 http_status: status.as_u16(),
122 http_request_text: request_body,
123 http_response_text: Some(response_text),
124 })
125 })?;
126
127 Ok(parsed_response)
128}
129
130pub async fn handle_no_response(
132 response: reqwest::Response,
133 request_body: Option<String>,
134) -> Result<()> {
135 let status = response.status();
136 let url = response.url().to_string();
137
138 if !status.is_success() {
139 let response_text =
140 response
141 .text()
142 .await
143 .into_alien_error()
144 .context(ErrorData::HttpRequestFailed {
145 message: "Failed to read error response body".to_string(),
146 })?;
147 return Err(AlienError::new(ErrorData::HttpResponseError {
148 message: format!(
149 "Request failed with HTTP {}: {}",
150 status.as_u16(),
151 status.canonical_reason().unwrap_or("Unknown error")
152 ),
153 url,
154 http_status: status.as_u16(),
155 http_request_text: request_body,
156 http_response_text: Some(response_text),
157 }));
158 }
159
160 Ok(())
161}
162
163#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
165#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
166pub trait RequestBuilderExt {
167 fn with_retry(self) -> RetriableRequestBuilder;
178
179 async fn send_json<T: DeserializeOwned + 'static>(self) -> Result<T>;
181
182 async fn send_xml<T: DeserializeOwned + 'static>(self) -> Result<T>;
184
185 async fn send_no_response(self) -> Result<()>;
187
188 async fn send_raw(self) -> Result<reqwest::Response>;
190}
191
192pub struct RetriableRequestBuilder {
196 inner: reqwest::RequestBuilder,
197 backoff: ExponentialBuilder,
198}
199
200impl RetriableRequestBuilder {
201 pub fn backoff(mut self, backoff: ExponentialBuilder) -> Self {
203 self.backoff = backoff;
204 self
205 }
206
207 fn is_retryable_error(e: &AlienError<ErrorData>) -> bool {
209 e.retryable
210 }
211
212 fn default_backoff() -> ExponentialBuilder {
214 ExponentialBuilder::default()
215 .with_max_times(3)
216 .with_max_delay(Duration::from_secs(20))
217 .with_jitter()
218 }
219
220 pub async fn send_json<T: DeserializeOwned + Send + 'static>(self) -> Result<T> {
222 let backoff = self.backoff;
223 let builder = self.inner;
224
225 let retryable = move || {
226 let attempt_builder = builder.try_clone();
227 async move {
228 let attempt_builder = attempt_builder.ok_or_else(|| {
229 AlienError::new(ErrorData::GenericError {
230 message: "Request retry preparation failed".into(),
231 })
232 })?;
233
234 let (client, request, body_string) = build_and_extract_body(attempt_builder)?;
235 let new_builder = reqwest::RequestBuilder::from_parts(client, request);
236
237 #[cfg(target_arch = "wasm32")]
238 {
239 let resp = new_builder.send().await.into_alien_error().context(
240 ErrorData::HttpRequestFailed {
241 message: "Network error during HTTP request".to_string(),
242 },
243 )?;
244 handle_json_response(resp, body_string).await
245 }
246 #[cfg(not(target_arch = "wasm32"))]
247 {
248 let resp = new_builder.send().await.into_alien_error().context(
249 ErrorData::HttpRequestFailed {
250 message: "Network error during HTTP request".to_string(),
251 },
252 )?;
253 handle_json_response(resp, body_string).await
254 }
255 }
256 };
257
258 retryable
259 .retry(backoff)
260 .when(Self::is_retryable_error)
261 .await
262 }
263
264 pub async fn send_xml<T: DeserializeOwned + Send + 'static>(self) -> Result<T> {
266 let backoff = self.backoff;
267 let builder = self.inner;
268
269 let retryable = move || {
270 let attempt_builder = builder.try_clone();
271 async move {
272 let attempt_builder = attempt_builder.ok_or_else(|| {
273 AlienError::new(ErrorData::GenericError {
274 message: "Request retry preparation failed".into(),
275 })
276 })?;
277
278 let (client, request, body_string) = build_and_extract_body(attempt_builder)?;
279 let new_builder = reqwest::RequestBuilder::from_parts(client, request);
280
281 #[cfg(target_arch = "wasm32")]
282 {
283 let resp = new_builder.send().await.into_alien_error().context(
284 ErrorData::HttpRequestFailed {
285 message: "Network error during HTTP request".to_string(),
286 },
287 )?;
288 handle_xml_response(resp, body_string).await
289 }
290 #[cfg(not(target_arch = "wasm32"))]
291 {
292 let resp = new_builder.send().await.into_alien_error().context(
293 ErrorData::HttpRequestFailed {
294 message: "Network error during HTTP request".to_string(),
295 },
296 )?;
297 handle_xml_response(resp, body_string).await
298 }
299 }
300 };
301
302 retryable
303 .retry(backoff)
304 .when(Self::is_retryable_error)
305 .await
306 }
307
308 pub async fn send_no_response(self) -> Result<()> {
310 let backoff = self.backoff;
311 let builder = self.inner;
312
313 let retryable = move || {
314 let attempt_builder = builder.try_clone();
315 async move {
316 let attempt_builder = attempt_builder.ok_or_else(|| {
317 AlienError::new(ErrorData::GenericError {
318 message: "Request retry preparation failed".into(),
319 })
320 })?;
321
322 let (client, request, body_string) = build_and_extract_body(attempt_builder)?;
323 let new_builder = reqwest::RequestBuilder::from_parts(client, request);
324
325 #[cfg(target_arch = "wasm32")]
326 {
327 let resp = new_builder.send().await.into_alien_error().context(
328 ErrorData::HttpRequestFailed {
329 message: "Network error during HTTP request".to_string(),
330 },
331 )?;
332 handle_no_response(resp, body_string).await
333 }
334 #[cfg(not(target_arch = "wasm32"))]
335 {
336 let resp = new_builder.send().await.into_alien_error().context(
337 ErrorData::HttpRequestFailed {
338 message: "Network error during HTTP request".to_string(),
339 },
340 )?;
341 handle_no_response(resp, body_string).await
342 }
343 }
344 };
345
346 retryable
347 .retry(backoff)
348 .when(Self::is_retryable_error)
349 .await
350 }
351
352 pub async fn send_raw(self) -> Result<reqwest::Response> {
354 let backoff = self.backoff;
355 let builder = self.inner;
356
357 let retryable = move || {
358 let attempt_builder = builder.try_clone();
359 async move {
360 let attempt_builder = attempt_builder.ok_or_else(|| {
361 AlienError::new(ErrorData::GenericError {
362 message: "Request retry preparation failed".into(),
363 })
364 })?;
365
366 let (client, request, _body_string) = build_and_extract_body(attempt_builder)?;
367 let new_builder = reqwest::RequestBuilder::from_parts(client, request);
368
369 #[cfg(target_arch = "wasm32")]
370 {
371 new_builder.send().await.into_alien_error().context(
372 ErrorData::HttpRequestFailed {
373 message: "Network error during HTTP request".to_string(),
374 },
375 )
376 }
377 #[cfg(not(target_arch = "wasm32"))]
378 {
379 new_builder.send().await.into_alien_error().context(
380 ErrorData::HttpRequestFailed {
381 message: "Network error during HTTP request".to_string(),
382 },
383 )
384 }
385 }
386 };
387
388 retryable
389 .retry(backoff)
390 .when(Self::is_retryable_error)
391 .await
392 }
393}
394
395#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
396#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
397impl RequestBuilderExt for reqwest::RequestBuilder {
398 fn with_retry(self) -> RetriableRequestBuilder {
399 RetriableRequestBuilder {
400 inner: self,
401 backoff: RetriableRequestBuilder::default_backoff(),
402 }
403 }
404
405 async fn send_json<T: DeserializeOwned + 'static>(self) -> Result<T> {
406 let (client, request, body_string) = build_and_extract_body(self)?;
407 let builder = reqwest::RequestBuilder::from_parts(client, request);
408
409 #[cfg(target_arch = "wasm32")]
410 {
411 let resp =
412 builder
413 .send()
414 .await
415 .into_alien_error()
416 .context(ErrorData::HttpRequestFailed {
417 message: "Network error during HTTP request".to_string(),
418 })?;
419 handle_json_response(resp, body_string).await
420 }
421 #[cfg(not(target_arch = "wasm32"))]
422 {
423 let resp =
424 builder
425 .send()
426 .await
427 .into_alien_error()
428 .context(ErrorData::HttpRequestFailed {
429 message: "Network error during HTTP request".to_string(),
430 })?;
431 handle_json_response(resp, body_string).await
432 }
433 }
434
435 async fn send_xml<T: DeserializeOwned + 'static>(self) -> Result<T> {
436 let (client, request, body_string) = build_and_extract_body(self)?;
437 let builder = reqwest::RequestBuilder::from_parts(client, request);
438
439 #[cfg(target_arch = "wasm32")]
440 {
441 let resp =
442 builder
443 .send()
444 .await
445 .into_alien_error()
446 .context(ErrorData::HttpRequestFailed {
447 message: "Network error during HTTP request".to_string(),
448 })?;
449 handle_xml_response(resp, body_string).await
450 }
451 #[cfg(not(target_arch = "wasm32"))]
452 {
453 let resp =
454 builder
455 .send()
456 .await
457 .into_alien_error()
458 .context(ErrorData::HttpRequestFailed {
459 message: "Network error during HTTP request".to_string(),
460 })?;
461 handle_xml_response(resp, body_string).await
462 }
463 }
464
465 async fn send_no_response(self) -> Result<()> {
466 let (client, request, body_string) = build_and_extract_body(self)?;
467 let builder = reqwest::RequestBuilder::from_parts(client, request);
468
469 #[cfg(target_arch = "wasm32")]
470 {
471 let resp =
472 builder
473 .send()
474 .await
475 .into_alien_error()
476 .context(ErrorData::HttpRequestFailed {
477 message: "Network error during HTTP request".to_string(),
478 })?;
479 handle_no_response(resp, body_string).await
480 }
481 #[cfg(not(target_arch = "wasm32"))]
482 {
483 let resp =
484 builder
485 .send()
486 .await
487 .into_alien_error()
488 .context(ErrorData::HttpRequestFailed {
489 message: "Network error during HTTP request".to_string(),
490 })?;
491 handle_no_response(resp, body_string).await
492 }
493 }
494
495 async fn send_raw(self) -> Result<reqwest::Response> {
496 let (client, request, _body_string) = build_and_extract_body(self)?;
497 let builder = reqwest::RequestBuilder::from_parts(client, request);
498
499 #[cfg(target_arch = "wasm32")]
500 {
501 builder
502 .send()
503 .await
504 .into_alien_error()
505 .context(ErrorData::HttpRequestFailed {
506 message: "Network error during HTTP request".to_string(),
507 })
508 }
509 #[cfg(not(target_arch = "wasm32"))]
510 {
511 builder
512 .send()
513 .await
514 .into_alien_error()
515 .context(ErrorData::HttpRequestFailed {
516 message: "Network error during HTTP request".to_string(),
517 })
518 }
519 }
520}