Лучшие практики
Паттерны, которые делают сервисы на 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-макросы · Контекст и метаданные · Виды и коды ошибок · Миграция