1#[cfg(not(target_arch = "wasm32"))]
4use crate::transport::Socks5Proxy;
5#[cfg(not(target_arch = "wasm32"))]
6use dapi_grpc::tonic::transport::Certificate;
7use std::time::Duration;
8
9const DEFAULT_CONNECT_TIMEOUT: Option<Duration> = None;
11const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
12const DEFAULT_RETRIES: usize = 5;
13const DEFAULT_BAN_FAILED_ADDRESS: bool = true;
14
15#[derive(Debug, Clone, Copy, Default)]
23pub struct RequestSettings {
24 pub connect_timeout: Option<Duration>,
26 pub timeout: Option<Duration>,
41 pub retries: Option<usize>,
44 pub ban_failed_address: Option<bool>,
46 pub max_decoding_message_size: Option<usize>,
48}
49
50impl RequestSettings {
51 pub const fn default() -> Self {
54 RequestSettings {
55 connect_timeout: None,
56 timeout: None,
57 retries: None,
58 ban_failed_address: None,
59 max_decoding_message_size: None,
60 }
61 }
62
63 pub fn override_by(self, rhs: RequestSettings) -> Self {
67 RequestSettings {
68 connect_timeout: rhs.connect_timeout.or(self.connect_timeout),
69 timeout: rhs.timeout.or(self.timeout),
70 retries: rhs.retries.or(self.retries),
71 ban_failed_address: rhs.ban_failed_address.or(self.ban_failed_address),
72 max_decoding_message_size: rhs
73 .max_decoding_message_size
74 .or(self.max_decoding_message_size),
75 }
76 }
77
78 pub fn finalize(self) -> AppliedRequestSettings {
80 AppliedRequestSettings {
81 connect_timeout: self.connect_timeout.or(DEFAULT_CONNECT_TIMEOUT),
82 timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
83 retries: self.retries.unwrap_or(DEFAULT_RETRIES),
84 ban_failed_address: self
85 .ban_failed_address
86 .unwrap_or(DEFAULT_BAN_FAILED_ADDRESS),
87 max_decoding_message_size: self.max_decoding_message_size,
88 #[cfg(not(target_arch = "wasm32"))]
89 ca_certificate: None,
90 #[cfg(not(target_arch = "wasm32"))]
91 proxy: None,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
102pub struct AppliedRequestSettings {
103 pub connect_timeout: Option<Duration>,
105 pub timeout: Duration,
107 pub retries: usize,
109 pub ban_failed_address: bool,
111 pub max_decoding_message_size: Option<usize>,
113 #[cfg(not(target_arch = "wasm32"))]
115 pub ca_certificate: Option<Certificate>,
116 #[cfg(not(target_arch = "wasm32"))]
118 pub proxy: Option<Socks5Proxy>,
119}
120impl AppliedRequestSettings {
121 #[cfg(not(target_arch = "wasm32"))]
125 pub fn with_ca_certificate(mut self, ca_cert: Option<Certificate>) -> Self {
126 self.ca_certificate = ca_cert;
127 self
128 }
129
130 pub fn attempt_deadline(&self) -> Option<Duration> {
137 if self.timeout.is_zero() {
138 return None;
139 }
140 Some(
141 self.timeout
142 .saturating_add(self.effective_connect_timeout().unwrap_or_default()),
143 )
144 }
145
146 pub(crate) fn effective_connect_timeout(&self) -> Option<Duration> {
150 #[cfg(not(target_arch = "wasm32"))]
151 if self.connect_timeout.is_none() && self.proxy.is_some() {
152 return Some(crate::transport::PROXY_CONNECT_TIMEOUT);
153 }
154 self.connect_timeout
155 }
156
157 #[cfg(not(target_arch = "wasm32"))]
161 pub fn with_proxy(mut self, proxy: Option<Socks5Proxy>) -> Self {
162 self.proxy = proxy;
163 self
164 }
165
166 pub(crate) fn connection_key(&self) -> String {
173 let Self {
176 #[cfg(not(target_arch = "wasm32"))]
177 connect_timeout,
178 #[cfg(target_arch = "wasm32")]
179 connect_timeout: _,
180 timeout: _,
181 retries: _,
182 ban_failed_address: _,
183 max_decoding_message_size,
184 #[cfg(not(target_arch = "wasm32"))]
185 ca_certificate,
186 #[cfg(not(target_arch = "wasm32"))]
187 proxy,
188 } = self;
189
190 #[cfg(target_arch = "wasm32")]
194 let connect_timeout = None::<Duration>;
195
196 #[cfg(not(target_arch = "wasm32"))]
200 let ca_certificate = ca_certificate.as_ref().map(|cert| {
201 use std::fmt::Write;
202 let bytes = cert.as_ref();
203 let mut hex = String::with_capacity(bytes.len() * 2);
204 for byte in bytes {
205 write!(hex, "{byte:02x}").expect("writing to a String cannot fail");
206 }
207 hex
208 });
209 #[cfg(target_arch = "wasm32")]
210 let ca_certificate: Option<String> = None;
211
212 #[cfg(not(target_arch = "wasm32"))]
214 let proxy = proxy.as_ref().map(Socks5Proxy::connection_key);
215 #[cfg(target_arch = "wasm32")]
216 let proxy: Option<String> = None;
217
218 format!(
219 "connect_timeout={:?},max_decoding_message_size={:?},ca_certificate={:?},proxy={:?}",
220 connect_timeout, max_decoding_message_size, ca_certificate, proxy
221 )
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_request_settings_override_by() {
231 let base = RequestSettings {
232 timeout: Some(Duration::from_secs(5)),
233 retries: Some(3),
234 connect_timeout: Some(Duration::from_secs(2)),
235 ban_failed_address: Some(true),
236 max_decoding_message_size: Some(1024),
237 };
238
239 let override_settings = RequestSettings {
241 timeout: Some(Duration::from_secs(10)),
242 retries: None,
243 connect_timeout: None,
244 ban_failed_address: None,
245 max_decoding_message_size: None,
246 };
247
248 let result = base.override_by(override_settings);
249 assert_eq!(result.timeout, Some(Duration::from_secs(10))); assert_eq!(result.retries, Some(3)); assert_eq!(result.connect_timeout, Some(Duration::from_secs(2))); assert_eq!(result.ban_failed_address, Some(true)); assert_eq!(result.max_decoding_message_size, Some(1024)); }
255
256 #[test]
257 fn test_request_settings_override_by_empty() {
258 let base = RequestSettings {
259 timeout: Some(Duration::from_secs(5)),
260 retries: Some(3),
261 connect_timeout: None,
262 ban_failed_address: None,
263 max_decoding_message_size: None,
264 };
265
266 let result = base.override_by(RequestSettings::default());
267 assert_eq!(result.timeout, Some(Duration::from_secs(5)));
268 assert_eq!(result.retries, Some(3));
269 }
270
271 #[test]
272 fn test_request_settings_finalize_defaults() {
273 let settings = RequestSettings::default();
274 let applied = settings.finalize();
275
276 assert_eq!(applied.connect_timeout, None);
277 assert_eq!(applied.timeout, Duration::from_secs(10));
278 assert_eq!(applied.retries, 5);
279 assert!(applied.ban_failed_address);
280 assert!(applied.max_decoding_message_size.is_none());
281 }
282
283 #[test]
284 fn test_request_settings_finalize_custom() {
285 let settings = RequestSettings {
286 connect_timeout: Some(Duration::from_secs(3)),
287 timeout: Some(Duration::from_secs(30)),
288 retries: Some(10),
289 ban_failed_address: Some(false),
290 max_decoding_message_size: Some(4096),
291 };
292
293 let applied = settings.finalize();
294 assert_eq!(applied.connect_timeout, Some(Duration::from_secs(3)));
295 assert_eq!(applied.timeout, Duration::from_secs(30));
296 assert_eq!(applied.retries, 10);
297 assert!(!applied.ban_failed_address);
298 assert_eq!(applied.max_decoding_message_size, Some(4096));
299 }
300
301 #[cfg(not(target_arch = "wasm32"))]
302 #[test]
303 fn test_applied_settings_with_ca_certificate_none() {
304 let applied = RequestSettings::default().finalize();
305 let result = applied.with_ca_certificate(None);
306 assert!(result.ca_certificate.is_none());
307 }
308
309 #[cfg(not(target_arch = "wasm32"))]
310 #[test]
311 fn test_applied_settings_with_ca_certificate_some() {
312 let applied = RequestSettings::default().finalize();
313 let cert = Certificate::from_pem("fake-pem-data");
314 let result = applied.with_ca_certificate(Some(cert));
315 assert!(result.ca_certificate.is_some());
316 }
317
318 #[test]
319 fn should_bound_attempt_by_timeout_plus_connect_timeout() {
320 let applied = RequestSettings {
321 timeout: Some(Duration::from_secs(10)),
322 connect_timeout: Some(Duration::from_secs(3)),
323 ..RequestSettings::default()
324 }
325 .finalize();
326 assert_eq!(applied.attempt_deadline(), Some(Duration::from_secs(13)));
327
328 let default = RequestSettings::default().finalize();
329 assert_eq!(default.attempt_deadline(), Some(Duration::from_secs(10)));
330 }
331
332 #[cfg(not(target_arch = "wasm32"))]
333 #[test]
334 fn should_count_the_proxy_connect_default_in_the_attempt_deadline() {
335 use crate::transport::{ProxyEndpoint, Socks5Auth};
336
337 let proxied = RequestSettings::default()
338 .finalize()
339 .with_proxy(Some(Socks5Proxy {
340 endpoint: ProxyEndpoint::Tcp("127.0.0.1:9050".parse().expect("address")),
341 auth: Socks5Auth::None,
342 }));
343 assert_eq!(
344 proxied.effective_connect_timeout(),
345 Some(crate::transport::PROXY_CONNECT_TIMEOUT)
346 );
347 assert_eq!(
348 proxied.attempt_deadline(),
349 Some(DEFAULT_TIMEOUT + crate::transport::PROXY_CONNECT_TIMEOUT)
350 );
351
352 let explicit = RequestSettings {
353 connect_timeout: Some(Duration::from_secs(3)),
354 ..RequestSettings::default()
355 }
356 .finalize()
357 .with_proxy(proxied.proxy.clone());
358 assert_eq!(
359 explicit.effective_connect_timeout(),
360 Some(Duration::from_secs(3))
361 );
362
363 let direct = RequestSettings::default().finalize();
364 assert_eq!(direct.effective_connect_timeout(), None);
365 }
366
367 #[test]
368 fn should_not_bound_attempt_when_timeout_is_zero() {
369 let applied = RequestSettings {
370 timeout: Some(Duration::ZERO),
371 connect_timeout: Some(Duration::from_secs(3)),
372 ..RequestSettings::default()
373 }
374 .finalize();
375 assert_eq!(applied.attempt_deadline(), None);
376 }
377
378 #[test]
379 fn should_saturate_attempt_deadline_instead_of_overflowing() {
380 let applied = RequestSettings {
381 timeout: Some(Duration::MAX),
382 connect_timeout: Some(Duration::from_secs(1)),
383 ..RequestSettings::default()
384 }
385 .finalize();
386 assert_eq!(applied.attempt_deadline(), Some(Duration::MAX));
387 }
388
389 #[test]
390 fn test_connection_key_ignores_per_request_settings() {
391 let custom = RequestSettings {
392 timeout: Some(Duration::from_secs(30)),
393 retries: Some(1),
394 ban_failed_address: Some(false),
395 ..RequestSettings::default()
396 }
397 .finalize();
398 let default = RequestSettings::default().finalize();
399
400 assert_eq!(
401 custom.connection_key(),
402 default.connection_key(),
403 "timeout/retries/banning must not split pooled connections"
404 );
405 }
406
407 #[test]
408 fn test_connection_key_differs_on_connection_settings() {
409 let default = RequestSettings::default().finalize();
410
411 let connect_timeout = RequestSettings {
412 connect_timeout: Some(Duration::from_secs(3)),
413 ..RequestSettings::default()
414 }
415 .finalize();
416 assert_ne!(default.connection_key(), connect_timeout.connection_key());
417
418 let decode_limit = RequestSettings {
419 max_decoding_message_size: Some(16 * 1024 * 1024),
420 ..RequestSettings::default()
421 }
422 .finalize();
423 assert_ne!(default.connection_key(), decode_limit.connection_key());
424 }
425
426 #[cfg(not(target_arch = "wasm32"))]
427 #[test]
428 fn test_connection_key_differs_on_ca_certificate() {
429 let default = RequestSettings::default().finalize();
430 let with_ca = RequestSettings::default()
431 .finalize()
432 .with_ca_certificate(Some(Certificate::from_pem("fake-pem-data")));
433
434 assert_ne!(default.connection_key(), with_ca.connection_key());
435
436 let with_other_ca = RequestSettings::default()
437 .finalize()
438 .with_ca_certificate(Some(Certificate::from_pem("other-pem-data")));
439 assert_ne!(with_ca.connection_key(), with_other_ca.connection_key());
440 }
441
442 #[cfg(not(target_arch = "wasm32"))]
443 #[test]
444 fn test_connection_key_differs_on_proxy() {
445 use crate::transport::{ProxyEndpoint, Socks5Auth};
446
447 let with_proxy = |auth| {
448 RequestSettings::default()
449 .finalize()
450 .with_proxy(Some(Socks5Proxy {
451 endpoint: ProxyEndpoint::Tcp("127.0.0.1:9050".parse().expect("address")),
452 auth,
453 }))
454 .connection_key()
455 };
456 let password = |password: &str| Socks5Auth::Password {
457 username: "alice".to_string(),
458 password: password.to_string(),
459 };
460 let direct = RequestSettings::default().finalize().connection_key();
461
462 assert_ne!(direct, with_proxy(Socks5Auth::None));
463 assert_ne!(
464 with_proxy(Socks5Auth::None),
465 with_proxy(Socks5Auth::RandomPerConnection)
466 );
467 assert_eq!(with_proxy(password("one")), with_proxy(password("one")));
468 assert_ne!(with_proxy(password("one")), with_proxy(password("two")));
469 assert!(!with_proxy(password("secret")).contains("secret"));
470 }
471}