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