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 horizon_url: Some("https://horizon-testnet.stellar.org".to_string()),
452 }
453 }
454
455 #[test]
456 fn test_from_hex_error() {
457 let hex_error = hex::FromHexError::OddLength;
458 let provider_error: ProviderError = hex_error.into();
459 assert!(matches!(provider_error, ProviderError::InvalidAddress(_)));
460 }
461
462 #[test]
463 fn test_from_addr_parse_error() {
464 let addr_error = "invalid:address"
465 .parse::<std::net::SocketAddr>()
466 .unwrap_err();
467 let provider_error: ProviderError = addr_error.into();
468 assert!(matches!(
469 provider_error,
470 ProviderError::NetworkConfiguration(_)
471 ));
472 }
473
474 #[test]
475 fn test_from_parse_int_error() {
476 let parse_error = "not_a_number".parse::<u64>().unwrap_err();
477 let provider_error: ProviderError = parse_error.into();
478 assert!(matches!(provider_error, ProviderError::Other(_)));
479 }
480
481 #[actix_rt::test]
482 async fn test_categorize_reqwest_error_timeout() {
483 let client = reqwest::Client::new();
484 let timeout_err = client
485 .get("http://example.com")
486 .timeout(Duration::from_nanos(1))
487 .send()
488 .await
489 .unwrap_err();
490
491 assert!(timeout_err.is_timeout());
492
493 let provider_error = categorize_reqwest_error(&timeout_err);
494 assert!(matches!(provider_error, ProviderError::Timeout));
495 }
496
497 #[actix_rt::test]
498 async fn test_categorize_reqwest_error_rate_limited() {
499 let mut mock_server = mockito::Server::new_async().await;
500
501 let _mock = mock_server
502 .mock("GET", mockito::Matcher::Any)
503 .with_status(429)
504 .create_async()
505 .await;
506
507 let client = reqwest::Client::new();
508 let response = client
509 .get(mock_server.url())
510 .send()
511 .await
512 .expect("Failed to get response");
513
514 let err = response
515 .error_for_status()
516 .expect_err("Expected error for status 429");
517
518 assert!(err.status().is_some());
519 assert_eq!(err.status().unwrap().as_u16(), 429);
520
521 let provider_error = categorize_reqwest_error(&err);
522 assert!(matches!(provider_error, ProviderError::RateLimited));
523 }
524
525 #[actix_rt::test]
526 async fn test_categorize_reqwest_error_bad_gateway() {
527 let mut mock_server = mockito::Server::new_async().await;
528
529 let _mock = mock_server
530 .mock("GET", mockito::Matcher::Any)
531 .with_status(502)
532 .create_async()
533 .await;
534
535 let client = reqwest::Client::new();
536 let response = client
537 .get(mock_server.url())
538 .send()
539 .await
540 .expect("Failed to get response");
541
542 let err = response
543 .error_for_status()
544 .expect_err("Expected error for status 502");
545
546 assert!(err.status().is_some());
547 assert_eq!(err.status().unwrap().as_u16(), 502);
548
549 let provider_error = categorize_reqwest_error(&err);
550 assert!(matches!(provider_error, ProviderError::BadGateway));
551 }
552
553 #[actix_rt::test]
554 async fn test_categorize_reqwest_error_other() {
555 let client = reqwest::Client::new();
556 let err = client
557 .get("http://non-existent-host-12345.local")
558 .send()
559 .await
560 .unwrap_err();
561
562 assert!(!err.is_timeout());
563 assert!(err.status().is_none()); let provider_error = categorize_reqwest_error(&err);
566 assert!(matches!(provider_error, ProviderError::Other(_)));
567 }
568
569 #[test]
570 fn test_from_eyre_report_other_error() {
571 let eyre_error: eyre::Report = eyre::eyre!("Generic error");
572 let provider_error: ProviderError = eyre_error.into();
573 assert!(matches!(provider_error, ProviderError::Other(_)));
574 }
575
576 #[test]
577 fn test_get_evm_network_provider_valid_network() {
578 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
579 setup_test_env();
580
581 let network = create_test_evm_network();
582 let result = get_network_provider(&network, None);
583
584 cleanup_test_env();
585 assert!(result.is_ok());
586 }
587
588 #[test]
589 fn test_get_evm_network_provider_with_custom_urls() {
590 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
591 setup_test_env();
592
593 let network = create_test_evm_network();
594 let custom_urls = vec![
595 RpcConfig {
596 url: "https://custom-rpc1.example.com".to_string(),
597 weight: 1,
598 },
599 RpcConfig {
600 url: "https://custom-rpc2.example.com".to_string(),
601 weight: 1,
602 },
603 ];
604 let result = get_network_provider(&network, Some(custom_urls));
605
606 cleanup_test_env();
607 assert!(result.is_ok());
608 }
609
610 #[test]
611 fn test_get_evm_network_provider_with_empty_custom_urls() {
612 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
613 setup_test_env();
614
615 let network = create_test_evm_network();
616 let custom_urls: Vec<RpcConfig> = vec![];
617 let result = get_network_provider(&network, Some(custom_urls));
618
619 cleanup_test_env();
620 assert!(result.is_ok()); }
622
623 #[test]
624 fn test_get_solana_network_provider_valid_network_mainnet_beta() {
625 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
626 setup_test_env();
627
628 let network = create_test_solana_network("mainnet-beta");
629 let result = get_network_provider(&network, None);
630
631 cleanup_test_env();
632 assert!(result.is_ok());
633 }
634
635 #[test]
636 fn test_get_solana_network_provider_valid_network_testnet() {
637 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
638 setup_test_env();
639
640 let network = create_test_solana_network("testnet");
641 let result = get_network_provider(&network, None);
642
643 cleanup_test_env();
644 assert!(result.is_ok());
645 }
646
647 #[test]
648 fn test_get_solana_network_provider_with_custom_urls() {
649 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
650 setup_test_env();
651
652 let network = create_test_solana_network("testnet");
653 let custom_urls = vec![
654 RpcConfig {
655 url: "https://custom-rpc1.example.com".to_string(),
656 weight: 1,
657 },
658 RpcConfig {
659 url: "https://custom-rpc2.example.com".to_string(),
660 weight: 1,
661 },
662 ];
663 let result = get_network_provider(&network, Some(custom_urls));
664
665 cleanup_test_env();
666 assert!(result.is_ok());
667 }
668
669 #[test]
670 fn test_get_solana_network_provider_with_empty_custom_urls() {
671 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
672 setup_test_env();
673
674 let network = create_test_solana_network("testnet");
675 let custom_urls: Vec<RpcConfig> = vec![];
676 let result = get_network_provider(&network, Some(custom_urls));
677
678 cleanup_test_env();
679 assert!(result.is_ok()); }
681
682 #[test]
684 fn test_get_stellar_network_provider_valid_network_fallback_public() {
685 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
686 setup_test_env();
687
688 let network = create_test_stellar_network();
689 let result = get_network_provider(&network, None); cleanup_test_env();
692 assert!(result.is_ok()); }
695
696 #[test]
697 fn test_get_stellar_network_provider_with_custom_urls() {
698 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
699 setup_test_env();
700
701 let network = create_test_stellar_network();
702 let custom_urls = vec![
703 RpcConfig::new("https://custom-stellar-rpc1.example.com".to_string()),
704 RpcConfig::with_weight("http://custom-stellar-rpc2.example.com".to_string(), 50)
705 .unwrap(),
706 ];
707 let result = get_network_provider(&network, Some(custom_urls));
708
709 cleanup_test_env();
710 assert!(result.is_ok());
711 }
713
714 #[test]
715 fn test_get_stellar_network_provider_with_empty_custom_urls_fallback() {
716 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
717 setup_test_env();
718
719 let network = create_test_stellar_network();
720 let custom_urls: Vec<RpcConfig> = vec![]; let result = get_network_provider(&network, Some(custom_urls));
722
723 cleanup_test_env();
724 assert!(result.is_ok()); }
727
728 #[test]
729 fn test_get_stellar_network_provider_custom_urls_with_zero_weight() {
730 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
731 setup_test_env();
732
733 let network = create_test_stellar_network();
734 let custom_urls = vec![
735 RpcConfig::with_weight("http://zero-weight-rpc.example.com".to_string(), 0).unwrap(),
736 RpcConfig::new("http://active-rpc.example.com".to_string()), ];
738 let result = get_network_provider(&network, Some(custom_urls));
739 cleanup_test_env();
740 assert!(result.is_ok()); }
742
743 #[test]
744 fn test_get_stellar_network_provider_all_custom_urls_zero_weight_fallback() {
745 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
746 setup_test_env();
747
748 let network = create_test_stellar_network();
749 let custom_urls = vec![
750 RpcConfig::with_weight("http://zero1.example.com".to_string(), 0).unwrap(),
751 RpcConfig::with_weight("http://zero2.example.com".to_string(), 0).unwrap(),
752 ];
753 let result = get_network_provider(&network, Some(custom_urls));
759 cleanup_test_env();
760 assert!(result.is_err());
761 match result.unwrap_err() {
762 ProviderError::NetworkConfiguration(msg) => {
763 assert!(msg.contains("No active RPC configurations provided"));
764 }
765 _ => panic!("Unexpected error type"),
766 }
767 }
768
769 #[test]
770 fn test_provider_error_rpc_error_code_variant() {
771 let error = ProviderError::RpcErrorCode {
772 code: -32000,
773 message: "insufficient funds".to_string(),
774 };
775 let error_string = format!("{}", error);
776 assert!(error_string.contains("-32000"));
777 assert!(error_string.contains("insufficient funds"));
778 }
779
780 #[test]
781 fn test_get_stellar_network_provider_invalid_custom_url_scheme() {
782 let _lock = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
783 setup_test_env();
784 let network = create_test_stellar_network();
785 let custom_urls = vec![RpcConfig::new("ftp://custom-ftp.example.com".to_string())];
786 let result = get_network_provider(&network, Some(custom_urls));
787 cleanup_test_env();
788 assert!(result.is_err());
789 match result.unwrap_err() {
790 ProviderError::NetworkConfiguration(msg) => {
791 assert!(msg.contains("Invalid URL scheme"));
793 }
794 _ => panic!("Unexpected error type"),
795 }
796 }
797
798 #[test]
799 fn test_should_mark_provider_failed_server_errors() {
800 for status_code in 500..=599 {
802 let error = ProviderError::RequestError {
803 error: format!("Server error {}", status_code),
804 status_code,
805 };
806 assert!(
807 should_mark_provider_failed(&error),
808 "Status code {} should mark provider as failed",
809 status_code
810 );
811 }
812 }
813
814 #[test]
815 fn test_should_mark_provider_failed_auth_errors() {
816 let auth_errors = [401, 403];
818 for &status_code in &auth_errors {
819 let error = ProviderError::RequestError {
820 error: format!("Auth error {}", status_code),
821 status_code,
822 };
823 assert!(
824 should_mark_provider_failed(&error),
825 "Status code {} should mark provider as failed",
826 status_code
827 );
828 }
829 }
830
831 #[test]
832 fn test_should_mark_provider_failed_not_found_errors() {
833 let not_found_errors = [404, 410];
835 for &status_code in ¬_found_errors {
836 let error = ProviderError::RequestError {
837 error: format!("Not found error {}", status_code),
838 status_code,
839 };
840 assert!(
841 should_mark_provider_failed(&error),
842 "Status code {} should mark provider as failed",
843 status_code
844 );
845 }
846 }
847
848 #[test]
849 fn test_should_mark_provider_failed_client_errors_not_failed() {
850 let client_errors = [400, 405, 413, 414, 415, 422, 429];
852 for &status_code in &client_errors {
853 let error = ProviderError::RequestError {
854 error: format!("Client error {}", status_code),
855 status_code,
856 };
857 assert!(
858 !should_mark_provider_failed(&error),
859 "Status code {} should NOT mark provider as failed",
860 status_code
861 );
862 }
863 }
864
865 #[test]
866 fn test_should_mark_provider_failed_other_error_types() {
867 let errors = [
869 ProviderError::Timeout,
870 ProviderError::RateLimited,
871 ProviderError::BadGateway,
872 ProviderError::InvalidAddress("test".to_string()),
873 ProviderError::NetworkConfiguration("test".to_string()),
874 ProviderError::Other("test".to_string()),
875 ];
876
877 for error in errors {
878 assert!(
879 !should_mark_provider_failed(&error),
880 "Error type {:?} should NOT mark provider as failed",
881 error
882 );
883 }
884 }
885
886 #[test]
887 fn test_should_mark_provider_failed_edge_cases() {
888 let edge_cases = [
890 (200, false), (300, false), (418, false), (451, false), (499, false), ];
896
897 for (status_code, should_fail) in edge_cases {
898 let error = ProviderError::RequestError {
899 error: format!("Edge case error {}", status_code),
900 status_code,
901 };
902 assert_eq!(
903 should_mark_provider_failed(&error),
904 should_fail,
905 "Status code {} should {} mark provider as failed",
906 status_code,
907 if should_fail { "" } else { "NOT" }
908 );
909 }
910 }
911
912 #[test]
913 fn test_is_retriable_error_retriable_types() {
914 let retriable_errors = [
916 ProviderError::Timeout,
917 ProviderError::RateLimited,
918 ProviderError::BadGateway,
919 ProviderError::TransportError("test".to_string()),
920 ];
921
922 for error in retriable_errors {
923 assert!(
924 is_retriable_error(&error),
925 "Error type {:?} should be retriable",
926 error
927 );
928 }
929 }
930
931 #[test]
932 fn test_is_retriable_error_non_retriable_types() {
933 let non_retriable_errors = [
935 ProviderError::InvalidAddress("test".to_string()),
936 ProviderError::NetworkConfiguration("test".to_string()),
937 ProviderError::RequestError {
938 error: "Some error".to_string(),
939 status_code: 400,
940 },
941 ];
942
943 for error in non_retriable_errors {
944 assert!(
945 !is_retriable_error(&error),
946 "Error type {:?} should NOT be retriable",
947 error
948 );
949 }
950 }
951
952 #[test]
953 fn test_is_retriable_error_message_based_detection() {
954 let retriable_messages = [
956 "Connection timeout occurred",
957 "Network connection reset",
958 "Connection refused",
959 "TIMEOUT error happened",
960 "Connection was reset by peer",
961 ];
962
963 for message in retriable_messages {
964 let error = ProviderError::Other(message.to_string());
965 assert!(
966 is_retriable_error(&error),
967 "Error with message '{}' should be retriable",
968 message
969 );
970 }
971 }
972
973 #[test]
974 fn test_is_retriable_error_message_based_non_retriable() {
975 let non_retriable_messages = [
977 "Invalid address format",
978 "Bad request parameters",
979 "Authentication failed",
980 "Method not found",
981 "Some other error",
982 ];
983
984 for message in non_retriable_messages {
985 let error = ProviderError::Other(message.to_string());
986 assert!(
987 !is_retriable_error(&error),
988 "Error with message '{}' should NOT be retriable",
989 message
990 );
991 }
992 }
993
994 #[test]
995 fn test_is_retriable_error_case_insensitive() {
996 let case_variations = [
998 "TIMEOUT",
999 "Timeout",
1000 "timeout",
1001 "CONNECTION",
1002 "Connection",
1003 "connection",
1004 "RESET",
1005 "Reset",
1006 "reset",
1007 ];
1008
1009 for message in case_variations {
1010 let error = ProviderError::Other(message.to_string());
1011 assert!(
1012 is_retriable_error(&error),
1013 "Error with message '{}' should be retriable (case insensitive)",
1014 message
1015 );
1016 }
1017 }
1018
1019 #[test]
1020 fn test_is_retriable_error_request_error_retriable_5xx() {
1021 let retriable_5xx = vec![
1023 (500, "Internal Server Error"),
1024 (502, "Bad Gateway"),
1025 (503, "Service Unavailable"),
1026 (504, "Gateway Timeout"),
1027 (506, "Variant Also Negotiates"),
1028 (507, "Insufficient Storage"),
1029 (508, "Loop Detected"),
1030 (510, "Not Extended"),
1031 (511, "Network Authentication Required"),
1032 (599, "Network Connect Timeout Error"),
1033 ];
1034
1035 for (status_code, description) in retriable_5xx {
1036 let error = ProviderError::RequestError {
1037 error: description.to_string(),
1038 status_code,
1039 };
1040 assert!(
1041 is_retriable_error(&error),
1042 "Status code {} ({}) should be retriable",
1043 status_code,
1044 description
1045 );
1046 }
1047 }
1048
1049 #[test]
1050 fn test_is_retriable_error_request_error_non_retriable_5xx() {
1051 let non_retriable_5xx = vec![
1053 (501, "Not Implemented"),
1054 (505, "HTTP Version Not Supported"),
1055 ];
1056
1057 for (status_code, description) in non_retriable_5xx {
1058 let error = ProviderError::RequestError {
1059 error: description.to_string(),
1060 status_code,
1061 };
1062 assert!(
1063 !is_retriable_error(&error),
1064 "Status code {} ({}) should NOT be retriable",
1065 status_code,
1066 description
1067 );
1068 }
1069 }
1070
1071 #[test]
1072 fn test_is_retriable_error_request_error_retriable_4xx() {
1073 let retriable_4xx = vec![
1075 (408, "Request Timeout"),
1076 (425, "Too Early"),
1077 (429, "Too Many Requests"),
1078 ];
1079
1080 for (status_code, description) in retriable_4xx {
1081 let error = ProviderError::RequestError {
1082 error: description.to_string(),
1083 status_code,
1084 };
1085 assert!(
1086 is_retriable_error(&error),
1087 "Status code {} ({}) should be retriable",
1088 status_code,
1089 description
1090 );
1091 }
1092 }
1093
1094 #[test]
1095 fn test_is_retriable_error_request_error_non_retriable_4xx() {
1096 let non_retriable_4xx = vec![
1098 (400, "Bad Request"),
1099 (401, "Unauthorized"),
1100 (403, "Forbidden"),
1101 (404, "Not Found"),
1102 (405, "Method Not Allowed"),
1103 (406, "Not Acceptable"),
1104 (407, "Proxy Authentication Required"),
1105 (409, "Conflict"),
1106 (410, "Gone"),
1107 (411, "Length Required"),
1108 (412, "Precondition Failed"),
1109 (413, "Payload Too Large"),
1110 (414, "URI Too Long"),
1111 (415, "Unsupported Media Type"),
1112 (416, "Range Not Satisfiable"),
1113 (417, "Expectation Failed"),
1114 (418, "I'm a teapot"),
1115 (421, "Misdirected Request"),
1116 (422, "Unprocessable Entity"),
1117 (423, "Locked"),
1118 (424, "Failed Dependency"),
1119 (426, "Upgrade Required"),
1120 (428, "Precondition Required"),
1121 (431, "Request Header Fields Too Large"),
1122 (451, "Unavailable For Legal Reasons"),
1123 (499, "Client Closed Request"),
1124 ];
1125
1126 for (status_code, description) in non_retriable_4xx {
1127 let error = ProviderError::RequestError {
1128 error: description.to_string(),
1129 status_code,
1130 };
1131 assert!(
1132 !is_retriable_error(&error),
1133 "Status code {} ({}) should NOT be retriable",
1134 status_code,
1135 description
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn test_is_retriable_error_request_error_other_status_codes() {
1142 let other_status_codes = vec![
1144 (100, "Continue"),
1145 (101, "Switching Protocols"),
1146 (200, "OK"),
1147 (201, "Created"),
1148 (204, "No Content"),
1149 (300, "Multiple Choices"),
1150 (301, "Moved Permanently"),
1151 (302, "Found"),
1152 (304, "Not Modified"),
1153 (600, "Custom status"),
1154 (999, "Unknown status"),
1155 ];
1156
1157 for (status_code, description) in other_status_codes {
1158 let error = ProviderError::RequestError {
1159 error: description.to_string(),
1160 status_code,
1161 };
1162 assert!(
1163 !is_retriable_error(&error),
1164 "Status code {} ({}) should NOT be retriable",
1165 status_code,
1166 description
1167 );
1168 }
1169 }
1170
1171 #[test]
1172 fn test_is_retriable_error_request_error_boundary_cases() {
1173 let test_cases = vec![
1175 (407, false, "Proxy Authentication Required"),
1177 (408, true, "Request Timeout - first retriable 4xx"),
1178 (409, false, "Conflict"),
1179 (424, false, "Failed Dependency"),
1181 (425, true, "Too Early"),
1182 (426, false, "Upgrade Required"),
1183 (428, false, "Precondition Required"),
1185 (429, true, "Too Many Requests"),
1186 (430, false, "Would be non-retriable if it existed"),
1187 (499, false, "Last 4xx"),
1189 (500, true, "First 5xx - retriable"),
1190 (501, false, "Not Implemented - exception"),
1191 (502, true, "Bad Gateway - retriable"),
1192 (505, false, "HTTP Version Not Supported - exception"),
1193 (506, true, "First after 505 exception"),
1194 (599, true, "Last defined 5xx"),
1195 ];
1196
1197 for (status_code, should_be_retriable, description) in test_cases {
1198 let error = ProviderError::RequestError {
1199 error: description.to_string(),
1200 status_code,
1201 };
1202 assert_eq!(
1203 is_retriable_error(&error),
1204 should_be_retriable,
1205 "Status code {} ({}) should{} be retriable",
1206 status_code,
1207 description,
1208 if should_be_retriable { "" } else { " NOT" }
1209 );
1210 }
1211 }
1212}