1use std::fmt;
7use thiserror::Error;
8
9#[derive(Error, Debug)]
11pub enum SentinelError {
12 #[error("Configuration error: {message}")]
14 Config {
15 message: String,
16 #[source]
17 source: Option<Box<dyn std::error::Error + Send + Sync>>,
18 },
19
20 #[error("Upstream error: {upstream} - {message}")]
22 Upstream {
23 upstream: String,
24 message: String,
25 retryable: bool,
26 #[source]
27 source: Option<Box<dyn std::error::Error + Send + Sync>>,
28 },
29
30 #[error("Agent error: {agent} - {message}")]
32 Agent {
33 agent: String,
34 message: String,
35 event: String,
36 #[source]
37 source: Option<Box<dyn std::error::Error + Send + Sync>>,
38 },
39
40 #[error("Request validation failed: {reason}")]
42 RequestValidation {
43 reason: String,
44 correlation_id: Option<String>,
45 },
46
47 #[error("Response validation failed: {reason}")]
49 ResponseValidation {
50 reason: String,
51 correlation_id: Option<String>,
52 },
53
54 #[error("Limit exceeded: {limit_type} - {message}")]
56 LimitExceeded {
57 limit_type: LimitType,
58 message: String,
59 current_value: usize,
60 limit: usize,
61 },
62
63 #[error("Timeout: {operation} after {duration_ms}ms")]
65 Timeout {
66 operation: String,
67 duration_ms: u64,
68 correlation_id: Option<String>,
69 },
70
71 #[error("Circuit breaker open: {component}")]
73 CircuitBreakerOpen {
74 component: String,
75 consecutive_failures: u32,
76 last_error: String,
77 },
78
79 #[error("WAF blocked request: {reason}")]
81 WafBlocked {
82 reason: String,
83 rule_ids: Vec<String>,
84 confidence: f32,
85 correlation_id: String,
86 },
87
88 #[error("Authentication failed: {reason}")]
90 AuthenticationFailed {
91 reason: String,
92 correlation_id: Option<String>,
93 },
94
95 #[error("Authorization failed: {reason}")]
96 AuthorizationFailed {
97 reason: String,
98 correlation_id: Option<String>,
99 required_permissions: Vec<String>,
100 },
101
102 #[error("TLS error: {message}")]
104 Tls {
105 message: String,
106 #[source]
107 source: Option<Box<dyn std::error::Error + Send + Sync>>,
108 },
109
110 #[error("Internal error: {message}")]
112 Internal {
113 message: String,
114 correlation_id: Option<String>,
115 #[source]
116 source: Option<Box<dyn std::error::Error + Send + Sync>>,
117 },
118
119 #[error("IO error: {message}")]
121 Io {
122 message: String,
123 path: Option<String>,
124 #[source]
125 source: std::io::Error,
126 },
127
128 #[error("Parse error: {message}")]
130 Parse {
131 message: String,
132 input: Option<String>,
133 #[source]
134 source: Option<Box<dyn std::error::Error + Send + Sync>>,
135 },
136
137 #[error("Service unavailable: {service}")]
139 ServiceUnavailable {
140 service: String,
141 retry_after_seconds: Option<u32>,
142 },
143
144 #[error("Rate limit exceeded: {message}")]
146 RateLimit {
147 message: String,
148 limit: u32,
149 window_seconds: u32,
150 retry_after_seconds: Option<u32>,
151 },
152
153 #[error("No healthy upstream available")]
155 NoHealthyUpstream,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum LimitType {
161 HeaderSize,
162 HeaderCount,
163 BodySize,
164 RequestRate,
165 ConnectionCount,
166 InFlightRequests,
167 DecompressionSize,
168 BufferSize,
169 QueueDepth,
170}
171
172impl fmt::Display for LimitType {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 match self {
175 Self::HeaderSize => write!(f, "header_size"),
176 Self::HeaderCount => write!(f, "header_count"),
177 Self::BodySize => write!(f, "body_size"),
178 Self::RequestRate => write!(f, "request_rate"),
179 Self::ConnectionCount => write!(f, "connection_count"),
180 Self::InFlightRequests => write!(f, "in_flight_requests"),
181 Self::DecompressionSize => write!(f, "decompression_size"),
182 Self::BufferSize => write!(f, "buffer_size"),
183 Self::QueueDepth => write!(f, "queue_depth"),
184 }
185 }
186}
187
188pub type SentinelResult<T> = Result<T, SentinelError>;
190
191#[derive(Debug, Clone)]
193pub struct ErrorContext {
194 pub correlation_id: String,
195 pub route: Option<String>,
196 pub upstream: Option<String>,
197 pub client_addr: Option<String>,
198 pub failure_mode: FailureMode,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum FailureMode {
204 Open,
206 Closed,
208}
209
210impl SentinelError {
211 pub fn is_circuit_breaker_eligible(&self) -> bool {
213 matches!(
214 self,
215 Self::Upstream { .. }
216 | Self::Timeout { .. }
217 | Self::ServiceUnavailable { .. }
218 | Self::Agent { .. }
219 )
220 }
221
222 pub fn is_retryable(&self) -> bool {
224 match self {
225 Self::Upstream { retryable, .. } => *retryable,
226 Self::Timeout { .. } => true,
227 Self::ServiceUnavailable { .. } => true,
228 Self::Io { .. } => true,
229 _ => false,
230 }
231 }
232
233 pub fn to_http_status(&self) -> u16 {
235 match self {
236 Self::Config { .. } => 500,
237 Self::Upstream { .. } => 502,
238 Self::Agent { .. } => 500,
239 Self::RequestValidation { .. } => 400,
240 Self::ResponseValidation { .. } => 502,
241 Self::LimitExceeded { .. } => 429,
242 Self::Timeout { .. } => 504,
243 Self::CircuitBreakerOpen { .. } => 503,
244 Self::WafBlocked { .. } => 403,
245 Self::AuthenticationFailed { .. } => 401,
246 Self::AuthorizationFailed { .. } => 403,
247 Self::Tls { .. } => 495, Self::Internal { .. } => 500,
249 Self::Io { .. } => 500,
250 Self::Parse { .. } => 400,
251 Self::ServiceUnavailable { .. } => 503,
252 Self::RateLimit { .. } => 429,
253 Self::NoHealthyUpstream => 503,
254 }
255 }
256
257 pub fn client_message(&self) -> String {
259 match self {
260 Self::Config { .. } => "Internal server error".to_string(),
261 Self::Upstream { .. } => "Bad gateway".to_string(),
262 Self::Agent { .. } => "Internal server error".to_string(),
263 Self::RequestValidation { reason, .. } => format!("Bad request: {}", reason),
264 Self::ResponseValidation { .. } => "Bad gateway".to_string(),
265 Self::LimitExceeded { limit_type, .. } => {
266 format!("Request limit exceeded: {}", limit_type)
267 }
268 Self::Timeout { .. } => "Gateway timeout".to_string(),
269 Self::CircuitBreakerOpen { .. } => "Service temporarily unavailable".to_string(),
270 Self::WafBlocked { reason, .. } => format!("Request blocked: {}", reason),
271 Self::AuthenticationFailed { .. } => "Authentication required".to_string(),
272 Self::AuthorizationFailed { .. } => "Access denied".to_string(),
273 Self::Tls { .. } => "TLS handshake failed".to_string(),
274 Self::Internal { .. } => "Internal server error".to_string(),
275 Self::Io { .. } => "Internal server error".to_string(),
276 Self::Parse { .. } => "Bad request".to_string(),
277 Self::ServiceUnavailable { service, .. } => {
278 format!("Service '{}' temporarily unavailable", service)
279 }
280 Self::RateLimit { .. } => "Rate limit exceeded".to_string(),
281 Self::NoHealthyUpstream => "No healthy upstream available".to_string(),
282 }
283 }
284
285 pub fn upstream(upstream: impl Into<String>, message: impl Into<String>) -> Self {
287 Self::Upstream {
288 upstream: upstream.into(),
289 message: message.into(),
290 retryable: false,
291 source: None,
292 }
293 }
294
295 pub fn upstream_retryable(upstream: impl Into<String>, message: impl Into<String>) -> Self {
297 Self::Upstream {
298 upstream: upstream.into(),
299 message: message.into(),
300 retryable: true,
301 source: None,
302 }
303 }
304
305 pub fn timeout(operation: impl Into<String>, duration_ms: u64) -> Self {
307 Self::Timeout {
308 operation: operation.into(),
309 duration_ms,
310 correlation_id: None,
311 }
312 }
313
314 pub fn limit_exceeded(
316 limit_type: LimitType,
317 current_value: usize,
318 limit: usize,
319 ) -> Self {
320 Self::LimitExceeded {
321 limit_type,
322 message: format!("Current value {} exceeds limit {}", current_value, limit),
323 current_value,
324 limit,
325 }
326 }
327
328 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
330 match &mut self {
331 Self::RequestValidation { correlation_id: cid, .. }
332 | Self::ResponseValidation { correlation_id: cid, .. }
333 | Self::Timeout { correlation_id: cid, .. }
334 | Self::AuthenticationFailed { correlation_id: cid, .. }
335 | Self::AuthorizationFailed { correlation_id: cid, .. }
336 | Self::Internal { correlation_id: cid, .. } => {
337 *cid = Some(correlation_id.into());
338 }
339 _ => {}
340 }
341 self
342 }
343}
344
345pub trait ErrorContextExt {
347 fn context_sentinel(self, context: ErrorContext) -> SentinelError;
349}
350
351impl<E> ErrorContextExt for E
352where
353 E: std::error::Error + Send + Sync + 'static,
354{
355 fn context_sentinel(self, context: ErrorContext) -> SentinelError {
356 SentinelError::Internal {
357 message: format!("Error in route {:?}: {}", context.route, self),
358 correlation_id: Some(context.correlation_id),
359 source: Some(Box::new(self)),
360 }
361 }
362}
363
364impl From<std::io::Error> for SentinelError {
366 fn from(err: std::io::Error) -> Self {
367 Self::Io {
368 message: err.to_string(),
369 path: None,
370 source: err,
371 }
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_error_http_status() {
381 assert_eq!(SentinelError::upstream("backend", "connection refused").to_http_status(), 502);
382 assert_eq!(SentinelError::timeout("upstream", 5000).to_http_status(), 504);
383 assert_eq!(
384 SentinelError::limit_exceeded(LimitType::HeaderSize, 2048, 1024).to_http_status(),
385 429
386 );
387 }
388
389 #[test]
390 fn test_error_retryable() {
391 assert!(!SentinelError::upstream("backend", "error").is_retryable());
392 assert!(SentinelError::upstream_retryable("backend", "error").is_retryable());
393 assert!(SentinelError::timeout("operation", 1000).is_retryable());
394 }
395
396 #[test]
397 fn test_error_circuit_breaker() {
398 assert!(SentinelError::upstream("backend", "error").is_circuit_breaker_eligible());
399 assert!(SentinelError::timeout("operation", 1000).is_circuit_breaker_eligible());
400 assert!(!SentinelError::RequestValidation {
401 reason: "invalid".to_string(),
402 correlation_id: None
403 }
404 .is_circuit_breaker_eligible());
405 }
406
407 #[test]
408 fn test_client_message() {
409 let err = SentinelError::Internal {
410 message: "Database connection failed".to_string(),
411 correlation_id: Some("123".to_string()),
412 source: None,
413 };
414 assert_eq!(err.client_message(), "Internal server error");
415
416 let err = SentinelError::WafBlocked {
417 reason: "SQL injection detected".to_string(),
418 rule_ids: vec!["942100".to_string()],
419 confidence: 0.95,
420 correlation_id: "456".to_string(),
421 };
422 assert_eq!(err.client_message(), "Request blocked: SQL injection detected");
423 }
424}