ActixMiddleTechnical
Как работает обработка ошибок в Actix-web? Что такое ResponseError?
В Actix-web любой тип, реализующий трейт ResponseError, автоматически конвертируется в HTTP-ответ. Достаточно реализовать error_response() и опционально status_code().
Модель обработки ошибок в Actix-web
Обработчики Actix-web возвращают Result<impl Responder, E>, где E реализует трейт actix_web::ResponseError. При возврате Err(e) фреймворк вызывает e.error_response() и отдаёт клиенту полученный HttpResponse. Встроенные типы ошибок Actix (например, actix_web::error::ErrorBadRequest) уже реализуют этот трейт.
Трейт ResponseError
// Определение в actix-web
pub trait ResponseError: fmt::Debug + fmt::Display {
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR // дефолт
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
}
}
Собственный тип ошибки
use actix_web::{ResponseError, HttpResponse};
use actix_web::http::StatusCode;
use derive_more::{Display, Error};
use serde::Serialize;
#[derive(Debug, Display, Error)]
pub enum AppError {
#[display(fmt = "resource not found: {}", id)]
NotFound { id: String },
#[display(fmt = "validation failed: {}", message)]
Validation { message: String },
#[display(fmt = "internal error")]
Internal,
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
code: u16,
}
impl ResponseError for AppError {
fn status_code(&self) -> StatusCode {
match self {
AppError::NotFound { .. } => StatusCode::NOT_FOUND,
AppError::Validation { .. } => StatusCode::UNPROCESSABLE_ENTITY,
AppError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
let status = self.status_code();
HttpResponse::build(status).json(ErrorResponse {
error: self.to_string(),
code: status.as_u16(),
})
}
}
// Использование в обработчике
async fn get_user(path: web::Path<String>) -> Result<HttpResponse, AppError> {
let id = path.into_inner();
if id == "unknown" {
return Err(AppError::NotFound { id });
}
Ok(HttpResponse::Ok().json(serde_json::json!({"id": id})))
}
Конвертация сторонних ошибок
Для ошибок из внешних библиотек (sqlx, redis и др.) реализуйте From:
impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
match e {
sqlx::Error::RowNotFound => AppError::NotFound {
id: "database record".to_string(),
},
_ => {
tracing::error!("DB error: {:?}", e);
AppError::Internal
}
}
}
}
// Теперь можно использовать ? в обработчиках:
async fn get_user_db(
path: web::Path<i64>,
pool: web::Data<sqlx::PgPool>,
) -> Result<HttpResponse, AppError> {
let user = sqlx::query_as::<_, User>(
"SELECT id, name FROM users WHERE id = $1"
)
.bind(path.into_inner())
.fetch_one(pool.get_ref())
.await?; // sqlx::Error -> AppError через From
Ok(HttpResponse::Ok().json(user))
}
Вспомогательные функции actix_web::error
Для быстрого создания ошибок без собственного типа:
use actix_web::error;
async fn quick_handler() -> actix_web::Result<HttpResponse> {
// actix_web::Result = Result<T, actix_web::Error>
let val: i32 = "abc".parse().map_err(|e| error::ErrorBadRequest(e))?;
Ok(HttpResponse::Ok().body(val.to_string()))
}
Подводные камни
- Дефолтная реализация
error_response()возвращает HTML с текстом ошибки — в REST API клиенты ожидают JSON; всегда переопределяйте метод. - Внутренние детали (
InternalError, SQL-запросы) не должны попадать вDisplayреализацию — она идёт клиенту; логируйте детали черезtracing::error!до возврата ошибки. - Тип
actix_web::Error(не путать сAppError) — это динамический объект-трейт; конвертация черезinto()стирает тип и усложняет тестирование. - Если
AppErrorне реализуетstd::error::Error, трейтResponseErrorне скомпилируется — убедитесь, что есть#[derive(Error)]или ручная реализация. - Middleware-ошибки не проходят через
ResponseErrorобработчика — они обрабатываются на уровне фреймворка; для кастомизации используйтеErrorHandlersmiddleware. - При использовании
?оператора тип ошибки должен реализовыватьInto<actix_web::Error>или совпадать с явно указанным типом возврата. - Паника в обработчике не перехватывается как
ResponseError— клиент получит 500 без вашего JSON-формата; используйтеResultвезде. - Не храните чувствительные данные в сообщении ошибки
Display— оно попадает в HTTP-ответ и может оказаться в логах клиента.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.