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