Skip to main content

wisegate_core/
error.rs

1//! Error types for WiseGate.
2//!
3//! This module provides a unified error type for all WiseGate operations,
4//! enabling better error handling and propagation throughout the codebase.
5
6use thiserror::Error;
7
8/// Unified error type for WiseGate operations.
9///
10/// This enum covers all error cases that can occur during request processing,
11/// configuration, and proxying operations.
12///
13/// # Example
14///
15/// ```
16/// use wisegate_core::error::WiseGateError;
17///
18/// fn validate_ip(ip: &str) -> Result<(), WiseGateError> {
19///     if ip.is_empty() {
20///         return Err(WiseGateError::InvalidIp("IP address cannot be empty".into()));
21///     }
22///     Ok(())
23/// }
24/// ```
25#[derive(Debug, Error)]
26pub enum WiseGateError {
27    /// Invalid IP address format or value.
28    #[error("Invalid IP address: {0}")]
29    InvalidIp(String),
30
31    /// Configuration error (missing or invalid values).
32    #[error("Configuration error: {0}")]
33    ConfigError(String),
34
35    /// Error during request proxying.
36    #[error("Proxy error: {0}")]
37    ProxyError(String),
38
39    /// Rate limit exceeded for a client.
40    #[error("Rate limit exceeded for IP: {0}")]
41    RateLimitExceeded(String),
42
43    /// Request blocked by IP filter.
44    #[error("IP blocked: {0}")]
45    IpBlocked(String),
46
47    /// Request blocked by URL pattern filter.
48    #[error("URL pattern blocked: {0}")]
49    PatternBlocked(String),
50
51    /// Request blocked by HTTP method filter.
52    #[error("HTTP method blocked: {0}")]
53    MethodBlocked(String),
54
55    /// Upstream connection failed.
56    #[error("Upstream connection failed: {0}")]
57    UpstreamConnectionFailed(String),
58
59    /// Upstream request timed out.
60    #[error("Upstream timeout: {0}")]
61    UpstreamTimeout(String),
62
63    /// Request body too large.
64    #[error("Request body too large: {size} bytes (max: {max} bytes)")]
65    BodyTooLarge {
66        /// Actual body size in bytes.
67        size: usize,
68        /// Maximum allowed size in bytes.
69        max: usize,
70    },
71
72    /// Failed to read request or response body.
73    #[error("Body read error: {0}")]
74    BodyReadError(String),
75
76    /// The client did not finish sending its body before the deadline.
77    #[error("Client request body timed out")]
78    ClientBodyTimeout,
79
80    /// HTTP client error (from reqwest).
81    #[error("HTTP client error: {0}")]
82    HttpClientError(#[from] reqwest::Error),
83}
84
85impl WiseGateError {
86    /// Returns the appropriate HTTP status code for this error.
87    ///
88    /// # Returns
89    ///
90    /// The HTTP status code that should be returned to the client.
91    pub fn status_code(&self) -> hyper::StatusCode {
92        use hyper::StatusCode;
93
94        match self {
95            Self::InvalidIp(_) => StatusCode::BAD_REQUEST,
96            Self::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
97            Self::ProxyError(_) => StatusCode::BAD_GATEWAY,
98            Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
99            Self::IpBlocked(_) => StatusCode::FORBIDDEN,
100            Self::PatternBlocked(_) => StatusCode::NOT_FOUND,
101            Self::MethodBlocked(_) => StatusCode::METHOD_NOT_ALLOWED,
102            Self::UpstreamConnectionFailed(_) => StatusCode::BAD_GATEWAY,
103            Self::UpstreamTimeout(_) => StatusCode::GATEWAY_TIMEOUT,
104            Self::BodyTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
105            Self::BodyReadError(_) => StatusCode::BAD_REQUEST,
106            Self::ClientBodyTimeout => StatusCode::REQUEST_TIMEOUT,
107            Self::HttpClientError(_) => StatusCode::BAD_GATEWAY,
108        }
109    }
110
111    /// Returns a user-friendly error message suitable for HTTP responses.
112    ///
113    /// This method returns a sanitized message that doesn't expose
114    /// internal details to clients.
115    pub fn user_message(&self) -> &str {
116        match self {
117            Self::InvalidIp(_) => "Invalid request",
118            Self::ConfigError(_) => "Internal server error",
119            Self::ProxyError(_) => "Bad gateway",
120            Self::RateLimitExceeded(_) => "Rate limit exceeded",
121            Self::IpBlocked(_) => "Access denied",
122            Self::PatternBlocked(_) => "Not found",
123            Self::MethodBlocked(_) => "Method not allowed",
124            Self::UpstreamConnectionFailed(_) => "Service unavailable",
125            Self::UpstreamTimeout(_) => "Gateway timeout",
126            Self::BodyTooLarge { .. } => "Request body too large",
127            Self::BodyReadError(_) => "Bad request",
128            Self::ClientBodyTimeout => "Request timeout",
129            Self::HttpClientError(_) => "Bad gateway",
130        }
131    }
132
133    /// Returns true if this error should be logged at error level.
134    ///
135    /// Some errors (like rate limiting) are expected and should only
136    /// be logged at debug/info level.
137    pub fn is_server_error(&self) -> bool {
138        matches!(
139            self,
140            Self::ConfigError(_)
141                | Self::ProxyError(_)
142                | Self::UpstreamConnectionFailed(_)
143                | Self::UpstreamTimeout(_)
144                | Self::HttpClientError(_)
145        )
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use hyper::StatusCode;
153
154    #[test]
155    fn test_error_display() {
156        let err = WiseGateError::InvalidIp("192.168.1.999".into());
157        assert_eq!(err.to_string(), "Invalid IP address: 192.168.1.999");
158
159        let err = WiseGateError::RateLimitExceeded("10.0.0.1".into());
160        assert_eq!(err.to_string(), "Rate limit exceeded for IP: 10.0.0.1");
161
162        let err = WiseGateError::BodyTooLarge {
163            size: 200,
164            max: 100,
165        };
166        assert_eq!(
167            err.to_string(),
168            "Request body too large: 200 bytes (max: 100 bytes)"
169        );
170    }
171
172    #[test]
173    fn test_status_codes_all_variants() {
174        assert_eq!(
175            WiseGateError::InvalidIp("".into()).status_code(),
176            StatusCode::BAD_REQUEST
177        );
178        assert_eq!(
179            WiseGateError::ConfigError("".into()).status_code(),
180            StatusCode::INTERNAL_SERVER_ERROR
181        );
182        assert_eq!(
183            WiseGateError::ProxyError("".into()).status_code(),
184            StatusCode::BAD_GATEWAY
185        );
186        assert_eq!(
187            WiseGateError::RateLimitExceeded("".into()).status_code(),
188            StatusCode::TOO_MANY_REQUESTS
189        );
190        assert_eq!(
191            WiseGateError::IpBlocked("".into()).status_code(),
192            StatusCode::FORBIDDEN
193        );
194        assert_eq!(
195            WiseGateError::PatternBlocked("".into()).status_code(),
196            StatusCode::NOT_FOUND
197        );
198        assert_eq!(
199            WiseGateError::MethodBlocked("".into()).status_code(),
200            StatusCode::METHOD_NOT_ALLOWED
201        );
202        assert_eq!(
203            WiseGateError::UpstreamConnectionFailed("".into()).status_code(),
204            StatusCode::BAD_GATEWAY
205        );
206        assert_eq!(
207            WiseGateError::UpstreamTimeout("".into()).status_code(),
208            StatusCode::GATEWAY_TIMEOUT
209        );
210        assert_eq!(
211            WiseGateError::BodyTooLarge { size: 0, max: 0 }.status_code(),
212            StatusCode::PAYLOAD_TOO_LARGE
213        );
214        assert_eq!(
215            WiseGateError::BodyReadError("".into()).status_code(),
216            StatusCode::BAD_REQUEST
217        );
218    }
219
220    #[test]
221    fn test_user_messages_do_not_leak_internals() {
222        assert_eq!(
223            WiseGateError::ConfigError("database connection string".into()).user_message(),
224            "Internal server error"
225        );
226        assert_eq!(
227            WiseGateError::ProxyError("connection refused".into()).user_message(),
228            "Bad gateway"
229        );
230        assert_eq!(
231            WiseGateError::IpBlocked("10.0.0.1".into()).user_message(),
232            "Access denied"
233        );
234        assert_eq!(
235            WiseGateError::UpstreamConnectionFailed("".into()).user_message(),
236            "Service unavailable"
237        );
238        assert_eq!(
239            WiseGateError::BodyReadError("".into()).user_message(),
240            "Bad request"
241        );
242    }
243
244    #[test]
245    fn test_is_server_error_all_variants() {
246        // Server errors
247        assert!(WiseGateError::ConfigError("".into()).is_server_error());
248        assert!(WiseGateError::ProxyError("".into()).is_server_error());
249        assert!(WiseGateError::UpstreamConnectionFailed("".into()).is_server_error());
250        assert!(WiseGateError::UpstreamTimeout("".into()).is_server_error());
251
252        // Client/expected errors
253        assert!(!WiseGateError::InvalidIp("".into()).is_server_error());
254        assert!(!WiseGateError::RateLimitExceeded("".into()).is_server_error());
255        assert!(!WiseGateError::IpBlocked("".into()).is_server_error());
256        assert!(!WiseGateError::PatternBlocked("".into()).is_server_error());
257        assert!(!WiseGateError::MethodBlocked("".into()).is_server_error());
258        assert!(!WiseGateError::BodyTooLarge { size: 0, max: 0 }.is_server_error());
259        assert!(!WiseGateError::BodyReadError("".into()).is_server_error());
260    }
261}