remote_mcp_kernel/
error.rs

1//! Error handling for MCP OAuth server
2//!
3//! This module provides centralized error handling with consistent
4//! error types and HTTP responses, following the principle of
5//! explicit error handling and separation of concerns.
6
7use axum::{
8    Json,
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12use serde_json::json;
13
14/// Application error types
15#[derive(Debug, thiserror::Error)]
16pub enum AppError {
17    #[error("Configuration error: {0}")]
18    Config(#[from] crate::config::ConfigError),
19
20    #[error("OAuth error: {0}")]
21    OAuth(String),
22
23    #[error("GitHub API error: {0}")]
24    GitHub(String),
25
26    #[error("Token validation error: {0}")]
27    TokenValidation(String),
28
29    #[error("Authorization error: {0}")]
30    Authorization(String),
31
32    #[error("HTTP client error: {0}")]
33    HttpClient(#[from] reqwest::Error),
34
35    #[error("JSON serialization error: {0}")]
36    Json(#[from] serde_json::Error),
37
38    #[error("Internal server error: {0}")]
39    Internal(String),
40}
41
42impl AppError {
43    /// Get the HTTP status code for this error
44    pub fn status_code(&self) -> StatusCode {
45        match self {
46            AppError::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
47            AppError::OAuth(_) => StatusCode::BAD_REQUEST,
48            AppError::GitHub(_) => StatusCode::BAD_GATEWAY,
49            AppError::TokenValidation(_) => StatusCode::UNAUTHORIZED,
50            AppError::Authorization(_) => StatusCode::UNAUTHORIZED,
51            AppError::HttpClient(_) => StatusCode::BAD_GATEWAY,
52            AppError::Json(_) => StatusCode::BAD_REQUEST,
53            AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
54        }
55    }
56
57    /// Get the error code for API responses
58    pub fn error_code(&self) -> &'static str {
59        match self {
60            AppError::Config(_) => "configuration_error",
61            AppError::OAuth(_) => "oauth_error",
62            AppError::GitHub(_) => "github_api_error",
63            AppError::TokenValidation(_) => "invalid_token",
64            AppError::Authorization(_) => "authorization_failed",
65            AppError::HttpClient(_) => "http_client_error",
66            AppError::Json(_) => "json_error",
67            AppError::Internal(_) => "internal_error",
68        }
69    }
70
71    /// Create a standardized error response
72    pub fn error_response(&self) -> Json<serde_json::Value> {
73        Json(json!({
74            "error": self.error_code(),
75            "description": self.to_string(),
76            "status": self.status_code().as_u16()
77        }))
78    }
79}
80
81impl IntoResponse for AppError {
82    fn into_response(self) -> Response {
83        let status = self.status_code();
84        let body = self.error_response();
85
86        tracing::error!("Application error: {} (status: {})", self, status);
87
88        (status, body).into_response()
89    }
90}
91
92/// Authentication error types
93#[derive(Debug, thiserror::Error)]
94pub enum AuthError {
95    #[error("Missing authorization header")]
96    MissingAuthHeader,
97
98    #[error("Invalid authorization header format")]
99    InvalidAuthFormat,
100
101    #[error("Invalid authorization header encoding")]
102    InvalidAuthEncoding,
103
104    #[error("Missing bearer token")]
105    MissingToken,
106
107    #[error("Empty bearer token")]
108    EmptyToken,
109
110    #[error("Invalid token")]
111    InvalidToken,
112
113    #[error("Token expired")]
114    TokenExpired,
115
116    #[error("Insufficient token scope")]
117    InsufficientScope,
118}
119
120impl AuthError {
121    /// Convert to application error
122    pub fn into_app_error(self) -> AppError {
123        AppError::TokenValidation(self.to_string())
124    }
125
126    /// Create standardized auth error response
127    pub fn error_response(&self) -> Json<serde_json::Value> {
128        Json(json!({
129            "error": self.error_code(),
130            "description": self.to_string()
131        }))
132    }
133
134    /// Get error code for OAuth responses
135    pub fn error_code(&self) -> &'static str {
136        match self {
137            AuthError::MissingAuthHeader => "missing_authorization",
138            AuthError::InvalidAuthFormat => "invalid_authorization_format",
139            AuthError::InvalidAuthEncoding => "invalid_authorization_header",
140            AuthError::MissingToken => "missing_token",
141            AuthError::EmptyToken => "empty_token",
142            AuthError::InvalidToken => "invalid_token",
143            AuthError::TokenExpired => "token_expired",
144            AuthError::InsufficientScope => "insufficient_scope",
145        }
146    }
147}
148
149impl IntoResponse for AuthError {
150    fn into_response(self) -> Response {
151        let body = self.error_response();
152        (StatusCode::UNAUTHORIZED, body).into_response()
153    }
154}
155
156/// Result type alias for application operations
157pub type AppResult<T> = Result<T, AppError>;
158
159/// Result type alias for authentication operations
160pub type AuthResult<T> = Result<T, AuthError>;
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_app_error_status_codes() {
168        assert_eq!(
169            AppError::OAuth("test".to_string()).status_code(),
170            StatusCode::BAD_REQUEST
171        );
172        assert_eq!(
173            AppError::TokenValidation("test".to_string()).status_code(),
174            StatusCode::UNAUTHORIZED
175        );
176        assert_eq!(
177            AppError::Internal("test".to_string()).status_code(),
178            StatusCode::INTERNAL_SERVER_ERROR
179        );
180    }
181
182    #[test]
183    fn test_auth_error_codes() {
184        assert_eq!(
185            AuthError::MissingAuthHeader.error_code(),
186            "missing_authorization"
187        );
188        assert_eq!(AuthError::InvalidToken.error_code(), "invalid_token");
189        assert_eq!(AuthError::TokenExpired.error_code(), "token_expired");
190    }
191
192    #[test]
193    fn test_error_response_format() {
194        let error = AppError::OAuth("test error".to_string());
195        let response = error.error_response();
196
197        let value = response.0;
198        assert_eq!(value["error"], "oauth_error");
199        assert_eq!(value["description"], "OAuth error: test error");
200        assert_eq!(value["status"], 400);
201    }
202}