Skip to main content

oxigdal_gateway/
error.rs

1//! Error types for the OxiGDAL API Gateway.
2//!
3//! This module provides comprehensive error handling for gateway operations including
4//! rate limiting, authentication, GraphQL, WebSocket, and middleware errors.
5
6/// Result type for gateway operations.
7pub type Result<T> = std::result::Result<T, GatewayError>;
8
9/// Comprehensive error types for API gateway operations.
10#[derive(Debug, thiserror::Error)]
11pub enum GatewayError {
12    /// Rate limit exceeded error.
13    #[error("Rate limit exceeded: {message}")]
14    RateLimitExceeded {
15        /// Error message
16        message: String,
17        /// Retry after duration in seconds
18        retry_after: Option<u64>,
19    },
20
21    /// Authentication error.
22    #[error("Authentication failed: {0}")]
23    AuthenticationFailed(String),
24
25    /// Authorization error.
26    #[error("Authorization failed: {0}")]
27    AuthorizationFailed(String),
28
29    /// Invalid API key error.
30    #[error("Invalid API key")]
31    InvalidApiKey,
32
33    /// Expired token error.
34    #[error("Token expired")]
35    TokenExpired,
36
37    /// Invalid token error.
38    #[error("Invalid token: {0}")]
39    InvalidToken(String),
40
41    /// JWT error.
42    #[error("JWT error: {0}")]
43    JwtError(#[from] jsonwebtoken::errors::Error),
44
45    /// OAuth2 error.
46    #[error("OAuth2 error: {0}")]
47    OAuth2Error(String),
48
49    /// GraphQL error.
50    #[error("GraphQL error: {0}")]
51    GraphQLError(String),
52
53    /// WebSocket error.
54    #[error("WebSocket error: {0}")]
55    WebSocketError(String),
56
57    /// Invalid request error.
58    #[error("Invalid request: {0}")]
59    InvalidRequest(String),
60
61    /// Unsupported API version error.
62    #[error("Unsupported API version: {version}")]
63    UnsupportedVersion {
64        /// Requested version
65        version: String,
66        /// Supported versions
67        supported: Vec<String>,
68    },
69
70    /// Transformation error.
71    #[error("Transformation error: {0}")]
72    TransformationError(String),
73
74    /// Schema validation error.
75    #[error("Schema validation error: {0}")]
76    SchemaValidationError(String),
77
78    /// Load balancer error.
79    #[error("Load balancer error: {0}")]
80    LoadBalancerError(String),
81
82    /// Backend unavailable error.
83    #[error("Backend unavailable: {0}")]
84    BackendUnavailable(String),
85
86    /// Circuit breaker open error.
87    #[error("Circuit breaker open for backend: {0}")]
88    CircuitBreakerOpen(String),
89
90    /// Timeout error.
91    #[error("Operation timed out: {0}")]
92    Timeout(String),
93
94    /// Redis connection error.
95    #[cfg(feature = "redis")]
96    #[error("Redis error: {0}")]
97    RedisError(#[from] redis::RedisError),
98
99    /// Serialization error.
100    #[error("Serialization error: {0}")]
101    SerializationError(#[from] serde_json::Error),
102
103    /// HTTP error.
104    #[error("HTTP error: {0}")]
105    HttpError(String),
106
107    /// Internal server error.
108    #[error("Internal server error: {0}")]
109    InternalError(String),
110
111    /// Configuration error.
112    #[error("Configuration error: {0}")]
113    ConfigError(String),
114}
115
116impl GatewayError {
117    /// Returns HTTP status code for this error.
118    pub fn status_code(&self) -> http::StatusCode {
119        match self {
120            Self::RateLimitExceeded { .. } => http::StatusCode::TOO_MANY_REQUESTS,
121            Self::AuthenticationFailed(_) | Self::InvalidApiKey | Self::InvalidToken(_) => {
122                http::StatusCode::UNAUTHORIZED
123            }
124            Self::AuthorizationFailed(_) => http::StatusCode::FORBIDDEN,
125            Self::TokenExpired => http::StatusCode::UNAUTHORIZED,
126            Self::InvalidRequest(_) | Self::SchemaValidationError(_) => {
127                http::StatusCode::BAD_REQUEST
128            }
129            Self::UnsupportedVersion { .. } => http::StatusCode::NOT_ACCEPTABLE,
130            Self::BackendUnavailable(_) | Self::CircuitBreakerOpen(_) => {
131                http::StatusCode::SERVICE_UNAVAILABLE
132            }
133            Self::Timeout(_) => http::StatusCode::GATEWAY_TIMEOUT,
134            _ => http::StatusCode::INTERNAL_SERVER_ERROR,
135        }
136    }
137
138    /// Returns whether this error is retryable.
139    pub fn is_retryable(&self) -> bool {
140        matches!(
141            self,
142            Self::RateLimitExceeded { .. }
143                | Self::BackendUnavailable(_)
144                | Self::CircuitBreakerOpen(_)
145                | Self::Timeout(_)
146                | Self::LoadBalancerError(_)
147        )
148    }
149
150    /// Returns retry-after duration in seconds if applicable.
151    pub fn retry_after(&self) -> Option<u64> {
152        match self {
153            Self::RateLimitExceeded { retry_after, .. } => *retry_after,
154            Self::BackendUnavailable(_) => Some(5),
155            Self::CircuitBreakerOpen(_) => Some(30),
156            _ => None,
157        }
158    }
159
160    /// Converts error to JSON error response.
161    pub fn to_json_response(&self) -> serde_json::Value {
162        serde_json::json!({
163            "error": {
164                "code": self.error_code(),
165                "message": self.to_string(),
166                "status": self.status_code().as_u16(),
167                "retryable": self.is_retryable(),
168                "retry_after": self.retry_after(),
169            }
170        })
171    }
172
173    /// Returns error code string.
174    pub fn error_code(&self) -> &str {
175        match self {
176            Self::RateLimitExceeded { .. } => "RATE_LIMIT_EXCEEDED",
177            Self::AuthenticationFailed(_) => "AUTHENTICATION_FAILED",
178            Self::AuthorizationFailed(_) => "AUTHORIZATION_FAILED",
179            Self::InvalidApiKey => "INVALID_API_KEY",
180            Self::TokenExpired => "TOKEN_EXPIRED",
181            Self::InvalidToken(_) => "INVALID_TOKEN",
182            Self::JwtError(_) => "JWT_ERROR",
183            Self::OAuth2Error(_) => "OAUTH2_ERROR",
184            Self::GraphQLError(_) => "GRAPHQL_ERROR",
185            Self::WebSocketError(_) => "WEBSOCKET_ERROR",
186            Self::InvalidRequest(_) => "INVALID_REQUEST",
187            Self::UnsupportedVersion { .. } => "UNSUPPORTED_VERSION",
188            Self::TransformationError(_) => "TRANSFORMATION_ERROR",
189            Self::SchemaValidationError(_) => "SCHEMA_VALIDATION_ERROR",
190            Self::LoadBalancerError(_) => "LOAD_BALANCER_ERROR",
191            Self::BackendUnavailable(_) => "BACKEND_UNAVAILABLE",
192            Self::CircuitBreakerOpen(_) => "CIRCUIT_BREAKER_OPEN",
193            Self::Timeout(_) => "TIMEOUT",
194            #[cfg(feature = "redis")]
195            Self::RedisError(_) => "REDIS_ERROR",
196            Self::SerializationError(_) => "SERIALIZATION_ERROR",
197            Self::HttpError(_) => "HTTP_ERROR",
198            Self::InternalError(_) => "INTERNAL_ERROR",
199            Self::ConfigError(_) => "CONFIG_ERROR",
200        }
201    }
202}
203
204impl From<GatewayError> for http::StatusCode {
205    fn from(error: GatewayError) -> Self {
206        error.status_code()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_error_status_codes() {
216        assert_eq!(
217            GatewayError::RateLimitExceeded {
218                message: "test".to_string(),
219                retry_after: Some(60)
220            }
221            .status_code(),
222            http::StatusCode::TOO_MANY_REQUESTS
223        );
224
225        assert_eq!(
226            GatewayError::AuthenticationFailed("test".to_string()).status_code(),
227            http::StatusCode::UNAUTHORIZED
228        );
229
230        assert_eq!(
231            GatewayError::AuthorizationFailed("test".to_string()).status_code(),
232            http::StatusCode::FORBIDDEN
233        );
234    }
235
236    #[test]
237    fn test_error_retryable() {
238        assert!(
239            GatewayError::RateLimitExceeded {
240                message: "test".to_string(),
241                retry_after: Some(60)
242            }
243            .is_retryable()
244        );
245
246        assert!(GatewayError::BackendUnavailable("test".to_string()).is_retryable());
247
248        assert!(!GatewayError::InvalidApiKey.is_retryable());
249    }
250
251    #[test]
252    fn test_retry_after() {
253        let error = GatewayError::RateLimitExceeded {
254            message: "test".to_string(),
255            retry_after: Some(60),
256        };
257        assert_eq!(error.retry_after(), Some(60));
258
259        assert_eq!(
260            GatewayError::BackendUnavailable("test".to_string()).retry_after(),
261            Some(5)
262        );
263    }
264
265    #[test]
266    fn test_json_response() {
267        let error = GatewayError::InvalidApiKey;
268        let json = error.to_json_response();
269
270        assert_eq!(json["error"]["code"], "INVALID_API_KEY");
271        assert_eq!(json["error"]["status"], 401);
272        assert_eq!(json["error"]["retryable"], false);
273    }
274}