openzeppelin_monitor/services/blockchain/transports/
error.rs

1//! Error types for blockchain transport services
2//!
3//! Provides error handling for network communication, JSON parsing, request serialization and URL rotation.
4
5use crate::utils::logging::error::{ErrorContext, TraceableError};
6use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum TransportError {
11	/// HTTP error
12	#[error("HTTP error: status {status_code} for URL {url}")]
13	Http {
14		status_code: reqwest::StatusCode,
15		url: String,
16		body: String,
17		context: ErrorContext,
18	},
19
20	/// Network error
21	#[error("Network error: {0}")]
22	Network(ErrorContext),
23
24	/// JSON parsing error
25	#[error("Failed to parse JSON response: {0}")]
26	ResponseParse(ErrorContext),
27
28	/// Request body serialization error
29	#[error("Failed to serialize request JSON: {0}")]
30	RequestSerialization(ErrorContext),
31
32	/// URL rotation error
33	#[error("URL rotation failed: {0}")]
34	UrlRotation(ErrorContext),
35}
36
37impl TransportError {
38	pub fn http(
39		status_code: reqwest::StatusCode,
40		url: String,
41		body: String,
42		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
43		metadata: Option<HashMap<String, String>>,
44	) -> Self {
45		let msg = format!("HTTP error: status {} for URL {}", status_code, url);
46
47		Self::Http {
48			status_code,
49			url,
50			body,
51			context: ErrorContext::new_with_log(msg, source, metadata),
52		}
53	}
54
55	pub fn network(
56		msg: impl Into<String>,
57		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
58		metadata: Option<HashMap<String, String>>,
59	) -> Self {
60		Self::Network(ErrorContext::new_with_log(msg, source, metadata))
61	}
62
63	pub fn response_parse(
64		msg: impl Into<String>,
65		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
66		metadata: Option<HashMap<String, String>>,
67	) -> Self {
68		Self::ResponseParse(ErrorContext::new_with_log(msg, source, metadata))
69	}
70
71	pub fn request_serialization(
72		msg: impl Into<String>,
73		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
74		metadata: Option<HashMap<String, String>>,
75	) -> Self {
76		Self::RequestSerialization(ErrorContext::new_with_log(msg, source, metadata))
77	}
78	pub fn url_rotation(
79		msg: impl Into<String>,
80		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
81		metadata: Option<HashMap<String, String>>,
82	) -> Self {
83		Self::UrlRotation(ErrorContext::new_with_log(msg, source, metadata))
84	}
85}
86
87impl TraceableError for TransportError {
88	fn trace_id(&self) -> String {
89		match self {
90			Self::Http { context, .. } => context.trace_id.clone(),
91			Self::Network(ctx) => ctx.trace_id.clone(),
92			Self::ResponseParse(ctx) => ctx.trace_id.clone(),
93			Self::RequestSerialization(ctx) => ctx.trace_id.clone(),
94			Self::UrlRotation(ctx) => ctx.trace_id.clone(),
95		}
96	}
97}
98
99#[cfg(test)]
100mod tests {
101	use super::*;
102	use std::io::{Error as IoError, ErrorKind};
103
104	#[test]
105	fn test_http_error_formatting() {
106		let error = TransportError::http(
107			reqwest::StatusCode::NOT_FOUND,
108			"http://example.com".to_string(),
109			"Not Found".to_string(),
110			None,
111			None,
112		);
113		assert_eq!(
114			error.to_string(),
115			"HTTP error: status 404 Not Found for URL http://example.com"
116		);
117	}
118
119	#[test]
120	fn test_network_error_formatting() {
121		let error = TransportError::network("test error", None, None);
122		assert_eq!(error.to_string(), "Network error: test error");
123
124		let source_error = IoError::new(ErrorKind::NotFound, "test source");
125		let error = TransportError::network(
126			"test error",
127			Some(Box::new(source_error)),
128			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
129		);
130		assert_eq!(error.to_string(), "Network error: test error [key1=value1]");
131	}
132
133	#[test]
134	fn test_response_parse_error_formatting() {
135		let error = TransportError::response_parse("test error", None, None);
136		assert_eq!(
137			error.to_string(),
138			"Failed to parse JSON response: test error"
139		);
140
141		let source_error = IoError::new(ErrorKind::NotFound, "test source");
142		let error = TransportError::response_parse(
143			"test error",
144			Some(Box::new(source_error)),
145			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
146		);
147		assert_eq!(
148			error.to_string(),
149			"Failed to parse JSON response: test error [key1=value1]"
150		);
151	}
152
153	#[test]
154	fn test_request_serialization_error_formatting() {
155		let error = TransportError::request_serialization("test error", None, None);
156		assert_eq!(
157			error.to_string(),
158			"Failed to serialize request JSON: test error"
159		);
160
161		let source_error = IoError::new(ErrorKind::NotFound, "test source");
162		let error = TransportError::request_serialization(
163			"test error",
164			Some(Box::new(source_error)),
165			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
166		);
167		assert_eq!(
168			error.to_string(),
169			"Failed to serialize request JSON: test error [key1=value1]"
170		);
171	}
172
173	#[test]
174	fn test_url_rotation_error_formatting() {
175		let error = TransportError::url_rotation("test error", None, None);
176		assert_eq!(error.to_string(), "URL rotation failed: test error");
177
178		let source_error = IoError::new(ErrorKind::NotFound, "test source");
179		let error = TransportError::url_rotation(
180			"test error",
181			Some(Box::new(source_error)),
182			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
183		);
184		assert_eq!(
185			error.to_string(),
186			"URL rotation failed: test error [key1=value1]"
187		);
188	}
189
190	#[test]
191	fn test_error_source_chain() {
192		let io_error = std::io::Error::new(std::io::ErrorKind::Other, "while reading config");
193
194		let outer_error = TransportError::http(
195			reqwest::StatusCode::INTERNAL_SERVER_ERROR,
196			"http://example.com".to_string(),
197			"Internal Server Error".to_string(),
198			Some(Box::new(io_error)),
199			None,
200		);
201
202		// Just test the string representation instead of the source chain
203		assert!(outer_error.to_string().contains("Internal Server Error"));
204
205		// For TransportError::Http, we know the implementation details
206		if let TransportError::Http { context, .. } = &outer_error {
207			// Check that the context has the right message
208			assert_eq!(
209				context.message,
210				"HTTP error: status 500 Internal Server Error for URL http://example.com"
211			);
212
213			// Check that the context has the source error
214			assert!(context.source.is_some());
215
216			if let Some(src) = &context.source {
217				assert_eq!(src.to_string(), "while reading config");
218			}
219		} else {
220			panic!("Expected Http variant");
221		}
222	}
223
224	#[test]
225	fn test_trace_id_propagation() {
226		// Create an error context with a known trace ID
227		let error_context = ErrorContext::new("Inner error", None, None);
228		let original_trace_id = error_context.trace_id.clone();
229
230		// Wrap it in a TransportError
231		let transport_error = TransportError::Http {
232			status_code: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
233			url: "http://example.com".to_string(),
234			body: "Internal Server Error".to_string(),
235			context: error_context,
236		};
237
238		// Verify the trace ID is preserved
239		assert_eq!(transport_error.trace_id(), original_trace_id);
240
241		// Test trace ID propagation through error chain
242		let source_error = IoError::new(ErrorKind::Other, "Source error");
243		let error_context = ErrorContext::new("Middle error", Some(Box::new(source_error)), None);
244		let original_trace_id = error_context.trace_id.clone();
245
246		let transport_error = TransportError::Http {
247			status_code: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
248			url: "http://example.com".to_string(),
249			body: "Internal Server Error".to_string(),
250			context: error_context,
251		};
252		assert_eq!(transport_error.trace_id(), original_trace_id);
253	}
254}