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 // pop the reqwest::Error
277 let err = if let Some(err) = err.source() {
278 err
279 } else {
280 return false;
281 };
282 // pop the legacy::Error
283 let err = if let Some(err) = err.source() {
284 err
285 } else {
286 return false;
287 };
288
289 #[cfg(not(any(feature = "http3", feature = "http2")))]
290 let _err = err;
291
292 #[cfg(feature = "http3")]
293 if let Some(cause) = err.source() {
294 if let Some(err) = cause.downcast_ref::<h3::error::ConnectionError>() {
295 log::trace!("determining if HTTP/3 error {err} can be retried");
296 // TODO: Does h3 provide an API for checking the error?
297 return err.to_string().as_str() == "timeout";
298 }
299 }
300
301 #[cfg(feature = "http2")]
302 if let Some(cause) = err.source() {
303 if let Some(err) = cause.downcast_ref::<h2::Error>() {
304 // They sent us a graceful shutdown, try with a new connection!
305 if err.is_go_away() && err.is_remote() && err.reason() == Some(h2::Reason::NO_ERROR) {
306 return true;
307 }
308
309 // REFUSED_STREAM was sent from the server, which is safe to retry.
310 // https://www.rfc-editor.org/rfc/rfc9113.html#section-8.7-3.2
311 if err.is_reset() && err.is_remote() && err.reason() == Some(h2::Reason::REFUSED_STREAM)
312 {
313 return true;
314 }
315 }
316 }
317 false
318}
319
320// sealed types and traits on purpose while exploring design space
321mod scope {
322 pub trait Scope: Send + Sync + 'static {
323 fn applies_to(&self, req: &super::Req) -> bool;
324 }
325
326 // I think scopes likely make the most sense being to hosts.
327 // If that's the case, then it should probably be easiest to check for
328 // the host. Perhaps also considering the ability to add more things
329 // to scope off in the future...
330
331 // For Future Whoever: making a blanket impl for any closure sounds nice,
332 // but it causes inference issues at the call site. Every closure would
333 // need to include `: ReqRep` in the arguments.
334 //
335 // An alternative is to make things like `ScopeFn`. Slightly more annoying,
336 // but also more forwards-compatible. :shrug:
337
338 pub struct ScopeFn<F>(pub(super) F);
339
340 impl<F> Scope for ScopeFn<F>
341 where
342 F: Fn(&super::Req) -> bool + Send + Sync + 'static,
343 {
344 fn applies_to(&self, req: &super::Req) -> bool {
345 (self.0)(req)
346 }
347 }
348
349 #[derive(Clone)]
350 pub(super) enum Scoped {
351 Unscoped,
352 Dyn(std::sync::Arc<dyn Scope>),
353 }
354
355 impl Scoped {
356 pub(super) fn applies_to(&self, req: &super::Req) -> bool {
357 let ret = match self {
358 Self::Unscoped => true,
359 Self::Dyn(s) => s.applies_to(req),
360 };
361 log::trace!("retry in scope: {ret}");
362 ret
363 }
364 }
365
366 impl std::fmt::Debug for Scoped {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 match self {
369 Self::Unscoped => f.write_str("Unscoped"),
370 Self::Dyn(_) => f.write_str("Scoped"),
371 }
372 }
373 }
374}
375
376// sealed types and traits on purpose while exploring design space
377mod classify {
378 pub trait Classify: Send + Sync + 'static {
379 fn classify(&self, req_rep: ReqRep<'_>) -> Action;
380 }
381
382 // For Future Whoever: making a blanket impl for any closure sounds nice,
383 // but it causes inference issues at the call site. Every closure would
384 // need to include `: ReqRep` in the arguments.
385 //
386 // An alternative is to make things like `ClassifyFn`. Slightly more
387 // annoying, but also more forwards-compatible. :shrug:
388 pub struct ClassifyFn<F>(pub(super) F);
389
390 impl<F> Classify for ClassifyFn<F>
391 where
392 F: Fn(ReqRep<'_>) -> Action + Send + Sync + 'static,
393 {
394 fn classify(&self, req_rep: ReqRep<'_>) -> Action {
395 (self.0)(req_rep)
396 }
397 }
398
399 /// A request/response result to inspect for possible retries.
400 ///
401 /// This is passed to a `classify` function.
402 #[derive(Debug)]
403 pub struct ReqRep<'a>(&'a super::Req, Result<http::StatusCode, &'a crate::Error>);
404
405 impl ReqRep<'_> {
406 /// Access the request method.
407 pub fn method(&self) -> &http::Method {
408 self.0.method()
409 }
410
411 /// Access the request URI.
412 pub fn uri(&self) -> &http::Uri {
413 self.0.uri()
414 }
415
416 /// Access the response status, if it did not error.
417 pub fn status(&self) -> Option<http::StatusCode> {
418 self.1.ok()
419 }
420
421 /// Access the error, if a response was not received.
422 pub fn error(&self) -> Option<&(dyn std::error::Error + 'static)> {
423 self.1.as_ref().err().map(|e| &**e as _)
424 }
425
426 /// Classify this attempt as retryable.
427 pub fn retryable(self) -> Action {
428 Action::Retryable
429 }
430
431 /// Classify this attempt as success.
432 ///
433 /// Even if it was a domain error, a "success" means it will not retry.
434 pub fn success(self) -> Action {
435 Action::Success
436 }
437
438 fn is_protocol_nack(&self) -> bool {
439 self.1
440 .as_ref()
441 .err()
442 .map(|&e| super::is_retryable_error(e))
443 .unwrap_or(false)
444 }
445 }
446
447 #[must_use]
448 #[derive(Debug)]
449 pub enum Action {
450 Success,
451 Retryable,
452 }
453
454 #[derive(Clone)]
455 pub(super) enum Classifier {
456 Never,
457 ProtocolNacks,
458 Dyn(std::sync::Arc<dyn Classify>),
459 }
460
461 impl Classifier {
462 pub(super) fn classify<B>(
463 &self,
464 req: &super::Req,
465 res: &Result<http::Response<B>, crate::Error>,
466 ) -> Action {
467 let req_rep = ReqRep(req, res.as_ref().map(|r| r.status()));
468 match self {
469 Self::Never => Action::Success,
470 Self::ProtocolNacks => {
471 if req_rep.is_protocol_nack() {
472 Action::Retryable
473 } else {
474 Action::Success
475 }
476 }
477 Self::Dyn(c) => c.classify(req_rep),
478 }
479 }
480 }
481
482 impl std::fmt::Debug for Classifier {
483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 match self {
485 Self::Never => f.write_str("Never"),
486 Self::ProtocolNacks => f.write_str("ProtocolNacks"),
487 Self::Dyn(_) => f.write_str("Classifier"),
488 }
489 }
490 }
491}