Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Веб-фреймворки

masterror отображает ошибки на HTTP на транспортной границе. Доменный код возвращает AppResult<T>; адаптер фреймворка преобразует ошибку в ответ RFC 7807 application/problem+json, сбрасывает телеметрию и применяет редактирование. В крейте существует ровно одна реализация IntoResponse / ResponseError для AppError — вам никогда не придётся писать отображение вручную.

Флаги функций

ФлагЧто включает
axumIntoResponse для AppError, ProblemJson, ErrorResponse; подтягивает serde_json
actixResponseError для AppError; Responder для ProblemJson, ErrorResponse
multipartFrom<axum::extract::multipart::MultipartError> для Error (подразумевает axum)
openapiСхема utoipa для ErrorResponse
[dependencies]
masterror = { version = "0.28", features = ["axum"] }   # or ["actix"]

Формат на проводе

Оба адаптера сериализуют ProblemJson:

ПолеТипПримечания
typeстрока-URIКанонический класс проблемы, например https://errors.masterror.rs/not-found
titleстрокаКраткое описание, выводимое из AppErrorKind
statusчислоКод статуса HTTP
detailстрока?Публичное сообщение; опускается, когда ошибка редактируемая
detailsобъект?Структурированные детали (функция serde_json)
codeстрокаСтабильный машиночитаемый AppCode, например NOT_FOUND
grpcобъект?Отображение gRPC { name, value } для мультипротокольных клиентов
metadataобъект?Очищенные поля из Metadata; опускается при редактировании

Транспортные подсказки становятся заголовками, а не полями тела:

  • AppError::with_retry_after_secs(n)Retry-After: n
  • AppError::with_www_authenticate(challenge)WWW-Authenticate: challenge

Внутренние источники (цепочка std::error::Error) только логируются и никогда не сериализуются клиентам.

Axum

Функция axum реализует IntoResponse для AppError, ProblemJson и ErrorResponse, а также собственный метод AppError::http_status(), возвращающий axum::http::StatusCode, выводимый из вида ошибки. Преобразование в ответ сбрасывает телеметрию (событие tracing, счётчик метрик, ленивый бэктрейс) — см. Наблюдаемость.

use axum::{Router, routing::get};
use masterror::{AppError, AppResult};

async fn handler() -> AppResult<&'static str> {
    Err(AppError::forbidden("no access"))
}

let app: Router = Router::new().route("/demo", get(handler));

401 с подсказками:

use masterror::AppError;

let err = AppError::unauthorized("missing token")
    .with_retry_after_secs(7)
    .with_www_authenticate("Bearer realm=\"api\"");

выдаёт статус 401, заголовки Retry-After: 7 и WWW-Authenticate: Bearer realm="api", а также тело:

{
  "type": "https://errors.masterror.rs/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "missing token",
  "code": "UNAUTHORIZED",
  "grpc": { "name": "UNAUTHENTICATED", "value": 16 }
}

Доменные ошибки в обработчиках

Паттерн из examples/axum-rest-api: выведите доменный enum через derive, один раз преобразуйте его в AppError, а затем переиспользуйте IntoResponse из крейта.

use axum::response::{IntoResponse, Response};
use masterror::{AppError, Error};

#[derive(Debug, Error, Clone)]
pub enum UserError {
    #[error("user not found")]
    NotFound,
    #[error("email already exists")]
    DuplicateEmail,
    #[error("invalid email format")]
    InvalidEmail
}

impl From<UserError> for AppError {
    fn from(err: UserError) -> Self {
        match err {
            UserError::NotFound => AppError::not_found(err.to_string()),
            UserError::DuplicateEmail => AppError::conflict(err.to_string()),
            UserError::InvalidEmail => AppError::validation(err.to_string())
        }
    }
}

impl IntoResponse for UserError {
    fn into_response(self) -> Response {
        AppError::from(self).into_response()
    }
}

С #[app_error(kind = ..., code = ...)] на derive реализация From<UserError> for AppError генерируется за вас — см. Derive-макросы.

Actix Web

Функция actix реализует actix_web::ResponseError для AppError, поэтому обработчики, возвращающие AppResult<T>, работают из коробки. error_response() испускает телеметрию и строит ту же полезную нагрузку problem+json через ProblemJson::from_ref.

use actix_web::{App, HttpServer, get};
use masterror::{AppError, AppResult};

#[get("/forbidden")]
async fn forbidden() -> AppResult<&'static str> {
    Err(AppError::forbidden("no access"))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(forbidden))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

Клиент получает 403 с телом:

{
  "type": "https://errors.masterror.rs/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "no access",
  "code": "FORBIDDEN",
  "grpc": { "name": "PERMISSION_DENIED", "value": 7 }
}

ProblemJson и ErrorResponse также реализуют Responder, поэтому обработчик может возвращать их напрямую. Отображение статусов использует ту же стабильную таблицу AppErrorKind → StatusCode, что и Axum.

Multipart

multipart (подразумевает axum) преобразует axum::extract::multipart::MultipartError в Error с AppErrorKind::BadRequest, сохраняя сообщение парсера:

use axum::extract::multipart::Multipart;
use masterror::{AppErrorKind, Error};

async fn upload(mut multipart: Multipart) -> Result<(), Error> {
    while let Some(field) = multipart.next_field().await? {
        let _ = field.bytes().await?;
    }
    Ok(())
}

Некорректные клиентские полезные нагрузки проявляются как 400 Bad Request, а не как 500.

Ручное построение ответов

Для тестов или собственных транспортов полезную нагрузку можно построить без фреймворка:

use masterror::{AppError, ProblemJson};

let problem = ProblemJson::from_app_error(AppError::not_found("resource not found"));
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");

ProblemJson::from_ref(&err) заимствует вместо поглощения, а ProblemJson::from_error_response(resp) повышает устаревший тип на проводе ErrorResponse.

См. также: Виды и коды ошибок · Интеграции · Наблюдаемость · Флаги возможностей