masterror
Фреймворк-независимые типы ошибок для приложений со стабильными кодами, отображениями HTTP/gRPC и встроенной телеметрией
Что такое masterror?
masterror — это рабочее пространство для обработки ошибок в сервисах на Rust, которым нужно больше, чем Display и source(). Там, где thiserror ограничивается генерацией реализаций трейтов, а anyhow — распространением ошибок со стёртым типом, masterror доводит ошибку до самой транспортной границы:
AppError— насыщенное значение ошибки с семантической категорией (AppErrorKind), стабильным машиночитаемым кодом (AppCode), опциональным безопасным публичным сообщением, структурированными метаданными и транспортными подсказками (Retry-After,WWW-Authenticate).- Консервативные отображения HTTP и gRPC — каждый вид и код детерминированно отображается в HTTP-статус, дискриминант
tonic::Codeи URItypeпо RFC 7807. - Типизированная телеметрия — метаданные хранятся как типизированные поля (строки, целые числа, числа с плавающей точкой, длительности, IP-адреса, UUID, JSON) с политиками редактирования для каждого поля, а не как самодельные
String-карты. - Нативные derive-макросы —
#[derive(Error)]повторяет синтаксисthiserror, а#[app_error(...)]и#[derive(Masterror)]встраивают доменные ошибки вAppErrorс кодами, категориями, редактированием и таблицами отображений. - Редактирование по замыслу — источники никогда не сериализуются для клиентов; сообщения, детали и поля метаданных могут быть удалены, захешированы или замаскированы на границе.
Без unsafe, с зафиксированным MSRV и поддержкой no_std при отключённой функции std, включённой по умолчанию.
Какую проблему он решает
| Задача | thiserror | anyhow | masterror |
|---|---|---|---|
Derive для Display / source() | Да | — | Да (тот же синтаксис) |
| Распространение со стёртым типом и контекстом | — | Да | Да (.ctx() / .context()) |
| Стабильные машиночитаемые коды ошибок | Вручную | Вручную | AppCode, часть wire-контракта |
| Отображение в HTTP-статус | Вручную | Вручную | AppErrorKind::http_status(), стабильная таблица |
| Отображение в gRPC-статус | Вручную | Вручную | CODE_MAPPINGS, конверсия в tonic::Status |
RFC 7807 problem+json | Вручную | Вручную | ProblemJson::from_app_error |
| Структурированные типизированные метаданные | — | — | Metadata + конструкторы field::* |
| Редактирование секретов на границе | — | — | MessageEditPolicy, FieldRedaction |
| Эмиссия tracing / metrics / backtrace | — | — | За флагами возможностей, автоматически при создании |
Enum, произведённый thiserror, говорит, что случилось. masterror дополнительно решает, что увидит клиент (статус, код, безопасное сообщение, problem+json), что увидят операторы (структурированные поля, события tracing, счётчики) и что никогда не утечёт (источники, отредактированные поля).
Основные возможности
| Область | Что вы получаете |
|---|---|
| Базовая таксономия | AppError, AppErrorKind (23 стабильные категории), AppCode (коды в SCREAMING_SNAKE_CASE, поддержка собственных кодов), AppResult<T> |
| Derive-макросы | #[derive(Error)], #[derive(Masterror)], #[app_error(...)], #[masterror(...)], провайдеры телеметрии #[provide(...)] |
| Управление потоком | ensure! / fail! — типизированные ранние возвраты без выделения памяти |
| Контекст | ResultExt::ctx / ResultExt::context, билдер Context с отслеживанием вызывающей стороны |
| Wire-полезные нагрузки | ErrorResponse (устаревший JSON), ProblemJson (RFC 7807) с подсказками повторных попыток и аутентификации |
| Транспорты | Axum IntoResponse, Actix ResponseError/Responder, tonic::Status, WASM JsValue, схема OpenAPI |
| Интеграции | sqlx, redis, reqwest, validator, config, tokio, teloxide, init data Telegram Mini Apps, Turnkey |
| Наблюдаемость | События tracing, счётчики metrics, ленивый захват backtrace, цветной вывод в терминале, DisplayMode (prod/staging/local) |
Быстрый пример
use masterror::{AppError, AppErrorKind, AppResult, ProblemJson, field};
fn find_user(id: u64) -> AppResult<()> {
masterror::ensure!(id != 0, AppError::bad_request("id must be non-zero"));
Err(AppError::not_found("user not found")
.with_field(field::u64("user_id", id))
.with_field(field::str("request_id", "abc123")))
}
let err = find_user(42).unwrap_err();
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.kind.http_status(), 404);
let problem = ProblemJson::from_app_error(err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
assert_eq!(problem.grpc.expect("grpc").name, "NOT_FOUND");
Или объявите доменную ошибку один раз и позвольте derive-макросу выполнить отображение:
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("missing flag: {name}")]
#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)]
struct MissingFlag {
name: &'static str
}
let app: AppError = MissingFlag { name: "feature" }.into();
assert!(matches!(app.kind, AppErrorKind::BadRequest));
Крейты рабочего пространства
| Крейт | Роль |
|---|---|
masterror | Основные типы ошибок, метаданные, транспорты, интеграции, прелюдия |
masterror-derive | Процедурные макросы за #[derive(Error)] и #[derive(Masterror)] (подключается автоматически) |
masterror-template | Общий парсер шаблонов #[error("...")] |
Документация
Начало работы
- Начало работы — установка, первые ошибки, макросы, первый derive
- Флаги возможностей — полный справочник флагов с зависимостями
Основные концепции
- Виды и коды ошибок — таксономия, таблицы HTTP/gRPC, problem+json
- Derive-макросы —
#[derive(Error)],#[derive(Masterror)]и их атрибуты - Контекст и метаданные —
Context,ResultExt, поля, редактирование, цепочки
Интеграции
- Веб-фреймворки — Axum, Actix, tonic
- Интеграции — sqlx, redis, reqwest, validator и другие
- Наблюдаемость — tracing, metrics, бэктрейсы, режимы отображения
Продвинутое
- Без std — работа без стандартной библиотеки
- Лучшие практики — паттерны для сервисов и библиотек
- Миграция — переход с
thiserror/anyhow
Начало работы
Эта страница проведёт вас через установку masterror, возврат первого AppError, ранние возвраты с ensure!/fail!, использование прелюдии и написание первого derive.
Установка
Сборка по умолчанию включает только функцию std — без веб-фреймворков и бэкендов телеметрии:
[dependencies]
masterror = "0.28"
Включайте интеграции по мере необходимости (полный список — в Флагах возможностей):
[dependencies]
masterror = { version = "0.28", features = ["axum", "serde_json", "tracing"] }
MSRV — 1.96. Крейт запрещает unsafe и поддерживает no_std при сборке с default-features = false.
Ваша первая ошибка
AppError объединяет семантическую категорию (AppErrorKind) с опциональным публичным сообщением. AppResult<T> — псевдоним для Result<T, AppError>:
use masterror::{AppError, AppErrorKind, AppResult};
fn do_work(flag: bool) -> AppResult<()> {
if !flag {
return Err(AppError::new(AppErrorKind::BadRequest, "Flag must be set"));
}
Ok(())
}
let err = do_work(false).unwrap_err();
assert!(matches!(err.kind, AppErrorKind::BadRequest));
assert_eq!(err.kind.http_status(), 400);
У каждого вида есть именованный конструктор, так что в местах вызова редко приходится писать AppErrorKind явно:
use masterror::AppError;
let _ = AppError::not_found("user not found"); // 404
let _ = AppError::validation("invalid email"); // 422
let _ = AppError::unauthorized("token expired"); // 401
let _ = AppError::forbidden("no access"); // 403
let _ = AppError::conflict("already exists"); // 409
let _ = AppError::rate_limited("slow down"); // 429
let _ = AppError::internal("unexpected failure"); // 500
let _ = AppError::service("orchestration failed"); // 500
let _ = AppError::timeout("upstream timed out"); // 504
let _ = AppError::bare(masterror::AppErrorKind::NotFound); // no message
Прикрепляйте структурированные метаданные и вышестоящий источник, не отказываясь от типизации:
use masterror::{AppError, field};
let err = AppError::service("downstream degraded")
.with_field(field::str("request_id", "abc123"))
.with_field(field::i64("attempt", 2))
.with_context(std::io::Error::other("connection reset"));
assert_eq!(err.metadata().len(), 2);
assert!(err.source_ref().is_some());
Источник доступен для логов и обхода через chain(), но никогда не сериализуется для клиентов.
ensure! и fail!
ensure! и fail! — типизированные альтернативы anyhow::ensure!/anyhow::bail!. Выражение ошибки вычисляется лениво, поэтому на успешном пути нет ни форматирования, ни выделения памяти:
use masterror::{AppError, AppErrorKind, AppResult};
fn guard(flag: bool) -> AppResult<()> {
masterror::ensure!(flag, AppError::bad_request("flag must be set"));
Ok(())
}
fn bail() -> AppResult<()> {
masterror::fail!(AppError::unauthorized("token expired"));
}
assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));
ensure! также принимает развёрнутую форму для сложных условий:
use masterror::{AppError, AppResult};
fn bounded(value: i32, max: i32) -> AppResult<()> {
masterror::ensure!(
cond = value <= max,
else = AppError::service("value too large")
);
Ok(())
}
Прелюдия
masterror::prelude реэкспортирует только основные типы (AppError, AppErrorKind, AppCode, AppResult, ErrorResponse, а также хелперы turnkey, когда включена соответствующая функция):
use masterror::prelude::*;
fn handler(flag: bool) -> AppResult<()> {
if !flag {
return Err(AppError::bad_request("Flag must be set"));
}
Ok(())
}
Реализации трейтов фреймворков (Axum IntoResponse, Actix Responder) активируются флагами возможностей и не требуют дополнительных импортов.
Добавление контекста к внешним ошибкам
ResultExt превращает любой Result<T, E: Error> в AppResult<T>:
use masterror::{AppErrorKind, Context, ResultExt, field};
fn read_config() -> Result<String, std::io::Error> {
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
// Simple, anyhow-style message:
let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());
// Full control over category, code, metadata and redaction:
let err = read_config()
.ctx(|| Context::new(AppErrorKind::Config).with(field::str("path", "app.toml")))
.unwrap_err();
assert_eq!(err.kind, AppErrorKind::Config);
Полный API Context описан в разделе Контекст и метаданные.
Ваш первый derive
#[derive(Error)] повторяет синтаксис thiserror, а #[app_error(...)] добавляет конверсию в AppError:
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("I/O failed: {source}")]
#[app_error(kind = AppErrorKind::Internal, code = AppCode::Internal, message)]
pub struct DomainError {
#[from]
#[source]
source: std::io::Error
}
fn load() -> Result<(), DomainError> {
Err(std::io::Error::other("disk offline").into())
}
let err = load().unwrap_err();
assert_eq!(err.to_string(), "I/O failed: disk offline");
let app: AppError = err.into();
assert!(matches!(app.kind, AppErrorKind::Internal));
#[error("...")]задаёт шаблонDisplayс плейсхолдерами{field}.#[from]генерируетFrom<std::io::Error>для обёртки.#[source]пробрасывает внутреннюю ошибку черезsource().#[app_error(kind = ..., code = ..., message)]генерируетFrom<DomainError> for AppError(иfor AppCode); флагmessageделает выводDisplayпубличным сообщением.
Enum работают так же — с атрибутами #[error] и #[app_error] для каждого варианта. Когда вам также нужны метаданные, политика редактирования и таблицы отображений gRPC/problem+json, используйте #[derive(Masterror)] — он описан в разделе Derive-макросы.
Куда двигаться дальше
- Отображение ошибок в HTTP-ответы в Axum или Actix — Веб-фреймворки
- Понимание таксономии и wire-контракта — Виды и коды ошибок
- Включение интеграций для sqlx, redis, reqwest — Флаги возможностей
См. также: Флаги возможностей · Виды и коды ошибок · Derive-макросы · Контекст и метаданные · Миграция
Флаги возможностей
masterror держит сборку по умолчанию компактной: из коробки включена только функция std. Всё остальное — веб-транспорты, телеметрия, интеграции с библиотеками — подключается опционально. Эта страница — полный справочник по каждому флагу, объявленному в Cargo.toml.
[dependencies]
masterror = { version = "0.28", default-features = false }
# or with features:
# masterror = { version = "0.28", features = [
# "std", "axum", "actix", "openapi",
# "serde_json", "tracing", "metrics", "backtrace",
# "colored", "sqlx", "sqlx-migrate", "reqwest",
# "redis", "validator", "config", "tokio",
# "multipart", "teloxide", "init-data", "tonic",
# "frontend", "turnkey", "benchmarks"
# ] }
Ядро
| Флаг | Что включает | Доп. зависимости |
|---|---|---|
std (по умолчанию) | Поддержка стандартной библиотеки; требуется всем runtime-интеграциям. Отключите для no_std (см. Без std) | — |
Веб-транспорты
| Флаг | Что включает | Доп. зависимости |
|---|---|---|
axum | IntoResponse для AppError и ProblemJson с JSON-телами по RFC 7807; AppErrorKind::status_code() | axum (json, multipart), serde_json |
actix | Actix Web ResponseError для AppError и Responder для ProblemJson | actix-web |
multipart | Отображает axum::extract::multipart::MultipartError → BadRequest (подразумевает axum) | через axum |
openapi | utoipa::ToSchema для ErrorResponse и AppCode, чтобы полезные нагрузки ошибок попадали в спецификации OpenAPI | utoipa |
serde_json | Структурированные JSON-details в AppError/ErrorResponse/ProblemJson; FieldValue::Json и field::json | serde_json |
tonic | Конверсия ошибок в tonic::Status с санитизированными метаданными; экспортирует StatusConversionError | tonic |
Телеметрия и наблюдаемость
| Флаг | Что включает | Доп. зависимости |
|---|---|---|
tracing | Структурированные события tracing, эмитируемые при создании ошибок | tracing, log, log-mdc |
metrics | Инкремент счётчика error_total{code,category} для каждого AppError | metrics |
backtrace | Ленивый захват std::backtrace::Backtrace (учитывает RUST_BACKTRACE), билдер with_backtrace() | — |
colored | Цветной многострочный вывод в терминале с автоматическим определением TTY; более насыщенный Display для AppError | owo-colors |
Асинхронные интеграции и ввод/вывод
Каждый флаг интеграции добавляет конверсию From<...> for AppError, классифицирующую ошибку библиотеки в рамках таксономии:
| Флаг | Конверсия | Доп. зависимости |
|---|---|---|
sqlx | sqlx_core::Error → NotFound / Database (компактный sqlx-core, без драйверов и TLS) | sqlx-core |
sqlx-migrate | sqlx::migrate::MigrateError → Database (полный sqlx только с функцией migrate) | sqlx |
redis | redis::RedisError → Cache | redis |
reqwest | reqwest::Error → Timeout / Network / ExternalApi | reqwest |
tokio | tokio::time::error::Elapsed → Timeout | tokio (time) |
validator | validator::ValidationErrors → Validation | validator |
config | config::ConfigError → Config | config |
sqlx и sqlx-migrate разделены намеренно: для классификации ошибок достаточно sqlx-core, тогда как отображение ошибок миграций тянет полный крейт sqlx.
Обмен сообщениями и боты
| Флаг | Конверсия | Доп. зависимости |
|---|---|---|
teloxide | teloxide_core::RequestError → RateLimited / Network / ExternalApi / Deserialization / Internal | teloxide-core |
init-data | init_data_rs::InitDataError → TelegramAuth (валидация init data Telegram Mini Apps) | init-data-rs |
Фронтенд и домен
| Флаг | Что включает | Доп. зависимости |
|---|---|---|
frontend | Модуль frontend: конверсия ошибок в wasm_bindgen::JsValue и логирование через console.error в контекстах WASM/браузера | wasm-bindgen, js-sys, serde-wasm-bindgen |
turnkey | Модуль turnkey: TurnkeyErrorKind, TurnkeyError, classify_turnkey_error и конверсии в AppError | — |
benchmarks | Набор бенчмарков Criterion и инструментарий базовых линий CI (только для локального профилирования) | — |
Базовые конверсии (доступны всегда)
Без каких-либо флагов возможностей AppError уже конвертируется из:
| Источник | Целевой вид |
|---|---|
std::io::Error | Internal |
String | BadRequest |
Рецепты
REST API на Axum с problem+json, документацией OpenAPI и tracing:
masterror = { version = "0.28", features = ["axum", "openapi", "serde_json", "tracing"] }
Сервис с базой данных на sqlx, миграциями и метриками:
masterror = { version = "0.28", features = ["sqlx", "sqlx-migrate", "metrics", "tracing"] }
gRPC-сервис:
masterror = { version = "0.28", features = ["tonic", "tracing", "backtrace"] }
Telegram-бот с аутентификацией Mini App:
masterror = { version = "0.28", features = ["teloxide", "init-data", "reqwest", "tokio"] }
WASM-фронтенд:
masterror = { version = "0.28", features = ["frontend", "serde_json"] }
no_std для встраиваемых систем или библиотек:
masterror = { version = "0.28", default-features = false }
Примечания
- Все флаги интеграций подразумевают
std, кромеsqlxиsqlx-migrate, которые на уровне флага остаются независимыми отstd. axumиactixподтягиваютserde_jsonтранзитивно, потому что тела их ответов — JSON.- Флаги возможностей никогда не меняют wire-контракт уже включённых полей
ErrorResponse/ProblemJson— они лишь добавляют возможности (например,serde_jsonпревращаетdetailsиз простого текста в структурированный JSON) или реализации трейтов.
См. также: Начало работы · Веб-фреймворки · Интеграции · Наблюдаемость · Без std
Виды и коды ошибок
Два типа образуют основу таксономии:
AppErrorKind— внутренняя семантическая категория сбоя. Небольшая, стабильная, независимая от фреймворков. Определяет HTTP-статус по умолчанию.AppCode— публичный машиночитаемый код, отдаваемый клиентам строкой в SCREAMING_SNAKE_CASE (например,"NOT_FOUND"). Часть wire-контракта.
Каждый AppError несёт оба. AppCode::from(kind) даёт каноническое отображение 1:1, а AppError::with_code(...) переопределяет публичный код, не меняя категорию.
Таксономия AppErrorKind
| Вариант | Значение | HTTP |
|---|---|---|
NotFound | Ресурс не существует или не виден вызывающей стороне | 404 |
Validation | Структурированный ввод не прошёл валидацию | 422 |
Conflict | Конфликт состояния (нарушение уникального ключа, несовпадение версий) | 409 |
Unauthorized | Требуется аутентификация или она не удалась | 401 |
Forbidden | Аутентифицирован, но доступ запрещён | 403 |
NotImplemented | Операция не поддерживается этим развёртыванием | 501 |
BadRequest | Некорректный запрос или отсутствующие параметры | 400 |
TelegramAuth | Сбой процедуры аутентификации Telegram | 401 |
InvalidJwt | JWT просрочен, повреждён или имеет неверную подпись/клеймы | 401 |
RateLimited | Клиент превысил лимиты запросов или квоту | 429 |
Timeout | Операция не завершилась вовремя | 504 |
Network | Ошибка сетевого уровня (DNS, соединение, TLS) | 503 |
DependencyUnavailable | Внешняя зависимость недоступна или деградировала | 503 |
Internal | Неожиданный сбой на стороне сервера | 500 |
Database | Сбой базы данных (запрос, соединение, миграция) | 500 |
Service | Общий сбой сервисного слоя / бизнес-логики | 500 |
Config | Отсутствующая или некорректная конфигурация | 500 |
Turnkey | Сбой подсистемы Turnkey | 500 |
Serialization | Не удалось закодировать данные | 500 |
Deserialization | Не удалось декодировать данные | 500 |
ExternalApi | Вышестоящий API вернул ошибку | 500 |
Queue | Сбой публикации/потребления/подтверждения в очереди | 500 |
Cache | Сбой чтения/записи/кодирования кэша | 500 |
use masterror::AppErrorKind;
let kind = AppErrorKind::NotFound;
assert_eq!(kind.http_status(), 404); // always available, u16
assert_eq!(kind.label(), "Not found"); // human-readable title
// With the `axum` feature: kind.status_code() -> axum::http::StatusCode
Правила, заложенные в отображение: проблемы инфраструктуры и ввода/вывода по умолчанию дают 5xx; Unauthorized (401) означает, что аутентификация не удалась, Forbidden (403) — что аутентификация прошла, но в доступе отказано; используйте Network для сбоев соединения/построения запроса и ExternalApi для ошибочных HTTP-статусов от вышестоящих сервисов.
AppCode
AppCode поставляет константы для каждого вида (AppCode::NotFound → "NOT_FOUND", AppCode::RateLimited → "RATE_LIMITED", …) плюс AppCode::UserAlreadyExists ("USER_ALREADY_EXISTS", отображается как конфликт). Он помечен #[non_exhaustive] и поддерживает пользовательские коды:
use std::str::FromStr;
use masterror::AppCode;
// Compile-time literal — panics at compile-time evaluation if not SCREAMING_SNAKE_CASE
const INVALID_JSON: AppCode = AppCode::new("INVALID_JSON");
// Runtime value — validated, returns Result<AppCode, ParseAppCodeError>
let dynamic = AppCode::try_new(String::from("THIRD_PARTY_FAILURE")).expect("valid code");
assert_eq!(dynamic.as_str(), "THIRD_PARTY_FAILURE");
// Parsing round-trips through the same validation
let parsed = AppCode::from_str("NOT_FOUND").expect("known code");
assert_eq!(parsed, AppCode::NotFound);
Допустимые коды содержат только A-Z, 0-9 и одиночные разделители _ и сериализуются как обычные JSON-строки.
Таблица отображений HTTP / gRPC / problem+json
CODE_MAPPINGS (и функция поиска mapping_for_code) задают каноническое транспортное отображение для каждого встроенного кода. Неизвестные пользовательские коды сводятся к INTERNAL (500 / gRPC 13):
| AppCode | HTTP | gRPC | problem type |
|---|---|---|---|
NOT_FOUND | 404 | NOT_FOUND (5) | https://errors.masterror.rs/not-found |
VALIDATION | 422 | INVALID_ARGUMENT (3) | .../validation |
CONFLICT | 409 | ALREADY_EXISTS (6) | .../conflict |
USER_ALREADY_EXISTS | 409 | ALREADY_EXISTS (6) | .../user-already-exists |
UNAUTHORIZED | 401 | UNAUTHENTICATED (16) | .../unauthorized |
FORBIDDEN | 403 | PERMISSION_DENIED (7) | .../forbidden |
NOT_IMPLEMENTED | 501 | UNIMPLEMENTED (12) | .../not-implemented |
BAD_REQUEST | 400 | INVALID_ARGUMENT (3) | .../bad-request |
RATE_LIMITED | 429 | RESOURCE_EXHAUSTED (8) | .../rate-limited |
TELEGRAM_AUTH | 401 | UNAUTHENTICATED (16) | .../telegram-auth |
INVALID_JWT | 401 | UNAUTHENTICATED (16) | .../invalid-jwt |
INTERNAL | 500 | INTERNAL (13) | .../internal |
DATABASE | 500 | INTERNAL (13) | .../database |
SERVICE | 500 | INTERNAL (13) | .../service |
CONFIG | 500 | INTERNAL (13) | .../config |
TURNKEY | 500 | INTERNAL (13) | .../turnkey |
TIMEOUT | 504 | DEADLINE_EXCEEDED (4) | .../timeout |
NETWORK | 503 | UNAVAILABLE (14) | .../network |
DEPENDENCY_UNAVAILABLE | 503 | UNAVAILABLE (14) | .../dependency-unavailable |
SERIALIZATION | 500 | INTERNAL (13) | .../serialization |
DESERIALIZATION | 500 | INTERNAL (13) | .../deserialization |
EXTERNAL_API | 500 | UNAVAILABLE (14) | .../external-api |
QUEUE | 500 | UNAVAILABLE (14) | .../queue |
CACHE | 500 | UNAVAILABLE (14) | .../cache |
Значения gRPC совпадают с дискриминантами tonic::Code, поэтому функция tonic конвертирует их напрямую.
use masterror::{AppCode, mapping_for_code};
let mapping = mapping_for_code(&AppCode::Timeout);
assert_eq!(mapping.http_status(), 504);
assert_eq!(mapping.grpc().name, "DEADLINE_EXCEEDED");
assert_eq!(mapping.grpc().value, 4);
assert_eq!(mapping.problem_type(), "https://errors.masterror.rs/timeout");
Подсказки повторных попыток и аутентификации
Транспортные адаптеры транслируют две опциональные подсказки в HTTP-заголовки:
use std::time::Duration;
use masterror::{AppError, AppErrorKind, ProblemJson};
let problem = ProblemJson::from_app_error(
AppError::new(AppErrorKind::Unauthorized, "Token expired")
.with_retry_after_secs(30)
.with_www_authenticate(r#"Bearer realm="api", error="invalid_token""#)
);
assert_eq!(problem.status, 401);
assert_eq!(problem.retry_after, Some(30)); // -> Retry-After header
assert!(problem.www_authenticate.is_some()); // -> WWW-Authenticate header
assert_eq!(problem.grpc.expect("grpc").name, "UNAUTHENTICATED");
У ErrorResponse эквивалентные билдеры — with_retry_after_secs, with_retry_after_duration и with_www_authenticate.
Семантика редактирования
Сообщения AppError задуманы безопасными для клиентов, но ошибку можно пометить как редактируемую, чтобы граница удалила сообщение:
use masterror::{AppError, MessageEditPolicy, ProblemJson};
let err = AppError::internal("host db-3 credentials rejected").redactable();
assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
let problem = ProblemJson::from_app_error(err);
assert!(problem.detail.is_none()); // message stripped
assert!(problem.metadata.is_none()); // metadata stripped too
Когда edit_policy равна Redact, ProblemJson отбрасывает detail, details и всю секцию metadata. Отдельные поля метаданных дополнительно несут собственную политику FieldRedaction (None, Redact, Hash, Last4), применяемую при сериализации — см. Контекст и метаданные. Источники ошибок (source_ref()) не сериализуются никогда, независимо от политики.
Wire-полезные нагрузки
ProblemJson — application/problem+json по RFC 7807, создаётся через ProblemJson::from_app_error (владеющий вариант) или ProblemJson::from_ref (заимствующий). Поля: type, title (метка вида), status, detail, опциональный details, code, grpc ({name, value}), metadata, плюс несериализуемые retry_after/www_authenticate для заголовков.
ErrorResponse — устаревшая плоская JSON-нагрузка: status, code, message, опциональные details, retry, www_authenticate. С функцией openapi реализует utoipa::ToSchema.
use masterror::{AppCode, AppError, AppErrorKind, ErrorResponse};
let app_err = AppError::new(AppErrorKind::NotFound, "user_not_found");
let resp: ErrorResponse = (&app_err).into();
assert_eq!(resp.status, 404);
assert_eq!(resp.code, AppCode::NotFound);
Для новых API предпочитайте ProblemJson; ErrorResponse остаётся для сервисов, уже привязанных к плоскому формату.
См. также: Начало работы · Derive-макросы · Контекст и метаданные · Веб-фреймворки
Derive-макросы
masterror поставляет два derive-макроса через прилагаемый крейт masterror-derive:
#[derive(Error)]— прямая заменаthiserror::Error(те же атрибуты#[error],#[from],#[source],#[backtrace]), расширенная конверсиями#[app_error(...)]и телеметрией#[provide(...)].#[derive(Masterror)]— строится на том же синтаксисе и встраивает доменную ошибку напрямую вmasterror::Errorс метаданными, политикой редактирования и таблицами транспортных отображений через#[masterror(...)].
Оба реэкспортируются из корня: use masterror::{Error, Masterror};.
Шаблоны #[error("...")]
Шаблон определяет генерируемую реализацию Display. Плейсхолдеры ссылаются на поля по имени ({field}), индексу кортежа ({0}) или через явные аргументы. Разбор выполняет общий крейт masterror-template, семантика повторяет thiserror.
use masterror::Error;
#[derive(Debug, Error)]
#[error("{kind}: {message}")]
struct NamedError {
kind: &'static str,
message: &'static str
}
#[derive(Debug, Error)]
#[error("{0} -> {1:?}")]
struct TupleError(&'static str, u8);
Трейты форматирования и спецификаторы
Плейсхолдеры поддерживают полный набор форматтеров — {x:?}, {x:#?}, {x:x}, {x:#X}, {x:b}, {x:o}, {x:e}, {x:E}, {x:p} — а display-спецификаторы вроде {value:>8} или {ratio:.3} пробрасываются как есть. Для программного анализа шаблонов masterror::error::template предоставляет ErrorTemplate, TemplateFormatter и TemplateFormatterKind.
Аргументы форматирования и проекции
Шаблоны принимают именованные и позиционные аргументы, включая выражения над self и проекции полей через сокращение .field:
use masterror::Error;
#[derive(Debug, Error)]
#[error("{formatted}", formatted = self.message.to_uppercase())]
struct FormatArgExpressionError {
message: &'static str
}
#[derive(Debug, Error)]
#[error("{}, {label}, {}", label = self.label, self.first, self.second)]
struct MixedImplicitArgsError {
label: &'static str,
first: &'static str,
second: &'static str
}
#[derive(Debug, Error)]
#[error("{value}", value = .value)]
struct FieldShortcutError {
value: &'static str
}
transparent и fmt = ...
use masterror::Error;
#[derive(Debug, Error)]
#[error("inner failure")]
struct Inner;
// Forwards Display and source() to the single wrapped field
#[derive(Debug, Error)]
#[error(transparent)]
struct Wrapper(#[from] Inner);
// Delegate rendering to a function: fields first, formatter last
fn render(count: &usize, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "count={count}")
}
#[derive(Debug, Error)]
#[error(fmt = crate::render)]
struct CustomFormat {
count: usize
}
transparent требует ровно одно поле и не сочетается с fmt или строкой шаблона. fmt = path указывает на функцию, принимающую ссылки на все поля и Formatter последним аргументом.
Атрибуты полей
| Атрибут | Эффект |
|---|---|
#[source] | Поле возвращается из source(). Поддерживается Option<E>. |
#[from] | Генерирует From<FieldType> для обёртки; подразумевает #[source] на том же поле. |
#[backtrace] | Поле хранит std::backtrace::Backtrace (или Option<Backtrace>), доступный через интроспекцию ошибки, либо делегирует к бэктрейсу источника в сочетании с #[source]. |
Автовывод: поле с именем source автоматически считается источником, а поле типа std::backtrace::Backtrace (или Option<Backtrace>) распознаётся как бэктрейс без атрибута.
Enum принимают #[error] и #[from]/#[source]/#[backtrace] для каждого варианта:
use masterror::Error;
#[derive(Debug, Error)]
#[error("leaf failure")]
struct LeafError;
#[derive(Debug, Error)]
enum EnumError {
#[error("unit failure")]
Unit,
#[error("{code}")]
Code {
code: u16,
#[source]
cause: LeafError
},
#[error(transparent)]
Wrapped(#[from] LeafError)
}
#[app_error(...)] — конверсии в AppError
Описывает, как производная ошибка транслируется в AppError/AppCode. Опции: kind (обязательная), code (опциональная), message (флаг), no_source (флаг).
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("missing flag: {name}")]
#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)]
struct MissingFlag {
name: &'static str
}
let app: AppError = MissingFlag { name: "feature" }.into();
assert!(matches!(app.kind, AppErrorKind::BadRequest));
let code: AppCode = MissingFlag { name: "other" }.into();
assert_eq!(code, AppCode::BadRequest);
kind = ...выбираетAppErrorKind; генерируетFrom<T> for AppError.code = ...дополнительно генерируетFrom<T> for AppCode.messageпробрасывает выводDisplayкак публичное сообщение; опустите его, чтобы сообщение осталось внутренним.no_sourceотключает привязку источника и отбрасывает доменную ошибку при конверсии (для типов, не являющихсяSend + Sync + 'static).
Сгенерированный From<T> for AppError прикрепляет исходную доменную ошибку как source: она остаётся доступной через downcast_ref, видна в chain()/root_cause(), а данные #[provide] пробрасываются через AppError на тулчейнах с error_generic_member_access. Для привязки источника требуется T: Send + Sync + 'static.
Enum выбирают отображение для каждого варианта, при этом derive всё равно генерирует единственную реализацию From<Enum> for AppError.
#[provide(...)] — типизированная телеметрия
Предоставляет типизированный контекст через std::error::Request (nightly error_generic_member_access; компилируется автоматически при доступности). Поля Option регистрируют провайдер, только когда заполнены:
use masterror::{AppCode, AppErrorKind, Error};
#[derive(Clone, Debug, PartialEq, Eq)]
struct TelemetrySnapshot {
name: &'static str,
value: u64
}
#[derive(Debug, Error)]
#[error("structured telemetry {snapshot:?}")]
#[app_error(kind = AppErrorKind::Service, code = AppCode::Service)]
struct StructuredTelemetryError {
#[provide(ref = TelemetrySnapshot, value = TelemetrySnapshot)]
snapshot: TelemetrySnapshot
}
Потребители извлекают снимок вызовом std::error::request_ref::<TelemetrySnapshot>(&err) на доменной ошибке или на сконвертированном AppError — конверсия пробрасывает провайдеры через прикреплённый source.
#[derive(Masterror)] — сквозные доменные ошибки
#[derive(Masterror)] генерирует Display, std::error::Error, From<T> for masterror::Error и таблицы транспортных отображений на этапе компиляции — всё конфигурируется одним атрибутом #[masterror(...)]:
use masterror::{
AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy, mapping::HttpMapping
};
#[derive(Debug, Masterror)]
#[error("user {user_id} missing flag {flag}")]
#[masterror(
code = AppCode::NotFound,
category = AppErrorKind::NotFound,
message,
redact(message, fields("user_id" = hash)),
telemetry(
Some(masterror::field::str("user_id", user_id.clone())),
attempt.map(|value| masterror::field::u64("attempt", value))
),
map.grpc = 5,
map.problem = "https://errors.example.com/not-found"
)]
struct MissingFlag {
user_id: String,
flag: &'static str,
attempt: Option<u64>,
#[source]
source: Option<std::io::Error>
}
let err = MissingFlag {
user_id: "alice".into(),
flag: "beta",
attempt: Some(2),
source: None
};
let converted: Error = err.into();
assert_eq!(converted.code, AppCode::NotFound);
assert_eq!(converted.kind, AppErrorKind::NotFound);
assert_eq!(converted.edit_policy, MessageEditPolicy::Redact);
assert!(converted.metadata().get("user_id").is_some());
assert_eq!(
MissingFlag::HTTP_MAPPING,
HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound)
);
Опции #[masterror(...)]
| Опция | Значение |
|---|---|
code = AppCode::... | Публичный машиночитаемый код |
category = AppErrorKind::... | Семантическая категория (определяет HTTP-статус) |
message | Сделать отформатированный вывод Display безопасным публичным сообщением |
redact(message) | Установить MessageEditPolicy::Redact, чтобы транспорты удаляли сообщение |
redact(fields("name" = hash, "card" = last4)) | Переопределить политики метаданных для полей: hash, last4, redact, none |
telemetry(expr, ...) | Выражения, вычисляющиеся в Option<masterror::Field>; заполненные поля вставляются в Metadata. telemetry() — если полей нет |
map.grpc = <i32> | Код статуса gRPC (совпадает с дискриминантами tonic::Code) |
map.problem = "<uri>" | URI type по RFC 7807 |
Генерируемые таблицы отображений
Для структур derive порождает ассоциированные константы; для enum — массив и срезы, агрегирующие отображения по вариантам:
| Форма | Константы |
|---|---|
| Структура | T::HTTP_MAPPING: HttpMapping, T::GRPC_MAPPING: Option<GrpcMapping>, T::PROBLEM_MAPPING: Option<ProblemMapping> |
| Enum | T::HTTP_MAPPINGS: [HttpMapping; N], T::GRPC_MAPPINGS: &'static [GrpcMapping], T::PROBLEM_MAPPINGS: &'static [ProblemMapping] |
Типы-дескрипторы живут в masterror::mapping (HttpMapping::status() выводит HTTP-код из категории; GrpcMapping::status() возвращает i32; ProblemMapping::type_uri() возвращает URI).
#[from], #[source] и #[backtrace] продолжают работать под #[derive(Masterror)]; источники и захваченные бэктрейсы автоматически прикрепляются к результирующему masterror::Error, а источники, обёрнутые в Arc, переиспользуются без дополнительного клонирования.
Выбор между derive-макросами
| Потребность | Используйте |
|---|---|
Display + source + From в стиле thiserror | #[derive(Error)] |
Плюс конверсия в AppError/AppCode | #[derive(Error)] + #[app_error(...)] |
Типизированный контекст через std::error::Request | добавьте #[provide(...)] |
| Метаданные, политика редактирования, таблицы gRPC/problem+json | #[derive(Masterror)] + #[masterror(...)] |
См. также: Начало работы · Виды и коды ошибок · Контекст и метаданные · Миграция
Контекст и метаданные
masterror заменяет контекст, склеенный из строк (format!("failed to X: {e}")), тремя структурированными механизмами: билдером Context, типизированными полями Metadata и политиками редактирования, применяемыми на транспортной границе.
ResultExt: продвижение внешних ошибок
ResultExt реализован для каждого Result<T, E>, где E: Error + Send + Sync + 'static, и предлагает два метода:
.context(msg) — в стиле anyhow
Оборачивает ошибку сообщением; исходная ошибка становится источником:
use masterror::ResultExt;
fn read_config() -> Result<String, std::io::Error> {
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());
Если нижележащая ошибка уже является masterror::Error, .context() сохраняет её классификацию: вид, код, метаданные, политика редактирования, подсказка повторной попытки и детали переносятся, заменяется только сообщение, а исходная ошибка сохраняется как источник.
.ctx(|| Context) — полный контроль
use masterror::{AppErrorKind, Context, ResultExt, field};
fn validate() -> Result<(), std::io::Error> {
Err(std::io::Error::other("boom"))
}
let err = validate()
.ctx(|| {
Context::new(AppErrorKind::Validation)
.with(field::str("phase", "validate"))
.redact(true)
.track_caller()
})
.unwrap_err();
assert_eq!(err.kind, AppErrorKind::Validation);
assert!(err.metadata().get("phase").is_some());
Замыкание вычисляется только на пути ошибки.
Билдер Context
| Метод | Эффект |
|---|---|
Context::new(kind) | Целевая категория; AppCode по умолчанию берётся из канонического отображения для этого вида |
.code(AppCode) | Переопределить публичный код |
.category(kind) | Сменить категорию; код остаётся синхронизированным, если не был переопределён |
.with(field) | Прикрепить Field метаданных |
.redact(bool) | Переключить редактирование сообщения (MessageEditPolicy::Redact / Preserve) |
.redact_field(name, FieldRedaction) | Переопределить политику редактирования именованного поля |
.track_caller() | Записать место вызова в метаданные caller.file, caller.line, caller.column |
Поля метаданных
Metadata — отсортированная карта типизированных полей с inline-размещением (0–4 поля остаются на стеке). Создавайте поля через модуль masterror::field:
| Конструктор | Вариант FieldValue |
|---|---|
field::str("key", value) | Str(Cow<'static, str>) |
field::i64("key", -1) | I64 |
field::u64("key", 42) | U64 |
field::f64("key", 0.5) | F64 |
field::bool("key", true) | Bool |
field::uuid("key", uuid) | Uuid |
field::duration("key", dur) | Duration |
field::ip("key", addr) | Ip (v4 или v6) |
field::json("key", json!({...})) | Json (требует функции serde_json) |
Прикрепляйте поля при создании ошибок или через Context:
use core::time::Duration;
use masterror::{AppError, FieldValue, field};
let err = AppError::service("downstream degraded")
.with_field(field::str("request_id", "abc123"))
.with_field(field::duration("elapsed", Duration::from_millis(1500)))
.with_field(field::u64("attempt", 2));
assert_eq!(err.metadata().len(), 3);
assert_eq!(err.metadata().get("attempt"), Some(&FieldValue::U64(2)));
for (name, value) in err.metadata().iter() {
println!("{name}={value}");
}
with_fields(iter) расширяет карту из итератора, with_metadata(meta) заменяет контейнер, а Metadata::insert возвращает прежнее значение при перезаписи ключа.
Политики редактирования
Политика сообщения: MessageEditPolicy
Preserve (по умолчанию) сохраняет публичное сообщение; Redact велит транспортам удалить его. Устанавливается через .redactable() на ошибке, .redact(true) на Context или redact(message) в #[masterror(...)]:
use masterror::{AppError, MessageEditPolicy, ProblemJson};
let err = AppError::internal("db-3 credentials rejected").redactable();
assert_eq!(err.edit_policy, MessageEditPolicy::Redact);
let problem = ProblemJson::from_app_error(err);
assert!(problem.detail.is_none());
Политика поля: FieldRedaction
Каждое поле несёт собственную политику, применяемую при сериализации метаданных в ProblemJson:
| Политика | Эффект на публичную нагрузку |
|---|---|
None | Значение сохраняется как есть |
Redact | Поле удаляется целиком |
Hash | Значение заменяется дайджестом SHA-256 |
Last4 | Маскируется всё, кроме последних четырёх символов |
use masterror::{AppError, FieldRedaction, field};
let err = AppError::internal("payment failed")
.with_field(field::str("card_number", "4111111111111111"))
.redact_field("card_number", FieldRedaction::Last4);
Имена, похожие на секреты, автоматически получают безопасное значение по умолчанию при создании поля: имена, содержащие password, secret, authorization, cookie, session, jwt, bearer, otp, pin, по умолчанию получают Redact; имена в духе токенов и ключей (api_token, refresh_token, key, apikey) — Hash; сегменты карт/счетов в сочетании с числовым сегментом (card_number, iban_no, account_id) — Last4. Распознавание регистронезависимо. Явный redact_field/with_redaction всегда имеет приоритет.
Цепочки ошибок
Ошибки сохраняют полную причинную цепочку. chain() итерирует от самой ошибки до первопричины; root_cause() сразу переходит к самой глубокой ошибке:
use masterror::AppError;
let io_err = std::io::Error::other("disk offline");
let app_err = AppError::internal("db down").with_context(io_err);
let chain: Vec<_> = app_err.chain().collect();
assert_eq!(chain.len(), 2);
assert_eq!(app_err.root_cause().to_string(), "disk offline");
with_context(...) — предпочтительный способ прикрепить вышестоящую ошибку: он принимает владеемые ошибки или разделяемые значения Arc<dyn Error + Send + Sync> и переиспользует существующие аллокации. with_source(...) / with_source_arc(...) — их низкоуровневые эквиваленты.
Даункастинг
Проверяйте прикреплённый источник через is и downcast_ref/downcast_mut:
use masterror::AppError;
let io_err = std::io::Error::other("disk offline");
let err = AppError::internal("boom").with_context(io_err);
assert!(err.is::<std::io::Error>());
if let Some(io) = err.downcast_ref::<std::io::Error>() {
assert_eq!(io.to_string(), "disk offline");
}
is::<E>()—true, когда непосредственный источник имеет типE(не обходит всю цепочку).downcast_ref::<E>()— заимствует источник какE.downcast::<E>()/downcast_mut::<E>()— пока заглушки (downcastвсегда возвращаетErr(self),downcast_mutвсегда возвращаетNone), поэтому предпочитайтеdowncast_ref.
Для более глубоких проверок обходите chain() и вызывайте source.is::<E>() / source.downcast_ref::<E>() на каждом элементе.
Бэктрейсы
С функцией backtrace метод err.backtrace() возвращает лениво захваченный std::backtrace::Backtrace, учитывающий RUST_BACKTRACE, а with_backtrace(bt) прикрепляет явно захваченный. Бэктрейсы разделяются через Arc при переоборачивании ошибок, поэтому цепочки .context() не выполняют повторный захват.
См. также: Начало работы · Виды и коды ошибок · Derive-макросы · Наблюдаемость · Лучшие практики
Веб-фреймворки
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.
См. также: Виды и коды ошибок · Интеграции · Наблюдаемость · Флаги возможностей
Интеграции
Опциональные флаги функций добавляют конверсии From<...> из популярных
сторонних типов ошибок в masterror::Error, так что один ? в месте вызова
даёт классифицированную ошибку со структурированными метаданными. Каждая
конверсия выбирает стабильный AppErrorKind и
присоединяет телеметрические поля (никогда — секреты) для наблюдаемости.
Матрица конверсий
| Флаг | Исходный тип | Результирующий AppErrorKind |
|---|---|---|
sqlx | sqlx_core::error::Error | NotFound, Conflict, Validation, Timeout, DependencyUnavailable, Config, BadRequest, Serialization, Deserialization, Network, Database, Internal |
sqlx-migrate | sqlx::migrate::MigrateError | Database (с метаданными фазы миграции) |
redis | redis::RedisError | Cache (по умолчанию), Timeout, DependencyUnavailable |
reqwest | reqwest::Error | Timeout, Network, RateLimited, DependencyUnavailable, ExternalApi |
validator | validator::ValidationErrors | Validation |
config | config::ConfigError | Config (с метаданными config.phase) |
tokio | tokio::time::error::Elapsed | Timeout |
serde_json | serde_json::Error | Serialization (I/O), Deserialization (синтаксис/данные/EOF) |
teloxide | teloxide_core::RequestError | ExternalApi, Unauthorized, RateLimited, Network, Deserialization, Internal |
init-data | init_data_rs::InitDataError | TelegramAuth |
tonic | masterror::Error → tonic::Status | исходящее отображение, см. ниже |
multipart | axum::extract::multipart::MultipartError | BadRequest (см. Веб-фреймворки) |
sqlx и sqlx-migrate
sqlx зависит только от sqlx-core (без драйверов, без TLS). Ключевые
отображения:
Error::RowNotFound→NotFound- Таймаут пула →
Timeout; закрытый пул и сбои I/O →DependencyUnavailable; ошибки TLS →Network - Нарушения ограничений классифицируются по виду ошибки
sqlx: нарушения уникальности и внешних ключей →Conflict, нарушения not-null / check →Validation, всё остальное →Database - Кодирование →
Serialization, декодирование →Deserialization
Ошибки базы данных сохраняют SQLSTATE и имена ограничений в метаданных.
Известные коды SQLSTATE переопределяют публичный
AppCode: 23505 → USER_ALREADY_EXISTS, 23503 →
CONFLICT, 23502/23514 → VALIDATION. Транзиентные SQLSTATE (40001 —
сбой сериализации, 55P03 — блокировка недоступна) присоединяют подсказки
для повторных попыток.
use masterror::{AppErrorKind, Error};
async fn load_user(pool: &sqlx::PgPool, id: i64) -> Result<User, Error> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_one(pool)
.await?; // RowNotFound becomes AppErrorKind::NotFound
Ok(user)
}
sqlx-migrate подтягивает полный sqlx (всё ещё без функций по умолчанию)
и отображает sqlx::migrate::MigrateError в Database, записывая фазу и
версию миграции в метаданные.
redis
Все значения redis::RedisError по умолчанию отображаются в Cache; ошибки
с характером таймаута становятся Timeout, а сбои соединения —
DependencyUnavailable. Категория и код ошибки сохраняются в метаданных.
reqwest
Рассматривает reqwest как клиент внешнего HTTP API:
is_timeout()→Timeoutis_connect()/is_request()→Network- Ошибки HTTP-статусов:
429→RateLimited,408→Timeout,5xx→DependencyUnavailable, остальные →ExternalApi - всё остальное →
ExternalApi
Метаданные записывают конечную точку, статус и низкоуровневые флаги; URL помечается для хеширования/редактирования в публичных полезных нагрузках.
validator
validator::ValidationErrors → Validation, с агрегированным контекстом в
метаданных: имена невалидных полей (validation.fields), количество полей и
ошибок, а также первые коды валидации (validation.codes):
use masterror::AppResult;
use validator::Validate;
#[derive(Validate)]
struct Payload {
#[validate(length(min = 5))]
name: String
}
fn check(p: &Payload) -> AppResult<()> {
p.validate()?; // ValidationErrors -> AppErrorKind::Validation
Ok(())
}
config, tokio, serde_json
config::ConfigError→Config, с полем метаданныхconfig.phase(not_found,file_parse,type, …), идентифицирующим стадию сбоя.tokio::time::error::Elapsed→Timeoutс полем метаданныхtimeout.source = "tokio::time::timeout". Ошибка не несёт собственного сообщения, поэтому клиенты видят фиксированный заголовок вида"Operation timed out".serde_json::Errorклассифицируется черезError::classify(): I/O →Serialization; синтаксис, данные и EOF →Deserialization.
teloxide
Отображение teloxide_core::RequestError:
| Вариант | AppErrorKind |
|---|---|
Api | ExternalApi (невалидный токен → Unauthorized) |
MigrateToChatId | ExternalApi |
RetryAfter | RateLimited |
Network | Network |
InvalidJson | Deserialization |
Io | Internal |
init-data (Telegram Mini Apps)
Каждый вариант init_data_rs::InitDataError (отсутствующий/невалидный хеш,
истёкшая полезная нагрузка, сбои подписи) отображается в TelegramAuth,
отделяя сбои аутентификации Mini App от обычных некорректных запросов.
tonic (исходящий gRPC)
tonic конвертирует в обратном направлении: masterror::Error →
tonic::Status через From. AppCode отображается на
канонический tonic::Code через ту же таблицу CODE_MAPPINGS, что
используется для HTTP. Статус несёт записи метаданных app-code,
app-http-status и app-problem-type, а также подсказки о повторных попытках
и www-authenticate, если они заданы. У редактируемых ошибок сообщение
заменяется меткой вида, а метаданные удаляются.
use masterror::AppError;
use tonic::{Code, Status};
let status = Status::from(AppError::not_found("missing"));
assert_eq!(status.code(), Code::NotFound);
frontend (WASM / браузер)
Флаг frontend добавляет trait masterror::frontend::BrowserConsoleExt для
AppError и ErrorResponse на базе wasm-bindgen:
to_js_value()— сериализовать ошибку вwasm_bindgen::JsValuelog_to_browser_console()— вывести её черезconsole.error
Оба метода работают на целевых платформах wasm32; на нативных платформах они
возвращают BrowserConsoleError::UnsupportedTarget. Режимы сбоя (отсутствие
консоли, невызываемый console.error, сбой сериализации) покрыты enum
BrowserConsoleError.
use masterror::{AppError, frontend::BrowserConsoleExt};
let err = AppError::not_found("user not found");
err.log_to_browser_console()?;
turnkey
Флаг turnkey предоставляет небольшую стабильную доменную таксономию в
masterror::turnkey:
TurnkeyErrorKind | AppErrorKind |
|---|---|
UniqueLabel | Conflict |
RateLimited | RateLimited |
Timeout | Timeout |
Auth | Unauthorized |
Network | Network |
Service | Turnkey |
TurnkeyError::new(kind, msg) строит доменную ошибку; From<TurnkeyError> for AppError и From<TurnkeyErrorKind> for AppErrorKind выполняют отображение.
classify_turnkey_error(&str) эвристически классифицирует сырое сообщение
провайдера (без учёта регистра, с учётом границ слов) в TurnkeyErrorKind:
use masterror::turnkey::{TurnkeyError, TurnkeyErrorKind, classify_turnkey_error};
use masterror::{AppError, AppErrorKind};
let kind = classify_turnkey_error("429 rate-limit reached");
assert_eq!(kind, TurnkeyErrorKind::RateLimited);
let app: AppError = TurnkeyError::new(kind, "quota exceeded").into();
assert_eq!(app.kind, AppErrorKind::RateLimited);
См. также: Флаги возможностей · Веб-фреймворки · Виды и коды ошибок · Наблюдаемость
Наблюдаемость
masterror рассматривает телеметрию как часть жизненного цикла ошибки. Каждый
AppError отслеживает флаг изменения; телеметрия испускается один раз на
каждое изменение состояния — при конструировании, после мутации или когда
ошибка пересекает транспортную границу (IntoResponse в Axum,
error_response() в Actix, конверсия в Status для tonic). Вручную почти
ничего вызывать не нужно.
Флаги функций
| Флаг | Что добавляет |
|---|---|
tracing | Структурированное событие tracing на каждую ошибку, trace_id через log-mdc |
metrics | Счётчик error_total{code,category} через крейт metrics |
backtrace | Ленивый захват std::backtrace::Backtrace, управляемый RUST_BACKTRACE |
colored | ANSI-раскраска терминального вывода с детекцией TTY |
[dependencies]
masterror = { version = "0.28", features = ["tracing", "metrics", "backtrace"] }
Tracing
С включённым tracing каждая ошибка испускает одно событие уровня ERROR с
target masterror::error:
| Поле | Содержимое |
|---|---|
code | Строка AppCode, например NOT_FOUND |
category | Метка AppErrorKind, например Database |
message | Публичное сообщение, если есть |
retry_seconds | Рекомендация о повторе, если задана |
redactable | Редактируется ли сообщение на транспортных границах |
metadata_len | Количество присоединённых полей метаданных |
www_authenticate | Вызов аутентификации, если задан |
trace_id | Берётся из ключа trace_id контекста log-mdc, если присутствует |
Испускание учитывает подписчика: если ни один подписчик не заинтересован в событиях уровня ERROR для этого target, событие остаётся в ожидании и повторяется при следующем сбросе, так что ничего не теряется при поздней установке подписчика.
Чтобы соотносить ошибки с запросами, сохраните trace ID в MDC в middleware запроса:
log_mdc::insert("trace_id", request_id);
Каждая ошибка, сконструированная пока ключ установлен, несёт его в событии.
Метрики
С включённым metrics каждая свежеизменённая ошибка инкрементирует:
error_total{code="NOT_FOUND", category="NotFound"}
Обе метки — стабильные строки (AppCode::as_str() и метка AppErrorKind),
поэтому дашборды и алерты переживают рефакторинг ваших доменных типов.
Подключайте любой recorder metrics (Prometheus, StatsD, …) как обычно;
masterror использует только metrics::counter!.
Бэктрейсы
С включённым backtrace снимок Backtrace захватывается лениво в момент
сброса телеметрии, а не при каждом конструировании. Захват управляется
RUST_BACKTRACE: не задано, пустое, 0, off и false отключают его; любое
другое значение включает. Настройка читается один раз и кэшируется на процесс.
#[cfg(feature = "backtrace")] {
use masterror::AppError;
let err = AppError::internal("db down");
if let Some(bt) = err.backtrace() {
eprintln!("{bt}");
}
}
Можно также присоединить заранее захваченный трейс через
AppError::with_backtrace(backtrace) — он имеет приоритет над ленивым
захватом.
Ручной сброс через .log()
Конструкторы и конверсии испускают телеметрию автоматически. После мутации ошибки (добавление полей, изменение рекомендации о повторе) можно принудительно испустить её заново:
use masterror::{AppError, field};
let err = AppError::service("upstream degraded")
.with_field(field::str("upstream", "billing"));
err.log();
log() идемпотентен в рамках состояния: если с последнего испускания ничего
не изменилось, он ничего не делает. HTTP/gRPC-адаптеры делают такой же сброс
на границе, поэтому ошибка, которая сконструирована, обогащена и затем
возвращена из обработчика Axum, испускается один раз на состояние — один раз
при конструировании и один раз на границе для обогащённого состояния — и
никогда дважды для одного состояния.
Инспекция цепочки
Независимо от флагов, AppError предоставляет инструменты, нужные конвейерам
логирования:
#[cfg(feature = "std")] {
use std::io::Error as IoError;
use masterror::AppError;
let err = AppError::internal("db down").with_context(IoError::other("disk offline"));
assert_eq!(err.chain().count(), 2);
let _root = err.root_cause();
assert!(err.metadata().is_empty());
}
metadata().iter_with_redaction() выдаёт тройки (key, value, policy), чтобы
слой логирования мог соблюдать редактирование полей — см.
Контекст и метаданные.
Цветной терминальный вывод
Флаг colored добавляет masterror::colored::style для CLI-инструментов.
Цвета применяются только когда stderr — это TTY, NO_COLOR не задан, TERM
не равен dumb и терминал поддерживает ANSI; иначе текст проходит без
изменений. Результат детекции кэшируется на процесс.
| Функция | Стиль | Для чего |
|---|---|---|
error_kind_critical | красный | критические виды сбоев |
error_kind_warning | жёлтый | восстановимые виды/предупреждения |
error_code | голубой | машинные коды |
error_message | ярко-белый | основное сообщение |
source_context | приглушённый | вторичная информация об источнике |
metadata_key | зелёный | имена структурированных полей |
#[cfg(feature = "colored")] {
use masterror::colored::style;
eprintln!(
"{}: {}",
style::error_code("ERR_DB_001"),
style::error_message("Database connection failed")
);
}
Запускаемое демо:
examples/colored_cli.rs.
См. также: Флаги возможностей · Контекст и метаданные · Веб-фреймворки · Лучшие практики
Поддержка no_std
masterror собирается без стандартной библиотеки Rust. Корень крейта
объявляет #![cfg_attr(not(feature = "std"), no_std)], и включённая по
умолчанию функция std — единственное, что отделяет вас от сборки, дружелюбной
к embedded/WASM:
[dependencies]
masterror = { version = "0.28", default-features = false }
alloc обязателен
masterror — это no_std, но не no_alloc. Крейт безусловно объявляет
extern crate alloc и использует Cow<'static, str>, String, Arc и
BTreeMap для сообщений, метаданных и цепочек источников. Вашей целевой
платформе нужен глобальный аллокатор; окружения только с core не
поддерживаются.
Что работает без std
Всё фреймворк-независимое ядро:
| Область | Доступно в no_std |
|---|---|
| Основные типы | Error / AppError, AppResult, AppErrorKind, AppCode |
| Метаданные | Metadata, Field, FieldValue, FieldRedaction, хелперы field::* |
| Контекст | Context, ResultExt::{ctx, context} |
| Управление потоком | ensure!, fail! |
| Derive | #[derive(Error)], #[derive(Masterror)] со всеми атрибутами |
| Типы на проводе | ProblemJson, ErrorResponse, CODE_MAPPINGS, mapping_for_code |
| Интроспекция | chain(), root_cause(), is/downcast/downcast_ref/downcast_mut, render_message() |
| Serde | serde с alloc (JSON-сериализация типов на проводе) |
Источники ошибок работают через core::error::Error: крейт реализует и
использует core::error::Error (внутренний псевдоним CoreError) вместо
std::error::Error, поэтому with_source(...), цепочки источников и
даункастинг полностью функциональны в сборках no_std.
use masterror::{AppCode, AppError, AppErrorKind, field};
let err = AppError::new(AppErrorKind::Timeout, "deadline exceeded")
.with_field(field::u64("attempt", 3));
assert_eq!(err.code, AppCode::Timeout);
assert_eq!(err.metadata().len(), 1);
Что требует std
Каждая runtime-интеграция явно включает std обратно в определении своей
функции. Из Cargo.toml:
tracing,metrics,backtrace,coloredaxum,actix,multipart,tonic,openapiserde_json,redis,validator,config,tokio,reqwest,teloxide,init-data,frontend,turnkey
backtrace нуждается в std::backtrace::Backtrace и доступе к окружению;
colored — в детекции TTY; веб- и клиентским интеграциям нужны их хост-крейты,
которые сами работают только со std.
Матрица функций в CI
CI-задача no_std (.github/workflows/ci.yml) проверяет эти комбинации на
каждом pull request и push в main:
| Задача | Команда | Что проверяет |
|---|---|---|
bare | cargo check --no-default-features | настоящую сборку no_std + alloc |
std-only | cargo check --features std | стандартную поверхность std |
tracing | cargo check --no-default-features --features tracing | что одиночная телеметрическая функция собирается автономно |
metrics | cargo check --no-default-features --features metrics | то же для metrics |
colored | cargo check --no-default-features --features colored | то же для colored |
all-features | cargo check --all-features | полное объединение функций |
Обратите внимание на семантику: только задача bare — настоящая компиляция
no_std. tracing = [..., "std"], metrics = [..., "std"] и
colored = [..., "std"] транзитивно включают std обратно, поэтому эти
задачи проверяют, что каждая телеметрическая функция самодостаточна при
отключённых значениях по умолчанию — а не то, что телеметрия работает без
стандартной библиотеки. Если нужна телеметрия — нужен std.
Практическая настройка
Библиотечные крейты, которые хотят оставаться транспортно-независимыми и
совместимыми с no_std:
[dependencies]
masterror = { version = "0.28", default-features = false }
[features]
std = ["masterror/std"]
Бинарный или сервисный крейт затем включает std плюс нужные ему интеграции:
[dependencies]
masterror = { version = "0.28", features = ["axum", "tracing", "metrics"] }
Поскольку AppErrorKind, AppCode и типы на проводе живут в ядре no_std,
доменные крейты могут классифицировать ошибки и даже строить полезные нагрузки
ProblemJson, тогда как отображение на HTTP происходит только в сервисном
крейте — см. Лучшие практики.
Тулчейн
Крейт нацелен на edition 2024 с rust-version = "1.96" в Cargo.toml.
core::error::Error (основа цепочек источников в no_std) стабилен начиная с
Rust 1.81, поэтому никакие nightly-функции не задействованы.
См. также: Флаги возможностей · Начало работы · Лучшие практики
Лучшие практики
Паттерны, которые делают сервисы на masterror предсказуемыми: типизированные
доменные ошибки, одна стабильная таксономия кодов, отображение на транспорты
на границе и публичные сообщения, которые никогда не раскрывают внутренности.
Выводите доменные ошибки через derive и отображайте их один раз
Моделируйте каждый ограниченный контекст как enum с #[derive(Error)] и
объявляйте отображение на AppError прямо на месте через #[app_error(...)].
Derive генерирует Display, From<...> для обёрнутых источников и конверсию
в AppError/AppCode — никакого рукописного match в каждом месте вызова.
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
pub enum UserError {
#[error("user {0} not found")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
NotFound(u64),
#[error("email already registered")]
#[app_error(kind = AppErrorKind::Conflict, code = AppCode::Conflict, message)]
DuplicateEmail,
#[error("storage failure")]
#[app_error(kind = AppErrorKind::Database, code = AppCode::Database)]
Storage(#[from] std::io::Error)
}
let app: AppError = UserError::DuplicateEmail.into();
assert_eq!(app.kind, AppErrorKind::Conflict);
Указывайте message только на вариантах, чей вывод Display безопасно
показывать клиентам. Опускайте его (как на Storage), чтобы текст остался
внутренним — клиент увидит только заголовок вида. Полный справочник
атрибутов: Derive-макросы.
Одна таксономия AppCode на сервис
AppCode — ваш публичный API-контракт; клиенты ветвятся по нему. Держите
набор небольшим, документированным и отдельным для каждого сервиса:
- Предпочитайте встроенные коды (
NOT_FOUND,CONFLICT,VALIDATION, …) — они уже несут канонические отображения HTTP/gRPC/problem-type. - Заводите собственные коды централизованно, а не в местах вызова:
use masterror::AppCode;
pub const CODE_PLAN_LIMIT: AppCode = AppCode::new("PLAN_LIMIT_EXCEEDED");
AppCode::new — const и паникует на этапе компиляции на всём, что не
SCREAMING_SNAKE_CASE; для строк времени выполнения используйте
AppCode::try_new. Переименование кода — ломающее изменение API; относитесь
к добавлениям как к добавлению варианта enum.
Отображайте на транспорты только на границе
Доменный и сервисный слои возвращают AppResult<T> и ничего не знают об HTTP.
Единственная реализация IntoResponse/ResponseError/Status в крейте
выполняет отображение в слое обработчиков:
async fn get_user(id: u64, repo: &Repo) -> masterror::AppResult<User> {
repo.find(id).await? // sqlx::Error -> AppError::NotFound/Database
}
Никогда не конструируйте коды статусов вручную в бизнес-логике и никогда не
реализуйте вторую конверсию в ответ — стабильная таблица
AppErrorKind → status в Веб-фреймворках — единственный
источник истины.
Редактируйте чувствительные данные, сохраняйте телеметрию
Два независимых рычага:
- Редактирование сообщения —
err.redactable()(илиredact(message)в#[masterror(...)]) скрываетdetailиз полезных нагрузок на проводе, тогда как логи его сохраняют. - Редактирование полей — политика для каждого поля, применяемая при сериализации метаданных:
use masterror::{AppError, FieldRedaction, field};
let err = AppError::bad_request("Invalid credentials")
.with_field(field::str("email", "user@example.com").with_redaction(FieldRedaction::Hash))
.with_field(field::str("card", "4111111111111111").with_redaction(FieldRedaction::Last4))
.with_field(field::str("ip", "192.168.1.100").with_redaction(FieldRedaction::Redact));
Hash сохраняет корреляционную ценность (одинаковый вход → одинаковый
дайджест), не раскрывая исходную строку; Last4 подходит для суффиксов
карт/токенов; Redact удаляет значение полностью. По умолчанию — None.
Редактируйте всё, что идентифицирует пользователя, по умолчанию и осознанно
отказывайтесь от этого, а не наоборот.
Context против derive
- Derive — когда тип ошибки часть вашего доменного словаря: у него есть варианты, он появляется в сигнатурах, а его отображение статично.
Context(черезResultExt::ctx) — когда инфраструктурная ошибка оборачивается ситуативно в месте вызова и классификация зависит от операции, а не от типа:
#[cfg(feature = "std")] {
use masterror::{AppErrorKind, Context, ResultExt, field};
fn read_state() -> masterror::AppResult<Vec<u8>> {
std::fs::read("/var/lib/app/state.bin").ctx(|| {
Context::new(AppErrorKind::Internal)
.with(field::str("path", "/var/lib/app/state.bin"))
.track_caller()
})
}
}
ctx ленив — замыкание выполняется только на пути ошибки. Используйте простой
.context("message"), когда достаточно человекочитаемой заметки. Подробности:
Контекст и метаданные.
Тестируйте виды и коды, а не строки
Утверждайте стабильную таксономию, никогда — отформатированные сообщения:
use masterror::{AppCode, AppError, AppErrorKind, ProblemJson};
let err = AppError::not_found("user 42 missing");
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.code, AppCode::NotFound);
let problem = ProblemJson::from_ref(&err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
ProblemJson::from_refпозволяет интеграционным тестам проверять точный контракт на проводе без поднятия сервера.mapping_for_code(&code)предоставляет канонический HTTP-статус, gRPC-код и URI problem-type для табличных тестов.- Для тестов редактирования утверждайте
problem.detail.is_none()на ошибке сredactable()и проверяйте политики черезmetadata().iter_with_redaction().
Публичное сообщение против внутренней телеметрии
Полезное правило для каждой конструируемой ошибки:
| Канал | Содержимое |
|---|---|
message / detail | Ориентировано на человека, без чувствительных данных, достаточно стабильно для показа пользователю |
Поля Metadata | ID, попытки, конечные точки, длительности — для логов/метрик, с политиками редактирования |
Цепочка source | Сырые нижележащие ошибки — логируются, никогда не сериализуются клиентам |
masterror принудительно обеспечивает последнюю строку (источники никогда не
записываются в полезные нагрузки на проводе), но первые две — ваша
ответственность: если строка содержит что-то, что вы не напечатали бы в
браузере, поместите это в метаданные с политикой редактирования или пометьте
ошибку redactable().
См. также: Derive-макросы · Контекст и метаданные · Виды и коды ошибок · Миграция
Миграция
masterror спроектирован как прямая замена одновременно thiserror
(синтаксис derive) и anyhow (эргономика). Большинство миграций — это замена
зависимости плюс постепенное принятие типизированных возможностей. Запускаемые
пошаговые примеры:
examples/migrate_from_thiserror.rs
и
examples/migrate_from_anyhow.rs.
С thiserror
Шаг 1 механический — поменяйте импорт; синтаксис derive совместим:
-use thiserror::Error;
+use masterror::Error;
Совместимость атрибутов
| thiserror | masterror | Примечания |
|---|---|---|
#[error("...")] с плейсхолдерами {field} | так же | 1:1, включая позиционные {0} |
Спецификаторы формата :>8, :.3, :x, :p, :e | так же | TemplateFormatter повторяет детекцию форматтеров thiserror |
#[error(transparent)] | так же | требует обёрток с одним полем, пробрасывающих Display/source |
#[from] | так же | генерирует From<...>, проверяет форму обёртки |
#[source] | так же | подключает цепочку source() |
#[backtrace] | так же | учитывается на полях |
| — | #[app_error(kind = ..., code = ..., message)] | добавлено: генерирует From<YourError> for AppError (и AppCode); message пробрасывает Display как публичное сообщение |
| — | #[provide(ref = T, value = T)] | добавлено: типизированная телеметрия через std::error::Request; поля Option<T> предоставляют значение только при Some |
| — | #[derive(Masterror)] + #[masterror(...)] | добавлено: полное отображение с category, redact(message, fields(...)), telemetry(...), map.grpc, map.problem |
Существующие enum продолжают компилироваться без изменений. Что даёт их аннотирование:
use masterror::{AppCode, AppError, AppErrorKind, Error};
#[derive(Debug, Error)]
#[error("user {user_id} not found")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
struct UserMissing {
user_id: u64
}
let app: AppError = UserMissing { user_id: 42 }.into();
assert_eq!(app.kind, AppErrorKind::NotFound);
Enum отображаются повариантно — каждый вариант несёт собственный
#[app_error(...)], а derive генерирует единственный From<Enum> for AppError.
Рекомендуемый порядок: (1) поменять импорт, (2) добавить #[app_error] типам,
пересекающим границу API, (3) заменить рукописные реализации
From<DomainError> for AppError сгенерированными, (4) принять
#[masterror(...)] там, где нужны редактирование или метаданные.
С anyhow
| anyhow | masterror | Примечания |
|---|---|---|
anyhow::Result<T> | masterror::AppResult<T> | псевдоним для Result<T, AppError> |
anyhow::Error | masterror::AppError / Error | несёт вид, код и метаданные вместо «блоба» |
.context("msg") | .context("msg") | идентично, через masterror::ResultExt |
.with_context(|| ...) | .ctx(|| Context::new(kind)...) | ленив, как в anyhow, но строит типизированный Context (вид, код, поля, редактирование) |
bail!(err) | fail!(err) | принимает выражение типизированной ошибки, без машинерии форматирования |
ensure!(cond, "msg {x}") | ensure!(cond, AppError::...) | условие + типизированная ошибка; без форматирования на успешном пути |
err.chain() | err.chain() | тот же итератор по цепочке источников |
err.root_cause() | err.root_cause() | так же |
err.is::<E>() / downcast / downcast_ref / downcast_mut | те же имена на AppError | паритет по даункастингу |
Обёртывание в стиле #[from] | Реализации From<...> за флагами функций | sqlx/redis/reqwest/… приходят уже классифицированными |
.context() работает ровно так, как вы ожидаете:
use masterror::{AppResult, ResultExt};
fn read_config(path: &str) -> AppResult<String> {
let content = std::fs::read_to_string(path).context("Failed to read config file")?;
Ok(content)
}
ensure!/fail! меняют строковое форматирование anyhow на типизированные
ошибки:
use masterror::{AppError, AppResult, ensure, fail, field};
fn parse(content: &str, path: &str) -> AppResult<()> {
ensure!(
!content.is_empty(),
AppError::bad_request("Config file is empty")
.with_field(field::str("path", path.to_owned()))
);
if content.starts_with("invalid") {
fail!(AppError::bad_request("Invalid config format"));
}
Ok(())
}
Выражение ошибки вычисляется только при срабатывании условия, поэтому счастливый путь остаётся без выделений памяти — та же гарантия, что даёт anyhow, плюс машиночитаемый код.
Где у anyhow нет эквивалента
Миграция даёт возможности, у которых нет аналога в anyhow:
- Типизированная таксономия —
AppErrorKind(внутренний) иAppCode(публичный, SCREAMING_SNAKE_CASE) вместо строкового контекста. См. Виды и коды ошибок. - Транспортные отображения — RFC 7807
problem+jsonдля Axum/Actix иtonic::Statusдля gRPC, выводимые из одной таблицы кодов. См. Веб-фреймворки. - Телеметрия — автоматические события
tracing, метрикиerror_total{code,category}и ленивые бэктрейсы на границе. См. Наблюдаемость. - Редактирование — сообщения
redactable()и политикиHash/Last4/Redactдля каждого поля, соблюдаемые каждым транспортом. См. Лучшие практики. - Структурированные метаданные — типизированные
field::str/u64/duration/ip/...вместо форматирования значений в сообщение.
На что обратить внимание
- У формы anyhow
ensure!(cond, "format {}", x)с форматированным сообщением нет прямого двойника: конструируйте ошибку явно (AppError::bad_request(format!(...))) или, что лучше, используйте статическое сообщение плюс поля метаданных. anyhow::Errorпринимает любойE: Error + Send + Sync. В masterror вы выбираете вид в момент оборачивания (Context::new(kind)или конверсияFrom) — эта точка принятия решения и есть возможность, а не трение: именно здесь происходит классификация.- И
ensure!, иfail!разворачиваются вreturn Err(...), поэтому работают в любой функции, возвращающейResult<_, E>, где ваше выражение уже является типом ошибки — конверсияIntoне вставляется.
См. также: Начало работы · Derive-макросы · Контекст и метаданные · Лучшие практики