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
64impl Error {
65 pub fn status(&self) -> Option<StatusCode> {
67 match self {
68 Self::BareRedirect => None,
69 Self::Server { status, .. } => Some(*status),
70 Self::Client { status, .. } => Some(*status),
71 Self::Reqwest { source, .. } => source.status(),
72 }
73 }
74
75 pub fn body(&self) -> Option<&str> {
77 match self {
78 Self::Client { body, .. } => body.as_deref(),
79 Self::Server { body, .. } => body.as_deref(),
80 Self::BareRedirect => None,
81 Self::Reqwest { .. } => None,
82 }
83 }
84
85 pub fn error(self) -> crate::Error {
86 match self.status() {
87 Some(StatusCode::NOT_FOUND) => crate::Error::NotFound {
88 source: Box::new(self),
89 },
90 Some(StatusCode::NOT_MODIFIED) => crate::Error::NotModified {
91 source: Box::new(self),
92 },
93 Some(StatusCode::PRECONDITION_FAILED) => crate::Error::Precondition {
94 source: Box::new(self),
95 },
96 Some(StatusCode::CONFLICT) => crate::Error::AlreadyExists {
97 source: Box::new(self),
98 },
99 Some(StatusCode::FORBIDDEN) => crate::Error::PermissionDenied {
100 source: Box::new(self),
101 },
102 Some(StatusCode::UNAUTHORIZED) => crate::Error::Unauthenticated {
103 source: Box::new(self),
104 },
105 _ => crate::Error::Generic {
106 source: Box::new(self),
107 },
108 }
109 }
110}
111
112impl From<Error> for std::io::Error {
113 fn from(err: Error) -> Self {
114 use std::io::ErrorKind;
115 match &err {
116 Error::Client {
117 status: StatusCode::NOT_FOUND,
118 ..
119 } => Self::new(ErrorKind::NotFound, err),
120 Error::Client {
121 status: StatusCode::BAD_REQUEST,
122 ..
123 } => Self::new(ErrorKind::InvalidInput, err),
124 Error::Client {
125 status: StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN,
126 ..
127 } => Self::new(ErrorKind::PermissionDenied, err),
128 Error::Reqwest { source, .. } if source.is_timeout() => {
129 Self::new(ErrorKind::TimedOut, err)
130 }
131 Error::Reqwest { source, .. } if source.is_connect() => {
132 Self::new(ErrorKind::NotConnected, err)
133 }
134 _ => Self::other(err),
135 }
136 }
137}
138
139pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
140
141#[derive(Debug, Clone)]
163pub struct RetryConfig {
164 pub backoff: BackoffConfig,
166
167 pub max_retries: usize,
171
172 pub retry_timeout: Duration,
184}
185
186impl Default for RetryConfig {
187 fn default() -> Self {
188 Self {
189 backoff: Default::default(),
190 max_retries: 10,
191 retry_timeout: Duration::from_secs(3 * 60),
192 }
193 }
194}
195
196pub(crate) struct RetryableRequest {
197 service: Arc<dyn HttpService>,
198 request: Request,
199
200 max_retries: usize,
201 retry_timeout: Duration,
202 backoff: Backoff,
203
204 sensitive: bool,
205 idempotent: Option<bool>,
206}
207
208impl RetryableRequest {
209 pub(crate) fn idempotent(self, idempotent: bool) -> Self {
214 Self {
215 idempotent: Some(idempotent),
216 ..self
217 }
218 }
219
220 #[allow(unused)]
224 pub(crate) fn sensitive(self, sensitive: bool) -> Self {
225 Self { sensitive, ..self }
226 }
227
228 pub(crate) async fn send(self) -> Result<Response> {
229 let max_retries = self.max_retries;
230 let retry_timeout = self.retry_timeout;
231 let mut retries = 0;
232 let now = Instant::now();
233
234 let mut backoff = self.backoff;
235 let is_idempotent = self
236 .idempotent
237 .unwrap_or_else(|| self.request.method().is_safe());
238
239 let sanitize_err = move |e: reqwest::Error| match self.sensitive {
240 true => e.without_url(),
241 false => e,
242 };
243
244 loop {
245 let request = self
246 .request
247 .try_clone()
248 .expect("request body must be cloneable");
249
250 match self.service.call(request).await {
251 Ok(r) => match r.error_for_status_ref() {
252 Ok(_) if r.status().is_success() => {
253 return Ok(r);
254 }
255 Ok(r) if r.status() == StatusCode::NOT_MODIFIED => {
256 return Err(Error::Client {
257 body: None,
258 status: StatusCode::NOT_MODIFIED,
259 });
260 }
261 Ok(r) => {
262 let is_bare_redirect =
263 r.status().is_redirection() && !r.headers().contains_key(LOCATION);
264 return match is_bare_redirect {
265 true => Err(Error::BareRedirect),
266 false => Err(Error::Client {
268 body: None,
269 status: r.status(),
270 }),
271 };
272 }
273 Err(e) => {
274 let e = sanitize_err(e);
275 let status = r.status();
276 if retries == max_retries
277 || now.elapsed() > retry_timeout
278 || !status.is_server_error()
279 {
280 return Err(match status.is_client_error() {
281 true => match r.text().await {
282 Ok(body) => Error::Client {
283 body: Some(body).filter(|b| !b.is_empty()),
284 status,
285 },
286 Err(e) => Error::Reqwest {
287 retries,
288 max_retries,
289 elapsed: now.elapsed(),
290 retry_timeout,
291 source: e,
292 },
293 },
294 false => Error::Reqwest {
295 retries,
296 max_retries,
297 elapsed: now.elapsed(),
298 retry_timeout,
299 source: e,
300 },
301 });
302 }
303
304 let sleep = backoff.next();
305 retries += 1;
306 info!(
307 "Encountered server error, backing off for {} seconds, retry {} of {}: {}",
308 sleep.as_secs_f32(),
309 retries,
310 max_retries,
311 e,
312 );
313 tokio::time::sleep(sleep).await;
314 }
315 },
316 Err(e) => {
317 let e = sanitize_err(e);
318
319 let mut do_retry = false;
320 if e.is_connect()
321 || e.is_body()
322 || (e.is_request() && !e.is_timeout())
323 || (is_idempotent && e.is_timeout())
324 {
325 do_retry = true
326 } else {
327 let mut source = e.source();
328 while let Some(e) = source {
329 if let Some(e) = e.downcast_ref::<hyper::Error>() {
330 do_retry = e.is_closed()
331 || e.is_incomplete_message()
332 || e.is_body_write_aborted()
333 || (is_idempotent && e.is_timeout());
334 break;
335 }
336 if let Some(e) = e.downcast_ref::<std::io::Error>() {
337 if e.kind() == std::io::ErrorKind::TimedOut {
338 do_retry = is_idempotent;
339 } else {
340 do_retry = matches!(
341 e.kind(),
342 std::io::ErrorKind::ConnectionReset
343 | std::io::ErrorKind::ConnectionAborted
344 | std::io::ErrorKind::BrokenPipe
345 | std::io::ErrorKind::UnexpectedEof
346 );
347 }
348 break;
349 }
350 source = e.source();
351 }
352 }
353
354 if retries == max_retries || now.elapsed() > retry_timeout || !do_retry {
355 return Err(Error::Reqwest {
356 retries,
357 max_retries,
358 elapsed: now.elapsed(),
359 retry_timeout,
360 source: e,
361 });
362 }
363 let sleep = backoff.next();
364 retries += 1;
365 info!(
366 "Encountered transport error backing off for {} seconds, retry {} of {}: {}",
367 sleep.as_secs_f32(),
368 retries,
369 max_retries,
370 e,
371 );
372 tokio::time::sleep(sleep).await;
373 }
374 }
375 }
376 }
377}
378
379pub(crate) trait RetryExt {
380 fn retryable(self, config: &RetryConfig, service: Arc<dyn HttpService>) -> RetryableRequest;
382
383 fn send_retry(
389 self,
390 config: &RetryConfig,
391 service: Arc<dyn HttpService>,
392 ) -> BoxFuture<'static, Result<Response>>;
393}
394
395impl RetryExt for reqwest::RequestBuilder {
396 fn retryable(self, config: &RetryConfig, service: Arc<dyn HttpService>) -> RetryableRequest {
397 let (_client, request) = self.build_split();
398 let request = request.expect("request must be valid");
399
400 RetryableRequest {
401 service,
402 request,
403 max_retries: config.max_retries,
404 retry_timeout: config.retry_timeout,
405 backoff: Backoff::new(&config.backoff),
406 sensitive: false,
407 idempotent: None,
408 }
409 }
410
411 fn send_retry(
412 self,
413 config: &RetryConfig,
414 service: Arc<dyn HttpService>,
415 ) -> BoxFuture<'static, Result<Response>> {
416 let request = self.retryable(config, service);
417 Box::pin(async move { request.send().await })
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::RetryConfig;
424 use crate::retry::{Error, RetryExt};
425 use crate::service::ReqwestService;
426
427 use reqwest::{Client, Method, StatusCode};
428 use std::sync::Arc;
429 use std::time::Duration;
430
431 #[tokio::test]
432 async fn test_retry() {
433 let mut server = mockito::Server::new_async().await;
434
435 let retry = RetryConfig {
436 backoff: Default::default(),
437 max_retries: 2,
438 retry_timeout: Duration::from_secs(1000),
439 };
440
441 let client = Client::builder()
442 .timeout(Duration::from_millis(100))
443 .build()
444 .unwrap();
445
446 let service = Arc::new(ReqwestService::new(client.clone()));
447 let server_url = server.url();
448 let do_request = || {
449 client
450 .request(Method::GET, &server_url)
451 .send_retry(&retry, service.clone())
452 };
453
454 let _mock1 = server
456 .mock("GET", "/")
457 .with_status(200)
458 .with_body("")
459 .create_async()
460 .await;
461
462 let r = do_request().await.unwrap();
463 assert_eq!(r.status(), StatusCode::OK);
464
465 let _mock2 = server
467 .mock("GET", "/")
468 .with_status(400)
469 .with_body("cupcakes")
470 .create_async()
471 .await;
472
473 let e = do_request().await.unwrap_err();
474 assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
475 assert_eq!(e.body(), Some("cupcakes"));
476 assert_eq!(
477 e.to_string(),
478 "Client error with status 400 Bad Request: cupcakes"
479 );
480
481 let _mock3 = server
483 .mock("GET", "/")
484 .with_status(400)
485 .with_body("")
486 .create_async()
487 .await;
488
489 let e = do_request().await.unwrap_err();
490 assert_eq!(e.status().unwrap(), StatusCode::BAD_REQUEST);
491 assert_eq!(e.body(), None);
492 assert_eq!(
493 e.to_string(),
494 "Client error with status 400 Bad Request: No Body"
495 );
496
497 let _mock4 = server
499 .mock("GET", "/")
500 .with_status(502)
501 .with_body("")
502 .create_async()
503 .await;
504
505 let _mock5 = server
506 .mock("GET", "/")
507 .with_status(200)
508 .with_body("")
509 .create_async()
510 .await;
511
512 let r = do_request().await.unwrap();
513 assert_eq!(r.status(), StatusCode::OK);
514
515 let _mock6 = server
517 .mock("GET", "/")
518 .with_status(204)
519 .with_body("")
520 .create_async()
521 .await;
522
523 let r = do_request().await.unwrap();
524 assert_eq!(r.status(), StatusCode::NO_CONTENT);
525
526 let _mock7 = server
528 .mock("GET", "/")
529 .with_status(302)
530 .with_header("location", "/foo")
531 .with_body("")
532 .create_async()
533 .await;
534
535 let _mock8 = server
536 .mock("GET", "/foo")
537 .with_status(200)
538 .with_body("")
539 .create_async()
540 .await;
541
542 let r = do_request().await.unwrap();
543 assert_eq!(r.status(), StatusCode::OK);
544 assert_eq!(r.url().path(), "/foo");
545
546 let _mock_redirect = server
548 .mock("GET", "/")
549 .with_status(302)
550 .with_body("")
551 .create_async()
552 .await;
553
554 let e = do_request().await.unwrap_err();
555 assert!(matches!(e, Error::BareRedirect));
556 assert_eq!(
557 e.to_string(),
558 "Received redirect without LOCATION, this normally indicates an incorrectly configured region"
559 );
560
561 let _timeout_mock1 = server
563 .mock("GET", "/")
564 .with_status(200)
565 .with_body("")
566 .create_async()
567 .await;
568
569 let _timeout_mock2 = server
570 .mock("GET", "/")
571 .with_status(200)
572 .with_body("")
573 .create_async()
574 .await;
575
576 do_request().await.unwrap();
577
578 let sensitive_url = format!("{}/SENSITIVE", server_url);
580 let _sensitive_mock = server
581 .mock("GET", "/SENSITIVE")
582 .with_status(502)
583 .with_body("ignored")
584 .create_async()
585 .await;
586
587 let res = client
588 .request(Method::GET, &sensitive_url)
589 .send_retry(&retry, service.clone())
590 .await;
591 let err = res.unwrap_err().to_string();
592 assert!(err.contains("SENSITIVE"), "{err}");
593
594 let _sensitive_mock2 = server
596 .mock("GET", "/SENSITIVE")
597 .with_status(502)
598 .with_body("ignored")
599 .create_async()
600 .await;
601
602 let req = client
603 .request(Method::GET, &sensitive_url)
604 .retryable(&retry, service.clone())
605 .sensitive(true);
606 let err = req.send().await.unwrap_err().to_string();
607 assert!(!err.contains("SENSITIVE"), "{err}");
608 }
609}