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
191impl SentinelError {
192 pub fn is_circuit_breaker_eligible(&self) -> bool {
194 matches!(
195 self,
196 Self::Upstream { .. }
197 | Self::Timeout { .. }
198 | Self::ServiceUnavailable { .. }
199 | Self::Agent { .. }
200 )
201 }
202
203 pub fn is_retryable(&self) -> bool {
205 match self {
206 Self::Upstream { retryable, .. } => *retryable,
207 Self::Timeout { .. } => true,
208 Self::ServiceUnavailable { .. } => true,
209 Self::Io { .. } => true,
210 _ => false,
211 }
212 }
213
214 pub fn to_http_status(&self) -> u16 {
216 match self {
217 Self::Config { .. } => 500,
218 Self::Upstream { .. } => 502,
219 Self::Agent { .. } => 500,
220 Self::RequestValidation { .. } => 400,
221 Self::ResponseValidation { .. } => 502,
222 Self::LimitExceeded { .. } => 429,
223 Self::Timeout { .. } => 504,
224 Self::CircuitBreakerOpen { .. } => 503,
225 Self::WafBlocked { .. } => 403,
226 Self::AuthenticationFailed { .. } => 401,
227 Self::AuthorizationFailed { .. } => 403,
228 Self::Tls { .. } => 495, Self::Internal { .. } => 500,
230 Self::Io { .. } => 500,
231 Self::Parse { .. } => 400,
232 Self::ServiceUnavailable { .. } => 503,
233 Self::RateLimit { .. } => 429,
234 Self::NoHealthyUpstream => 503,
235 }
236 }
237
238 pub fn client_message(&self) -> String {
240 match self {
241 Self::Config { .. } => "Internal server error".to_string(),
242 Self::Upstream { .. } => "Bad gateway".to_string(),
243 Self::Agent { .. } => "Internal server error".to_string(),
244 Self::RequestValidation { reason, .. } => format!("Bad request: {}", reason),
245 Self::ResponseValidation { .. } => "Bad gateway".to_string(),
246 Self::LimitExceeded { limit_type, .. } => {
247 format!("Request limit exceeded: {}", limit_type)
248 }
249 Self::Timeout { .. } => "Gateway timeout".to_string(),
250 Self::CircuitBreakerOpen { .. } => "Service temporarily unavailable".to_string(),
251 Self::WafBlocked { reason, .. } => format!("Request blocked: {}", reason),
252 Self::AuthenticationFailed { .. } => "Authentication required".to_string(),
253 Self::AuthorizationFailed { .. } => "Access denied".to_string(),
254 Self::Tls { .. } => "TLS handshake failed".to_string(),
255 Self::Internal { .. } => "Internal server error".to_string(),
256 Self::Io { .. } => "Internal server error".to_string(),
257 Self::Parse { .. } => "Bad request".to_string(),
258 Self::ServiceUnavailable { service, .. } => {
259 format!("Service '{}' temporarily unavailable", service)
260 }
261 Self::RateLimit { .. } => "Rate limit exceeded".to_string(),
262 Self::NoHealthyUpstream => "No healthy upstream available".to_string(),
263 }
264 }
265
266 pub fn upstream(upstream: impl Into<String>, message: impl Into<String>) -> Self {
268 Self::Upstream {
269 upstream: upstream.into(),
270 message: message.into(),
271 retryable: false,
272 source: None,
273 }
274 }
275
276 pub fn upstream_retryable(upstream: impl Into<String>, message: impl Into<String>) -> Self {
278 Self::Upstream {
279 upstream: upstream.into(),
280 message: message.into(),
281 retryable: true,
282 source: None,
283 }
284 }
285
286 pub fn timeout(operation: impl Into<String>, duration_ms: u64) -> Self {
288 Self::Timeout {
289 operation: operation.into(),
290 duration_ms,
291 correlation_id: None,
292 }
293 }
294
295 pub fn limit_exceeded(limit_type: LimitType, current_value: usize, limit: usize) -> Self {
297 Self::LimitExceeded {
298 limit_type,
299 message: format!("Current value {} exceeds limit {}", current_value, limit),
300 current_value,
301 limit,
302 }
303 }
304
305 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
307 match &mut self {
308 Self::RequestValidation {
309 correlation_id: cid,
310 ..
311 }
312 | Self::ResponseValidation {
313 correlation_id: cid,
314 ..
315 }
316 | Self::Timeout {
317 correlation_id: cid,
318 ..
319 }
320 | Self::AuthenticationFailed {
321 correlation_id: cid,
322 ..
323 }
324 | Self::AuthorizationFailed {
325 correlation_id: cid,
326 ..
327 }
328 | Self::Internal {
329 correlation_id: cid,
330 ..
331 } => {
332 *cid = Some(correlation_id.into());
333 }
334 _ => {}
335 }
336 self
337 }
338}
339
340impl From<std::io::Error> for SentinelError {
342 fn from(err: std::io::Error) -> Self {
343 Self::Io {
344 message: err.to_string(),
345 path: None,
346 source: err,
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_error_http_status() {
357 assert_eq!(
358 SentinelError::upstream("backend", "connection refused").to_http_status(),
359 502
360 );
361 assert_eq!(
362 SentinelError::timeout("upstream", 5000).to_http_status(),
363 504
364 );
365 assert_eq!(
366 SentinelError::limit_exceeded(LimitType::HeaderSize, 2048, 1024).to_http_status(),
367 429
368 );
369 }
370
371 #[test]
372 fn test_error_retryable() {
373 assert!(!SentinelError::upstream("backend", "error").is_retryable());
374 assert!(SentinelError::upstream_retryable("backend", "error").is_retryable());
375 assert!(SentinelError::timeout("operation", 1000).is_retryable());
376 }
377
378 #[test]
379 fn test_error_circuit_breaker() {
380 assert!(SentinelError::upstream("backend", "error").is_circuit_breaker_eligible());
381 assert!(SentinelError::timeout("operation", 1000).is_circuit_breaker_eligible());
382 assert!(!SentinelError::RequestValidation {
383 reason: "invalid".to_string(),
384 correlation_id: None
385 }
386 .is_circuit_breaker_eligible());
387 }
388
389 #[test]
390 fn test_client_message() {
391 let err = SentinelError::Internal {
392 message: "Database connection failed".to_string(),
393 correlation_id: Some("123".to_string()),
394 source: None,
395 };
396 assert_eq!(err.client_message(), "Internal server error");
397
398 let err = SentinelError::WafBlocked {
399 reason: "SQL injection detected".to_string(),
400 rule_ids: vec!["942100".to_string()],
401 confidence: 0.95,
402 correlation_id: "456".to_string(),
403 };
404 assert_eq!(
405 err.client_message(),
406 "Request blocked: SQL injection detected"
407 );
408 }
409}