#![cfg_attr(doc, doc = include_str!("../README.md"))]
use core::time::Duration;
use std::time::SystemTime;
pub fn client_builder() -> reqwest::ClientBuilder {
let builder =
reqwest::Client::builder().redirect(reqwest::redirect::Policy::custom(|attempt| {
if !attempt
.previous()
.last()
.is_some_and(|url| url.origin() == attempt.url().origin())
{
attempt.stop()
} else if attempt.previous().len() > 10 {
attempt.error("too many redirects")
} else {
attempt.follow()
}
}));
#[cfg(feature = "rustls")]
let builder = builder.tls_backend_rustls();
#[cfg(all(feature = "native-tls", not(feature = "rustls")))]
let builder = builder.tls_backend_native();
builder
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
max_retries: u8,
initial_delay: Duration,
max_delay: Duration,
non_idempotent: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self::new(2, Duration::from_millis(200), Duration::from_secs(30))
}
}
impl RetryPolicy {
#[must_use]
pub const fn new(max_retries: u8, initial_delay: Duration, max_delay: Duration) -> Self {
Self {
max_retries,
initial_delay,
max_delay,
non_idempotent: false,
}
}
#[must_use]
pub const fn none() -> Self {
Self::new(0, Duration::ZERO, Duration::ZERO)
}
#[must_use]
pub const fn retry_non_idempotent(mut self, enabled: bool) -> Self {
self.non_idempotent = enabled;
self
}
const fn permits(self, method: &reqwest::Method) -> bool {
self.max_retries != 0
&& (self.non_idempotent
|| matches!(
*method,
reqwest::Method::GET | reqwest::Method::HEAD | reqwest::Method::OPTIONS
))
}
fn delay(self, response: &reqwest::Response, attempt: u8) -> Option<Duration> {
let backoff = self
.initial_delay
.saturating_mul(2_u32.saturating_pow(u32::from(attempt)))
.min(self.max_delay);
let delay = match response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
{
Some(value) if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) => {
Duration::from_secs(value.parse().ok()?)
}
Some(value) => httpdate::parse_http_date(value)
.ok()
.map_or(backoff, |date| {
date.duration_since(SystemTime::now())
.unwrap_or(Duration::ZERO)
}),
None => backoff,
};
(delay <= self.max_delay).then_some(delay)
}
}
pub async fn execute(
client: &reqwest::Client,
mut request: reqwest::Request,
policy: RetryPolicy,
) -> Result<reqwest::Response, reqwest::Error> {
if !policy.permits(request.method()) {
return client.execute(request).await;
}
let Some(template) = request.try_clone() else {
return client.execute(request).await;
};
let mut attempt = 0;
loop {
let response = client.execute(request).await?;
if response.status() != reqwest::StatusCode::TOO_MANY_REQUESTS
|| attempt >= policy.max_retries
{
return Ok(response);
}
let Some(delay) = policy.delay(&response, attempt) else {
return Ok(response);
};
let Some(next) = template.try_clone() else {
return Ok(response);
};
drop(response);
tokio::time::sleep(delay).await;
attempt = attempt.saturating_add(1);
request = next;
}
}