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