qubit_http/
http_client_factory.rs1use std::net::{IpAddr, SocketAddr};
12
13use reqwest::dns::{Addrs, Name, Resolve, Resolving};
14
15use crate::HttpConfigError;
16use crate::{HttpClient, HttpClientOptions, HttpError, HttpResult};
17use qubit_config::ConfigReader;
18
19#[derive(Debug, Clone, Copy, Default)]
21struct Ipv4OnlyResolver;
22
23impl Resolve for Ipv4OnlyResolver {
24 fn resolve(&self, name: Name) -> Resolving {
35 let host = name.as_str().to_string();
36 Box::pin(async move {
37 let resolved = tokio::net::lookup_host((host.as_str(), 0))
38 .await
39 .map_err(|error| Box::new(error) as Box<dyn std::error::Error + Send + Sync>)?;
40 let ipv4_addrs: Vec<SocketAddr> = resolved.filter(SocketAddr::is_ipv4).collect();
41 if ipv4_addrs.is_empty() {
42 let error = std::io::Error::new(
43 std::io::ErrorKind::AddrNotAvailable,
44 format!("No IPv4 address found for host '{host}'"),
45 );
46 return Err(Box::new(error) as Box<dyn std::error::Error + Send + Sync>);
47 }
48 Ok(Box::new(ipv4_addrs.into_iter()) as Addrs)
49 })
50 }
51}
52
53#[derive(Debug, Default, Clone, Copy)]
55pub struct HttpClientFactory;
56
57impl HttpClientFactory {
58 pub fn new() -> Self {
63 Self
64 }
65
66 pub fn create(&self) -> HttpResult<HttpClient> {
71 self.create_with_options(HttpClientOptions::default())
72 }
73
74 pub fn create_with_options(&self, options: HttpClientOptions) -> HttpResult<HttpClient> {
82 options.validate().map_err(map_validation_error)?;
83
84 let mut builder = reqwest::Client::builder();
85
86 builder = builder.connect_timeout(options.timeouts.connect_timeout);
87 if let Some(request_timeout) = options.timeouts.request_timeout {
88 builder = builder.timeout(request_timeout);
89 }
90 if options.ipv4_only {
91 builder = builder.dns_resolver(Ipv4OnlyResolver);
92 }
93
94 if options.proxy.enabled {
95 let host = options
96 .proxy
97 .host
98 .clone()
99 .expect("proxy.host must exist after HttpClientOptions::validate");
100 if options.ipv4_only && is_ipv6_literal_host(&host) {
101 return Err(HttpError::proxy_config(format!(
102 "Proxy host '{host}' is IPv6, which is not allowed when ipv4_only=true",
103 )));
104 }
105 let port = options
106 .proxy
107 .port
108 .expect("proxy.port must exist after HttpClientOptions::validate");
109
110 let proxy_url = format!("{}://{}:{}", options.proxy.proxy_type.scheme(), host, port);
111 let mut proxy = reqwest::Proxy::all(&proxy_url).map_err(|error| {
112 HttpError::proxy_config(format!("Invalid proxy URL '{}': {}", proxy_url, error))
113 })?;
114
115 if let Some(username) = options.proxy.username.clone() {
116 let password = options.proxy.password.as_deref().unwrap_or("");
117 proxy = proxy.basic_auth(&username, password);
118 }
119
120 builder = builder.proxy(proxy);
121 } else {
122 builder = builder.no_proxy();
125 }
126
127 let client = builder.build().map_err(HttpError::from)?;
128
129 Ok(HttpClient::new(client, options))
130 }
131
132 pub fn create_from_config<R>(&self, config: &R) -> Result<HttpClient, HttpConfigError>
143 where
144 R: ConfigReader + ?Sized,
145 {
146 let options =
147 HttpClientOptions::from_config(config).map_err(|e| resolve_config_error(config, e))?;
148 options
149 .validate()
150 .map_err(|e| resolve_config_error(config, e))?;
151 self.create_with_options(options).map_err(|e| {
152 HttpConfigError::new(
153 crate::HttpConfigErrorKind::InvalidValue,
154 config.resolve_key(""),
155 e.to_string(),
156 )
157 })
158 }
159}
160
161fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
162where
163 R: ConfigReader + ?Sized,
164{
165 error.path = if error.path.is_empty() {
166 config.resolve_key("")
167 } else {
168 config.resolve_key(&error.path)
169 };
170 error
171}
172
173fn map_validation_error(error: HttpConfigError) -> HttpError {
182 if error.path.starts_with("proxy.") {
183 HttpError::proxy_config(error.to_string())
184 } else {
185 HttpError::other(format!("Invalid HTTP client options: {error}"))
186 }
187}
188
189fn is_ipv6_literal_host(host: &str) -> bool {
197 let trimmed = host.trim().trim_start_matches('[').trim_end_matches(']');
198 matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
199}