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).map_err(|error| {
135 HttpError::proxy_config(format!("Invalid proxy URL '{}': {}", proxy_url, error))
136 })?;
137
138 if let Some(username) = options.proxy.username.clone() {
139 let password = options.proxy.password.as_deref().unwrap_or("");
140 proxy = proxy.basic_auth(&username, password);
141 }
142
143 builder = builder.proxy(proxy);
144 } else if !options.use_env_proxy {
145 builder = builder.no_proxy();
149 }
150
151 let backend = builder.build().map_err(HttpError::from)?;
152
153 Ok(HttpClient::new(backend, options))
154 }
155
156 pub fn create_from_config<R>(&self, config: &R) -> Result<HttpClient, HttpConfigError>
167 where
168 R: ConfigReader + ?Sized,
169 {
170 let options =
171 HttpClientOptions::from_config(config).map_err(|e| resolve_config_error(config, e))?;
172 options
173 .validate()
174 .map_err(|e| resolve_config_error(config, e))?;
175 self.create(options).map_err(|e| {
176 HttpConfigError::new(
177 crate::HttpConfigErrorKind::InvalidValue,
178 config.resolve_key(""),
179 e.to_string(),
180 )
181 })
182 }
183}
184
185fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
186where
187 R: ConfigReader + ?Sized,
188{
189 error.path = if error.path.is_empty() {
190 config.resolve_key("")
191 } else {
192 config.resolve_key(&error.path)
193 };
194 error
195}
196
197fn filter_ipv4_addrs<I>(host: &str, resolved: I) -> Result<Addrs, BoxError>
210where
211 I: IntoIterator<Item = SocketAddr>,
212{
213 let ipv4_addrs: Vec<SocketAddr> = resolved.into_iter().filter(SocketAddr::is_ipv4).collect();
214 if ipv4_addrs.is_empty() {
215 let error = std::io::Error::new(
216 std::io::ErrorKind::AddrNotAvailable,
217 format!("No IPv4 address found for host '{host}'"),
218 );
219 return Err(error.into_box_error());
220 }
221 Ok(Box::new(ipv4_addrs.into_iter()) as Addrs)
222}
223
224fn map_validation_error(error: HttpConfigError) -> HttpError {
233 if error.path.starts_with("proxy.") {
234 HttpError::proxy_config(error.to_string())
235 } else {
236 HttpError::other(format!("Invalid HTTP client options: {error}"))
237 }
238}
239
240fn is_ipv6_literal_host(host: &str) -> bool {
248 let trimmed = host.trim().trim_start_matches('[').trim_end_matches(']');
249 matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
250}