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 предсказуемыми: типизированные доменные ошибки, одна стабильная таксономия кодов, отображение на транспорты на границе и публичные сообщения, которые никогда не раскрывают внутренности.

Выводите доменные ошибки через 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::newconst и паникует на этапе компиляции на всём, что не 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Ориентировано на человека, без чувствительных данных, достаточно стабильно для показа пользователю
Поля MetadataID, попытки, конечные точки, длительности — для логов/метрик, с политиками редактирования
Цепочка sourceСырые нижележащие ошибки — логируются, никогда не сериализуются клиентам

masterror принудительно обеспечивает последнюю строку (источники никогда не записываются в полезные нагрузки на проводе), но первые две — ваша ответственность: если строка содержит что-то, что вы не напечатали бы в браузере, поместите это в метаданные с политикой редактирования или пометьте ошибку redactable().

См. также: Derive-макросы · Контекст и метаданные · Виды и коды ошибок · Миграция