use std::time::Duration;
use http::HeaderValue;
use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::rt::TokioExecutor;
use thiserror::Error;
use tower::ServiceBuilder;
use tower::util::BoxCloneSyncService;
use defect_core::error::BoxError;
mod fetch;
mod proxy;
mod retry;
mod trace;
mod user_agent;
pub use fetch::{
FetchHttpClient, build_default_fetch_client_arc, build_fetch_client, build_fetch_client_arc,
};
pub use proxy::{ProxyAwareConnector, build_proxy_connector};
pub use user_agent::default_user_agent;
pub type HttpStack =
BoxCloneSyncService<toac::Request, http::Response<hyper::body::Incoming>, HttpStackError>;
#[derive(Debug, Clone)]
pub struct HttpStackConfig {
pub total_timeout: Option<Duration>,
pub transport_retries: u8,
pub initial_backoff: Duration,
pub user_agent: Option<String>,
pub proxy: ProxyConfig,
}
impl Default for HttpStackConfig {
fn default() -> Self {
Self {
total_timeout: Some(Duration::from_secs(600)),
transport_retries: 2,
initial_backoff: Duration::from_millis(200),
user_agent: None,
proxy: ProxyConfig::FromEnv,
}
}
}
#[derive(Debug, Clone, Default)]
pub enum ProxyConfig {
#[default]
FromEnv,
Explicit(ProxySettings),
Disabled,
}
#[derive(Debug, Clone, Default)]
pub struct ProxySettings {
pub http_proxy: Option<http::Uri>,
pub https_proxy: Option<http::Uri>,
pub no_proxy: Vec<String>,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum HttpStackError {
#[error("HTTP transport error: {0}")]
Transport(#[source] BoxError),
#[error("HTTP request timed out (phase = {phase:?})")]
Timeout { phase: TimeoutPhase },
#[error("HTTP layer config invalid: {hint}")]
Config { hint: String },
#[error("proxy CONNECT failed: {hint}")]
ProxyConnect { hint: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimeoutPhase {
Connect,
ReadHeaders,
ReadBody,
Idle,
Total,
}
pub fn build_http_stack(config: HttpStackConfig) -> Result<HttpStack, HttpStackError> {
let connector = proxy::build_proxy_connector(&config.proxy)?;
let inner =
HyperClient::builder(TokioExecutor::default()).build::<_, toac::body::Body>(connector);
let transport = ServiceBuilder::new()
.map_err(|e: hyper_util::client::legacy::Error| HttpStackError::Transport(BoxError::new(e)))
.service(inner);
let ua_value = match &config.user_agent {
Some(s) => HeaderValue::from_str(s).map_err(|e| HttpStackError::Config {
hint: format!("invalid user_agent: {e}"),
})?,
None => user_agent::default_user_agent(),
};
let retry_layer = (config.transport_retries > 0)
.then(|| retry::TransportRetryLayer::new(config.transport_retries, config.initial_backoff));
let retried = ServiceBuilder::new()
.option_layer(retry_layer)
.service(transport);
let stack = if let Some(timeout) = config.total_timeout {
let s = ServiceBuilder::new()
.layer(user_agent::UserAgentLayer::new(ua_value))
.layer(trace::TraceLayer)
.map_err(map_timeout_error)
.layer(tower::timeout::TimeoutLayer::new(timeout))
.service(retried);
BoxCloneSyncService::new(s)
} else {
let s = ServiceBuilder::new()
.layer(user_agent::UserAgentLayer::new(ua_value))
.layer(trace::TraceLayer)
.service(retried);
BoxCloneSyncService::new(s)
};
Ok(stack)
}
fn map_timeout_error(err: tower::BoxError) -> HttpStackError {
if err.is::<tower::timeout::error::Elapsed>() {
return HttpStackError::Timeout {
phase: TimeoutPhase::Total,
};
}
match err.downcast::<HttpStackError>() {
Ok(boxed) => *boxed,
Err(other) => HttpStackError::Transport(BoxError::from(other)),
}
}