286 lines
11 KiB
Rust
286 lines
11 KiB
Rust
//! Error handling middleware and response types
|
|
|
|
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt;
|
|
|
|
/// Standard API error response
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct ErrorResponse {
|
|
/// Error message
|
|
pub error: String,
|
|
/// Optional error code
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub code: Option<String>,
|
|
/// Optional additional details
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<serde_json::Value>,
|
|
}
|
|
|
|
impl ErrorResponse {
|
|
/// Create a new error response
|
|
pub fn new(error: impl Into<String>) -> Self {
|
|
Self {
|
|
error: error.into(),
|
|
code: None,
|
|
details: None,
|
|
}
|
|
}
|
|
|
|
/// Set error code
|
|
pub fn with_code(mut self, code: impl Into<String>) -> Self {
|
|
self.code = Some(code.into());
|
|
self
|
|
}
|
|
|
|
/// Set error details
|
|
pub fn with_details(mut self, details: serde_json::Value) -> Self {
|
|
self.details = Some(details);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// API error type that can be converted to HTTP responses
|
|
#[derive(Debug)]
|
|
pub enum ApiError {
|
|
/// Bad request (400)
|
|
BadRequest(String),
|
|
/// Unauthorized (401)
|
|
Unauthorized(String),
|
|
/// Forbidden (403)
|
|
Forbidden(String),
|
|
/// Not found (404)
|
|
NotFound(String),
|
|
/// Conflict (409)
|
|
Conflict(String),
|
|
/// Unprocessable entity (422)
|
|
UnprocessableEntity(String),
|
|
/// Too many requests (429)
|
|
TooManyRequests(String),
|
|
/// Internal server error (500)
|
|
InternalServerError(String),
|
|
/// Not implemented (501)
|
|
NotImplemented(String),
|
|
/// Database error
|
|
DatabaseError(String),
|
|
/// Validation error
|
|
ValidationError(String),
|
|
}
|
|
|
|
impl ApiError {
|
|
/// Get the HTTP status code for this error
|
|
pub fn status_code(&self) -> StatusCode {
|
|
match self {
|
|
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
|
ApiError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
|
|
ApiError::Forbidden(_) => StatusCode::FORBIDDEN,
|
|
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
|
|
ApiError::Conflict(_) => StatusCode::CONFLICT,
|
|
ApiError::UnprocessableEntity(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
|
ApiError::ValidationError(_) => StatusCode::UNPROCESSABLE_ENTITY,
|
|
ApiError::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
|
|
ApiError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
|
|
ApiError::InternalServerError(_) | ApiError::DatabaseError(_) => {
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get the error message
|
|
pub fn message(&self) -> &str {
|
|
match self {
|
|
ApiError::BadRequest(msg)
|
|
| ApiError::Unauthorized(msg)
|
|
| ApiError::Forbidden(msg)
|
|
| ApiError::NotFound(msg)
|
|
| ApiError::Conflict(msg)
|
|
| ApiError::UnprocessableEntity(msg)
|
|
| ApiError::TooManyRequests(msg)
|
|
| ApiError::NotImplemented(msg)
|
|
| ApiError::InternalServerError(msg)
|
|
| ApiError::DatabaseError(msg)
|
|
| ApiError::ValidationError(msg) => msg,
|
|
}
|
|
}
|
|
|
|
/// Get the error code
|
|
pub fn code(&self) -> &str {
|
|
match self {
|
|
ApiError::BadRequest(_) => "BAD_REQUEST",
|
|
ApiError::Unauthorized(_) => "UNAUTHORIZED",
|
|
ApiError::Forbidden(_) => "FORBIDDEN",
|
|
ApiError::NotFound(_) => "NOT_FOUND",
|
|
ApiError::Conflict(_) => "CONFLICT",
|
|
ApiError::UnprocessableEntity(_) => "UNPROCESSABLE_ENTITY",
|
|
ApiError::TooManyRequests(_) => "TOO_MANY_REQUESTS",
|
|
ApiError::NotImplemented(_) => "NOT_IMPLEMENTED",
|
|
ApiError::ValidationError(_) => "VALIDATION_ERROR",
|
|
ApiError::DatabaseError(_) => "DATABASE_ERROR",
|
|
ApiError::InternalServerError(_) => "INTERNAL_SERVER_ERROR",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ApiError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.message())
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ApiError {}
|
|
|
|
impl IntoResponse for ApiError {
|
|
fn into_response(self) -> Response {
|
|
let status = self.status_code();
|
|
let error_response = ErrorResponse::new(self.message()).with_code(self.code());
|
|
|
|
(status, Json(error_response)).into_response()
|
|
}
|
|
}
|
|
|
|
// Convert from common error types
|
|
impl From<sqlx::Error> for ApiError {
|
|
fn from(err: sqlx::Error) -> Self {
|
|
match err {
|
|
sqlx::Error::RowNotFound => ApiError::NotFound("Resource not found".to_string()),
|
|
sqlx::Error::Database(db_err) => {
|
|
// PostgreSQL error codes:
|
|
// 23505 = unique_violation → 409 Conflict
|
|
// 23503 = foreign_key_violation → 422 Unprocessable Entity
|
|
// 23514 = check_violation → 422 Unprocessable Entity
|
|
// P0001 = raise_exception → 400 Bad Request (trigger-raised errors)
|
|
let pg_code = db_err.code().map(|c| c.to_string()).unwrap_or_default();
|
|
if pg_code == "23505" {
|
|
// Unique constraint violation — duplicate key
|
|
let detail = db_err
|
|
.constraint()
|
|
.map(|c| format!(" ({})", c))
|
|
.unwrap_or_default();
|
|
ApiError::Conflict(format!("Already exists{}", detail))
|
|
} else if pg_code == "23503" {
|
|
// Foreign key violation — the referenced row doesn't exist
|
|
let detail = db_err
|
|
.constraint()
|
|
.map(|c| format!(" ({})", c))
|
|
.unwrap_or_default();
|
|
ApiError::UnprocessableEntity(format!(
|
|
"Referenced entity does not exist{}",
|
|
detail
|
|
))
|
|
} else if pg_code == "23514" {
|
|
// CHECK constraint violation — value doesn't meet constraint
|
|
let detail = db_err
|
|
.constraint()
|
|
.map(|c| format!(": {}", c))
|
|
.unwrap_or_default();
|
|
ApiError::UnprocessableEntity(format!("Validation constraint failed{}", detail))
|
|
} else if pg_code == "P0001" {
|
|
// RAISE EXCEPTION from a trigger or function
|
|
// Extract the human-readable message from the exception
|
|
let msg = db_err.message().to_string();
|
|
ApiError::BadRequest(msg)
|
|
} else if let Some(constraint) = db_err.constraint() {
|
|
ApiError::Conflict(format!("Constraint violation: {}", constraint))
|
|
} else {
|
|
ApiError::DatabaseError(format!("Database error: {}", db_err))
|
|
}
|
|
}
|
|
_ => ApiError::DatabaseError(format!("Database error: {}", err)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<attune_common::error::Error> for ApiError {
|
|
fn from(err: attune_common::error::Error) -> Self {
|
|
match err {
|
|
attune_common::error::Error::NotFound {
|
|
entity,
|
|
field,
|
|
value,
|
|
} => ApiError::NotFound(format!("{} with {}={} not found", entity, field, value)),
|
|
attune_common::error::Error::AlreadyExists {
|
|
entity,
|
|
field,
|
|
value,
|
|
} => ApiError::Conflict(format!(
|
|
"{} with {}={} already exists",
|
|
entity, field, value
|
|
)),
|
|
attune_common::error::Error::Validation(msg) => ApiError::BadRequest(msg),
|
|
attune_common::error::Error::SchemaValidation(msg) => ApiError::BadRequest(msg),
|
|
attune_common::error::Error::Database(err) => ApiError::from(err),
|
|
attune_common::error::Error::InvalidState(msg) => ApiError::BadRequest(msg),
|
|
attune_common::error::Error::PermissionDenied(msg) => ApiError::Forbidden(msg),
|
|
attune_common::error::Error::AuthenticationFailed(msg) => ApiError::Unauthorized(msg),
|
|
attune_common::error::Error::Configuration(msg) => ApiError::InternalServerError(msg),
|
|
attune_common::error::Error::Serialization(err) => {
|
|
ApiError::InternalServerError(format!("{}", err))
|
|
}
|
|
attune_common::error::Error::Io(msg)
|
|
| attune_common::error::Error::Encryption(msg)
|
|
| attune_common::error::Error::Timeout(msg)
|
|
| attune_common::error::Error::ExternalService(msg)
|
|
| attune_common::error::Error::Worker(msg)
|
|
| attune_common::error::Error::Execution(msg)
|
|
| attune_common::error::Error::Internal(msg) => ApiError::InternalServerError(msg),
|
|
attune_common::error::Error::Other(err) => {
|
|
ApiError::InternalServerError(format!("{}", err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<validator::ValidationErrors> for ApiError {
|
|
fn from(err: validator::ValidationErrors) -> Self {
|
|
ApiError::ValidationError(format!("Validation failed: {}", err))
|
|
}
|
|
}
|
|
|
|
impl From<crate::auth::jwt::JwtError> for ApiError {
|
|
fn from(err: crate::auth::jwt::JwtError) -> Self {
|
|
match err {
|
|
crate::auth::jwt::JwtError::Expired => {
|
|
ApiError::Unauthorized("Token has expired".to_string())
|
|
}
|
|
crate::auth::jwt::JwtError::Invalid => {
|
|
ApiError::Unauthorized("Invalid token".to_string())
|
|
}
|
|
crate::auth::jwt::JwtError::EncodeError(msg) => {
|
|
ApiError::InternalServerError(format!("Failed to encode token: {}", msg))
|
|
}
|
|
crate::auth::jwt::JwtError::DecodeError(msg) => {
|
|
ApiError::Unauthorized(format!("Failed to decode token: {}", msg))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<crate::auth::password::PasswordError> for ApiError {
|
|
fn from(err: crate::auth::password::PasswordError) -> Self {
|
|
match err {
|
|
crate::auth::password::PasswordError::HashError(msg) => {
|
|
ApiError::InternalServerError(format!("Failed to hash password: {}", msg))
|
|
}
|
|
crate::auth::password::PasswordError::VerifyError(msg) => {
|
|
ApiError::InternalServerError(format!("Failed to verify password: {}", msg))
|
|
}
|
|
crate::auth::password::PasswordError::InvalidHash => {
|
|
ApiError::InternalServerError("Invalid password hash format".to_string())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<std::num::ParseIntError> for ApiError {
|
|
fn from(err: std::num::ParseIntError) -> Self {
|
|
ApiError::BadRequest(format!("Invalid number format: {}", err))
|
|
}
|
|
}
|
|
|
|
/// Result type alias for API handlers
|
|
pub type ApiResult<T> = Result<T, ApiError>;
|