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
191impl SentinelError {
192    /// Determine if this error should trigger a circuit breaker
193    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    /// Determine if this error is retryable
204    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    /// Get the HTTP status code for this error
215    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, // SSL Certificate Error
229            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    /// Get a client-safe error message (without internal details)
239    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    /// Create an upstream error
267    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    /// Create a retryable upstream error
277    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    /// Create a timeout error
287    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    /// Create a limit exceeded error
296    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    /// Add correlation ID to the error
306    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
340/// Helper for converting IO errors
341impl 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}