sentinel_common/
errors.rs

1//! Error types for Sentinel proxy
2//!
3//! This module defines common error types used throughout the Sentinel platform,
4//! with a focus on clear failure modes and operational visibility.
5
6use std::fmt;
7use thiserror::Error;
8
9/// Main error type for Sentinel operations
10#[derive(Error, Debug)]
11pub enum SentinelError {
12    /// Configuration errors
13    #[error("Configuration error: {message}")]
14    Config {
15        message: String,
16        #[source]
17        source: Option<Box<dyn std::error::Error + Send + Sync>>,
18    },
19
20    /// Upstream connection errors
21    #[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    /// Agent communication errors
31    #[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    /// Request validation errors
41    #[error("Request validation failed: {reason}")]
42    RequestValidation {
43        reason: String,
44        correlation_id: Option<String>,
45    },
46
47    /// Response validation errors
48    #[error("Response validation failed: {reason}")]
49    ResponseValidation {
50        reason: String,
51        correlation_id: Option<String>,
52    },
53
54    /// Limit exceeded errors
55    #[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    /// Timeout errors
64    #[error("Timeout: {operation} after {duration_ms}ms")]
65    Timeout {
66        operation: String,
67        duration_ms: u64,
68        correlation_id: Option<String>,
69    },
70
71    /// Circuit breaker errors
72    #[error("Circuit breaker open: {component}")]
73    CircuitBreakerOpen {
74        component: String,
75        consecutive_failures: u32,
76        last_error: String,
77    },
78
79    /// WAF block errors
80    #[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    /// Authentication/Authorization errors
89    #[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    /// TLS/Certificate errors
103    #[error("TLS error: {message}")]
104    Tls {
105        message: String,
106        #[source]
107        source: Option<Box<dyn std::error::Error + Send + Sync>>,
108    },
109
110    /// Internal errors
111    #[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    /// IO errors
120    #[error("IO error: {message}")]
121    Io {
122        message: String,
123        path: Option<String>,
124        #[source]
125        source: std::io::Error,
126    },
127
128    /// Parsing errors
129    #[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    /// Service unavailable (for graceful degradation)
138    #[error("Service unavailable: {service}")]
139    ServiceUnavailable {
140        service: String,
141        retry_after_seconds: Option<u32>,
142    },
143
144    /// Rate limit errors
145    #[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    /// No healthy upstream available
154    #[error("No healthy upstream available")]
155    NoHealthyUpstream,
156}
157
158/// Types of limits that can be exceeded
159#[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
188/// Result type alias for Sentinel operations
189pub type SentinelResult<T> = Result<T, SentinelError>;
190
191/// Error context for operational visibility
192#[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/// Failure mode for degraded operation
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub enum FailureMode {
204    /// Allow traffic through on failure (fail-open)
205    Open,
206    /// Block traffic on failure (fail-closed)
207    Closed,
208}
209
210impl SentinelError {
211    /// Determine if this error should trigger a circuit breaker
212    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    /// Determine if this error is retryable
223    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    /// Get the HTTP status code for this error
234    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, // SSL Certificate Error
248            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    /// Get a client-safe error message (without internal details)
258    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    /// Create an upstream error
286    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    /// Create a retryable upstream error
296    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    /// Create a timeout error
306    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    /// Create a limit exceeded error
315    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    /// Add correlation ID to the error
329    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
345/// Extension trait for adding context to errors
346pub trait ErrorContextExt {
347    /// Add Sentinel-specific context to the error
348    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
364/// Helper for converting IO errors
365impl 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}