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 {} should mark provider as failed",
873 status_code
874 );
875 }
876 }
877
878 #[test]
879 fn test_should_mark_provider_failed_auth_errors() {
880 let auth_errors = [401, 403];
882 for &status_code in &auth_errors {
883 let error = ProviderError::RequestError {
884 error: format!("Auth error {}", status_code),
885 status_code,
886 };
887 assert!(
888 should_mark_provider_failed(&error),
889 "Status code {} should mark provider as failed",
890 status_code
891 );
892 }
893 }
894
895 #[test]
896 fn test_should_mark_provider_failed_not_found_errors() {
897 let not_found_errors = [404, 410];
899 for &status_code in ¬_found_errors {
900 let error = ProviderError::RequestError {
901 error: format!("Not found error {}", status_code),
902 status_code,
903 };
904 assert!(
905 should_mark_provider_failed(&error),
906 "Status code {} should mark provider as failed",
907 status_code
908 );
909 }
910 }
911
912 #[test]
913 fn test_should_mark_provider_failed_client_errors_not_failed() {
914 let client_errors = [400, 405, 413, 414, 415, 422, 429];
916 for &status_code in &client_errors {
917 let error = ProviderError::RequestError {
918 error: format!("Client error {}", status_code),
919 status_code,
920 };
921 assert!(
922 !should_mark_provider_failed(&error),
923 "Status code {} should NOT mark provider as failed",
924 status_code
925 );
926 }
927 }
928
929 #[test]
930 fn test_should_mark_provider_failed_other_error_types() {
931 let errors = [
933 ProviderError::Timeout,
934 ProviderError::RateLimited,
935 ProviderError::BadGateway,
936 ProviderError::InvalidAddress("test".to_string()),
937 ProviderError::NetworkConfiguration("test".to_string()),
938 ProviderError::Other("test".to_string()),
939 ];
940
941 for error in errors {
942 assert!(
943 !should_mark_provider_failed(&error),
944 "Error type {:?} should NOT mark provider as failed",
945 error
946 );
947 }
948 }
949
950 #[test]
951 fn test_should_mark_provider_failed_edge_cases() {
952 let edge_cases = [
954 (200, false), (300, false), (418, false), (451, false), (499, false), ];
960
961 for (status_code, should_fail) in edge_cases {
962 let error = ProviderError::RequestError {
963 error: format!("Edge case error {}", status_code),
964 status_code,
965 };
966 assert_eq!(
967 should_mark_provider_failed(&error),
968 should_fail,
969 "Status code {} should {} mark provider as failed",
970 status_code,
971 if should_fail { "" } else { "NOT" }
972 );
973 }
974 }
975
976 #[test]
977 fn test_is_retriable_error_retriable_types() {
978 let retriable_errors = [
980 ProviderError::Timeout,
981 ProviderError::RateLimited,
982 ProviderError::BadGateway,
983 ProviderError::TransportError("test".to_string()),
984 ];
985
986 for error in retriable_errors {
987 assert!(
988 is_retriable_error(&error),
989 "Error type {:?} should be retriable",
990 error
991 );
992 }
993 }
994
995 #[test]
996 fn test_is_retriable_error_non_retriable_types() {
997 let non_retriable_errors = [
999 ProviderError::InvalidAddress("test".to_string()),
1000 ProviderError::NetworkConfiguration("test".to_string()),
1001 ProviderError::RequestError {
1002 error: "Some error".to_string(),
1003 status_code: 400,
1004 },
1005 ];
1006
1007 for error in non_retriable_errors {
1008 assert!(
1009 !is_retriable_error(&error),
1010 "Error type {:?} should NOT be retriable",
1011 error
1012 );
1013 }
1014 }
1015
1016 #[test]
1017 fn test_is_retriable_error_message_based_detection() {
1018 let retriable_messages = [
1020 "Connection timeout occurred",
1021 "Network connection reset",
1022 "Connection refused",
1023 "TIMEOUT error happened",
1024 "Connection was reset by peer",
1025 ];
1026
1027 for message in retriable_messages {
1028 let error = ProviderError::Other(message.to_string());
1029 assert!(
1030 is_retriable_error(&error),
1031 "Error with message '{}' should be retriable",
1032 message
1033 );
1034 }
1035 }
1036
1037 #[test]
1038 fn test_is_retriable_error_message_based_non_retriable() {
1039 let non_retriable_messages = [
1041 "Invalid address format",
1042 "Bad request parameters",
1043 "Authentication failed",
1044 "Method not found",
1045 "Some other error",
1046 ];
1047
1048 for message in non_retriable_messages {
1049 let error = ProviderError::Other(message.to_string());
1050 assert!(
1051 !is_retriable_error(&error),
1052 "Error with message '{}' should NOT be retriable",
1053 message
1054 );
1055 }
1056 }
1057
1058 #[test]
1059 fn test_is_retriable_error_case_insensitive() {
1060 let case_variations = [
1062 "TIMEOUT",
1063 "Timeout",
1064 "timeout",
1065 "CONNECTION",
1066 "Connection",
1067 "connection",
1068 "RESET",
1069 "Reset",
1070 "reset",
1071 ];
1072
1073 for message in case_variations {
1074 let error = ProviderError::Other(message.to_string());
1075 assert!(
1076 is_retriable_error(&error),
1077 "Error with message '{}' should be retriable (case insensitive)",
1078 message
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn test_is_retriable_error_request_error_retriable_5xx() {
1085 let retriable_5xx = vec![
1087 (500, "Internal Server Error"),
1088 (502, "Bad Gateway"),
1089 (503, "Service Unavailable"),
1090 (504, "Gateway Timeout"),
1091 (506, "Variant Also Negotiates"),
1092 (507, "Insufficient Storage"),
1093 (508, "Loop Detected"),
1094 (510, "Not Extended"),
1095 (511, "Network Authentication Required"),
1096 (599, "Network Connect Timeout Error"),
1097 ];
1098
1099 for (status_code, description) in retriable_5xx {
1100 let error = ProviderError::RequestError {
1101 error: description.to_string(),
1102 status_code,
1103 };
1104 assert!(
1105 is_retriable_error(&error),
1106 "Status code {} ({}) should be retriable",
1107 status_code,
1108 description
1109 );
1110 }
1111 }
1112
1113 #[test]
1114 fn test_is_retriable_error_request_error_non_retriable_5xx() {
1115 let non_retriable_5xx = vec![
1117 (501, "Not Implemented"),
1118 (505, "HTTP Version Not Supported"),
1119 ];
1120
1121 for (status_code, description) in non_retriable_5xx {
1122 let error = ProviderError::RequestError {
1123 error: description.to_string(),
1124 status_code,
1125 };
1126 assert!(
1127 !is_retriable_error(&error),
1128 "Status code {} ({}) should NOT be retriable",
1129 status_code,
1130 description
1131 );
1132 }
1133 }
1134
1135 #[test]
1136 fn test_is_retriable_error_request_error_retriable_4xx() {
1137 let retriable_4xx = vec![
1139 (408, "Request Timeout"),
1140 (425, "Too Early"),
1141 (429, "Too Many Requests"),
1142 ];
1143
1144 for (status_code, description) in retriable_4xx {
1145 let error = ProviderError::RequestError {
1146 error: description.to_string(),
1147 status_code,
1148 };
1149 assert!(
1150 is_retriable_error(&error),
1151 "Status code {} ({}) should be retriable",
1152 status_code,
1153 description
1154 );
1155 }
1156 }
1157
1158 #[test]
1159 fn test_is_retriable_error_request_error_non_retriable_4xx() {
1160 let non_retriable_4xx = vec![
1162 (400, "Bad Request"),
1163 (401, "Unauthorized"),
1164 (403, "Forbidden"),
1165 (404, "Not Found"),
1166 (405, "Method Not Allowed"),
1167 (406, "Not Acceptable"),
1168 (407, "Proxy Authentication Required"),
1169 (409, "Conflict"),
1170 (410, "Gone"),
1171 (411, "Length Required"),
1172 (412, "Precondition Failed"),
1173 (413, "Payload Too Large"),
1174 (414, "URI Too Long"),
1175 (415, "Unsupported Media Type"),
1176 (416, "Range Not Satisfiable"),
1177 (417, "Expectation Failed"),
1178 (418, "I'm a teapot"),
1179 (421, "Misdirected Request"),
1180 (422, "Unprocessable Entity"),
1181 (423, "Locked"),
1182 (424, "Failed Dependency"),
1183 (426, "Upgrade Required"),
1184 (428, "Precondition Required"),
1185 (431, "Request Header Fields Too Large"),
1186 (451, "Unavailable For Legal Reasons"),
1187 (499, "Client Closed Request"),
1188 ];
1189
1190 for (status_code, description) in non_retriable_4xx {
1191 let error = ProviderError::RequestError {
1192 error: description.to_string(),
1193 status_code,
1194 };
1195 assert!(
1196 !is_retriable_error(&error),
1197 "Status code {} ({}) should NOT be retriable",
1198 status_code,
1199 description
1200 );
1201 }
1202 }
1203
1204 #[test]
1205 fn test_is_retriable_error_request_error_other_status_codes() {
1206 let other_status_codes = vec![
1208 (100, "Continue"),
1209 (101, "Switching Protocols"),
1210 (200, "OK"),
1211 (201, "Created"),
1212 (204, "No Content"),
1213 (300, "Multiple Choices"),
1214 (301, "Moved Permanently"),
1215 (302, "Found"),
1216 (304, "Not Modified"),
1217 (600, "Custom status"),
1218 (999, "Unknown status"),
1219 ];
1220
1221 for (status_code, description) in other_status_codes {
1222 let error = ProviderError::RequestError {
1223 error: description.to_string(),
1224 status_code,
1225 };
1226 assert!(
1227 !is_retriable_error(&error),
1228 "Status code {} ({}) should NOT be retriable",
1229 status_code,
1230 description
1231 );
1232 }
1233 }
1234
1235 #[test]
1236 fn test_is_retriable_error_request_error_boundary_cases() {
1237 let test_cases = vec![
1239 (407, false, "Proxy Authentication Required"),
1241 (408, true, "Request Timeout - first retriable 4xx"),
1242 (409, false, "Conflict"),
1243 (424, false, "Failed Dependency"),
1245 (425, true, "Too Early"),
1246 (426, false, "Upgrade Required"),
1247 (428, false, "Precondition Required"),
1249 (429, true, "Too Many Requests"),
1250 (430, false, "Would be non-retriable if it existed"),
1251 (499, false, "Last 4xx"),
1253 (500, true, "First 5xx - retriable"),
1254 (501, false, "Not Implemented - exception"),
1255 (502, true, "Bad Gateway - retriable"),
1256 (505, false, "HTTP Version Not Supported - exception"),
1257 (506, true, "First after 505 exception"),
1258 (599, true, "Last defined 5xx"),
1259 ];
1260
1261 for (status_code, should_be_retriable, description) in test_cases {
1262 let error = ProviderError::RequestError {
1263 error: description.to_string(),
1264 status_code,
1265 };
1266 assert_eq!(
1267 is_retriable_error(&error),
1268 should_be_retriable,
1269 "Status code {} ({}) should{} be retriable",
1270 status_code,
1271 description,
1272 if should_be_retriable { "" } else { " NOT" }
1273 );
1274 }
1275 }
1276}