1use std::num::ParseIntError;
2
3use crate::config::ServerConfig;
4use crate::models::{EvmNetwork, RpcConfig, SolanaNetwork, StellarNetwork};
5use serde::Serialize;
6use thiserror::Error;
7
8use alloy::transports::RpcError;
9
10pub mod evm;
11pub use evm::*;
12
13mod solana;
14pub use solana::*;
15
16mod stellar;
17pub use stellar::*;
18
19mod retry;
20pub use retry::*;
21
22pub mod rpc_selector;
23
24#[derive(Error, Debug, Serialize)]
25pub enum ProviderError {
26 #[error("RPC client error: {0}")]
27 SolanaRpcError(#[from] SolanaProviderError),
28 #[error("Invalid address: {0}")]
29 InvalidAddress(String),
30 #[error("Network configuration error: {0}")]
31 NetworkConfiguration(String),
32 #[error("Request timeout")]
33 Timeout,
34 #[error("Rate limited (HTTP 429)")]
35 RateLimited,
36 #[error("Bad gateway (HTTP 502)")]
37 BadGateway,
38 #[error("Request error (HTTP {status_code}): {error}")]
39 RequestError { error: String, status_code: u16 },
40 #[error("JSON-RPC error (code {code}): {message}")]
41 RpcErrorCode { code: i64, message: String },
42 #[error("Transport error: {0}")]
43 TransportError(String),
44 #[error("Other provider error: {0}")]
45 Other(String),
46}
47
48impl ProviderError {
49 pub fn is_transient(&self) -> bool {
51 is_retriable_error(self)
52 }
53}
54
55impl From<hex::FromHexError> for ProviderError {
56 fn from(err: hex::FromHexError) -> Self {
57 ProviderError::InvalidAddress(err.to_string())
58 }
59}
60
61impl From<std::net::AddrParseError> for ProviderError {
62 fn from(err: std::net::AddrParseError) -> Self {
63 ProviderError::NetworkConfiguration(format!("Invalid network address: {err}"))
64 }
65}
66
67impl From<ParseIntError> for ProviderError {
68 fn from(err: ParseIntError) -> Self {
69 ProviderError::Other(format!("Number parsing error: {err}"))
70 }
71}
72
73fn categorize_reqwest_error(err: &reqwest::Error) -> ProviderError {
90 if err.is_timeout() {
91 return ProviderError::Timeout;
92 }
93
94 if let Some(status) = err.status() {
95 match status.as_u16() {
96 429 => return ProviderError::RateLimited,
97 502 => return ProviderError::BadGateway,
98 _ => {
99 return ProviderError::RequestError {
100 error: err.to_string(),
101 status_code: status.as_u16(),
102 }
103 }
104 }
105 }
106
107 ProviderError::Other(err.to_string())
108}
109
110impl From<reqwest::Error> for ProviderError {
111 fn from(err: reqwest::Error) -> Self {
112 categorize_reqwest_error(&err)
113 }
114}
115
116impl From<&reqwest::Error> for ProviderError {
117 fn from(err: &reqwest::Error) -> Self {
118 categorize_reqwest_error(err)
119 }
120}
121
122impl From<eyre::Report> for ProviderError {
123 fn from(err: eyre::Report) -> Self {
124 if let Some(reqwest_err) = err.downcast_ref::<reqwest::Error>() {
126 return ProviderError::from(reqwest_err);
127 }
128
129 ProviderError::Other(err.to_string())
131 }
132}
133
134impl From<String> for ProviderError {
136 fn from(error: String) -> Self {
137 ProviderError::Other(error)
138 }
139}
140
141impl<E> From<RpcError<E>> for ProviderError
143where
144 E: std::fmt::Display + std::any::Any + 'static,
145{
146 fn from(err: RpcError<E>) -> Self {
147 match err {
148 RpcError::Transport(transport_err) => {
149 if let Some(reqwest_err) =
151 (&transport_err as &dyn std::any::Any).downcast_ref::<reqwest::Error>()
152 {
153 return categorize_reqwest_error(reqwest_err);
154 }
155
156 ProviderError::TransportError(transport_err.to_string())
157 }
158 RpcError::ErrorResp(json_rpc_err) => ProviderError::RpcErrorCode {
159 code: json_rpc_err.code,
160 message: json_rpc_err.message.to_string(),
161 },
162 _ => ProviderError::Other(format!("Other RPC error: {err}")),
163 }
164 }
165}
166
167impl From<rpc_selector::RpcSelectorError> for ProviderError {
169 fn from(err: rpc_selector::RpcSelectorError) -> Self {
170 ProviderError::NetworkConfiguration(format!("RPC selector error: {err}"))
171 }
172}
173
174pub trait NetworkConfiguration: Sized {
175 type Provider;
176
177 fn public_rpc_urls(&self) -> Vec<String>;
178
179 fn new_provider(
180 rpc_urls: Vec<RpcConfig>,
181 timeout_seconds: u64,
182 ) -> Result<Self::Provider, ProviderError>;
183}
184
185impl NetworkConfiguration for EvmNetwork {
186 type Provider = EvmProvider;
187
188 fn public_rpc_urls(&self) -> Vec<String> {
189 (*self)
190 .public_rpc_urls()
191 .map(|urls| urls.iter().map(|url| url.to_string()).collect())
192 .unwrap_or_default()
193 }
194
195 fn new_provider(
196 rpc_urls: Vec<RpcConfig>,
197 timeout_seconds: u64,
198 ) -> Result<Self::Provider, ProviderError> {
199 EvmProvider::new(rpc_urls, timeout_seconds)
200 }
201}
202
203impl NetworkConfiguration for SolanaNetwork {
204 type Provider = SolanaProvider;
205
206 fn public_rpc_urls(&self) -> Vec<String> {
207 (*self)
208 .public_rpc_urls()
209 .map(|urls| urls.to_vec())
210 .unwrap_or_default()
211 }
212
213 fn new_provider(
214 rpc_urls: Vec<RpcConfig>,
215 timeout_seconds: u64,
216 ) -> Result<Self::Provider, ProviderError> {
217 SolanaProvider::new(rpc_urls, timeout_seconds)
218 }
219}
220
221impl NetworkConfiguration for StellarNetwork {
222 type Provider = StellarProvider;
223
224 fn public_rpc_urls(&self) -> Vec<String> {
225 (*self)
226 .public_rpc_urls()
227 .map(|urls| urls.to_vec())
228 .unwrap_or_default()
229 }
230
231 fn new_provider(
232 rpc_urls: Vec<RpcConfig>,
233 timeout_seconds: u64,
234 ) -> Result<Self::Provider, ProviderError> {
235 StellarProvider::new(rpc_urls, timeout_seconds)
236 }
237}
238
239pub fn get_network_provider<N: NetworkConfiguration>(
262 network: &N,
263 custom_rpc_urls: Option<Vec<RpcConfig>>,
264) -> Result<N::Provider, ProviderError> {
265 let rpc_timeout_ms = ServerConfig::from_env().rpc_timeout_ms;
266 let timeout_seconds = rpc_timeout_ms / 1000; let rpc_urls = match custom_rpc_urls {
269 Some(configs) if !configs.is_empty() => configs,
270 _ => {
271 let urls = network.public_rpc_urls();
272 if urls.is_empty() {
273 return Err(ProviderError::NetworkConfiguration(
274 "No public RPC URLs available for this network".to_string(),
275 ));
276 }
277 urls.into_iter().map(RpcConfig::new).collect()
278 }
279 };
280
281 N::new_provider(rpc_urls, timeout_seconds)
282}
283
284pub fn should_mark_provider_failed_by_status_code(status_code: u16) -> bool {
296 match status_code {
297 500..=599 => true,
299
300 401 => true, 403 => true, 404 => true, 410 => true, _ => false,
307 }
308}
309
310pub fn should_mark_provider_failed(error: &ProviderError) -> bool {
311 match error {
312 ProviderError::RequestError { status_code, .. } => {
313 should_mark_provider_failed_by_status_code(*status_code)
314 }
315 _ => false,
316 }
317}
318
319pub fn is_retriable_error(error: &ProviderError) -> bool {
321 match error {
322 ProviderError::Timeout
324 | ProviderError::RateLimited
325 | ProviderError::BadGateway
326 | ProviderError::TransportError(_) => true,
327
328 ProviderError::RequestError { status_code, .. } => {
329 match *status_code {
330 501 | 505 => false, 500 | 502..=504 | 506..=599 => true,
335
336 408 | 425 | 429 => true,
338
339 400..=499 => false,
341
342 _ => false,
344 }
345 }
346
347 ProviderError::RpcErrorCode { code, .. } => {
349 match code {
350 -32002 => true,
352 -32005 => true,
354 -32603 => true,
356 -32000 => false,
358 -32001 => false,
360 -32003 => false,
362 -32004 => false,
364
365 -32700..=-32600 => false,
371
372 _ => false,
374 }
375 }
376
377 ProviderError::SolanaRpcError(err) => err.is_transient(),
378
379 _ => {
381 let err_msg = format!("{error}");
382 let msg_lower = err_msg.to_lowercase();
383 msg_lower.contains("timeout")
384 || msg_lower.contains("connection")
385 || msg_lower.contains("reset")
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use lazy_static::lazy_static;
394 use std::env;
395 use std::sync::Mutex;
396 use std::time::Duration;
397
398 lazy_static! {
400 static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
401 }
402
403 fn setup_test_env() {
404 env::set_var("API_KEY", "7EF1CB7C-5003-4696-B384-C72AF8C3E15D"); env::set_var("REDIS_URL", "redis://localhost:6379");
406 env::set_var("RPC_TIMEOUT_MS", "5000");
407 }
408
409 fn cleanup_test_env() {
410 env::remove_var("API_KEY");
411 env::remove_var("REDIS_URL");
412 env::remove_var("RPC_TIMEOUT_MS");
413 }
414
415 fn create_test_evm_network() -> EvmNetwork {
416 EvmNetwork {
417 network: "test-evm".to_string(),
418 rpc_urls: vec!["https://rpc.example.com".to_string()],
419 explorer_urls: None,
420 average_blocktime_ms: 12000,
421 is_testnet: true,
422 tags: vec![],
423 chain_id: 1337,
424 required_confirmations: 1,
425 features: vec![],
426 symbol: "ETH".to_string(),
427 gas_price_cache: None,
428 }
429 }
430
431 fn create_test_solana_network(network_str: &str) -> SolanaNetwork {
432 SolanaNetwork {
433 network: network_str.to_string(),
434 rpc_urls: vec!["https://api.testnet.solana.com".to_string()],
435 explorer_urls: None,
436 average_blocktime_ms: 400,
437 is_testnet: true,
438 tags: vec![],
439 }
440 }
441
442 fn create_test_stellar_network() -> StellarNetwork {
443 StellarNetwork {
444 network: "testnet".to_string(),
445 rpc_urls: vec!["https://soroban-testnet.stellar.org".to_string()],
446 explorer_urls: None,
447 average_blocktime_ms: 5000,
448 is_testnet: true,
449 tags: vec![],
450 passphrase: "Test SDF Network ; September 2015".to_string(),
451 }
452 }
453
454 #[test]
455 fn test_from_hex_error() {
456 let hex_error = hex::FromHexError::OddLength;
457 let provider_error: ProviderError = hex_error.into();
458 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
459 }
460
461 #[test]
462 fn test_from_addr_parse_error() {
463 let addr_error = "invalid:address"
464 .parse::<std::net::SocketAddr>()
465 .unwrap_err();
466 let provider_error: ProviderError = addr_error.into();
467 assert!(matches!(
468 provider_error,
469 ProviderError::NetworkConfiguration(_)
470 ));
471 }
472
473 #[test]
474 fn test_from_parse_int_error() {
475 let parse_error = "not_a_number".parse::<u64>().unwrap_err();
476 let provider_error: ProviderError = parse_error.into();
477 assert!(matches!(provider_error, ProviderError::Other(_)));
478 }
479
480 #[actix_rt::test]
481 async fn test_categorize_reqwest_error_timeout() {
482 let client = reqwest::Client::new();
483 let timeout_err = client
484 .get("http://example.com")
485 .timeout(Duration::from_nanos(1))
486 .send()
487 .await
488 .unwrap_err();
489
490 assert!(timeout_err.is_timeout());
491
492 let provider_error = categorize_reqwest_error(&timeout_err);
493 assert!(matches!(provider_error, ProviderError::Timeout));
494 }
495
496 #[actix_rt::test]
497 async fn test_categorize_reqwest_error_rate_limited() {
498 let mut mock_server = mockito::Server::new_async().await;
499
500 let _mock = mock_server
501 .mock("GET", mockito::Matcher::Any)
502 .with_status(429)
503 .create_async()
504 .await;
505
506 let client = reqwest::Client::new();
507 let response = client
508 .get(mock_server.url())
509 .send()
510 .await
511 .expect("Failed to get response");
512
513 let err = response
514 .error_for_status()
515 .expect_err("Expected error for status 429");
516
517 assert!(err.status().is_some());
518 assert_eq!(err.status().unwrap().as_u16(), 429);
519
520 let provider_error = categorize_reqwest_error(&err);
521 assert!(matches!(provider_error, ProviderError::RateLimited));
522 }
523
524 #[actix_rt::test]
525 async fn test_categorize_reqwest_error_bad_gateway() {
526 let mut mock_server = mockito::Server::new_async().await;
527
528 let _mock = mock_server
529 .mock("GET", mockito::Matcher::Any)
530 .with_status(502)
531 .create_async()
532 .await;
533
534 let client = reqwest::Client::new();
535 let response = client
536 .get(mock_server.url())
537 .send()
538 .await
539 .expect("Failed to get response");
540
541 let err = response
542 .error_for_status()
543 .expect_err("Expected error for status 502");
544
545 assert!(err.status().is_some());
546 assert_eq!(err.status().unwrap().as_u16(), 502);
547
548 let provider_error = categorize_reqwest_error(&err);
549 assert!(matches!(provider_error, ProviderError::BadGateway));
550 }
551
552 #[actix_rt::test]
553 async fn test_categorize_reqwest_error_other() {
554 let client = reqwest::Client::new();
555 let err = client
556 .get("http://non-existent-host-12345.local")
557 .send()
558 .await
559 .unwrap_err();
560
561 assert!(!err.is_timeout());
562 assert!(err.status().is_none()); let provider_error = categorize_reqwest_error(&err);
565 assert!(matches!(provider_error, ProviderError::Other(_)));
566 }
567
568 #[test]
569 fn test_from_eyre_report_other_error() {
570 let eyre_error: eyre::Report = eyre::eyre!("Generic error");
571 let provider_error: ProviderError = eyre_error.into();
572 assert!(matches!(provider_error, ProviderError::Other(_)));
573 }
574
575 #[test]
576 fn test_get_evm_network_provider_valid_network() {
577 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
578 setup_test_env();
579
580 let network = create_test_evm_network();
581 let result = get_network_provider(&network, None);
582
583 cleanup_test_env();
584 assert!(result.is_ok());
585 }
586
587 #[test]
588 fn test_get_evm_network_provider_with_custom_urls() {
589 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
590 setup_test_env();
591
592 let network = create_test_evm_network();
593 let custom_urls = vec![
594 RpcConfig {
595 url: "https://custom-rpc1.example.com".to_string(),
596 weight: 1,
597 },
598 RpcConfig {
599 url: "https://custom-rpc2.example.com".to_string(),
600 weight: 1,
601 },
602 ];
603 let result = get_network_provider(&network, Some(custom_urls));
604
605 cleanup_test_env();
606 assert!(result.is_ok());
607 }
608
609 #[test]
610 fn test_get_evm_network_provider_with_empty_custom_urls() {
611 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
612 setup_test_env();
613
614 let network = create_test_evm_network();
615 let custom_urls: Vec<RpcConfig> = vec![];
616 let result = get_network_provider(&network, Some(custom_urls));
617
618 cleanup_test_env();
619 assert!(result.is_ok()); }
621
622 #[test]
623 fn test_get_solana_network_provider_valid_network_mainnet_beta() {
624 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
625 setup_test_env();
626
627 let network = create_test_solana_network("mainnet-beta");
628 let result = get_network_provider(&network, None);
629
630 cleanup_test_env();
631 assert!(result.is_ok());
632 }
633
634 #[test]
635 fn test_get_solana_network_provider_valid_network_testnet() {
636 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
637 setup_test_env();
638
639 let network = create_test_solana_network("testnet");
640 let result = get_network_provider(&network, None);
641
642 cleanup_test_env();
643 assert!(result.is_ok());
644 }
645
646 #[test]
647 fn test_get_solana_network_provider_with_custom_urls() {
648 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
649 setup_test_env();
650
651 let network = create_test_solana_network("testnet");
652 let custom_urls = vec![
653 RpcConfig {
654 url: "https://custom-rpc1.example.com".to_string(),
655 weight: 1,
656 },
657 RpcConfig {
658 url: "https://custom-rpc2.example.com".to_string(),
659 weight: 1,
660 },
661 ];
662 let result = get_network_provider(&network, Some(custom_urls));
663
664 cleanup_test_env();
665 assert!(result.is_ok());
666 }
667
668 #[test]
669 fn test_get_solana_network_provider_with_empty_custom_urls() {
670 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
671 setup_test_env();
672
673 let network = create_test_solana_network("testnet");
674 let custom_urls: Vec<RpcConfig> = vec![];
675 let result = get_network_provider(&network, Some(custom_urls));
676
677 cleanup_test_env();
678 assert!(result.is_ok()); }
680
681 #[test]
683 fn test_get_stellar_network_provider_valid_network_fallback_public() {
684 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
685 setup_test_env();
686
687 let network = create_test_stellar_network();
688 let result = get_network_provider(&network, None); cleanup_test_env();
691 assert!(result.is_ok()); }
694
695 #[test]
696 fn test_get_stellar_network_provider_with_custom_urls() {
697 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
698 setup_test_env();
699
700 let network = create_test_stellar_network();
701 let custom_urls = vec![
702 RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
703 RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
704 .unwrap(),
705 ];
706 let result = get_network_provider(&network, Some(custom_urls));
707
708 cleanup_test_env();
709 assert!(result.is_ok());
710 }
712
713 #[test]
714 fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
715 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
716 setup_test_env();
717
718 let network = create_test_stellar_network();
719 let custom_urls: Vec<RpcConfig> = vec![]; let result = get_network_provider(&network, Some(custom_urls));
721
722 cleanup_test_env();
723 assert!(result.is_ok()); }
726
727 #[test]
728 fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
729 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
730 setup_test_env();
731
732 let network = create_test_stellar_network();
733 let custom_urls = vec![
734 RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
735 RpcConfig::new("http://active-rpc.example.com".to_string()), ];
737 let result = get_network_provider(&network, Some(custom_urls));
738 cleanup_test_env();
739 assert!(result.is_ok()); }
741
742 #[test]
743 fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
744 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
745 setup_test_env();
746
747 let network = create_test_stellar_network();
748 let custom_urls = vec![
749 RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
750 RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
751 ];
752 let result = get_network_provider(&network, Some(custom_urls));
758 cleanup_test_env();
759 assert!(result.is_err());
760 match result.unwrap_err() {
761 ProviderError::NetworkConfiguration(msg) => {
762 assert!(msg.contains("No active RPC configurations provided"));
763 }
764 _ => panic!("Unexpected error type"),
765 }
766 }
767
768 #[test]
769 fn test_provider_error_rpc_error_code_variant() {
770 let error = ProviderError::RpcErrorCode {
771 code: -32000,
772 message: "insufficient funds".to_string(),
773 };
774 let error_string = format!("{}", error);
775 assert!(error_string.contains("-32000"));
776 assert!(error_string.contains("insufficient funds"));
777 }
778
779 #[test]
780 fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
781 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
782 setup_test_env();
783 let network = create_test_stellar_network();
784 let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
785 let result = get_network_provider(&network, Some(custom_urls));
786 cleanup_test_env();
787 assert!(result.is_err());
788 match result.unwrap_err() {
789 ProviderError::NetworkConfiguration(msg) => {
790 assert!(msg.contains("Invalid URL scheme"));
792 }
793 _ => panic!("Unexpected error type"),
794 }
795 }
796
797 #[test]
798 fn test_should_mark_provider_failed_server_errors() {
799 for status_code in 500..=599 {
801 let error = ProviderError::RequestError {
802 error: format!("Server error {}", status_code),
803 status_code,
804 };
805 assert!(
806 should_mark_provider_failed(&error),
807 "Status code {} should mark provider as failed",
808 status_code
809 );
810 }
811 }
812
813 #[test]
814 fn test_should_mark_provider_failed_auth_errors() {
815 let auth_errors = [401, 403];
817 for &status_code in &auth_errors {
818 let error = ProviderError::RequestError {
819 error: format!("Auth error {}", status_code),
820 status_code,
821 };
822 assert!(
823 should_mark_provider_failed(&error),
824 "Status code {} should mark provider as failed",
825 status_code
826 );
827 }
828 }
829
830 #[test]
831 fn test_should_mark_provider_failed_not_found_errors() {
832 let not_found_errors = [404, 410];
834 for &status_code in ¬_found_errors {
835 let error = ProviderError::RequestError {
836 error: format!("Not found error {}", status_code),
837 status_code,
838 };
839 assert!(
840 should_mark_provider_failed(&error),
841 "Status code {} should mark provider as failed",
842 status_code
843 );
844 }
845 }
846
847 #[test]
848 fn test_should_mark_provider_failed_client_errors_not_failed() {
849 let client_errors = [400, 405, 413, 414, 415, 422, 429];
851 for &status_code in &client_errors {
852 let error = ProviderError::RequestError {
853 error: format!("Client error {}", status_code),
854 status_code,
855 };
856 assert!(
857 !should_mark_provider_failed(&error),
858 "Status code {} should NOT mark provider as failed",
859 status_code
860 );
861 }
862 }
863
864 #[test]
865 fn test_should_mark_provider_failed_other_error_types() {
866 let errors = [
868 ProviderError::Timeout,
869 ProviderError::RateLimited,
870 ProviderError::BadGateway,
871 ProviderError::InvalidAddress("test".to_string()),
872 ProviderError::NetworkConfiguration("test".to_string()),
873 ProviderError::Other("test".to_string()),
874 ];
875
876 for error in errors {
877 assert!(
878 !should_mark_provider_failed(&error),
879 "Error type {:?} should NOT mark provider as failed",
880 error
881 );
882 }
883 }
884
885 #[test]
886 fn test_should_mark_provider_failed_edge_cases() {
887 let edge_cases = [
889 (200, false), (300, false), (418, false), (451, false), (499, false), ];
895
896 for (status_code, should_fail) in edge_cases {
897 let error = ProviderError::RequestError {
898 error: format!("Edge case error {}", status_code),
899 status_code,
900 };
901 assert_eq!(
902 should_mark_provider_failed(&error),
903 should_fail,
904 "Status code {} should {} mark provider as failed",
905 status_code,
906 if should_fail { "" } else { "NOT" }
907 );
908 }
909 }
910
911 #[test]
912 fn test_is_retriable_error_retriable_types() {
913 let retriable_errors = [
915 ProviderError::Timeout,
916 ProviderError::RateLimited,
917 ProviderError::BadGateway,
918 ProviderError::TransportError("test".to_string()),
919 ];
920
921 for error in retriable_errors {
922 assert!(
923 is_retriable_error(&error),
924 "Error type {:?} should be retriable",
925 error
926 );
927 }
928 }
929
930 #[test]
931 fn test_is_retriable_error_non_retriable_types() {
932 let non_retriable_errors = [
934 ProviderError::InvalidAddress("test".to_string()),
935 ProviderError::NetworkConfiguration("test".to_string()),
936 ProviderError::RequestError {
937 error: "Some error".to_string(),
938 status_code: 400,
939 },
940 ];
941
942 for error in non_retriable_errors {
943 assert!(
944 !is_retriable_error(&error),
945 "Error type {:?} should NOT be retriable",
946 error
947 );
948 }
949 }
950
951 #[test]
952 fn test_is_retriable_error_message_based_detection() {
953 let retriable_messages = [
955 "Connection timeout occurred",
956 "Network connection reset",
957 "Connection refused",
958 "TIMEOUT error happened",
959 "Connection was reset by peer",
960 ];
961
962 for message in retriable_messages {
963 let error = ProviderError::Other(message.to_string());
964 assert!(
965 is_retriable_error(&error),
966 "Error with message '{}' should be retriable",
967 message
968 );
969 }
970 }
971
972 #[test]
973 fn test_is_retriable_error_message_based_non_retriable() {
974 let non_retriable_messages = [
976 "Invalid address format",
977 "Bad request parameters",
978 "Authentication failed",
979 "Method not found",
980 "Some other error",
981 ];
982
983 for message in non_retriable_messages {
984 let error = ProviderError::Other(message.to_string());
985 assert!(
986 !is_retriable_error(&error),
987 "Error with message '{}' should NOT be retriable",
988 message
989 );
990 }
991 }
992
993 #[test]
994 fn test_is_retriable_error_case_insensitive() {
995 let case_variations = [
997 "TIMEOUT",
998 "Timeout",
999 "timeout",
1000 "CONNECTION",
1001 "Connection",
1002 "connection",
1003 "RESET",
1004 "Reset",
1005 "reset",
1006 ];
1007
1008 for message in case_variations {
1009 let error = ProviderError::Other(message.to_string());
1010 assert!(
1011 is_retriable_error(&error),
1012 "Error with message '{}' should be retriable (case insensitive)",
1013 message
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn test_is_retriable_error_request_error_retriable_5xx() {
1020 let retriable_5xx = vec![
1022 (500, "Internal Server Error"),
1023 (502, "Bad Gateway"),
1024 (503, "Service Unavailable"),
1025 (504, "Gateway Timeout"),
1026 (506, "Variant Also Negotiates"),
1027 (507, "Insufficient Storage"),
1028 (508, "Loop Detected"),
1029 (510, "Not Extended"),
1030 (511, "Network Authentication Required"),
1031 (599, "Network Connect Timeout Error"),
1032 ];
1033
1034 for (status_code, description) in retriable_5xx {
1035 let error = ProviderError::RequestError {
1036 error: description.to_string(),
1037 status_code,
1038 };
1039 assert!(
1040 is_retriable_error(&error),
1041 "Status code {} ({}) should be retriable",
1042 status_code,
1043 description
1044 );
1045 }
1046 }
1047
1048 #[test]
1049 fn test_is_retriable_error_request_error_non_retriable_5xx() {
1050 let non_retriable_5xx = vec![
1052 (501, "Not Implemented"),
1053 (505, "HTTP Version Not Supported"),
1054 ];
1055
1056 for (status_code, description) in non_retriable_5xx {
1057 let error = ProviderError::RequestError {
1058 error: description.to_string(),
1059 status_code,
1060 };
1061 assert!(
1062 !is_retriable_error(&error),
1063 "Status code {} ({}) should NOT be retriable",
1064 status_code,
1065 description
1066 );
1067 }
1068 }
1069
1070 #[test]
1071 fn test_is_retriable_error_request_error_retriable_4xx() {
1072 let retriable_4xx = vec![
1074 (408, "Request Timeout"),
1075 (425, "Too Early"),
1076 (429, "Too Many Requests"),
1077 ];
1078
1079 for (status_code, description) in retriable_4xx {
1080 let error = ProviderError::RequestError {
1081 error: description.to_string(),
1082 status_code,
1083 };
1084 assert!(
1085 is_retriable_error(&error),
1086 "Status code {} ({}) should be retriable",
1087 status_code,
1088 description
1089 );
1090 }
1091 }
1092
1093 #[test]
1094 fn test_is_retriable_error_request_error_non_retriable_4xx() {
1095 let non_retriable_4xx = vec![
1097 (400, "Bad Request"),
1098 (401, "Unauthorized"),
1099 (403, "Forbidden"),
1100 (404, "Not Found"),
1101 (405, "Method Not Allowed"),
1102 (406, "Not Acceptable"),
1103 (407, "Proxy Authentication Required"),
1104 (409, "Conflict"),
1105 (410, "Gone"),
1106 (411, "Length Required"),
1107 (412, "Precondition Failed"),
1108 (413, "Payload Too Large"),
1109 (414, "URI Too Long"),
1110 (415, "Unsupported Media Type"),
1111 (416, "Range Not Satisfiable"),
1112 (417, "Expectation Failed"),
1113 (418, "I'm a teapot"),
1114 (421, "Misdirected Request"),
1115 (422, "Unprocessable Entity"),
1116 (423, "Locked"),
1117 (424, "Failed Dependency"),
1118 (426, "Upgrade Required"),
1119 (428, "Precondition Required"),
1120 (431, "Request Header Fields Too Large"),
1121 (451, "Unavailable For Legal Reasons"),
1122 (499, "Client Closed Request"),
1123 ];
1124
1125 for (status_code, description) in non_retriable_4xx {
1126 let error = ProviderError::RequestError {
1127 error: description.to_string(),
1128 status_code,
1129 };
1130 assert!(
1131 !is_retriable_error(&error),
1132 "Status code {} ({}) should NOT be retriable",
1133 status_code,
1134 description
1135 );
1136 }
1137 }
1138
1139 #[test]
1140 fn test_is_retriable_error_request_error_other_status_codes() {
1141 let other_status_codes = vec![
1143 (100, "Continue"),
1144 (101, "Switching Protocols"),
1145 (200, "OK"),
1146 (201, "Created"),
1147 (204, "No Content"),
1148 (300, "Multiple Choices"),
1149 (301, "Moved Permanently"),
1150 (302, "Found"),
1151 (304, "Not Modified"),
1152 (600, "Custom status"),
1153 (999, "Unknown status"),
1154 ];
1155
1156 for (status_code, description) in other_status_codes {
1157 let error = ProviderError::RequestError {
1158 error: description.to_string(),
1159 status_code,
1160 };
1161 assert!(
1162 !is_retriable_error(&error),
1163 "Status code {} ({}) should NOT be retriable",
1164 status_code,
1165 description
1166 );
1167 }
1168 }
1169
1170 #[test]
1171 fn test_is_retriable_error_request_error_boundary_cases() {
1172 let test_cases = vec![
1174 (407, false, "Proxy Authentication Required"),
1176 (408, true, "Request Timeout - first retriable 4xx"),
1177 (409, false, "Conflict"),
1178 (424, false, "Failed Dependency"),
1180 (425, true, "Too Early"),
1181 (426, false, "Upgrade Required"),
1182 (428, false, "Precondition Required"),
1184 (429, true, "Too Many Requests"),
1185 (430, false, "Would be non-retriable if it existed"),
1186 (499, false, "Last 4xx"),
1188 (500, true, "First 5xx - retriable"),
1189 (501, false, "Not Implemented - exception"),
1190 (502, true, "Bad Gateway - retriable"),
1191 (505, false, "HTTP Version Not Supported - exception"),
1192 (506, true, "First after 505 exception"),
1193 (599, true, "Last defined 5xx"),
1194 ];
1195
1196 for (status_code, should_be_retriable, description) in test_cases {
1197 let error = ProviderError::RequestError {
1198 error: description.to_string(),
1199 status_code,
1200 };
1201 assert_eq!(
1202 is_retriable_error(&error),
1203 should_be_retriable,
1204 "Status code {} ({}) should{} be retriable",
1205 status_code,
1206 description,
1207 if should_be_retriable { "" } else { " NOT" }
1208 );
1209 }
1210 }
1211}