use reqwest::header::HeaderMap;
use reqwest::{Client, ClientBuilder, StatusCode, Url};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration;
use tokio::sync::watch;
pub(crate) static CLIENT: LazyLock<Client> = LazyLock::new(|| {
ClientBuilder::new()
.user_agent(format!("vfox.rs/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("Failed to create reqwest client")
});
pub(crate) const URL_REWRITER_REGISTRY_KEY: &str = "url_rewriter_fn";
pub(crate) const HTTP_HEADERS_RESOLVER_REGISTRY_KEY: &str = "http_headers_resolver_fn";
pub(crate) type HttpHeadersResolver = Arc<dyn Fn(&Url) -> HeaderMap + Send + Sync>;
static HTTP_CANCELLATION: LazyLock<HttpCancellation> = LazyLock::new(Default::default);
#[derive(Clone)]
pub(crate) struct HttpCancellation {
signal: watch::Sender<()>,
}
pub(crate) struct HttpCancellationReceiver {
signal: watch::Receiver<()>,
}
impl Default for HttpCancellation {
fn default() -> Self {
Self {
signal: watch::channel(()).0,
}
}
}
impl HttpCancellation {
pub(crate) fn cancel(&self) {
self.signal.send_replace(());
}
pub(crate) fn subscribe(&self) -> HttpCancellationReceiver {
HttpCancellationReceiver {
signal: self.signal.subscribe(),
}
}
}
impl HttpCancellationReceiver {
pub(crate) async fn cancelled(&mut self) {
let _ = self.signal.changed().await;
}
}
pub fn cancel_http_requests() {
HTTP_CANCELLATION.cancel();
}
pub(crate) fn http_cancellation() -> &'static HttpCancellation {
&HTTP_CANCELLATION
}
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 sigstore_retry_config() -> mise_sigstore::RetryConfig {
mise_sigstore::RetryConfig {
retries: http_retries(),
..Default::default()
}
}
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"))
}