use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde_json::json;
#[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 {
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,
}
}
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",
}
}
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()
}
}
#[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 {
pub fn into_app_error(self) -> AppError {
AppError::TokenValidation(self.to_string())
}
pub fn error_response(&self) -> Json<serde_json::Value> {
Json(json!({
"error": self.error_code(),
"description": self.to_string()
}))
}
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()
}
}
pub type AppResult<T> = Result<T, AppError>;
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);
}
}