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")로 노출되는 공개적인 기계 판독 가능 코드. 와이어 계약의 일부입니다.

모든 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
TelegramAuthTelegram 인증 플로우 실패401
InvalidJwtJWT 만료, 형식 오류 또는 잘못된 서명/클레임401
RateLimited클라이언트가 속도 제한 또는 할당량을 초과함429
Timeout연산이 제시간에 완료되지 않음504
Network네트워크 수준 오류 (DNS, 연결, TLS)503
DependencyUnavailable외부 의존성이 다운되었거나 성능 저하됨503
Internal예기치 않은 서버 측 실패500
Database데이터베이스 실패 (쿼리, 연결, 마이그레이션)500
Service일반적인 서비스 계층/비즈니스 로직 실패500
Config누락되었거나 유효하지 않은 구성500
TurnkeyTurnkey 서브시스템 실패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)로 폴백합니다:

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_policyRedact이면 ProblemJsondetail, 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 매크로 · 컨텍스트와 메타데이터 · 웹 프레임워크