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 обработчика — они обрабатываются на уровне фреймворка; для кастомизации используйте ErrorHandlers middleware.
  • При использовании ? оператора тип ошибки должен реализовывать 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.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics