Spaces:
Running
Running
File size: 4,623 Bytes
18d6188 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 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>;
|