RTIX / src /domain /error.rs
github-actions
deploy: clean backend production release
d8ffec9
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use thiserror::Error;
/// Production-grade error codes for API clients
/// Follows Google API error code conventions
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Authentication failed: {0}")]
Auth(String),
#[error("Authorization failed: {0}")]
Forbidden(String),
#[error("Invalid request: {0}")]
BadRequest(String),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Internal server error: {0}")]
Internal(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Too many requests")]
RateLimited,
#[error("Request timeout")]
Timeout,
#[error("Conflict: {0}")]
Conflict(String),
#[error("Unprocessable entity: {0}")]
UnprocessableEntity(String),
}
#[derive(serde::Serialize)]
pub struct ErrorResponse {
pub success: bool,
pub error: ErrorDetails,
}
#[derive(serde::Serialize)]
pub struct ErrorDetails {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_code, message, details) = match self {
AppError::Database(ref e) => {
tracing::error!("Database error: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"Database operation failed".to_string(),
Some(format!("{:?}", e)),
)
}
AppError::Auth(msg) => {
tracing::warn!("Auth failure: {}", msg);
(StatusCode::UNAUTHORIZED, "UNAUTHENTICATED", msg, None)
}
AppError::Forbidden(msg) => {
tracing::warn!("Authorization failure: {}", msg);
(StatusCode::FORBIDDEN, "PERMISSION_DENIED", msg, None)
}
AppError::BadRequest(msg) => {
tracing::debug!("Bad request: {}", msg);
(StatusCode::BAD_REQUEST, "INVALID_ARGUMENT", msg, None)
}
AppError::NotFound(msg) => {
tracing::debug!("Resource not found: {}", msg);
(StatusCode::NOT_FOUND, "NOT_FOUND", msg, None)
}
AppError::Internal(msg) => {
tracing::error!("Internal error: {}", msg);
(
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An unexpected error occurred".to_string(),
Some(msg),
)
}
AppError::Validation(msg) => {
tracing::debug!("Validation error: {}", msg);
(StatusCode::BAD_REQUEST, "INVALID_ARGUMENT", msg, None)
}
AppError::RateLimited => {
tracing::warn!("Rate limit exceeded");
(
StatusCode::TOO_MANY_REQUESTS,
"RESOURCE_EXHAUSTED",
"Too many requests. Please try again later.".to_string(),
None,
)
}
AppError::Timeout => {
tracing::warn!("Request timeout");
(
StatusCode::REQUEST_TIMEOUT,
"DEADLINE_EXCEEDED",
"Request processing timeout. Please try again.".to_string(),
None,
)
}
AppError::Conflict(msg) => {
tracing::warn!("Conflict: {}", msg);
(StatusCode::CONFLICT, "ALREADY_EXISTS", msg, None)
}
AppError::UnprocessableEntity(msg) => {
tracing::warn!("Unprocessable entity: {}", msg);
(
StatusCode::UNPROCESSABLE_ENTITY,
"FAILED_PRECONDITION",
msg,
None,
)
}
};
let body = Json(ErrorResponse {
success: false,
error: ErrorDetails {
code: error_code.to_string(),
message,
details: if cfg!(debug_assertions) {
details
} else {
None
},
},
});
(status, body).into_response()
}
}
pub type AppResult<T> = Result<T, AppError>;