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

Виды и коды ошибок

Два типа образуют основу таксономии:

  • 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Сбой процедуры аутентификации Telegram401
InvalidJwtJWT просрочен, повреждён или имеет неверную подпись/клеймы401
RateLimitedКлиент превысил лимиты запросов или квоту429
TimeoutОперация не завершилась вовремя504
NetworkОшибка сетевого уровня (DNS, соединение, TLS)503
DependencyUnavailableВнешняя зависимость недоступна или деградировала503
InternalНеожиданный сбой на стороне сервера500
DatabaseСбой базы данных (запрос, соединение, миграция)500
ServiceОбщий сбой сервисного слоя / бизнес-логики500
ConfigОтсутствующая или некорректная конфигурация500
TurnkeyСбой подсистемы Turnkey500
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):

AppCodeHTTPgRPCproblem type
NOT_FOUND404NOT_FOUND (5)https://errors.masterror.rs/not-found
VALIDATION422INVALID_ARGUMENT (3).../validation
CONFLICT409ALREADY_EXISTS (6).../conflict
USER_ALREADY_EXISTS409ALREADY_EXISTS (6).../user-already-exists
UNAUTHORIZED401UNAUTHENTICATED (16).../unauthorized
FORBIDDEN403PERMISSION_DENIED (7).../forbidden
NOT_IMPLEMENTED501UNIMPLEMENTED (12).../not-implemented
BAD_REQUEST400INVALID_ARGUMENT (3).../bad-request
RATE_LIMITED429RESOURCE_EXHAUSTED (8).../rate-limited
TELEGRAM_AUTH401UNAUTHENTICATED (16).../telegram-auth
INVALID_JWT401UNAUTHENTICATED (16).../invalid-jwt
INTERNAL500INTERNAL (13).../internal
DATABASE500INTERNAL (13).../database
SERVICE500INTERNAL (13).../service
CONFIG500INTERNAL (13).../config
TURNKEY500INTERNAL (13).../turnkey
TIMEOUT504DEADLINE_EXCEEDED (4).../timeout
NETWORK503UNAVAILABLE (14).../network
DEPENDENCY_UNAVAILABLE503UNAVAILABLE (14).../dependency-unavailable
SERIALIZATION500INTERNAL (13).../serialization
DESERIALIZATION500INTERNAL (13).../deserialization
EXTERNAL_API500UNAVAILABLE (14).../external-api
QUEUE500UNAVAILABLE (14).../queue
CACHE500UNAVAILABLE (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-полезные нагрузки

ProblemJsonapplication/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-макросы · Контекст и метаданные · Веб-фреймворки