use thiserror::Error;
#[derive(Debug, Error)]
pub enum WiseGateError {
#[error("Invalid IP address: {0}")]
InvalidIp(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Proxy error: {0}")]
ProxyError(String),
#[error("Rate limit exceeded for IP: {0}")]
RateLimitExceeded(String),
#[error("IP blocked: {0}")]
IpBlocked(String),
#[error("URL pattern blocked: {0}")]
PatternBlocked(String),
#[error("HTTP method blocked: {0}")]
MethodBlocked(String),
#[error("Upstream connection failed: {0}")]
UpstreamConnectionFailed(String),
#[error("Upstream timeout: {0}")]
UpstreamTimeout(String),
#[error("Request body too large: {size} bytes (max: {max} bytes)")]
BodyTooLarge {
size: usize,
max: usize,
},
#[error("Body read error: {0}")]
BodyReadError(String),
#[error("HTTP client error: {0}")]
HttpClientError(#[from] reqwest::Error),
}
impl WiseGateError {
pub fn status_code(&self) -> hyper::StatusCode {
use hyper::StatusCode;
match self {
Self::InvalidIp(_) => StatusCode::BAD_REQUEST,
Self::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::ProxyError(_) => StatusCode::BAD_GATEWAY,
Self::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
Self::IpBlocked(_) => StatusCode::FORBIDDEN,
Self::PatternBlocked(_) => StatusCode::NOT_FOUND,
Self::MethodBlocked(_) => StatusCode::METHOD_NOT_ALLOWED,
Self::UpstreamConnectionFailed(_) => StatusCode::BAD_GATEWAY,
Self::UpstreamTimeout(_) => StatusCode::GATEWAY_TIMEOUT,
Self::BodyTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
Self::BodyReadError(_) => StatusCode::BAD_REQUEST,
Self::HttpClientError(_) => StatusCode::BAD_GATEWAY,
}
}
pub fn user_message(&self) -> &str {
match self {
Self::InvalidIp(_) => "Invalid request",
Self::ConfigError(_) => "Internal server error",
Self::ProxyError(_) => "Bad gateway",
Self::RateLimitExceeded(_) => "Rate limit exceeded",
Self::IpBlocked(_) => "Access denied",
Self::PatternBlocked(_) => "Not found",
Self::MethodBlocked(_) => "Method not allowed",
Self::UpstreamConnectionFailed(_) => "Service unavailable",
Self::UpstreamTimeout(_) => "Gateway timeout",
Self::BodyTooLarge { .. } => "Request body too large",
Self::BodyReadError(_) => "Bad request",
Self::HttpClientError(_) => "Bad gateway",
}
}
pub fn is_server_error(&self) -> bool {
matches!(
self,
Self::ConfigError(_)
| Self::ProxyError(_)
| Self::UpstreamConnectionFailed(_)
| Self::UpstreamTimeout(_)
| Self::HttpClientError(_)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use hyper::StatusCode;
#[test]
fn test_error_display() {
let err = WiseGateError::InvalidIp("192.168.1.999".into());
assert_eq!(err.to_string(), "Invalid IP address: 192.168.1.999");
let err = WiseGateError::RateLimitExceeded("10.0.0.1".into());
assert_eq!(err.to_string(), "Rate limit exceeded for IP: 10.0.0.1");
let err = WiseGateError::BodyTooLarge {
size: 200,
max: 100,
};
assert_eq!(
err.to_string(),
"Request body too large: 200 bytes (max: 100 bytes)"
);
}
#[test]
fn test_status_codes_all_variants() {
assert_eq!(
WiseGateError::InvalidIp("".into()).status_code(),
StatusCode::BAD_REQUEST
);
assert_eq!(
WiseGateError::ConfigError("".into()).status_code(),
StatusCode::INTERNAL_SERVER_ERROR
);
assert_eq!(
WiseGateError::ProxyError("".into()).status_code(),
StatusCode::BAD_GATEWAY
);
assert_eq!(
WiseGateError::RateLimitExceeded("".into()).status_code(),
StatusCode::TOO_MANY_REQUESTS
);
assert_eq!(
WiseGateError::IpBlocked("".into()).status_code(),
StatusCode::FORBIDDEN
);
assert_eq!(
WiseGateError::PatternBlocked("".into()).status_code(),
StatusCode::NOT_FOUND
);
assert_eq!(
WiseGateError::MethodBlocked("".into()).status_code(),
StatusCode::METHOD_NOT_ALLOWED
);
assert_eq!(
WiseGateError::UpstreamConnectionFailed("".into()).status_code(),
StatusCode::BAD_GATEWAY
);
assert_eq!(
WiseGateError::UpstreamTimeout("".into()).status_code(),
StatusCode::GATEWAY_TIMEOUT
);
assert_eq!(
WiseGateError::BodyTooLarge { size: 0, max: 0 }.status_code(),
StatusCode::PAYLOAD_TOO_LARGE
);
assert_eq!(
WiseGateError::BodyReadError("".into()).status_code(),
StatusCode::BAD_REQUEST
);
}
#[test]
fn test_user_messages_do_not_leak_internals() {
assert_eq!(
WiseGateError::ConfigError("database connection string".into()).user_message(),
"Internal server error"
);
assert_eq!(
WiseGateError::ProxyError("connection refused".into()).user_message(),
"Bad gateway"
);
assert_eq!(
WiseGateError::IpBlocked("10.0.0.1".into()).user_message(),
"Access denied"
);
assert_eq!(
WiseGateError::UpstreamConnectionFailed("".into()).user_message(),
"Service unavailable"
);
assert_eq!(
WiseGateError::BodyReadError("".into()).user_message(),
"Bad request"
);
}
#[test]
fn test_is_server_error_all_variants() {
assert!(WiseGateError::ConfigError("".into()).is_server_error());
assert!(WiseGateError::ProxyError("".into()).is_server_error());
assert!(WiseGateError::UpstreamConnectionFailed("".into()).is_server_error());
assert!(WiseGateError::UpstreamTimeout("".into()).is_server_error());
assert!(!WiseGateError::InvalidIp("".into()).is_server_error());
assert!(!WiseGateError::RateLimitExceeded("".into()).is_server_error());
assert!(!WiseGateError::IpBlocked("".into()).is_server_error());
assert!(!WiseGateError::PatternBlocked("".into()).is_server_error());
assert!(!WiseGateError::MethodBlocked("".into()).is_server_error());
assert!(!WiseGateError::BodyTooLarge { size: 0, max: 0 }.is_server_error());
assert!(!WiseGateError::BodyReadError("".into()).is_server_error());
}
}