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}