remote-mcp-kernel 0.1.0-alpha.6

A microkernel-based MCP (Model Context Protocol) server with OAuth authentication and multiple transport protocols
Documentation
//! Error handling for MCP OAuth server
//!
//! This module provides centralized error handling with consistent
//! error types and HTTP responses, following the principle of
//! explicit error handling and separation of concerns.

use axum::{
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use serde_json::json;

/// Application error types
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Configuration error: {0}")]
    Config(#[from] crate::config::ConfigError),

    #[error("OAuth error: {0}")]
    OAuth(String),

    #[error("GitHub API error: {0}")]
    GitHub(String),

    #[error("Token validation error: {0}")]
    TokenValidation(String),

    #[error("Authorization error: {0}")]
    Authorization(String),

    #[error("HTTP client error: {0}")]
    HttpClient(#[from] reqwest::Error),

    #[error("JSON serialization error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Internal server error: {0}")]
    Internal(String),
}

impl AppError {
    /// Get the HTTP status code for this error
    pub fn status_code(&self) -> StatusCode {
        match self {
            AppError::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
            AppError::OAuth(_) => StatusCode::BAD_REQUEST,
            AppError::GitHub(_) => StatusCode::BAD_GATEWAY,
            AppError::TokenValidation(_) => StatusCode::UNAUTHORIZED,
            AppError::Authorization(_) => StatusCode::UNAUTHORIZED,
            AppError::HttpClient(_) => StatusCode::BAD_GATEWAY,
            AppError::Json(_) => StatusCode::BAD_REQUEST,
            AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    /// Get the error code for API responses
    pub fn error_code(&self) -> &'static str {
        match self {
            AppError::Config(_) => "configuration_error",
            AppError::OAuth(_) => "oauth_error",
            AppError::GitHub(_) => "github_api_error",
            AppError::TokenValidation(_) => "invalid_token",
            AppError::Authorization(_) => "authorization_failed",
            AppError::HttpClient(_) => "http_client_error",
            AppError::Json(_) => "json_error",
            AppError::Internal(_) => "internal_error",
        }
    }

    /// Create a standardized error response
    pub fn error_response(&self) -> Json<serde_json::Value> {
        Json(json!({
            "error": self.error_code(),
            "description": self.to_string(),
            "status": self.status_code().as_u16()
        }))
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status = self.status_code();
        let body = self.error_response();

        tracing::error!("Application error: {} (status: {})", self, status);

        (status, body).into_response()
    }
}

/// Authentication error types
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
    #[error("Missing authorization header")]
    MissingAuthHeader,

    #[error("Invalid authorization header format")]
    InvalidAuthFormat,

    #[error("Invalid authorization header encoding")]
    InvalidAuthEncoding,

    #[error("Missing bearer token")]
    MissingToken,

    #[error("Empty bearer token")]
    EmptyToken,

    #[error("Invalid token")]
    InvalidToken,

    #[error("Token expired")]
    TokenExpired,

    #[error("Insufficient token scope")]
    InsufficientScope,
}

impl AuthError {
    /// Convert to application error
    pub fn into_app_error(self) -> AppError {
        AppError::TokenValidation(self.to_string())
    }

    /// Create standardized auth error response
    pub fn error_response(&self) -> Json<serde_json::Value> {
        Json(json!({
            "error": self.error_code(),
            "description": self.to_string()
        }))
    }

    /// Get error code for OAuth responses
    pub fn error_code(&self) -> &'static str {
        match self {
            AuthError::MissingAuthHeader => "missing_authorization",
            AuthError::InvalidAuthFormat => "invalid_authorization_format",
            AuthError::InvalidAuthEncoding => "invalid_authorization_header",
            AuthError::MissingToken => "missing_token",
            AuthError::EmptyToken => "empty_token",
            AuthError::InvalidToken => "invalid_token",
            AuthError::TokenExpired => "token_expired",
            AuthError::InsufficientScope => "insufficient_scope",
        }
    }
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let body = self.error_response();
        (StatusCode::UNAUTHORIZED, body).into_response()
    }
}

/// Result type alias for application operations
pub type AppResult<T> = Result<T, AppError>;

/// Result type alias for authentication operations
pub type AuthResult<T> = Result<T, AuthError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_app_error_status_codes() {
        assert_eq!(
            AppError::OAuth("test".to_string()).status_code(),
            StatusCode::BAD_REQUEST
        );
        assert_eq!(
            AppError::TokenValidation("test".to_string()).status_code(),
            StatusCode::UNAUTHORIZED
        );
        assert_eq!(
            AppError::Internal("test".to_string()).status_code(),
            StatusCode::INTERNAL_SERVER_ERROR
        );
    }

    #[test]
    fn test_auth_error_codes() {
        assert_eq!(
            AuthError::MissingAuthHeader.error_code(),
            "missing_authorization"
        );
        assert_eq!(AuthError::InvalidToken.error_code(), "invalid_token");
        assert_eq!(AuthError::TokenExpired.error_code(), "token_expired");
    }

    #[test]
    fn test_error_response_format() {
        let error = AppError::OAuth("test error".to_string());
        let response = error.error_response();

        let value = response.0;
        assert_eq!(value["error"], "oauth_error");
        assert_eq!(value["description"], "OAuth error: test error");
        assert_eq!(value["status"], 400);
    }
}