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    /// HTTP client error (from reqwest).
77    #[error("HTTP client error: {0}")]
78    HttpClientError(#[from] reqwest::Error),
79}
80
81impl WiseGateError {
82    /// Returns the appropriate HTTP status code for this error.
83    ///
84    /// # Returns
85    ///
86    /// The HTTP status code that should be returned to the client.
87    pub fn status_code(&self) -> hyper::StatusCode {
88        use hyper::StatusCode;
89
90        match self {
91            Self::InvalidIp(_) => StatusCode::BAD_REQUEST,
92            Self::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
93            Self::ProxyError(_) => StatusCode::BAD_GATEWAY,
94            Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
95            Self::IpBlocked(_) => StatusCode::FORBIDDEN,
96            Self::PatternBlocked(_) => StatusCode::NOT_FOUND,
97            Self::MethodBlocked(_) => StatusCode::METHOD_NOT_ALLOWED,
98            Self::UpstreamConnectionFailed(_) => StatusCode::BAD_GATEWAY,
99            Self::UpstreamTimeout(_) => StatusCode::GATEWAY_TIMEOUT,
100            Self::BodyTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
101            Self::BodyReadError(_) => StatusCode::BAD_REQUEST,
102            Self::HttpClientError(_) => StatusCode::BAD_GATEWAY,
103        }
104    }
105
106    /// Returns a user-friendly error message suitable for HTTP responses.
107    ///
108    /// This method returns a sanitized message that doesn't expose
109    /// internal details to clients.
110    pub fn user_message(&self) -> &str {
111        match self {
112            Self::InvalidIp(_) => "Invalid request",
113            Self::ConfigError(_) => "Internal server error",
114            Self::ProxyError(_) => "Bad gateway",
115            Self::RateLimitExceeded(_) => "Rate limit exceeded",
116            Self::IpBlocked(_) => "Access denied",
117            Self::PatternBlocked(_) => "Not found",
118            Self::MethodBlocked(_) => "Method not allowed",
119            Self::UpstreamConnectionFailed(_) => "Service unavailable",
120            Self::UpstreamTimeout(_) => "Gateway timeout",
121            Self::BodyTooLarge { .. } => "Request body too large",
122            Self::BodyReadError(_) => "Bad request",
123            Self::HttpClientError(_) => "Bad gateway",
124        }
125    }
126
127    /// Returns true if this error should be logged at error level.
128    ///
129    /// Some errors (like rate limiting) are expected and should only
130    /// be logged at debug/info level.
131    pub fn is_server_error(&self) -> bool {
132        matches!(
133            self,
134            Self::ConfigError(_)
135                | Self::ProxyError(_)
136                | Self::UpstreamConnectionFailed(_)
137                | Self::UpstreamTimeout(_)
138                | Self::HttpClientError(_)
139        )
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use hyper::StatusCode;
147
148    #[test]
149    fn test_error_display() {
150        let err = WiseGateError::InvalidIp("192.168.1.999".into());
151        assert_eq!(err.to_string(), "Invalid IP address: 192.168.1.999");
152
153        let err = WiseGateError::RateLimitExceeded("10.0.0.1".into());
154        assert_eq!(err.to_string(), "Rate limit exceeded for IP: 10.0.0.1");
155
156        let err = WiseGateError::BodyTooLarge {
157            size: 200,
158            max: 100,
159        };
160        assert_eq!(
161            err.to_string(),
162            "Request body too large: 200 bytes (max: 100 bytes)"
163        );
164    }
165
166    #[test]
167    fn test_status_codes_all_variants() {
168        assert_eq!(
169            WiseGateError::InvalidIp("".into()).status_code(),
170            StatusCode::BAD_REQUEST
171        );
172        assert_eq!(
173            WiseGateError::ConfigError("".into()).status_code(),
174            StatusCode::INTERNAL_SERVER_ERROR
175        );
176        assert_eq!(
177            WiseGateError::ProxyError("".into()).status_code(),
178            StatusCode::BAD_GATEWAY
179        );
180        assert_eq!(
181            WiseGateError::RateLimitExceeded("".into()).status_code(),
182            StatusCode::TOO_MANY_REQUESTS
183        );
184        assert_eq!(
185            WiseGateError::IpBlocked("".into()).status_code(),
186            StatusCode::FORBIDDEN
187        );
188        assert_eq!(
189            WiseGateError::PatternBlocked("".into()).status_code(),
190            StatusCode::NOT_FOUND
191        );
192        assert_eq!(
193            WiseGateError::MethodBlocked("".into()).status_code(),
194            StatusCode::METHOD_NOT_ALLOWED
195        );
196        assert_eq!(
197            WiseGateError::UpstreamConnectionFailed("".into()).status_code(),
198            StatusCode::BAD_GATEWAY
199        );
200        assert_eq!(
201            WiseGateError::UpstreamTimeout("".into()).status_code(),
202            StatusCode::GATEWAY_TIMEOUT
203        );
204        assert_eq!(
205            WiseGateError::BodyTooLarge { size: 0, max: 0 }.status_code(),
206            StatusCode::PAYLOAD_TOO_LARGE
207        );
208        assert_eq!(
209            WiseGateError::BodyReadError("".into()).status_code(),
210            StatusCode::BAD_REQUEST
211        );
212    }
213
214    #[test]
215    fn test_user_messages_do_not_leak_internals() {
216        assert_eq!(
217            WiseGateError::ConfigError("database connection string".into()).user_message(),
218            "Internal server error"
219        );
220        assert_eq!(
221            WiseGateError::ProxyError("connection refused".into()).user_message(),
222            "Bad gateway"
223        );
224        assert_eq!(
225            WiseGateError::IpBlocked("10.0.0.1".into()).user_message(),
226            "Access denied"
227        );
228        assert_eq!(
229            WiseGateError::UpstreamConnectionFailed("".into()).user_message(),
230            "Service unavailable"
231        );
232        assert_eq!(
233            WiseGateError::BodyReadError("".into()).user_message(),
234            "Bad request"
235        );
236    }
237
238    #[test]
239    fn test_is_server_error_all_variants() {
240        // Server errors
241        assert!(WiseGateError::ConfigError("".into()).is_server_error());
242        assert!(WiseGateError::ProxyError("".into()).is_server_error());
243        assert!(WiseGateError::UpstreamConnectionFailed("".into()).is_server_error());
244        assert!(WiseGateError::UpstreamTimeout("".into()).is_server_error());
245
246        // Client/expected errors
247        assert!(!WiseGateError::InvalidIp("".into()).is_server_error());
248        assert!(!WiseGateError::RateLimitExceeded("".into()).is_server_error());
249        assert!(!WiseGateError::IpBlocked("".into()).is_server_error());
250        assert!(!WiseGateError::PatternBlocked("".into()).is_server_error());
251        assert!(!WiseGateError::MethodBlocked("".into()).is_server_error());
252        assert!(!WiseGateError::BodyTooLarge { size: 0, max: 0 }.is_server_error());
253        assert!(!WiseGateError::BodyReadError("".into()).is_server_error());
254    }
255}