Skip to main content

reqwest_proxy_pool/
config.rs

1//! Configuration for the proxy pool.
2
3use crate::classifier::{DefaultResponseClassifier, ResponseClassifier};
4use std::fmt;
5use std::sync::Arc;
6use std::time::Duration;
7
8/// Strategy for selecting a proxy from the pool.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ProxySelectionStrategy {
11    /// Select the proxy with the fastest response time.
12    FastestResponse,
13    /// Select the proxy with the highest success rate.
14    MostReliable,
15    /// Select a random healthy proxy.
16    Random,
17    /// Select proxies in round-robin fashion.
18    RoundRobin,
19}
20
21/// Retry strategy for request retries.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RetryStrategy {
24    /// Keep current behavior: each retry re-selects by `selection_strategy`
25    /// and may pick the same proxy again.
26    DefaultSelection,
27    /// On retries, always pick a proxy that has not been used by this request yet.
28    NewProxyOnRetry,
29}
30
31/// Per-host configuration.
32///
33/// Each `HostConfig` initializes one dedicated proxy pool.
34#[derive(Clone)]
35pub struct HostConfig {
36    pub(crate) host: String,
37    pub(crate) primary: bool,
38    /// Interval between health checks.
39    pub(crate) health_check_interval: Duration,
40    /// Timeout for health checks.
41    pub(crate) health_check_timeout: Duration,
42    /// Minimum number of available proxies.
43    pub(crate) min_available_proxies: usize,
44    /// URL used for health checks.
45    pub(crate) health_check_url: String,
46    /// Number of times to retry a request with different proxies.
47    pub(crate) retry_count: usize,
48    /// Retry behavior.
49    pub(crate) retry_strategy: RetryStrategy,
50    /// Strategy for selecting proxies.
51    pub(crate) selection_strategy: ProxySelectionStrategy,
52    /// Minimum interval between requests on the same proxy instance.
53    pub(crate) min_request_interval_ms: u64,
54    /// Response classifier for business-level proxy health feedback.
55    pub(crate) response_classifier: Arc<dyn ResponseClassifier>,
56    /// Accept invalid TLS certificates (needed for most free SOCKS5 proxies).
57    pub(crate) danger_accept_invalid_certs: bool,
58}
59
60impl fmt::Debug for HostConfig {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("HostConfig")
63            .field("host", &self.host)
64            .field("primary", &self.primary)
65            .field("health_check_interval", &self.health_check_interval)
66            .field("health_check_timeout", &self.health_check_timeout)
67            .field("min_available_proxies", &self.min_available_proxies)
68            .field("health_check_url", &self.health_check_url)
69            .field("retry_count", &self.retry_count)
70            .field("retry_strategy", &self.retry_strategy)
71            .field("selection_strategy", &self.selection_strategy)
72            .field("min_request_interval_ms", &self.min_request_interval_ms)
73            .field("response_classifier", &"<dyn ResponseClassifier>")
74            .field(
75                "danger_accept_invalid_certs",
76                &self.danger_accept_invalid_certs,
77            )
78            .finish()
79    }
80}
81
82impl HostConfig {
83    /// Create a new builder.
84    pub fn builder(host: impl Into<String>) -> HostConfigBuilder {
85        HostConfigBuilder::new(host)
86    }
87
88    /// Bound host.
89    pub fn host(&self) -> &str {
90        &self.host
91    }
92
93    /// Whether this host is the primary fallback host pool.
94    pub fn primary(&self) -> bool {
95        self.primary
96    }
97
98    /// Interval between health checks.
99    pub fn health_check_interval(&self) -> Duration {
100        self.health_check_interval
101    }
102
103    /// Timeout for health checks.
104    pub fn health_check_timeout(&self) -> Duration {
105        self.health_check_timeout
106    }
107
108    /// Minimum number of available proxies.
109    pub fn min_available_proxies(&self) -> usize {
110        self.min_available_proxies
111    }
112
113    /// URL used for health checks.
114    pub fn health_check_url(&self) -> &str {
115        &self.health_check_url
116    }
117
118    /// Number of times to retry.
119    pub fn retry_count(&self) -> usize {
120        self.retry_count
121    }
122
123    /// Retry strategy.
124    pub fn retry_strategy(&self) -> RetryStrategy {
125        self.retry_strategy
126    }
127
128    /// Selection strategy.
129    pub fn selection_strategy(&self) -> ProxySelectionStrategy {
130        self.selection_strategy
131    }
132
133    /// Minimum interval between requests on the same proxy instance.
134    pub fn min_request_interval_ms(&self) -> u64 {
135        self.min_request_interval_ms
136    }
137
138    /// Response classifier.
139    pub fn response_classifier(&self) -> &Arc<dyn ResponseClassifier> {
140        &self.response_classifier
141    }
142
143    /// Whether invalid TLS certificates are accepted.
144    pub fn danger_accept_invalid_certs(&self) -> bool {
145        self.danger_accept_invalid_certs
146    }
147}
148
149/// Builder for `HostConfig`.
150pub struct HostConfigBuilder {
151    host: String,
152    primary: bool,
153    health_check_interval: Option<Duration>,
154    health_check_timeout: Option<Duration>,
155    min_available_proxies: Option<usize>,
156    health_check_url: Option<String>,
157    retry_count: Option<usize>,
158    retry_strategy: Option<RetryStrategy>,
159    selection_strategy: Option<ProxySelectionStrategy>,
160    min_request_interval_ms: Option<u64>,
161    response_classifier: Option<Arc<dyn ResponseClassifier>>,
162    danger_accept_invalid_certs: bool,
163}
164
165impl HostConfigBuilder {
166    /// Create builder with a target host.
167    pub fn new(host: impl Into<String>) -> Self {
168        Self {
169            host: normalize_host(host.into()),
170            primary: false,
171            health_check_interval: None,
172            health_check_timeout: None,
173            min_available_proxies: None,
174            health_check_url: None,
175            retry_count: None,
176            retry_strategy: None,
177            selection_strategy: None,
178            min_request_interval_ms: None,
179            response_classifier: None,
180            danger_accept_invalid_certs: false,
181        }
182    }
183
184    /// Set the interval between health checks.
185    pub fn health_check_interval(mut self, interval: Duration) -> Self {
186        self.health_check_interval = Some(interval);
187        self
188    }
189
190    /// Set whether this host is primary fallback.
191    pub fn primary(mut self, primary: bool) -> Self {
192        self.primary = primary;
193        self
194    }
195
196    /// Set the timeout for health checks.
197    pub fn health_check_timeout(mut self, timeout: Duration) -> Self {
198        self.health_check_timeout = Some(timeout);
199        self
200    }
201
202    /// Set the minimum number of available proxies.
203    pub fn min_available_proxies(mut self, count: usize) -> Self {
204        self.min_available_proxies = Some(count);
205        self
206    }
207
208    /// Set the URL used for health checks.
209    pub fn health_check_url(mut self, url: impl Into<String>) -> Self {
210        self.health_check_url = Some(url.into());
211        self
212    }
213
214    /// Set retry count.
215    pub fn retry_count(mut self, count: usize) -> Self {
216        self.retry_count = Some(count);
217        self
218    }
219
220    /// Set retry strategy.
221    pub fn retry_strategy(mut self, strategy: RetryStrategy) -> Self {
222        self.retry_strategy = Some(strategy);
223        self
224    }
225
226    /// Set selection strategy.
227    pub fn selection_strategy(mut self, strategy: ProxySelectionStrategy) -> Self {
228        self.selection_strategy = Some(strategy);
229        self
230    }
231
232    /// Set minimum interval milliseconds between requests on one proxy instance.
233    pub fn min_request_interval_ms(mut self, interval_ms: u64) -> Self {
234        self.min_request_interval_ms = Some(interval_ms);
235        self
236    }
237
238    /// Set custom classifier.
239    pub fn response_classifier(mut self, classifier: impl ResponseClassifier) -> Self {
240        self.response_classifier = Some(Arc::new(classifier));
241        self
242    }
243
244    /// Accept invalid TLS certificates.
245    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
246        self.danger_accept_invalid_certs = accept;
247        self
248    }
249
250    /// Build host config.
251    pub fn build(self) -> HostConfig {
252        let health_check_url = self
253            .health_check_url
254            .unwrap_or_else(|| "https://www.google.com".to_string());
255        let health_check_url = if health_check_url.trim().is_empty() {
256            "https://www.google.com".to_string()
257        } else {
258            health_check_url
259        };
260
261        HostConfig {
262            host: if self.host.is_empty() {
263                "default".to_string()
264            } else {
265                self.host
266            },
267            primary: self.primary,
268            health_check_interval: self
269                .health_check_interval
270                .unwrap_or(Duration::from_secs(300)),
271            health_check_timeout: self.health_check_timeout.unwrap_or(Duration::from_secs(10)),
272            min_available_proxies: self.min_available_proxies.unwrap_or(3),
273            health_check_url,
274            retry_count: self.retry_count.unwrap_or(3),
275            retry_strategy: self
276                .retry_strategy
277                .unwrap_or(RetryStrategy::DefaultSelection),
278            selection_strategy: self
279                .selection_strategy
280                .unwrap_or(ProxySelectionStrategy::FastestResponse),
281            min_request_interval_ms: self.min_request_interval_ms.unwrap_or(500).max(1),
282            response_classifier: self
283                .response_classifier
284                .unwrap_or_else(|| Arc::new(DefaultResponseClassifier)),
285            danger_accept_invalid_certs: self.danger_accept_invalid_certs,
286        }
287    }
288}
289
290/// Top-level configuration.
291#[derive(Clone, Debug)]
292pub struct ProxyPoolConfig {
293    /// Shared source URLs used to build proxy lists for all host pools.
294    pub(crate) sources: Vec<String>,
295    /// Host-specific pool definitions.
296    pub(crate) hosts: Vec<HostConfig>,
297}
298
299impl ProxyPoolConfig {
300    /// Create builder.
301    pub fn builder() -> ProxyPoolConfigBuilder {
302        ProxyPoolConfigBuilder::new()
303    }
304
305    /// Sources.
306    pub fn sources(&self) -> &[String] {
307        &self.sources
308    }
309
310    /// Host configs.
311    pub fn hosts(&self) -> &[HostConfig] {
312        &self.hosts
313    }
314}
315
316/// Builder for `ProxyPoolConfig`.
317pub struct ProxyPoolConfigBuilder {
318    sources: Vec<String>,
319    hosts: Vec<HostConfig>,
320}
321
322impl ProxyPoolConfigBuilder {
323    /// Create builder.
324    pub fn new() -> Self {
325        Self {
326            sources: Vec::new(),
327            hosts: Vec::new(),
328        }
329    }
330
331    /// Set source URLs.
332    pub fn sources(mut self, sources: Vec<impl Into<String>>) -> Self {
333        self.sources = sources.into_iter().map(Into::into).collect();
334        self
335    }
336
337    /// Set all host configs.
338    ///
339    /// Exactly one host should set `primary(true)` as fallback for unknown hosts.
340    pub fn hosts(mut self, hosts: Vec<HostConfig>) -> Self {
341        self.hosts = hosts;
342        self
343    }
344
345    /// Add one host config.
346    ///
347    /// Exactly one host should set `primary(true)` as fallback for unknown hosts.
348    pub fn add_host(mut self, host: HostConfig) -> Self {
349        self.hosts.push(host);
350        self
351    }
352
353    /// Build config.
354    pub fn build(self) -> ProxyPoolConfig {
355        ProxyPoolConfig {
356            sources: self.sources,
357            hosts: self.hosts,
358        }
359    }
360}
361
362impl Default for ProxyPoolConfigBuilder {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368fn normalize_host(host: String) -> String {
369    host.trim().to_ascii_lowercase()
370}
371
372#[cfg(test)]
373mod tests {
374    use super::{HostConfig, ProxyPoolConfig};
375
376    #[test]
377    fn host_config_normalizes_host() {
378        let host = HostConfig::builder(" API.EXAMPLE.COM ").build();
379        assert_eq!(host.host(), "api.example.com");
380    }
381
382    #[test]
383    fn pool_config_keeps_hosts() {
384        let api = HostConfig::builder("api.example.com").build();
385        let web = HostConfig::builder("web.example.com").build();
386        let config = ProxyPoolConfig::builder().hosts(vec![api, web]).build();
387        assert_eq!(config.hosts().len(), 2);
388    }
389}