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(
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    /// Add correlation ID to the error
310    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
326/// Helper for converting IO errors
327impl 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}