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(
297 limit_type: LimitType,
298 current_value: usize,
299 limit: usize,
300 ) -> Self {
301 Self::LimitExceeded {
302 limit_type,
303 message: format!("Current value {} exceeds limit {}", current_value, limit),
304 current_value,
305 limit,
306 }
307 }
308
309 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
311 match &mut self {
312 Self::RequestValidation { correlation_id: cid, .. }
313 | Self::ResponseValidation { correlation_id: cid, .. }
314 | Self::Timeout { correlation_id: cid, .. }
315 | Self::AuthenticationFailed { correlation_id: cid, .. }
316 | Self::AuthorizationFailed { correlation_id: cid, .. }
317 | Self::Internal { correlation_id: cid, .. } => {
318 *cid = Some(correlation_id.into());
319 }
320 _ => {}
321 }
322 self
323 }
324}
325
326impl From<std::io::Error> for SentinelError {
328 fn from(err: std::io::Error) -> Self {
329 Self::Io {
330 message: err.to_string(),
331 path: None,
332 source: err,
333 }
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_error_http_status() {
343 assert_eq!(SentinelError::upstream("backend", "connection refused").to_http_status(), 502);
344 assert_eq!(SentinelError::timeout("upstream", 5000).to_http_status(), 504);
345 assert_eq!(
346 SentinelError::limit_exceeded(LimitType::HeaderSize, 2048, 1024).to_http_status(),
347 429
348 );
349 }
350
351 #[test]
352 fn test_error_retryable() {
353 assert!(!SentinelError::upstream("backend", "error").is_retryable());
354 assert!(SentinelError::upstream_retryable("backend", "error").is_retryable());
355 assert!(SentinelError::timeout("operation", 1000).is_retryable());
356 }
357
358 #[test]
359 fn test_error_circuit_breaker() {
360 assert!(SentinelError::upstream("backend", "error").is_circuit_breaker_eligible());
361 assert!(SentinelError::timeout("operation", 1000).is_circuit_breaker_eligible());
362 assert!(!SentinelError::RequestValidation {
363 reason: "invalid".to_string(),
364 correlation_id: None
365 }
366 .is_circuit_breaker_eligible());
367 }
368
369 #[test]
370 fn test_client_message() {
371 let err = SentinelError::Internal {
372 message: "Database connection failed".to_string(),
373 correlation_id: Some("123".to_string()),
374 source: None,
375 };
376 assert_eq!(err.client_message(), "Internal server error");
377
378 let err = SentinelError::WafBlocked {
379 reason: "SQL injection detected".to_string(),
380 rule_ids: vec!["942100".to_string()],
381 confidence: 0.95,
382 correlation_id: "456".to_string(),
383 };
384 assert_eq!(err.client_message(), "Request blocked: SQL injection detected");
385 }
386}