use reqwest::{Client, ClientBuilder};
use std::sync::LazyLock;
use std::time::Duration;
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HttpTimeouts {
pub connect: Option<Duration>,
pub request: Option<Duration>,
pub download: Option<Duration>,
}
impl Default for HttpTimeouts {
fn default() -> Self {
Self {
connect: Some(DEFAULT_CONNECT_TIMEOUT),
request: Some(DEFAULT_REQUEST_TIMEOUT),
download: Some(DEFAULT_DOWNLOAD_TIMEOUT),
}
}
}
impl HttpTimeouts {
#[must_use]
pub const fn from_seconds(connect: u64, request: u64, download: u64) -> Self {
Self {
connect: duration_or_disabled(connect),
request: duration_or_disabled(request),
download: duration_or_disabled(download),
}
}
fn apply_to(self, mut builder: ClientBuilder) -> ClientBuilder {
if let Some(request) = self.request {
builder = builder.timeout(request);
}
if let Some(connect) = self.connect {
builder = builder.connect_timeout(connect);
}
builder
}
}
const fn duration_or_disabled(seconds: u64) -> Option<Duration> {
if seconds == 0 {
None
} else {
Some(Duration::from_secs(seconds))
}
}
static HTTP_TIMEOUTS: LazyLock<arc_swap::ArcSwap<HttpTimeouts>> =
LazyLock::new(|| arc_swap::ArcSwap::from_pointee(HttpTimeouts::default()));
pub fn install_http_timeouts(timeouts: HttpTimeouts) {
HTTP_TIMEOUTS.store(std::sync::Arc::new(timeouts));
}
#[must_use]
pub fn http_timeouts() -> HttpTimeouts {
**HTTP_TIMEOUTS.load()
}
fn apply_platform_proxy_policy(builder: ClientBuilder) -> ClientBuilder {
#[cfg(target_os = "macos")]
{
builder.no_proxy()
}
#[cfg(not(target_os = "macos"))]
{
builder
}
}
fn try_build_client<F>(configure: F) -> Result<Client, reqwest::Error>
where
F: Fn(ClientBuilder) -> ClientBuilder,
{
let primary_builder = configure(apply_platform_proxy_policy(ClientBuilder::new()));
match primary_builder.build() {
Ok(client) => Ok(client),
Err(primary_err) => {
let fallback_builder = HttpTimeouts::default().apply_to(apply_platform_proxy_policy(ClientBuilder::new()));
match fallback_builder.build() {
Ok(client) => Ok(client),
Err(fallback_err) => {
tracing::error!(
primary_error = %primary_err,
fallback_error = %fallback_err,
"HTTP client creation failed with both primary and fallback configurations"
);
Err(fallback_err)
}
}
}
}
}
fn build_client<F>(configure: F) -> Client
where
F: Fn(ClientBuilder) -> ClientBuilder,
{
try_build_client(configure).unwrap_or_else(|e| {
tracing::error!(
error = %e,
"HTTP client build failed (primary + fallback); falling back to bare default client. \
This usually indicates a TLS configuration issue or system resource exhaustion."
);
Client::new()
})
}
pub fn create_default_client() -> Client {
build_client(|builder| http_timeouts().apply_to(builder))
}
pub fn create_client_with_timeout(timeout: Duration) -> Client {
let connect = http_timeouts().connect;
build_client(move |builder| {
let builder = builder.timeout(timeout);
match connect {
Some(connect) => builder.connect_timeout(connect),
None => builder,
}
})
}
pub fn create_client_with_timeouts(connect_timeout: Duration, request_timeout: Duration) -> Client {
build_client(|builder| builder.timeout(request_timeout).connect_timeout(connect_timeout))
}
pub fn create_client_without_redirects(
connect_timeout: Duration,
request_timeout: Duration,
) -> Result<Client, reqwest::Error> {
apply_platform_proxy_policy(ClientBuilder::new())
.timeout(request_timeout)
.connect_timeout(connect_timeout)
.redirect(reqwest::redirect::Policy::none())
.build()
}
pub fn create_default_client_without_redirects() -> Result<Client, reqwest::Error> {
let timeouts = http_timeouts();
let mut builder = apply_platform_proxy_policy(ClientBuilder::new()).redirect(reqwest::redirect::Policy::none());
if let Some(request) = timeouts.request {
builder = builder.timeout(request);
}
if let Some(connect) = timeouts.connect {
builder = builder.connect_timeout(connect);
}
builder.build()
}
pub fn create_client_with_user_agent(user_agent: &str) -> Client {
let timeouts = http_timeouts();
build_client(move |mut builder| {
builder = builder.user_agent(user_agent);
timeouts.apply_to(builder)
})
}
pub fn create_streaming_client() -> Client {
let connect = http_timeouts().connect;
build_client(move |builder| {
let builder = builder.tcp_keepalive(Some(Duration::from_secs(60)));
match connect {
Some(connect) => builder.connect_timeout(connect),
None => builder,
}
})
}
pub fn create_download_client(user_agent: &str) -> Client {
let timeouts = http_timeouts();
build_client(move |mut builder| {
builder = builder.user_agent(user_agent).tcp_keepalive(Some(Duration::from_secs(60)));
if let Some(download) = timeouts.download {
builder = builder.timeout(download);
}
if let Some(connect) = timeouts.connect {
builder = builder.connect_timeout(connect);
}
builder
})
}
pub fn get_or_create_default_client(existing: Option<Client>) -> Client {
existing.unwrap_or_else(create_default_client)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_timeouts_match_historical_constants() {
let defaults = HttpTimeouts::default();
assert_eq!(defaults.connect, Some(Duration::from_secs(5)));
assert_eq!(defaults.request, Some(Duration::from_secs(30)));
assert_eq!(defaults.download, Some(Duration::from_secs(300)));
}
#[test]
fn zero_seconds_disables_the_phase() {
let timeouts = HttpTimeouts::from_seconds(0, 0, 0);
assert_eq!(timeouts.connect, None);
assert_eq!(timeouts.request, None);
assert_eq!(timeouts.download, None);
}
#[test]
fn from_seconds_preserves_explicit_values() {
let timeouts = HttpTimeouts::from_seconds(12, 45, 900);
assert_eq!(timeouts.connect, Some(Duration::from_secs(12)));
assert_eq!(timeouts.request, Some(Duration::from_secs(45)));
assert_eq!(timeouts.download, Some(Duration::from_secs(900)));
}
struct InstalledTimeoutsGuard(HttpTimeouts);
impl InstalledTimeoutsGuard {
fn recording_current() -> Self {
Self(http_timeouts())
}
}
impl Drop for InstalledTimeoutsGuard {
fn drop(&mut self) {
install_http_timeouts(self.0);
}
}
#[test]
#[serial_test::serial]
fn installing_timeouts_is_observable_and_restorable() {
let _guard = InstalledTimeoutsGuard::recording_current();
install_http_timeouts(HttpTimeouts::from_seconds(0, 7, 11));
assert_eq!(http_timeouts().connect, None);
assert_eq!(http_timeouts().request, Some(Duration::from_secs(7)));
assert_eq!(http_timeouts().download, Some(Duration::from_secs(11)));
}
#[test]
#[serial_test::serial]
fn clients_build_for_every_timeout_shape() {
let _guard = InstalledTimeoutsGuard::recording_current();
for timeouts in [
HttpTimeouts::default(),
HttpTimeouts::from_seconds(0, 0, 0),
HttpTimeouts::from_seconds(1, 1, 1),
] {
install_http_timeouts(timeouts);
drop(create_default_client());
drop(create_client_with_timeout(Duration::from_secs(3)));
drop(create_streaming_client());
drop(create_client_with_user_agent("vtcode-test"));
drop(create_download_client("vtcode-test"));
let origin_bound =
create_default_client_without_redirects().expect("client without redirects should build");
drop(origin_bound);
}
}
}