Веб-фреймворки
masterror отображает ошибки на HTTP на транспортной границе. Доменный код
возвращает AppResult<T>; адаптер фреймворка
преобразует ошибку в ответ RFC 7807 application/problem+json, сбрасывает
телеметрию и применяет редактирование. В крейте существует ровно одна
реализация IntoResponse / ResponseError для AppError — вам никогда не
придётся писать отображение вручную.
Флаги функций
| Флаг | Что включает |
|---|---|
axum | IntoResponse для AppError, ProblemJson, ErrorResponse; подтягивает serde_json |
actix | ResponseError для AppError; Responder для ProblemJson, ErrorResponse |
multipart | From<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: nAppError::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.
См. также: Виды и коды ошибок · Интеграции · Наблюдаемость · Флаги возможностей