Files
netrunner-backend/src/error.rs
T
Kirill d4a57c0b6a docs
2026-07-02 23:13:13 +07:00

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()
}
}