1use reqwest::Client;
19use std::time::Duration;
20use thiserror::Error;
21
22pub const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
24
25pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
27
28#[derive(Debug, Error)]
30pub enum HttpError {
31 #[error("Invalid proxy URL: {0}")]
32 InvalidProxy(String),
33
34 #[error("Failed to build HTTP client: {0}")]
35 BuildError(String),
36}
37
38impl From<reqwest::Error> for HttpError {
39 fn from(e: reqwest::Error) -> Self {
40 HttpError::BuildError(e.to_string())
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct HttpClientConfig {
47 pub timeout: Duration,
49 pub user_agent: String,
51 pub proxy: Option<String>,
53}
54
55impl Default for HttpClientConfig {
56 fn default() -> Self {
57 Self {
58 timeout: DEFAULT_TIMEOUT,
59 user_agent: DEFAULT_USER_AGENT.to_string(),
60 proxy: None,
61 }
62 }
63}
64
65impl HttpClientConfig {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 #[must_use]
73 pub fn with_timeout(mut self, timeout: Duration) -> Self {
74 self.timeout = timeout;
75 self
76 }
77
78 #[must_use]
80 pub fn with_timeout_secs(mut self, secs: u64) -> Self {
81 self.timeout = Duration::from_secs(secs);
82 self
83 }
84
85 #[must_use]
87 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
88 self.user_agent = user_agent.into();
89 self
90 }
91
92 #[must_use]
94 pub fn with_proxy(mut self, proxy: impl Into<String>) -> Self {
95 self.proxy = Some(proxy.into());
96 self
97 }
98
99 #[must_use]
101 pub fn with_optional_proxy(mut self, proxy: Option<String>) -> Self {
102 self.proxy = proxy;
103 self
104 }
105}
106
107pub fn build_client(config: &HttpClientConfig) -> Result<Client, HttpError> {
109 let mut builder = Client::builder()
110 .timeout(config.timeout)
111 .user_agent(&config.user_agent);
112
113 if let Some(ref proxy_url) = config.proxy {
114 let proxy = reqwest::Proxy::all(proxy_url)
115 .map_err(|e| HttpError::InvalidProxy(format!("{}: {}", proxy_url, e)))?;
116 builder = builder.proxy(proxy);
117 }
118
119 builder.build().map_err(HttpError::from)
120}
121
122pub fn build_default_client() -> Result<Client, HttpError> {
124 build_client(&HttpClientConfig::default())
125}
126
127pub fn build_client_with_proxy(proxy: Option<&str>) -> Result<Client, HttpError> {
129 let config = HttpClientConfig::default().with_optional_proxy(proxy.map(String::from));
130 build_client(&config)
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_default_config() {
139 let config = HttpClientConfig::default();
140 assert_eq!(config.timeout, DEFAULT_TIMEOUT);
141 assert_eq!(config.user_agent, DEFAULT_USER_AGENT);
142 assert!(config.proxy.is_none());
143 }
144
145 #[test]
146 fn test_config_builder() {
147 let config = HttpClientConfig::new()
148 .with_timeout_secs(60)
149 .with_user_agent("CustomAgent/1.0")
150 .with_proxy("http://proxy:8080");
151
152 assert_eq!(config.timeout, Duration::from_secs(60));
153 assert_eq!(config.user_agent, "CustomAgent/1.0");
154 assert_eq!(config.proxy, Some("http://proxy:8080".to_string()));
155 }
156
157 #[test]
158 fn test_build_default_client() {
159 let client = build_default_client();
160 assert!(client.is_ok());
161 }
162
163 #[test]
164 fn test_build_client_with_config() {
165 let config = HttpClientConfig::new().with_timeout_secs(45);
166 let client = build_client(&config);
167 assert!(client.is_ok());
168 }
169
170 #[test]
171 fn test_proxy_url_format() {
172 let config = HttpClientConfig::new().with_proxy("http://proxy.example.com:8080");
174 let result = build_client(&config);
175 assert!(result.is_ok());
176
177 }
180}