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