qubit_http/client/
http_client_factory.rs1use std::net::{
13 IpAddr,
14 SocketAddr,
15};
16
17use reqwest::dns::{
18 Addrs,
19 Name,
20 Resolve,
21 Resolving,
22};
23use reqwest::redirect::Policy;
24
25use crate::HttpConfigError;
26use crate::{
27 HttpClient,
28 HttpClientOptions,
29 HttpError,
30 HttpResult,
31};
32use qubit_config::ConfigReader;
33use qubit_error::{
34 BoxError,
35 IntoBoxError,
36};
37
38#[derive(Debug, Clone, Copy, Default)]
40struct Ipv4OnlyResolver;
41
42impl Resolve for Ipv4OnlyResolver {
43 fn resolve(&self, name: Name) -> Resolving {
54 let host = name.as_str().to_string();
55 Box::pin(async move {
56 let resolved = tokio::net::lookup_host((host.as_str(), 0))
57 .await
58 .map_err(|error| error.into_box_error())?;
59 filter_ipv4_addrs(&host, resolved)
60 })
61 }
62}
63
64#[derive(Debug, Default, Clone, Copy)]
66pub struct HttpClientFactory;
67
68impl HttpClientFactory {
69 pub fn new() -> Self {
74 Self
75 }
76
77 pub fn create_default(&self) -> HttpResult<HttpClient> {
82 self.create(HttpClientOptions::default())
83 }
84
85 pub fn create(&self, options: HttpClientOptions) -> HttpResult<HttpClient> {
93 options.validate().map_err(map_validation_error)?;
94
95 let mut builder = reqwest::Client::builder();
96
97 builder = builder.connect_timeout(options.timeouts.connect_timeout);
98 if let Some(request_timeout) = options.timeouts.request_timeout {
99 builder = builder.timeout(request_timeout);
100 }
101 if let Some(user_agent) = options.user_agent.as_deref() {
102 builder = builder.user_agent(user_agent);
103 }
104 if let Some(max_redirects) = options.max_redirects {
105 builder = builder.redirect(Policy::limited(max_redirects));
106 }
107 if let Some(pool_idle_timeout) = options.pool_idle_timeout {
108 builder = builder.pool_idle_timeout(pool_idle_timeout);
109 }
110 if let Some(pool_max_idle_per_host) = options.pool_max_idle_per_host {
111 builder = builder.pool_max_idle_per_host(pool_max_idle_per_host);
112 }
113 if options.ipv4_only {
114 builder = builder.dns_resolver(Ipv4OnlyResolver);
115 }
116
117 if options.proxy.enabled {
118 let host = options
119 .proxy
120 .host
121 .clone()
122 .expect("proxy.host must exist after HttpClientOptions::validate");
123 if options.ipv4_only && is_ipv6_literal_host(&host) {
124 return Err(HttpError::proxy_config(format!(
125 "Proxy host '{host}' is IPv6, which is not allowed when ipv4_only=true",
126 )));
127 }
128 let port = options
129 .proxy
130 .port
131 .expect("proxy.port must exist after HttpClientOptions::validate");
132
133 let proxy_url = format!("{}://{}:{}", options.proxy.proxy_type.scheme(), host, port);
134 let mut proxy = reqwest::Proxy::all(&proxy_url)
135 .map_err(|error| HttpError::proxy_config(format!("Invalid proxy URL '{}': {}", proxy_url, error)))?;
136
137 if let Some(username) = options.proxy.username.clone() {
138 let password = options.proxy.password.as_deref().unwrap_or("");
139 proxy = proxy.basic_auth(&username, password);
140 }
141
142 builder = builder.proxy(proxy);
143 } else if !options.use_env_proxy {
144 builder = builder.no_proxy();
148 }
149
150 let backend = builder.build().map_err(HttpError::from)?;
151
152 Ok(HttpClient::new(backend, options))
153 }
154
155 pub fn create_from_config<R>(&self, config: &R) -> Result<HttpClient, HttpConfigError>
166 where
167 R: ConfigReader + ?Sized,
168 {
169 let options = HttpClientOptions::from_config(config).map_err(|e| resolve_config_error(config, e))?;
170 options.validate().map_err(|e| resolve_config_error(config, e))?;
171 self.create(options).map_err(|e| {
172 HttpConfigError::new(
173 crate::HttpConfigErrorKind::InvalidValue,
174 config.resolve_key(""),
175 e.to_string(),
176 )
177 })
178 }
179}
180
181fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
182where
183 R: ConfigReader + ?Sized,
184{
185 error.path = if error.path.is_empty() {
186 config.resolve_key("")
187 } else {
188 config.resolve_key(&error.path)
189 };
190 error
191}
192
193fn filter_ipv4_addrs<I>(host: &str, resolved: I) -> Result<Addrs, BoxError>
206where
207 I: IntoIterator<Item = SocketAddr>,
208{
209 let ipv4_addrs: Vec<SocketAddr> = resolved.into_iter().filter(SocketAddr::is_ipv4).collect();
210 if ipv4_addrs.is_empty() {
211 let error = std::io::Error::new(
212 std::io::ErrorKind::AddrNotAvailable,
213 format!("No IPv4 address found for host '{host}'"),
214 );
215 return Err(error.into_box_error());
216 }
217 Ok(Box::new(ipv4_addrs.into_iter()) as Addrs)
218}
219
220fn map_validation_error(error: HttpConfigError) -> HttpError {
229 if error.path.starts_with("proxy.") {
230 HttpError::proxy_config(error.to_string())
231 } else {
232 HttpError::other(format!("Invalid HTTP client options: {error}"))
233 }
234}
235
236fn is_ipv6_literal_host(host: &str) -> bool {
244 let trimmed = host.trim().trim_start_matches('[').trim_end_matches(']');
245 matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
246}