use std::error::Error as StdError;
use std::fmt;
use std::io;
use std::time::Duration;
#[derive(Debug)]
pub struct HttpError {
source: Box<dyn StdError + Send + Sync + 'static>,
pub status: u16,
}
impl HttpError {
pub fn new<E>(source: E, status: u16) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
source: Box::new(source),
status,
}
}
pub fn from_response<E>(err: E, resp: Option<&reqwest::Response>) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self::new(err, resp.map(|r| r.status().as_u16()).unwrap_or(0))
}
}
pub fn http_status(err: &anyhow::Error) -> u16 {
err.chain()
.find_map(|e| e.downcast_ref::<HttpError>().map(|h| h.status))
.unwrap_or(0)
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.source, f)
}
}
impl StdError for HttpError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&*self.source)
}
}
#[derive(Debug)]
pub struct Retriable(Box<dyn StdError + Send + Sync + 'static>);
impl Retriable {
pub fn new<E>(source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self(Box::new(source))
}
}
impl fmt::Display for Retriable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl StdError for Retriable {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&*self.0)
}
}
pub fn is_network_error(err: &(dyn StdError + 'static)) -> bool {
let mut cur: Option<&(dyn StdError + 'static)> = Some(err);
while let Some(e) = cur {
if let Some(io_err) = e.downcast_ref::<io::Error>() {
match io_err.kind() {
io::ErrorKind::UnexpectedEof
| io::ErrorKind::TimedOut
| io::ErrorKind::ConnectionRefused
| io::ErrorKind::ConnectionReset
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::BrokenPipe => return true,
_ => {}
}
let m = io_err.to_string().to_lowercase();
if m == "eof" || m == "unexpected eof" {
return true;
}
}
let s = e.to_string().to_lowercase();
if NETWORK_ERROR_NEEDLES.iter().any(|n| s.contains(n)) {
return true;
}
cur = e.source();
}
false
}
const NETWORK_ERROR_NEEDLES: &[&str] = &[
"connection reset",
"network is unreachable",
"connection closed",
"connection refused",
"tls handshake timeout",
"i/o timeout",
"broken pipe",
"timeout awaiting response headers",
"context deadline exceeded",
"operation timed out",
"the network connection was aborted",
"an existing connection was forcibly closed",
"dns error",
"failed to lookup address",
"no such host is known",
];
pub fn is_retriable(err: &(dyn StdError + 'static)) -> bool {
let mut cur: Option<&(dyn StdError + 'static)> = Some(err);
while let Some(e) = cur {
if e.is::<Retriable>() {
return true;
}
if let Some(http) = e.downcast_ref::<HttpError>()
&& status_is_retriable(http.status)
{
return true;
}
cur = e.source();
}
is_network_error(err)
}
pub fn status_is_retriable(status: u16) -> bool {
status >= 500 || status == 429
}
pub fn is_retriable_opt(err: Option<&(dyn StdError + 'static)>) -> bool {
err.is_some_and(is_retriable)
}
pub fn jitter_duration(base: Duration) -> Duration {
let nanos = base.as_nanos() as u64;
let window = nanos / 5;
if window == 0 {
return base;
}
static JITTER_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let clock = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
let seq = JITTER_SEQ.fetch_add(0x9E37_79B9_7F4A_7C15, std::sync::atomic::Ordering::Relaxed);
let seed = clock ^ seq;
let offset = seed % (window * 2);
let jittered = nanos.saturating_sub(window).saturating_add(offset);
Duration::from_nanos(jittered)
}