ActixMiddleTechnical
Как стандартизировать error responses и map internal errors to HTTP?
Реализуем единый AppError enum с impl ResponseError — он конвертирует внутренние ошибки в HTTP-статусы и JSON-тело. Все хендлеры возвращают Result<_, AppError>.
Стандартизация error responses в Actix Web
Actix Web имеет трейт ResponseError, который позволяет превратить любой тип ошибки в HTTP-ответ. Цель — один enum для всех доменных ошибок с детерминированным маппингом в HTTP-статусы.
Определение AppError
use actix_web::{HttpResponse, ResponseError};
use serde::Serialize;
use std::fmt;
#[derive(Debug)]
pub enum AppError {
NotFound(String),
Unauthorized(String),
Forbidden(String),
Validation(String),
Database(sqlx::Error),
Internal(String),
}
#[derive(Serialize)]
struct ErrorResponse {
error: &'static str,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<serde_json::Value>,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
AppError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
AppError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
AppError::Validation(msg) => write!(f, "Validation: {}", msg),
AppError::Database(e) => write!(f, "Database error: {}", e),
AppError::Internal(msg) => write!(f, "Internal: {}", msg),
}
}
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
let (status, error_code, message) = match self {
AppError::NotFound(msg) => (
actix_web::http::StatusCode::NOT_FOUND,
"NOT_FOUND",
msg.clone(),
),
AppError::Unauthorized(msg) => (
actix_web::http::StatusCode::UNAUTHORIZED,
"UNAUTHORIZED",
msg.clone(),
),
AppError::Forbidden(msg) => (
actix_web::http::StatusCode::FORBIDDEN,
"FORBIDDEN",
msg.clone(),
),
AppError::Validation(msg) => (
actix_web::http::StatusCode::UNPROCESSABLE_ENTITY,
"VALIDATION_ERROR",
msg.clone(),
),
AppError::Database(e) => {
tracing::error!("Database error: {:?}", e);
(
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
"DATABASE_ERROR",
"A database error occurred".to_string(),
)
}
AppError::Internal(msg) => {
tracing::error!("Internal error: {}", msg);
(
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An internal error occurred".to_string(),
)
}
};
HttpResponse::build(status).json(ErrorResponse {
error: error_code,
message,
details: None,
})
}
}
Конвертация из sqlx и других ошибок
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => AppError::NotFound("Record not found".to_string()),
_ => AppError::Database(e),
}
}
}
Использование в хендлерах
async fn get_user(
pool: web::Data<PgPool>,
path: web::Path<Uuid>,
) -> Result<HttpResponse, AppError> {
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", *path)
.fetch_optional(pool.get_ref())
.await?
.ok_or_else(|| AppError::NotFound(format!("User {} not found", *path)))?;
Ok(HttpResponse::Ok().json(user))
}
Оператор ? автоматически конвертирует sqlx::Error в AppError через From-реализацию.
Подводные камни
- Не возвращайте детали внутренних ошибок клиенту — логируйте server-side, отправляйте generic message.
actix_web::Error(встроенный) и вашAppError— разные типы; при оборачивании сторонних middleware ошибки могут обходить ваш ResponseError.- Валидация через
web::Json<T>возвращаетJsonPayloadErrorдо вызова хендлера — нужен кастомный.app_data(web::JsonConfig::default().error_handler(...)). - При использовании
?в цепочке важно, чтобы все типы ошибок реализовывалиFrom<X> for AppError, иначе компилятор откажет. - Одинаковые HTTP-статусы для разных ошибок (оба 500) затрудняют мониторинг — используйте error_code поле в JSON для различения.
- Паника в хендлере не проходит через ResponseError — добавьте
.wrap(middleware::Logger::default())для логирования 500 от паник.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.