1use std::fmt;
7use std::sync::Arc;
8use std::time::Duration;
9
10use reqwest::Client;
11use reqwest::header::HeaderMap;
12
13use crate::client::proxy::ProxyConfig;
14
15const HTTP_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
17const HTTP_POOL_MAX_IDLE_PER_HOST: usize = 32;
18const HTTP_TCP_KEEPALIVE_SECS: u64 = 60;
19const REQUEST_TIMEOUT_SECS: u64 = 60;
20const CONNECT_TIMEOUT_SECS: u64 = 10;
21
22const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
23
24#[derive(Debug, Clone, Default)]
26pub struct HttpClientConfig<'a> {
27 pub proxy: Option<&'a ProxyConfig>,
28 pub timeout: Option<Duration>,
29 pub user_agent: Option<String>,
30 pub default_headers: Option<HeaderMap>,
31 pub http2_adaptive_window: bool,
32}
33
34impl fmt::Display for HttpClientConfig<'_> {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(
37 f,
38 "HttpClientConfig(proxy={}, timeout={}, http2={})",
39 self.proxy.is_some(),
40 self.timeout
41 .map_or("default".to_string(), |d| format!("{}s", d.as_secs())),
42 self.http2_adaptive_window
43 )
44 }
45}
46
47pub fn build_http_client(config: HttpClientConfig) -> crate::error::Result<Arc<Client>> {
61 let timeout = config.timeout.unwrap_or(Duration::from_secs(REQUEST_TIMEOUT_SECS));
62
63 tracing::debug!(
64 has_proxy = config.proxy.is_some(),
65 timeout_secs = timeout.as_secs(),
66 pool_idle_timeout_secs = HTTP_POOL_IDLE_TIMEOUT_SECS,
67 max_idle_per_host = HTTP_POOL_MAX_IDLE_PER_HOST,
68 http2 = config.http2_adaptive_window,
69 "⚙️ Creating HTTP client with connection pooling"
70 );
71
72 let mut builder = Client::builder()
73 .timeout(timeout)
74 .connect_timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
75 .pool_idle_timeout(Duration::from_secs(HTTP_POOL_IDLE_TIMEOUT_SECS))
76 .pool_max_idle_per_host(HTTP_POOL_MAX_IDLE_PER_HOST)
77 .tcp_keepalive(Duration::from_secs(HTTP_TCP_KEEPALIVE_SECS))
78 .tcp_nodelay(true)
79 .user_agent(config.user_agent.as_deref().unwrap_or(DEFAULT_USER_AGENT));
80
81 if config.http2_adaptive_window {
82 builder = builder.http2_adaptive_window(true);
83 }
84
85 if let Some(headers) = config.default_headers {
86 builder = builder.default_headers(headers);
87 }
88
89 if let Some(proxy_config) = config.proxy {
90 match proxy_config.to_reqwest_proxy() {
91 Ok(proxy) => {
92 tracing::debug!("⚙️ Adding proxy configuration to HTTP client");
93 builder = builder.proxy(proxy);
94 }
95 Err(e) => {
96 tracing::warn!(error = %e, "Proxy configuration failed — client will connect directly without proxy");
97 }
98 }
99 }
100
101 let client = builder.build()?;
102
103 tracing::debug!("✅ HTTP client created successfully");
104
105 Ok(Arc::new(client))
106}
107
108pub fn create_http_client(proxy: Option<&ProxyConfig>) -> crate::error::Result<Arc<Client>> {
122 build_http_client(HttpClientConfig {
123 proxy,
124 ..Default::default()
125 })
126}