use reqwest::{Client, ClientBuilder, StatusCode};
use std::sync::LazyLock;
use std::time::Duration;
pub static CLIENT: LazyLock<Client> = LazyLock::new(|| {
ClientBuilder::new()
.user_agent(format!("vfox.rs/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("Failed to create reqwest client")
});
const DEFAULT_HTTP_RETRIES: usize = 3;
const BACKOFF_SCHEDULE_MS: &[u64] = &[200, 1_000, 4_000, 15_000];
fn http_retries() -> usize {
std::env::var("MISE_HTTP_RETRIES")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(DEFAULT_HTTP_RETRIES)
}
pub(crate) fn http_retry_attempts() -> usize {
http_retries().saturating_add(1)
}
pub(crate) fn should_retry_status(status: StatusCode) -> bool {
let code = status.as_u16();
code == 408 || code == 429 || (500..600).contains(&code)
}
pub(crate) fn is_transient(err: &reqwest::Error) -> bool {
if err.is_timeout() || err.is_connect() || err.is_body() {
return true;
}
if let Some(status) = err.status() {
return should_retry_status(status);
}
false
}
pub(crate) fn retry_delay(attempt: usize) -> Duration {
let base_ms = BACKOFF_SCHEDULE_MS
.get(attempt)
.copied()
.unwrap_or_else(|| *BACKOFF_SCHEDULE_MS.last().unwrap());
let jitter_pct = 50
+ (std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() % 50)
.unwrap_or(0)) as u64;
Duration::from_millis(base_ms * jitter_pct / 100)
}
pub(crate) async fn retry_async<F, Fut, T>(
url: &str,
mut f: F,
) -> std::result::Result<T, reqwest::Error>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = std::result::Result<T, reqwest::Error>>,
{
let attempts = http_retry_attempts().max(1);
let mut last_err: Option<reqwest::Error> = None;
for attempt in 0..attempts {
match f().await {
Ok(value) => return Ok(value),
Err(err) => {
if !is_transient(&err) || attempt + 1 >= attempts {
return Err(err);
}
let delay = retry_delay(attempt);
log::warn!(
"HTTP {} attempt {} failed (transient): {}; retrying in {:?}",
url,
attempt + 1,
err,
delay
);
last_err = Some(err);
tokio::time::sleep(delay).await;
}
}
}
Err(last_err.expect("retry loop should always return"))
}