openzeppelin_monitor/services/blockchain/transports/
error.rs1use crate::utils::logging::error::{ErrorContext, TraceableError};
6use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum TransportError {
11 #[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 #[error("Network error: {0}")]
22 Network(ErrorContext),
23
24 #[error("Failed to parse JSON response: {0}")]
26 ResponseParse(ErrorContext),
27
28 #[error("Failed to serialize request JSON: {0}")]
30 RequestSerialization(ErrorContext),
31
32 #[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 assert!(outer_error.to_string().contains("Internal Server Error"));
204
205 if let TransportError::Http { context, .. } = &outer_error {
207 assert_eq!(
209 context.message,
210 "HTTP error: status 500 Internal Server Error for URL http://example.com"
211 );
212
213 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 let error_context = ErrorContext::new("Inner error", None, None);
228 let original_trace_id = error_context.trace_id.clone();
229
230 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 assert_eq!(transport_error.trace_id(), original_trace_id);
240
241 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}