-
-
Notifications
You must be signed in to change notification settings - Fork 0
Error Kinds and Codes en
Two types form the backbone of the taxonomy:
-
AppErrorKind— the internal, semantic category of a failure. Small, stable, framework-agnostic. Controls the default HTTP status. -
AppCode— the public, machine-readable code exposed to clients as a SCREAMING_SNAKE_CASE string (e.g."NOT_FOUND"). Part of the wire contract.
Every AppError carries both. AppCode::from(kind) gives the canonical 1:1 mapping, and AppError::with_code(...) overrides the public code without changing the category.
| Variant | Meaning | HTTP |
|---|---|---|
NotFound |
Resource does not exist or is not visible to the caller | 404 |
Validation |
Structured input failed validation | 422 |
Conflict |
State conflict (unique key violation, version mismatch) | 409 |
Unauthorized |
Authentication required or failed | 401 |
Forbidden |
Authenticated but not allowed | 403 |
NotImplemented |
Operation not supported by this deployment | 501 |
BadRequest |
Malformed request or missing parameters | 400 |
TelegramAuth |
Telegram authentication flow failed | 401 |
InvalidJwt |
JWT expired, malformed or has wrong signature/claims | 401 |
RateLimited |
Client exceeded rate limits or quota | 429 |
Timeout |
Operation did not complete in time | 504 |
Network |
Network-level error (DNS, connect, TLS) | 503 |
DependencyUnavailable |
External dependency down or degraded | 503 |
Internal |
Unexpected server-side failure | 500 |
Database |
Database failure (query, connection, migration) | 500 |
Service |
Generic service-layer/business-logic failure | 500 |
Config |
Missing or invalid configuration | 500 |
Turnkey |
Turnkey subsystem failure | 500 |
Serialization |
Failed to encode data | 500 |
Deserialization |
Failed to decode data | 500 |
ExternalApi |
Upstream API returned an error | 500 |
Queue |
Queue publish/consume/ack failure | 500 |
Cache |
Cache read/write/encoding failure | 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::StatusCodeDesign rules baked into the mapping: infrastructure and I/O issues default to 5xx; Unauthorized (401) means authentication failed, Forbidden (403) means authentication succeeded but access was denied; use Network for connect/build failures and ExternalApi for upstream HTTP status errors.
AppCode ships constants matching every kind (AppCode::NotFound → "NOT_FOUND", AppCode::RateLimited → "RATE_LIMITED", …) plus AppCode::UserAlreadyExists ("USER_ALREADY_EXISTS", mapped as a conflict). It is #[non_exhaustive] and supports caller-defined codes:
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);Valid codes contain only A-Z, 0-9 and single _ separators, and serialize as plain JSON strings.
CODE_MAPPINGS (and the mapping_for_code lookup) define the canonical transport mapping for every built-in code. Unknown custom codes fall back to 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 values match tonic::Code discriminants, so the tonic feature converts directly.
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");Transport adapters translate two optional hints into HTTP headers:
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");On ErrorResponse the equivalent builders are with_retry_after_secs, with_retry_after_duration and with_www_authenticate.
AppError messages are meant to be safe for clients, but you can mark an error as redactable so the boundary strips it:
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 tooWhen edit_policy is Redact, ProblemJson drops detail, details and the entire metadata section. Individual metadata fields additionally carry their own FieldRedaction policy (None, Redact, Hash, Last4) applied during serialization — see Context and Metadata. Error sources (source_ref()) are never serialized regardless of policy.
ProblemJson — RFC 7807 application/problem+json, produced by ProblemJson::from_app_error (owned) or ProblemJson::from_ref (borrowed). Fields: type, title (kind label), status, detail, optional details, code, grpc ({name, value}), metadata, plus non-serialized retry_after/www_authenticate for headers.
ErrorResponse — legacy flat JSON payload: status, code, message, optional details, retry, www_authenticate. With the openapi feature it derives 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);Prefer ProblemJson for new APIs; ErrorResponse remains for services already committed to the flat shape.
See also: Getting Started · Derive Macros · Context and Metadata · Web Frameworks
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어
Getting Started
Core Concepts
Integrations
Advanced
Начало работы
Основы
Интеграции
Продвинутое
시작하기
핵심 개념
통합
고급