오류 종류와 코드
두 타입이 분류 체계의 근간을 이룹니다:
AppErrorKind— 실패의 내부적인 의미론적 범주. 작고 안정적이며 프레임워크에 독립적입니다. 기본 HTTP 상태를 결정합니다.AppCode— 클라이언트에 SCREAMING_SNAKE_CASE 문자열(예:"NOT_FOUND")로 노출되는 공개적인 기계 판독 가능 코드. 와이어 계약의 일부입니다.
모든 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
매핑에 내재된 설계 규칙: 인프라 및 I/O 문제는 기본적으로 5xx로 처리합니다. Unauthorized(401)는 인증 실패를 의미하고, Forbidden(403)은 인증에는 성공했으나 접근이 거부되었음을 의미합니다. 연결/구성 실패에는 Network를, 업스트림 HTTP 상태 오류에는 ExternalApi를 사용하세요.
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())는 정책과 무관하게 절대 직렬화되지 않습니다.
와이어 페이로드
ProblemJson — RFC 7807 application/problem+json. 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 매크로 · 컨텍스트와 메타데이터 · 웹 프레임워크