Виды и коды ошибок
Два типа образуют основу таксономии:
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-макросы · Контекст и метаданные · Веб-фреймворки