use std::net::{
IpAddr,
SocketAddr,
};
use reqwest::dns::{
Addrs,
Name,
Resolve,
Resolving,
};
use reqwest::redirect::Policy;
use crate::HttpConfigError;
use crate::{
HttpClient,
HttpClientOptions,
HttpError,
HttpResult,
};
use qubit_config::ConfigReader;
use qubit_error::{
BoxError,
IntoBoxError,
};
#[derive(Debug, Clone, Copy, Default)]
struct Ipv4OnlyResolver;
impl Resolve for Ipv4OnlyResolver {
fn resolve(&self, name: Name) -> Resolving {
let host = name.as_str().to_string();
Box::pin(async move {
let resolved = tokio::net::lookup_host((host.as_str(), 0))
.await
.map_err(|error| error.into_box_error())?;
filter_ipv4_addrs(&host, resolved)
})
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct HttpClientFactory;
impl HttpClientFactory {
pub fn new() -> Self {
Self
}
pub fn create_default(&self) -> HttpResult<HttpClient> {
self.create(HttpClientOptions::default())
}
pub fn create(&self, options: HttpClientOptions) -> HttpResult<HttpClient> {
options.validate().map_err(map_validation_error)?;
let mut builder = reqwest::Client::builder();
builder = builder.connect_timeout(options.timeouts.connect_timeout);
if let Some(request_timeout) = options.timeouts.request_timeout {
builder = builder.timeout(request_timeout);
}
if let Some(user_agent) = options.user_agent.as_deref() {
builder = builder.user_agent(user_agent);
}
if let Some(max_redirects) = options.max_redirects {
builder = builder.redirect(Policy::limited(max_redirects));
}
if let Some(pool_idle_timeout) = options.pool_idle_timeout {
builder = builder.pool_idle_timeout(pool_idle_timeout);
}
if let Some(pool_max_idle_per_host) = options.pool_max_idle_per_host {
builder = builder.pool_max_idle_per_host(pool_max_idle_per_host);
}
if options.ipv4_only {
builder = builder.dns_resolver(Ipv4OnlyResolver);
}
if options.proxy.enabled {
let host = options
.proxy
.host
.clone()
.expect("proxy.host must exist after HttpClientOptions::validate");
if options.ipv4_only && is_ipv6_literal_host(&host) {
return Err(HttpError::proxy_config(format!(
"Proxy host '{host}' is IPv6, which is not allowed when ipv4_only=true",
)));
}
let port = options
.proxy
.port
.expect("proxy.port must exist after HttpClientOptions::validate");
let proxy_url = format!("{}://{}:{}", options.proxy.proxy_type.scheme(), host, port);
let mut proxy = reqwest::Proxy::all(&proxy_url).map_err(|error| {
HttpError::proxy_config(format!("Invalid proxy URL '{}': {}", proxy_url, error))
})?;
if let Some(username) = options.proxy.username.clone() {
let password = options.proxy.password.as_deref().unwrap_or("");
proxy = proxy.basic_auth(&username, password);
}
builder = builder.proxy(proxy);
} else if !options.use_env_proxy {
builder = builder.no_proxy();
}
let backend = builder.build().map_err(HttpError::from)?;
Ok(HttpClient::new(backend, options))
}
pub fn create_from_config<R>(&self, config: &R) -> Result<HttpClient, HttpConfigError>
where
R: ConfigReader + ?Sized,
{
let options =
HttpClientOptions::from_config(config).map_err(|e| resolve_config_error(config, e))?;
options
.validate()
.map_err(|e| resolve_config_error(config, e))?;
self.create(options).map_err(|e| {
HttpConfigError::new(
crate::HttpConfigErrorKind::InvalidValue,
config.resolve_key(""),
e.to_string(),
)
})
}
}
fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
where
R: ConfigReader + ?Sized,
{
error.path = if error.path.is_empty() {
config.resolve_key("")
} else {
config.resolve_key(&error.path)
};
error
}
fn filter_ipv4_addrs<I>(host: &str, resolved: I) -> Result<Addrs, BoxError>
where
I: IntoIterator<Item = SocketAddr>,
{
let ipv4_addrs: Vec<SocketAddr> = resolved.into_iter().filter(SocketAddr::is_ipv4).collect();
if ipv4_addrs.is_empty() {
let error = std::io::Error::new(
std::io::ErrorKind::AddrNotAvailable,
format!("No IPv4 address found for host '{host}'"),
);
return Err(error.into_box_error());
}
Ok(Box::new(ipv4_addrs.into_iter()) as Addrs)
}
fn map_validation_error(error: HttpConfigError) -> HttpError {
if error.path.starts_with("proxy.") {
HttpError::proxy_config(error.to_string())
} else {
HttpError::other(format!("Invalid HTTP client options: {error}"))
}
}
fn is_ipv6_literal_host(host: &str) -> bool {
let trimmed = host.trim().trim_start_matches('[').trim_end_matches(']');
matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
}