Skip to main content

reqwest/
retry.rs

1//! Retry requests
2//!
3//! A `Client` has the ability to retry requests, by sending additional copies
4//! to the server if a response is considered retryable.
5//!
6//! The [`Builder`] makes it easier to configure what requests to retry, along
7//! with including best practices by default, such as a retry budget.
8//!
9//! # Defaults
10//!
11//! The default retry behavior of a `Client` is to only retry requests where an
12//! error or low-level protocol NACK is encountered that is known to be safe to
13//! retry. Note however that providing a specific retry policy will override
14//! the default, and you will need to explicitly include that behavior.
15//!
16//! All policies default to including a retry budget that permits 20% extra
17//! requests to be sent.
18//!
19//! # Scoped
20//!
21//! A client's retry policy is scoped. That means that the policy doesn't
22//! apply to all requests, but only those within a user-defined scope.
23//!
24//! Since all policies include a budget by default, it doesn't make sense to
25//! apply it on _all_ requests. Rather, the retry history applied by a budget
26//! should likely only be applied to the same host.
27//!
28//! # Classifiers
29//!
30//! A retry policy needs to be configured with a classifier that determines
31//! if a request should be retried. Knowledge of the destination server's
32//! behavior is required to make a safe classifier. **Requests should not be
33//! retried** if the server cannot safely handle the same request twice, or if
34//! it causes side effects.
35//!
36//! Some common properties to check include if the request method is
37//! idempotent, or if the response status code indicates a transient error.
38
39use std::sync::Arc;
40use std::time::Duration;
41
42use tower::retry::budget::{Budget as _, TpsBudget as Budget};
43
44#[cfg(docsrs)]
45pub use classify::ReqRep;
46
47/// Builder to configure retries
48///
49/// Construct with [`for_host()`].
50#[derive(Debug)]
51pub struct Builder {
52    //backoff: Backoff,
53    budget: Option<f32>,
54    classifier: classify::Classifier,
55    max_retries_per_request: u32,
56    scope: scope::Scoped,
57}
58
59/// The internal type that we convert the builder into, that implements
60/// tower::retry::Policy privately.
61#[derive(Clone, Debug)]
62pub(crate) struct Policy {
63    budget: Option<Arc<Budget>>,
64    classifier: classify::Classifier,
65    max_retries_per_request: u32,
66    retry_cnt: u32,
67    scope: scope::Scoped,
68}
69
70//#[derive(Debug)]
71//struct Backoff;
72
73/// Create a retry builder with a request scope.
74///
75/// To provide a scope that isn't a closure, use the more general
76/// [`Builder::scoped()`].
77pub fn for_host<S>(host: S) -> Builder
78where
79    S: for<'a> PartialEq<&'a str> + Send + Sync + 'static,
80{
81    scoped(move |req| host == req.uri().host().unwrap_or(""))
82}
83
84/// Create a retry policy that will never retry any request.
85///
86/// This is useful for disabling the `Client`s default behavior of retrying
87/// protocol nacks.
88pub fn never() -> Builder {
89    scoped(|_| false).no_budget()
90}
91
92fn scoped<F>(func: F) -> Builder
93where
94    F: Fn(&Req) -> bool + Send + Sync + 'static,
95{
96    Builder::scoped(scope::ScopeFn(func))
97}
98
99// ===== impl Builder =====
100
101impl Builder {
102    /// Create a scoped retry policy.
103    ///
104    /// For a more convenient constructor, see [`for_host()`].
105    pub fn scoped(scope: impl scope::Scope) -> Self {
106        Self {
107            budget: Some(0.2),
108            classifier: classify::Classifier::Never,
109            max_retries_per_request: 2, // on top of the original
110            scope: scope::Scoped::Dyn(Arc::new(scope)),
111        }
112    }
113
114    /// Set no retry budget.
115    ///
116    /// Sets that no budget will be enforced. This could also be considered
117    /// to be an infinite budget.
118    ///
119    /// This is NOT recommended. Disabling the budget can make your system more
120    /// susceptible to retry storms.
121    pub fn no_budget(mut self) -> Self {
122        self.budget = None;
123        self
124    }
125
126    /// Sets the max extra load the budget will allow.
127    ///
128    /// Think of the amount of requests your client generates, and how much
129    /// load that puts on the server. This option configures as a percentage
130    /// how much extra load is allowed via retries.
131    ///
132    /// For example, if you send 1,000 requests per second, setting a maximum
133    /// extra load value of `0.3` would allow 300 more requests per second
134    /// in retries. A value of `2.5` would allow 2,500 more requests.
135    ///
136    /// # Panics
137    ///
138    /// The `extra_percent` value must be within reasonable values for a
139    /// percentage. This method will panic if it is less than `0.0`, or greater
140    /// than `1000.0`.
141    pub fn max_extra_load(mut self, extra_percent: f32) -> Self {
142        assert!(extra_percent >= 0.0);
143        assert!(extra_percent <= 1000.0);
144        self.budget = Some(extra_percent);
145        self
146    }
147
148    // pub fn max_replay_body
149
150    /// Set the max retries allowed per request.
151    ///
152    /// For each logical (initial) request, only retry up to `max` times.
153    ///
154    /// This value is used in combination with a token budget that is applied
155    /// to all requests. Even if the budget would allow more requests, this
156    /// limit will prevent. Likewise, the budget may prevent retrying up to
157    /// `max` times. This setting prevents a single request from consuming
158    /// the entire budget.
159    ///
160    /// Default is currently 2 retries.
161    pub fn max_retries_per_request(mut self, max: u32) -> Self {
162        self.max_retries_per_request = max;
163        self
164    }
165
166    /// Provide a classifier to determine if a request should be retried.
167    ///
168    /// # Example
169    ///
170    /// ```rust
171    /// # fn with_builder(builder: reqwest::retry::Builder) -> reqwest::retry::Builder {
172    /// builder.classify_fn(|req_rep| {
173    ///     match (req_rep.method(), req_rep.status()) {
174    ///         (&http::Method::GET, Some(http::StatusCode::SERVICE_UNAVAILABLE)) => {
175    ///             req_rep.retryable()
176    ///         },
177    ///         _ => req_rep.success()
178    ///     }
179    /// })
180    /// # }
181    /// ```
182    pub fn classify_fn<F>(self, func: F) -> Self
183    where
184        F: Fn(classify::ReqRep<'_>) -> classify::Action + Send + Sync + 'static,
185    {
186        self.classify(classify::ClassifyFn(func))
187    }
188
189    /// Provide a classifier to determine if a request should be retried.
190    pub fn classify(mut self, classifier: impl classify::Classify) -> Self {
191        self.classifier = classify::Classifier::Dyn(Arc::new(classifier));
192        self
193    }
194
195    pub(crate) fn default() -> Builder {
196        Self {
197            // unscoped protocols nacks doesn't need a budget
198            budget: None,
199            classifier: classify::Classifier::ProtocolNacks,
200            max_retries_per_request: 2, // on top of the original
201            scope: scope::Scoped::Unscoped,
202        }
203    }
204
205    pub(crate) fn into_policy(self) -> Policy {
206        let budget = self
207            .budget
208            .map(|p| Arc::new(Budget::new(Duration::from_secs(10), 10, p)));
209        Policy {
210            budget,
211            classifier: self.classifier,
212            max_retries_per_request: self.max_retries_per_request,
213            retry_cnt: 0,
214            scope: self.scope,
215        }
216    }
217}
218
219// ===== internal ======
220
221type Req = http::Request<crate::async_impl::body::Body>;
222
223impl<B> tower::retry::Policy<Req, http::Response<B>, crate::Error> for Policy {
224    // TODO? backoff futures...
225    type Future = std::future::Ready<()>;
226
227    fn retry(
228        &mut self,
229        req: &mut Req,
230        result: &mut crate::Result<http::Response<B>>,
231    ) -> Option<Self::Future> {
232        match self.classifier.classify(req, result) {
233            classify::Action::Success => {
234                log::trace!("shouldn't retry!");
235                if let Some(ref budget) = self.budget {
236                    budget.deposit();
237                }
238                None
239            }
240            classify::Action::Retryable => {
241                log::trace!("could retry!");
242                if self.budget.as_ref().map(|b| b.withdraw()).unwrap_or(true) {
243                    self.retry_cnt += 1;
244                    Some(std::future::ready(()))
245                } else {
246                    log::debug!("retryable but could not withdraw from budget");
247                    None
248                }
249            }
250        }
251    }
252
253    fn clone_request(&mut self, req: &Req) -> Option<Req> {
254        if self.retry_cnt > 0 && !self.scope.applies_to(req) {
255            return None;
256        }
257        if self.retry_cnt >= self.max_retries_per_request {
258            log::trace!("max_retries_per_request hit");
259            return None;
260        }
261        let body = req.body().try_clone()?;
262        let mut new = http::Request::new(body);
263        *new.method_mut() = req.method().clone();
264        *new.uri_mut() = req.uri().clone();
265        *new.version_mut() = req.version();
266        *new.headers_mut() = req.headers().clone();
267        *new.extensions_mut() = req.extensions().clone();
268
269        Some(new)
270    }
271}
272
273fn is_retryable_error(err: &crate::Error) -> bool {
274    use std::error::Error as _;
275
276    #[cfg(feature = "http3")]
277    {
278        let mut source = err.source();
279        while let Some(error) = source {
280            if error.is::<crate::async_impl::h3_client::transport::GoAway>() {
281                return true;
282            }
283            source = error.source();
284        }
285    }
286    // pop the reqwest::Error
287    let err = if let Some(err) = err.source() {
288        err
289    } else {
290        return false;
291    };
292    // pop the legacy::Error
293    let err = if let Some(err) = err.source() {
294        err
295    } else {
296        return false;
297    };
298
299    #[cfg(not(feature = "http2"))]
300    let _err = err;
301
302    #[cfg(feature = "http2")]
303    if let Some(cause) = err.source() {
304        if let Some(err) = cause.downcast_ref::<h2::Error>() {
305            // They sent us a graceful shutdown, try with a new connection!
306            if err.is_go_away() && err.is_remote() && err.reason() == Some(h2::Reason::NO_ERROR) {
307                return true;
308            }
309
310            // REFUSED_STREAM was sent from the server, which is safe to retry.
311            // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.7-3.2
312            if err.is_reset() && err.is_remote() && err.reason() == Some(h2::Reason::REFUSED_STREAM)
313            {
314                return true;
315            }
316        }
317    }
318    false
319}
320
321// sealed types and traits on purpose while exploring design space
322mod scope {
323    pub trait Scope: Send + Sync + 'static {
324        fn applies_to(&self, req: &super::Req) -> bool;
325    }
326
327    // I think scopes likely make the most sense being to hosts.
328    // If that's the case, then it should probably be easiest to check for
329    // the host. Perhaps also considering the ability to add more things
330    // to scope off in the future...
331
332    // For Future Whoever: making a blanket impl for any closure sounds nice,
333    // but it causes inference issues at the call site. Every closure would
334    // need to include `: ReqRep` in the arguments.
335    //
336    // An alternative is to make things like `ScopeFn`. Slightly more annoying,
337    // but also more forwards-compatible. :shrug:
338
339    pub struct ScopeFn<F>(pub(super) F);
340
341    impl<F> Scope for ScopeFn<F>
342    where
343        F: Fn(&super::Req) -> bool + Send + Sync + 'static,
344    {
345        fn applies_to(&self, req: &super::Req) -> bool {
346            (self.0)(req)
347        }
348    }
349
350    #[derive(Clone)]
351    pub(super) enum Scoped {
352        Unscoped,
353        Dyn(std::sync::Arc<dyn Scope>),
354    }
355
356    impl Scoped {
357        pub(super) fn applies_to(&self, req: &super::Req) -> bool {
358            let ret = match self {
359                Self::Unscoped => true,
360                Self::Dyn(s) => s.applies_to(req),
361            };
362            log::trace!("retry in scope: {ret}");
363            ret
364        }
365    }
366
367    impl std::fmt::Debug for Scoped {
368        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369            match self {
370                Self::Unscoped => f.write_str("Unscoped"),
371                Self::Dyn(_) => f.write_str("Scoped"),
372            }
373        }
374    }
375}
376
377// sealed types and traits on purpose while exploring design space
378mod classify {
379    pub trait Classify: Send + Sync + 'static {
380        fn classify(&self, req_rep: ReqRep<'_>) -> Action;
381    }
382
383    // For Future Whoever: making a blanket impl for any closure sounds nice,
384    // but it causes inference issues at the call site. Every closure would
385    // need to include `: ReqRep` in the arguments.
386    //
387    // An alternative is to make things like `ClassifyFn`. Slightly more
388    // annoying, but also more forwards-compatible. :shrug:
389    pub struct ClassifyFn<F>(pub(super) F);
390
391    impl<F> Classify for ClassifyFn<F>
392    where
393        F: Fn(ReqRep<'_>) -> Action + Send + Sync + 'static,
394    {
395        fn classify(&self, req_rep: ReqRep<'_>) -> Action {
396            (self.0)(req_rep)
397        }
398    }
399
400    /// A request/response result to inspect for possible retries.
401    ///
402    /// This is passed to a `classify` function.
403    #[derive(Debug)]
404    pub struct ReqRep<'a>(&'a super::Req, Result<http::StatusCode, &'a crate::Error>);
405
406    impl ReqRep<'_> {
407        /// Access the request method.
408        pub fn method(&self) -> &http::Method {
409            self.0.method()
410        }
411
412        /// Access the request URI.
413        pub fn uri(&self) -> &http::Uri {
414            self.0.uri()
415        }
416
417        /// Access the response status, if it did not error.
418        pub fn status(&self) -> Option<http::StatusCode> {
419            self.1.ok()
420        }
421
422        /// Access the error, if a response was not received.
423        pub fn error(&self) -> Option<&(dyn std::error::Error + 'static)> {
424            self.1.as_ref().err().map(|e| &**e as _)
425        }
426
427        /// Classify this attempt as retryable.
428        pub fn retryable(self) -> Action {
429            Action::Retryable
430        }
431
432        /// Classify this attempt as success.
433        ///
434        /// Even if it was a domain error, a "success" means it will not retry.
435        pub fn success(self) -> Action {
436            Action::Success
437        }
438
439        fn is_protocol_nack(&self) -> bool {
440            self.1
441                .as_ref()
442                .err()
443                .map(|&e| super::is_retryable_error(e))
444                .unwrap_or(false)
445        }
446    }
447
448    #[must_use]
449    #[derive(Debug)]
450    pub enum Action {
451        Success,
452        Retryable,
453    }
454
455    #[derive(Clone)]
456    pub(super) enum Classifier {
457        Never,
458        ProtocolNacks,
459        Dyn(std::sync::Arc<dyn Classify>),
460    }
461
462    impl Classifier {
463        pub(super) fn classify<B>(
464            &self,
465            req: &super::Req,
466            res: &Result<http::Response<B>, crate::Error>,
467        ) -> Action {
468            let req_rep = ReqRep(req, res.as_ref().map(|r| r.status()));
469            match self {
470                Self::Never => Action::Success,
471                Self::ProtocolNacks => {
472                    if req_rep.is_protocol_nack() {
473                        Action::Retryable
474                    } else {
475                        Action::Success
476                    }
477                }
478                Self::Dyn(c) => c.classify(req_rep),
479            }
480        }
481    }
482
483    impl std::fmt::Debug for Classifier {
484        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485            match self {
486                Self::Never => f.write_str("Never"),
487                Self::ProtocolNacks => f.write_str("ProtocolNacks"),
488                Self::Dyn(_) => f.write_str("Classifier"),
489            }
490        }
491    }
492}