116 lines
4.7 KiB
Rust
116 lines
4.7 KiB
Rust
//! Единый тип ошибки приложения. `AppError` реализует `IntoResponse`, поэтому любой
|
|
//! контроллер может просто возвращать `Result<T, AppError>` — маппинг в HTTP-статус,
|
|
//! безопасное для клиента сообщение и структурированный лог/метрику делает `into_response`.
|
|
//! Правило: детали (SQL-ошибки, внутренние сообщения) уходят только в логи, наружу — общие
|
|
//! формулировки, чтобы не раскрывать внутреннее устройство системы атакующему.
|
|
|
|
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde_json::json;
|
|
use tracing::{error, warn};
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
pub enum AppError {
|
|
#[error("User not found")]
|
|
UserNotFound,
|
|
#[error("Trial already used")]
|
|
TrialAlreadyUsed,
|
|
#[error("Database error: {0}")]
|
|
Database(#[from] sqlx::Error),
|
|
#[error("Transaction failed: {0}")]
|
|
Transaction(String),
|
|
#[error("Redis error: {0}")]
|
|
Redis(#[from] redis::RedisError),
|
|
#[error("Unauthorized: {0}")]
|
|
Unauthorized(String),
|
|
#[error("Not found: {0}")]
|
|
NotFound(String),
|
|
#[error("Business Logic: {0}")]
|
|
BusinessLogic(String),
|
|
#[error("Internal Server Error: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> Response {
|
|
let error_type = match &self {
|
|
AppError::Database(_) => "database",
|
|
AppError::Transaction(_) => "transaction",
|
|
AppError::UserNotFound => "user_not_found",
|
|
AppError::TrialAlreadyUsed => "trial_used",
|
|
AppError::Redis(_) => "redis",
|
|
AppError::Unauthorized(_) => "unauthorized",
|
|
AppError::NotFound(_) => "not_found",
|
|
AppError::BusinessLogic(_) => "business_logic",
|
|
// FIX: Add `(_)` to ignore the inner string when just grabbing the type
|
|
AppError::Internal(_) => "internal",
|
|
};
|
|
|
|
// Метрики остаются как были
|
|
metrics::counter!("app_errors_total", "type" => error_type).increment(1);
|
|
|
|
let (status, error_message) = match &self {
|
|
AppError::UserNotFound => {
|
|
warn!(error_type, "Attempted action for non-existent user");
|
|
(StatusCode::NOT_FOUND, "User profile not found".to_string())
|
|
}
|
|
AppError::TrialAlreadyUsed => {
|
|
warn!(error_type, "Attempted to activate exhausted trial");
|
|
(StatusCode::CONFLICT, "Trial already used".to_string())
|
|
}
|
|
AppError::Database(e) => {
|
|
error!(error_type, db_error = ?e, "Database execution failure");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Internal server database error".to_string(),
|
|
)
|
|
}
|
|
AppError::Transaction(msg) => {
|
|
error!(error_type, details = %msg, "Transaction commit failed");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Transaction processing failed".to_string(),
|
|
)
|
|
}
|
|
AppError::Redis(e) => {
|
|
error!(error_type, redis_error = ?e, "Redis connection/command failure");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Internal server error".to_string(),
|
|
)
|
|
}
|
|
AppError::Unauthorized(msg) => {
|
|
warn!(error_type, details = %msg, "Unauthorized access blocked");
|
|
(StatusCode::UNAUTHORIZED, msg.clone())
|
|
}
|
|
AppError::NotFound(msg) => {
|
|
warn!(error_type, details = %msg, "Resource not found");
|
|
(StatusCode::NOT_FOUND, msg.clone())
|
|
}
|
|
AppError::BusinessLogic(msg) => {
|
|
warn!(error_type, details = %msg, "Business logic violation");
|
|
(StatusCode::BAD_REQUEST, msg.clone())
|
|
}
|
|
// FIX: Bind the inner string to `msg` and log it!
|
|
AppError::Internal(msg) => {
|
|
error!(error_type, details = %msg, "Critical internal failure");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Critical failure".to_string(), // We keep the user-facing message safe/generic
|
|
)
|
|
}
|
|
};
|
|
|
|
let body = Json(json!({
|
|
"success": false,
|
|
"error": error_message,
|
|
"code": status.as_u16()
|
|
}));
|
|
|
|
(status, body).into_response()
|
|
}
|
|
}
|