1use std::error::Error as StdError;
21use std::sync::Arc;
22use std::time::{Duration, Instant};
23
24use futures::future::BoxFuture;
25use reqwest::header::LOCATION;
26use reqwest::{Request, Response, StatusCode};
27use tracing::info;
28
29use crate::backoff::{Backoff, BackoffConfig};
30use crate::service::HttpService;
31
32#[derive(Debug, thiserror::Error)]
34pub enum Error {
35 #[error(
36 "Received redirect without LOCATION, this normally indicates an incorrectly configured region"
37 )]
38 BareRedirect,
39
40 #[error("Server error, body contains Error, with status {status}: {}", body.as_deref().unwrap_or("No Body"))]
41 Server {
42 status: StatusCode,
43 body: Option<String>,
44 },
45
46 #[error("Client error with status {status}: {}", body.as_deref().unwrap_or("No Body"))]
47 Client {
48 status: StatusCode,
49 body: Option<String>,
50 },
51
52 #[error(
53 "Error after {retries} retries in {elapsed:?}, max_retries:{max_retries}, retry_timeout:{retry_timeout:?}, source:{source}"
54 )]
55 Reqwest {
56 retries: usize,
57 max_retries: usize,
58 elapsed: Duration,
59 retry_timeout: Duration,
60 source: reqwest::Error,
61 },
62
63 #[error("Transport error: {source}")]
66 Transport {
67 source: Box<dyn StdError + Send + Sync + 'static>,
68 },
69}
70
71impl Error {
72 pub fn status(&self) -> Option<StatusCode> {
74 match self {
75 Self::BareRedirect => None,
76 Self::Server { status, .. } => Some(*status),
77 Self::Client { status, .. } => Some(*status),
78 Self::Reqwest { source, .. } => source.status(),
79 Self::Transport { .. } => None,
80 }
81 }
82
83 pub fn body(&self) -> Option<&str> {
85 match self {
86 Self::Client { body, .. } => body.as_deref(),
87 Self::Server { body, .. } => body.as_deref(),
88 Self::BareRedirect => None,
89 Self::Reqwest { .. } => None,
90 Self::Transport { .. } => None,
91 }
92 }
93
94 pub fn error(self) -> crate::Error {
95 match self.status() {
96 Some(StatusCode::NOT_FOUND) => crate::Error::NotFound {
97 source: Box::new(self),
98 },
99 Some(StatusCode::NOT_MODIFIED) => crate::Error::NotModified {
100 source: Box::new(self),
101 },
102 Some(StatusCode::PRECONDITION_FAILED) => crate::Error::Precondition {
103 source: Box::new(self),
104 },
105 Some(StatusCode::CONFLICT) => crate::Error::AlreadyExists {
106 source: Box::new(self),
107 },
108 Some(StatusCode::FORBIDDEN) => crate::Error::PermissionDenied {
109 source: Box::new(self),
110 },
111 Some(StatusCode::UNAUTHORIZED) => crate::Error::Unauthenticated {
112 source: Box::new(self),
113 },
114 _ => crate::Error::Generic {
115 source: Box::new(self),
116 },
117 }
118 }
119}
120
121impl From<Error> for std::io::Error {
122 fn from(err: Error) -> Self {
123 use std::io::ErrorKind;
124 match &err {
125 Error::Client {
126 status: StatusCode::NOT_FOUND,
127 ..
128 } => Self::new(ErrorKind::NotFound, err),
129 Error::Client {
130 status: StatusCode::BAD_REQUEST,
131 ..
132 } => Self::new(ErrorKind::InvalidInput, err),
133 Error::Client {
134 status: StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN,
135 ..
136 } => Self::new(ErrorKind::PermissionDenied, err),
137 Error::Reqwest { source, .. } if source.is_timeout() => {
138 Self::new(ErrorKind::TimedOut, err)
139 }
140 Error::Reqwest { source, .. } if source.is_connect() => {
141 Self::new(ErrorKind::NotConnected, err)
142 }
143 _ => Self::other(err),
144 }
145 }
146}
147
148pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
149
150#[derive(Debug, Clone)]
172pub struct RetryConfig {
173 pub backoff: BackoffConfig,
175
176 pub max_retries: usize,
180
181 pub retry_timeout: Duration,
193}
194
195impl Default for RetryConfig {
196 fn default() -> Self {
197 Self {
198 backoff: Default::default(),
199 max_retries: 10,
200 retry_timeout: Duration::from_secs(3 * 60),
201 }
202 }
203}
204
205pub(crate) struct RetryableRequest {
206 service: Arc<dyn HttpService>,
207 request: Request,
208
209 max_retries: usize,
210 retry_timeout: Duration,
211 backoff: Backoff,
212
213 sensitive: bool,
214 idempotent: Option<bool>,
215}
216
217impl RetryableRequest {
218 pub(crate) fn idempotent(self, idempotent: bool) -> Self {
223 Self {
224 idempotent: Some(idempotent),
225 ..self
226 }
227 }
228
229 #[allow(unused)]
233 pub(crate) fn sensitive(self, sensitive: bool) -> Self {
234 Self { sensitive, ..self }
235 }
236
237 pub(crate) async fn send(self) -> Result<Response> {
238 let max_retries = self.max_retries;
239 let retry_timeout = self.retry_timeout;
240 let mut retries = 0;
241 let now = Instant::now();
242
243 let mut backoff = self.backoff;
244 let is_idempotent = self
245 .idempotent
246 .unwrap_or_else(|| self.request.method().is_safe());
247
248 let sanitize_err = move |e: reqwest::Error| match self.sensitive {
249 true => e.without_url(),
250 false => e,
251 };
252
253 loop {
254 let request = self
255 .request
256 .try_clone()
257 .expect("request body must be cloneable");
258
259 match self.service.call(request).await {
260 Ok(r) => match r.error_for_status_ref() {
261 Ok(_) if r.status().is_success() => {
262 return Ok(r);
263 }
264 Ok(r) if r.status() == StatusCode::NOT_MODIFIED => {
265 return Err(Error::Client {
266 body: None,
267 status: StatusCode::NOT_MODIFIED,
268 });
269 }
270 Ok(r) => {
271 let is_bare_redirect =
272 r.status().is_redirection() && !r.headers().contains_key(LOCATION);
273 return match is_bare_redirect {
274 true => Err(Error::BareRedirect),
275 false => Err(Error::Client {
277 body: None,
278 status: r.status(),
279 }),
280 };
281 }
282 Err(e) => {
283 let e = sanitize_err(e);
284 let status = r.status();
285 if retries == max_retries
286 || now.elapsed() > retry_timeout
287 || !status.is_server_error()
288 {
289 return Err(match status.is_client_error() {
290 true => match r.text().await {
291 Ok(body) => Error::Client {
292 body: Some(body).filter(|b| !b.is_empty()),
293 status,
294 },
295 Err(e) => Error::Reqwest {
296 retries,
297 max_retries,
298 elapsed: now.elapsed(),
299 retry_timeout,
300 source: e,
301 },
302 },
303 false => Error::Reqwest {
304 retries,
305 max_retries,
306 elapsed: now.elapsed(),
307 retry_timeout,
308 source: e,
309 },
310 });
311 }
312
313 let sleep = backoff.next();
314 retries += 1;
315 info!(
316 "Encountered server error, backing off for {} seconds, retry {} of {}: {}",
317 sleep.as_secs_f32(),
318 retries,
319 max_retries,
320 e,
321 );
322 tokio::time::sleep(sleep).await;
323 }
324 },
325 Err(crate::Error::ReqwestError(e)) => {
326 let e = sanitize_err(e);
327
328 let mut do_retry = false;
329 if e.is_connect()
330 || e.is_body()
331 || (e.is_request() && !e.is_timeout())
332 || (is_idempotent && e.is_timeout())
333 {
334 do_retry = true
335 } else {
336 let mut source = e.source();
337 while let Some(e) = source {
338 if let Some(e) = e.downcast_ref::<hyper::Error>() {
339 do_retry = e.is_closed()
340 || e.is_incomplete_message()
341 || e.is_body_write_aborted()
342 || (is_idempotent && e.is_timeout());
343 break;
344 }
345 if let Some(e) = e.downcast_ref::<std::io::Error>() {
346 if e.kind() == std::io::ErrorKind::TimedOut {
347 do_retry = is_idempotent;
348 } else {
349 do_retry = matches!(
350 e.kind(),
351 std::io::ErrorKind::ConnectionReset
352 | std::io::ErrorKind::ConnectionAborted
353 | std::io::ErrorKind::BrokenPipe
354 | std::io::ErrorKind::UnexpectedEof
355 );
356 }
357 break;
358 }
359 source = e.source();
360 }
361 }
362
363 if retries == max_retries || now.elapsed() > retry_timeout || !do_retry {
364 return Err(Error::Reqwest {
365 retries,
366 max_retries,
367 elapsed: now.elapsed(),
368 retry_timeout,
369 source: e,
370 });
371 }
372 let sleep = backoff.next();
373 retries += 1;
374 info!(
375 "Encountered transport error backing off for {} seconds, retry {} of {}: {}",
376 sleep.as_secs_f32(),
377 retries,
378 max_retries,
379 e,
380 );
381 tokio::time::sleep(sleep).await;
382 }
383 Err(other) => {
387 return Err(Error::Transport {
388 source: Box::new(other),
389 });
390 }
391 }
392 }
393 }
394}
395
396pub(crate) trait RetryExt {
397 fn retryable(self, config: &RetryConfig, service: Arc<dyn HttpService>) -> RetryableRequest;
399
400 fn send_retry(
406 self,
407 config: &RetryConfig,
408 service: Arc<dyn HttpService>,
409 ) -> BoxFuture<'static, Result<Response>>;
410}
411
412impl RetryExt for reqwest::RequestBuilder {
413 fn retryable(self, config: &RetryConfig, service: Arc<dyn HttpService>) -> RetryableRequest {
414 let (_client, request) = self.build_split();
415 let request = request.expect("request must be valid");
416
417 RetryableRequest {
418 service,
419 request,
420 max_retries: config.max_retries,
421 retry_timeout: config.retry_timeout,
422 backoff: Backoff::new(&config.backoff),
423 sensitive: false,
424 idempotent: None,
425 }
426 }
427
428 fn send_retry(
429 self,
430 config: &RetryConfig,
431 service: Arc<dyn HttpService>,
432 ) -> BoxFuture<'static, Result<Response>> {
433 let request = self.retryable(config, service);
434 Box::pin(async move { request.send().await })
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::RetryConfig;
441 use crate::retry::{Error, RetryExt};
442 use crate::service::ReqwestService;
443
444 use reqwest::{Client, Method, StatusCode};
445 use std::sync::Arc;
446 use std::time::Duration;
447
448 #[tokio::test]
449 async fn test_retry() {
450 let mut server = mockito::Server::new_async().await;
451
452 let retry = RetryConfig {
453 backoff: Default::default(),
454 max_retries: 2,
455 retry_timeout: Duration::from_secs(1000),
456 };
457
458 let client = Client::builder()
459 .timeout(Duration::from_millis(100))
460 .build()
461 .unwrap();
462
463 let service = Arc::new(ReqwestService::new(client.clone()));
464 let server_url = server.url();
465 let do_request = || {
466 client
467 .request(Method::GET, &server_url)
468 .send_retry(&retry, service.clone())
469 };
470
471 let _mock1 = server
473 .mock("GET", "/")
474 .with_status(200)
475 .with_body("")
476 .create_async()
477 .await;
478
479 let r = do_request().await.unwrap();
480 assert_eq!(r.status(), StatusCode::OK);
481
482 let _mock2 = server
484 .mock("GET", "/")
485 .with_status(400)
486 .with_body("cupcakes")
487 .create_async()
488 .await;
489
490 let e = do_request().await.unwrap_err();
491 assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
492 assert_eq!(e.body(), Some("cupcakes"));
493 assert_eq!(
494 e.to_string(),
495 "Client error with status 400 Bad Request: cupcakes"
496 );
497
498 let _mock3 = server
500 .mock("GET", "/")
501 .with_status(400)
502 .with_body("")
503 .create_async()
504 .await;
505
506 let e = do_request().await.unwrap_err();
507 assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
508 assert_eq!(e.body(), None);
509 assert_eq!(
510 e.to_string(),
511 "Client error with status 400 Bad Request: No Body"
512 );
513
514 let _mock4 = server
516 .mock("GET", "/")
517 .with_status(502)
518 .with_body("")
519 .create_async()
520 .await;
521
522 let _mock5 = server
523 .mock("GET", "/")
524 .with_status(200)
525 .with_body("")
526 .create_async()
527 .await;
528
529 let r = do_request().await.unwrap();
530 assert_eq!(r.status(), StatusCode::OK);
531
532 let _mock6 = server
534 .mock("GET", "/")
535 .with_status(204)
536 .with_body("")
537 .create_async()
538 .await;
539
540 let r = do_request().await.unwrap();
541 assert_eq!(r.status(), StatusCode::NO_CONTENT);
542
543 let _mock7 = server
545 .mock("GET", "/")
546 .with_status(302)
547 .with_header("location", "/foo")
548 .with_body("")
549 .create_async()
550 .await;
551
552 let _mock8 = server
553 .mock("GET", "/foo")
554 .with_status(200)
555 .with_body("")
556 .create_async()
557 .await;
558
559 let r = do_request().await.unwrap();
560 assert_eq!(r.status(), StatusCode::OK);
561 assert_eq!(r.url().path(), "/foo");
562
563 let _mock_redirect = server
565 .mock("GET", "/")
566 .with_status(302)
567 .with_body("")
568 .create_async()
569 .await;
570
571 let e = do_request().await.unwrap_err();
572 assert!(matches!(e, Error::BareRedirect));
573 assert_eq!(
574 e.to_string(),
575 "Received redirect without LOCATION, this normally indicates an incorrectly configured region"
576 );
577
578 let _timeout_mock1 = server
580 .mock("GET", "/")
581 .with_status(200)
582 .with_body("")
583 .create_async()
584 .await;
585
586 let _timeout_mock2 = server
587 .mock("GET", "/")
588 .with_status(200)
589 .with_body("")
590 .create_async()
591 .await;
592
593 do_request().await.unwrap();
594
595 let sensitive_url = format!("{server_url}/SENSITIVE");
597 let _sensitive_mock = server
598 .mock("GET", "/SENSITIVE")
599 .with_status(502)
600 .with_body("ignored")
601 .create_async()
602 .await;
603
604 let res = client
605 .request(Method::GET, &sensitive_url)
606 .send_retry(&retry, service.clone())
607 .await;
608 let err = res.unwrap_err().to_string();
609 assert!(err.contains("SENSITIVE"), "{err}");
610
611 let _sensitive_mock2 = server
613 .mock("GET", "/SENSITIVE")
614 .with_status(502)
615 .with_body("ignored")
616 .create_async()
617 .await;
618
619 let req = client
620 .request(Method::GET, &sensitive_url)
621 .retryable(&retry, service.clone())
622 .sensitive(true);
623 let err = req.send().await.unwrap_err().to_string();
624 assert!(!err.contains("SENSITIVE"), "{err}");
625 }
626
627 #[tokio::test]
628 async fn test_retry_exhaustion_returns_last_server_error() {
629 let mut server = mockito::Server::new_async().await;
630
631 let retry = RetryConfig {
632 backoff: crate::backoff::BackoffConfig {
634 init_backoff: Duration::from_millis(1),
635 max_backoff: Duration::from_millis(2),
636 base: 2.,
637 },
638 max_retries: 2,
639 retry_timeout: Duration::from_secs(1000),
640 };
641
642 let client = Client::builder()
643 .timeout(Duration::from_millis(100))
644 .build()
645 .unwrap();
646 let service = Arc::new(ReqwestService::new(client.clone()));
647 let server_url = server.url();
648
649 let mock = server
652 .mock("GET", "/")
653 .with_status(502)
654 .with_body("upstream boom")
655 .expect(3)
656 .create_async()
657 .await;
658
659 let err = client
660 .request(Method::GET, &server_url)
661 .send_retry(&retry, service.clone())
662 .await
663 .unwrap_err();
664
665 assert!(matches!(err, Error::Reqwest { .. }), "{err:?}");
668 assert_eq!(err.status(), Some(StatusCode::BAD_GATEWAY));
669 mock.assert_async().await;
670 }
671
672 #[tokio::test]
673 async fn test_no_retries_returns_immediately() {
674 let mut server = mockito::Server::new_async().await;
675
676 let retry = RetryConfig {
677 backoff: Default::default(),
678 max_retries: 0,
679 retry_timeout: Duration::from_secs(1000),
680 };
681
682 let client = Client::builder().build().unwrap();
683 let service = Arc::new(ReqwestService::new(client.clone()));
684 let server_url = server.url();
685
686 let mock = server
688 .mock("GET", "/")
689 .with_status(503)
690 .with_body("")
691 .expect(1)
692 .create_async()
693 .await;
694
695 let err = client
696 .request(Method::GET, &server_url)
697 .send_retry(&retry, service.clone())
698 .await
699 .unwrap_err();
700
701 assert_eq!(err.status(), Some(StatusCode::SERVICE_UNAVAILABLE));
702 mock.assert_async().await;
703 }
704}