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

masterror

안정적인 코드, HTTP/gRPC 매핑, 내장 텔레메트리를 갖춘 프레임워크 독립적인 애플리케이션 오류 타입

한국어 English Русский

Crates.io docs.rs MSRV License


masterror란 무엇인가요?

masterrorDisplaysource()만으로는 부족한 Rust 서비스를 위한 오류 처리 워크스페이스입니다. thiserror가 트레이트 구현 파생에서 멈추고 anyhow가 타입이 소거된 전파에서 멈추는 반면, masterror는 오류를 전송 경계까지 끝까지 운반합니다:

  • AppError — 의미론적 범주(AppErrorKind), 안정적인 기계 판독 가능 코드(AppCode), 선택적인 안전한 공개 메시지, 구조화된 메타데이터, 전송 힌트(Retry-After, WWW-Authenticate)를 갖춘 풍부한 오류 값입니다.
  • 보수적인 HTTP 및 gRPC 매핑 — 모든 종류와 코드는 HTTP 상태, tonic::Code 판별값, RFC 7807 type URI에 결정론적으로 매핑됩니다.
  • 타입 기반 텔레메트리 — 메타데이터는 임시방편의 String 맵이 아니라 필드별 리덕션 정책을 갖춘 타입 기반 필드(문자열, 정수, 부동 소수점, 기간, IP, UUID, JSON)로 저장됩니다.
  • 네이티브 파생#[derive(Error)]thiserror 구문을 미러링하며, #[app_error(...)]#[derive(Masterror)]는 코드, 범주, 리덕션 및 매핑 테이블과 함께 도메인 오류를 AppError에 연결합니다.
  • 설계 단계부터의 리덕션 — 소스는 절대 클라이언트에 직렬화되지 않으며, 메시지, 세부 정보 및 메타데이터 필드는 경계에서 리덕션, 해시 또는 마스킹될 수 있습니다.

unsafe 코드가 없고, MSRV가 고정되어 있으며, 기본 std 기능을 비활성화하면 no_std를 지원합니다.

해결하는 문제

관심사thiserroranyhowmasterror
Display / source() 파생지원지원 (동일한 구문)
컨텍스트를 포함한 타입 소거 전파지원지원 (.ctx() / .context())
안정적인 기계 판독 가능 오류 코드수동수동AppCode, 와이어 계약의 일부
HTTP 상태 매핑수동수동AppErrorKind::http_status(), 안정적인 테이블
gRPC 상태 매핑수동수동CODE_MAPPINGS, tonic::Status 변환
RFC 7807 problem+json수동수동ProblemJson::from_app_error
구조화된 타입 기반 메타데이터Metadata + field::* 빌더
경계에서의 비밀 정보 리덕션MessageEditPolicy, FieldRedaction
tracing / metrics / backtrace 발행기능 게이트, 생성 시 자동

thiserror로 파생한 열거형은 무슨 일이 일어났는지 알려줍니다. masterror는 여기에 더해 클라이언트가 무엇을 보는지(상태, 코드, 안전한 메시지, problem+json), 운영자가 무엇을 보는지(구조화된 필드, tracing 이벤트, 카운터), 무엇이 절대 유출되지 않는지(소스, 리덕션된 필드)까지 결정합니다.

주요 기능

영역제공 내용
코어 분류 체계AppError, AppErrorKind (23개의 안정적인 범주), AppCode (SCREAMING_SNAKE_CASE 코드, 커스텀 코드 지원), AppResult<T>
파생#[derive(Error)], #[derive(Masterror)], #[app_error(...)], #[masterror(...)], #[provide(...)] 텔레메트리 프로바이더
제어 흐름ensure! / fail! — 타입 기반의 할당 없는 조기 반환
컨텍스트ResultExt::ctx / ResultExt::context, 호출자 추적을 갖춘 Context 빌더
와이어 페이로드ErrorResponse (레거시 JSON), 재시도 및 인증 힌트를 갖춘 ProblemJson (RFC 7807)
전송Axum IntoResponse, Actix ResponseError/Responder, tonic::Status, WASM JsValue, OpenAPI 스키마
통합sqlx, redis, reqwest, validator, config, tokio, teloxide, Telegram Mini Apps init data, Turnkey
관측성tracing 이벤트, metrics 카운터, 지연 backtrace 캡처, 컬러 터미널 출력, DisplayMode (prod/staging/local)

빠른 예제

use masterror::{AppError, AppErrorKind, AppResult, ProblemJson, field};

fn find_user(id: u64) -> AppResult<()> {
    masterror::ensure!(id != 0, AppError::bad_request("id must be non-zero"));

    Err(AppError::not_found("user not found")
        .with_field(field::u64("user_id", id))
        .with_field(field::str("request_id", "abc123")))
}

let err = find_user(42).unwrap_err();
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.kind.http_status(), 404);

let problem = ProblemJson::from_app_error(err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
assert_eq!(problem.grpc.expect("grpc").name, "NOT_FOUND");

또는 도메인 오류를 한 번만 선언하고 매핑은 파생 매크로에 맡길 수 있습니다:

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("missing flag: {name}")]
#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)]
struct MissingFlag {
    name: &'static str
}

let app: AppError = MissingFlag { name: "feature" }.into();
assert!(matches!(app.kind, AppErrorKind::BadRequest));

워크스페이스 크레이트

크레이트역할
masterror코어 오류 타입, 메타데이터, 전송, 통합, 프렐루드
masterror-derive#[derive(Error)]#[derive(Masterror)]를 지원하는 프로시저 매크로 (자동으로 가져옴)
masterror-template공유 #[error("...")] 템플릿 파서

문서

시작하기

핵심 개념

통합

고급

시작하기

이 페이지에서는 masterror 설치, 첫 AppError 반환, ensure!/fail!을 통한 단락 처리, 프렐루드 사용, 첫 파생 작성을 차례로 살펴봅니다.

설치

기본 빌드는 std 기능만 활성화합니다 — 웹 프레임워크도, 텔레메트리 백엔드도 포함되지 않습니다:

[dependencies]
masterror = "0.28"

필요한 통합을 그때그때 활성화하세요 (전체 목록은 기능 플래그 참조):

[dependencies]
masterror = { version = "0.28", features = ["axum", "serde_json", "tracing"] }

MSRV는 1.96입니다. 이 크레이트는 unsafe를 금지하며 default-features = false로 빌드하면 no_std를 지원합니다.

첫 오류

AppError는 의미론적 범주(AppErrorKind)와 선택적인 공개 메시지를 결합합니다. AppResult<T>Result<T, AppError>의 별칭입니다:

use masterror::{AppError, AppErrorKind, AppResult};

fn do_work(flag: bool) -> AppResult<()> {
    if !flag {
        return Err(AppError::new(AppErrorKind::BadRequest, "Flag must be set"));
    }
    Ok(())
}

let err = do_work(false).unwrap_err();
assert!(matches!(err.kind, AppErrorKind::BadRequest));
assert_eq!(err.kind.http_status(), 400);

모든 종류에는 이름 있는 생성자가 있으므로 호출 지점에서 AppErrorKind를 직접 쓸 일은 거의 없습니다:

use masterror::AppError;

let _ = AppError::not_found("user not found");        // 404
let _ = AppError::validation("invalid email");        // 422
let _ = AppError::unauthorized("token expired");      // 401
let _ = AppError::forbidden("no access");             // 403
let _ = AppError::conflict("already exists");         // 409
let _ = AppError::rate_limited("slow down");          // 429
let _ = AppError::internal("unexpected failure");     // 500
let _ = AppError::service("orchestration failed");    // 500
let _ = AppError::timeout("upstream timed out");      // 504
let _ = AppError::bare(masterror::AppErrorKind::NotFound); // no message

타입을 포기하지 않고도 구조화된 메타데이터와 업스트림 소스를 첨부할 수 있습니다:

use masterror::{AppError, field};

let err = AppError::service("downstream degraded")
    .with_field(field::str("request_id", "abc123"))
    .with_field(field::i64("attempt", 2))
    .with_context(std::io::Error::other("connection reset"));

assert_eq!(err.metadata().len(), 2);
assert!(err.source_ref().is_some());

소스는 로그와 chain() 순회에서 사용할 수 있지만 절대 클라이언트에 직렬화되지 않습니다.

ensure!와 fail!

ensure!fail!anyhow::ensure!/anyhow::bail!의 타입 기반 대안입니다. 오류 표현식은 지연 평가되므로 성공 경로에서는 포매팅도 할당도 수행하지 않습니다:

use masterror::{AppError, AppErrorKind, AppResult};

fn guard(flag: bool) -> AppResult<()> {
    masterror::ensure!(flag, AppError::bad_request("flag must be set"));
    Ok(())
}

fn bail() -> AppResult<()> {
    masterror::fail!(AppError::unauthorized("token expired"));
}

assert!(guard(true).is_ok());
assert!(matches!(guard(false).unwrap_err().kind, AppErrorKind::BadRequest));
assert!(matches!(bail().unwrap_err().kind, AppErrorKind::Unauthorized));

ensure!는 복잡한 조건을 위한 상세 형식도 지원합니다:

use masterror::{AppError, AppResult};

fn bounded(value: i32, max: i32) -> AppResult<()> {
    masterror::ensure!(
        cond = value <= max,
        else = AppError::service("value too large")
    );
    Ok(())
}

프렐루드

masterror::prelude는 코어 타입만 재수출합니다 (AppError, AppErrorKind, AppCode, AppResult, ErrorResponse, 그리고 해당 기능이 켜져 있을 때의 turnkey 헬퍼):

use masterror::prelude::*;

fn handler(flag: bool) -> AppResult<()> {
    if !flag {
        return Err(AppError::bad_request("Flag must be set"));
    }
    Ok(())
}

프레임워크 트레이트 구현(Axum IntoResponse, Actix Responder)은 기능 플래그로 활성화되며 별도의 임포트가 필요하지 않습니다.

외부 오류에 컨텍스트 추가하기

ResultExt는 임의의 Result<T, E: Error>AppResult<T>로 승격합니다:

use masterror::{AppErrorKind, Context, ResultExt, field};

fn read_config() -> Result<String, std::io::Error> {
    Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}

// Simple, anyhow-style message:
let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());

// Full control over category, code, metadata and redaction:
let err = read_config()
    .ctx(|| Context::new(AppErrorKind::Config).with(field::str("path", "app.toml")))
    .unwrap_err();
assert_eq!(err.kind, AppErrorKind::Config);

전체 Context API는 컨텍스트와 메타데이터를 참조하세요.

첫 파생

#[derive(Error)]thiserror 구문을 미러링하고, #[app_error(...)]AppError로의 변환을 추가합니다:

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("I/O failed: {source}")]
#[app_error(kind = AppErrorKind::Internal, code = AppCode::Internal, message)]
pub struct DomainError {
    #[from]
    #[source]
    source: std::io::Error
}

fn load() -> Result<(), DomainError> {
    Err(std::io::Error::other("disk offline").into())
}

let err = load().unwrap_err();
assert_eq!(err.to_string(), "I/O failed: disk offline");

let app: AppError = err.into();
assert!(matches!(app.kind, AppErrorKind::Internal));
  • #[error("...")]{field} 플레이스홀더를 사용하는 Display 템플릿을 정의합니다.
  • #[from]은 래퍼에 대한 From<std::io::Error>를 생성합니다.
  • #[source]는 내부 오류를 source()를 통해 전달합니다.
  • #[app_error(kind = ..., code = ..., message)]From<DomainError> for AppError(그리고 for AppCode)를 생성합니다. message 플래그는 Display 출력을 공개 메시지로 노출합니다.

열거형도 변형별 #[error]#[app_error] 속성으로 동일하게 동작합니다. 메타데이터, 리덕션 정책, gRPC/problem+json 매핑 테이블까지 필요하다면 #[derive(Masterror)]를 사용하세요 — Derive 매크로에서 다룹니다.

다음 단계


함께 보기: 기능 플래그 · 오류 종류와 코드 · Derive 매크로 · 컨텍스트와 메타데이터 · 마이그레이션

기능 플래그

masterror는 기본 빌드를 가볍게 유지합니다. 기본적으로 활성화되는 것은 std뿐입니다. 그 외의 모든 것 — 웹 전송, 텔레메트리, 라이브러리 통합 — 은 옵트인입니다. 이 페이지는 Cargo.toml에 선언된 모든 플래그에 대한 완전한 레퍼런스입니다.

[dependencies]
masterror = { version = "0.28", default-features = false }
# or with features:
# masterror = { version = "0.28", features = [
#   "std", "axum", "actix", "openapi",
#   "serde_json", "tracing", "metrics", "backtrace",
#   "colored", "sqlx", "sqlx-migrate", "reqwest",
#   "redis", "validator", "config", "tokio",
#   "multipart", "teloxide", "init-data", "tonic",
#   "frontend", "turnkey", "benchmarks"
# ] }

코어

플래그활성화 내용추가 의존성
std (기본값)표준 라이브러리 지원. 모든 런타임 통합에 필요합니다. no_std에서는 비활성화하세요 (no_std 지원 참조)

웹 전송

플래그활성화 내용추가 의존성
axumRFC 7807 JSON 본문을 갖춘 AppErrorProblemJsonIntoResponse; AppErrorKind::status_code()axum (json, multipart), serde_json
actixAppError의 Actix Web ResponseErrorProblemJsonResponderactix-web
multipartaxum::extract::multipart::MultipartErrorBadRequest 매핑 (axum 포함)axum 경유
openapi오류 페이로드가 OpenAPI 스펙에 나타나도록 ErrorResponseAppCodeutoipa::ToSchema 제공utoipa
serde_jsonAppError/ErrorResponse/ProblemJson의 구조화된 JSON details; FieldValue::Jsonfield::jsonserde_json
tonic정제된 메타데이터와 함께 오류를 tonic::Status로 변환; StatusConversionError 익스포트tonic

텔레메트리와 관측성

플래그활성화 내용추가 의존성
tracing오류 생성 시 구조화된 tracing 이벤트 발행tracing, log, log-mdc
metricsAppError마다 error_total{code,category} 카운터 증가metrics
backtrace지연 std::backtrace::Backtrace 캡처 (RUST_BACKTRACE 존중), with_backtrace() 빌더
colored자동 TTY 감지를 갖춘 컬러 여러 줄 터미널 출력; AppError의 풍부한 Displayowo-colors

비동기 및 IO 통합

각 통합 플래그는 라이브러리 오류를 분류 체계로 분류하는 From<...> for AppError 변환을 추가합니다:

플래그변환추가 의존성
sqlxsqlx_core::ErrorNotFound / Database (드라이버나 TLS가 없는 경량 sqlx-core)sqlx-core
sqlx-migratesqlx::migrate::MigrateErrorDatabase (migrate 기능만 켠 전체 sqlx)sqlx
redisredis::RedisErrorCacheredis
reqwestreqwest::ErrorTimeout / Network / ExternalApireqwest
tokiotokio::time::error::ElapsedTimeouttokio (time)
validatorvalidator::ValidationErrorsValidationvalidator
configconfig::ConfigErrorConfigconfig

sqlxsqlx-migrate는 의도적으로 분리되어 있습니다. 오류 분류에는 sqlx-core만 필요하지만, 마이그레이션 오류 매핑은 전체 sqlx 크레이트를 가져옵니다.

메시징과 봇

플래그변환추가 의존성
teloxideteloxide_core::RequestErrorRateLimited / Network / ExternalApi / Deserialization / Internalteloxide-core
init-datainit_data_rs::InitDataErrorTelegramAuth (Telegram Mini Apps init-data 검증)init-data-rs

프런트엔드와 도메인

플래그활성화 내용추가 의존성
frontendfrontend 모듈: WASM/브라우저 컨텍스트에서 오류를 wasm_bindgen::JsValue로 변환하고 console.error 로그 발행wasm-bindgen, js-sys, serde-wasm-bindgen
turnkeyturnkey 모듈: TurnkeyErrorKind, TurnkeyError, classify_turnkey_errorAppError로의 변환
benchmarksCriterion 벤치마크 스위트와 CI 베이스라인 도구 (로컬 프로파일링 전용)

기본 변환 (항상 사용 가능)

기능 플래그가 없어도 AppError는 이미 다음에서 변환됩니다:

소스대상 종류
std::io::ErrorInternal
StringBadRequest

레시피

problem+json, OpenAPI 문서, tracing을 갖춘 Axum 기반 REST API:

masterror = { version = "0.28", features = ["axum", "openapi", "serde_json", "tracing"] }

sqlx, 마이그레이션, metrics를 갖춘 데이터베이스 서비스:

masterror = { version = "0.28", features = ["sqlx", "sqlx-migrate", "metrics", "tracing"] }

gRPC 서비스:

masterror = { version = "0.28", features = ["tonic", "tracing", "backtrace"] }

Mini App 인증을 갖춘 Telegram 봇:

masterror = { version = "0.28", features = ["teloxide", "init-data", "reqwest", "tokio"] }

WASM 프런트엔드:

masterror = { version = "0.28", features = ["frontend", "serde_json"] }

no_std 임베디드 또는 라이브러리 타깃:

masterror = { version = "0.28", default-features = false }

참고 사항

  • sqlxsqlx-migrate를 제외한 모든 통합 플래그는 std를 포함합니다. 이 두 플래그는 플래그 수준에서 std에 독립적입니다.
  • axumactix는 응답 본문이 JSON이므로 serde_json을 전이적으로 가져옵니다.
  • 기능 플래그는 이미 활성화된 ErrorResponse/ProblemJson 필드의 와이어 계약을 절대 변경하지 않습니다. 기능(예: serde_jsondetails를 일반 텍스트에서 구조화된 JSON으로 업그레이드)이나 트레이트 구현만 추가합니다.

함께 보기: 시작하기 · 웹 프레임워크 · 통합 · 관측성 · no_std 지원

오류 종류와 코드

두 타입이 분류 체계의 근간을 이룹니다:

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

Derive 매크로

masterror는 번들된 masterror-derive 크레이트를 통해 두 가지 파생을 제공합니다:

  • #[derive(Error)]thiserror::Error의 드롭인 대체품(동일한 #[error], #[from], #[source], #[backtrace] 속성)으로, #[app_error(...)] 변환과 #[provide(...)] 텔레메트리로 확장되었습니다.
  • #[derive(Masterror)] — 동일한 구문을 기반으로 #[masterror(...)]를 통해 메타데이터, 리덕션 정책, 전송 매핑 테이블과 함께 도메인 오류를 masterror::Error에 직접 연결합니다.

둘 다 루트에서 재수출됩니다: use masterror::{Error, Masterror};.

#[error("...")] 템플릿

템플릿은 생성되는 Display 구현을 결정합니다. 플레이스홀더는 필드 이름({field}), 튜플 인덱스({0}) 또는 명시적 인수를 참조합니다. 파싱은 공유 masterror-template 크레이트가 처리하며 thiserror 의미론을 미러링합니다.

use masterror::Error;

#[derive(Debug, Error)]
#[error("{kind}: {message}")]
struct NamedError {
    kind:    &'static str,
    message: &'static str
}

#[derive(Debug, Error)]
#[error("{0} -> {1:?}")]
struct TupleError(&'static str, u8);

포매터 트레이트와 스펙

플레이스홀더는 전체 포매터 팔레트를 지원하며 — {x:?}, {x:#?}, {x:x}, {x:#X}, {x:b}, {x:o}, {x:e}, {x:E}, {x:p}{value:>8}이나 {ratio:.3} 같은 디스플레이 전용 스펙은 그대로 전달됩니다. 프로그래밍 방식의 템플릿 검사를 위해 masterror::error::templateErrorTemplate, TemplateFormatter, TemplateFormatterKind를 노출합니다.

포맷 인수와 프로젝션

템플릿은 self에 대한 표현식과 .field 단축 표기를 통한 필드 프로젝션을 포함하여 이름 있는 인수와 위치 인수를 지원합니다:

use masterror::Error;

#[derive(Debug, Error)]
#[error("{formatted}", formatted = self.message.to_uppercase())]
struct FormatArgExpressionError {
    message: &'static str
}

#[derive(Debug, Error)]
#[error("{}, {label}, {}", label = self.label, self.first, self.second)]
struct MixedImplicitArgsError {
    label:  &'static str,
    first:  &'static str,
    second: &'static str
}

#[derive(Debug, Error)]
#[error("{value}", value = .value)]
struct FieldShortcutError {
    value: &'static str
}

transparentfmt = ...

use masterror::Error;

#[derive(Debug, Error)]
#[error("inner failure")]
struct Inner;

// Forwards Display and source() to the single wrapped field
#[derive(Debug, Error)]
#[error(transparent)]
struct Wrapper(#[from] Inner);

// Delegate rendering to a function: fields first, formatter last
fn render(count: &usize, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
    write!(f, "count={count}")
}

#[derive(Debug, Error)]
#[error(fmt = crate::render)]
struct CustomFormat {
    count: usize
}

transparent는 정확히 하나의 필드를 요구하며 fmt나 템플릿 문자열과 함께 사용할 수 없습니다. fmt = path는 모든 필드에 대한 참조와 Formatter를 받는 함수를 가리킵니다.

필드 속성

속성효과
#[source]필드가 source()에서 반환됩니다. Option<E>가 지원됩니다.
#[from]래퍼에 대한 From<FieldType>을 생성합니다. 같은 필드에 #[source]를 함축합니다.
#[backtrace]필드가 오류 인트로스펙션을 통해 노출되는 std::backtrace::Backtrace(또는 Option<Backtrace>)를 보유하거나, #[source]와 결합되면 소스의 백트레이스에 위임합니다.

추론: 문자 그대로 source라는 이름의 필드는 자동으로 소스로 취급되며, std::backtrace::Backtrace(또는 Option<Backtrace>) 타입의 필드는 속성 없이도 백트레이스로 인식됩니다.

열거형은 변형별 #[error]와 변형별 #[from]/#[source]/#[backtrace]를 지원합니다:

use masterror::Error;

#[derive(Debug, Error)]
#[error("leaf failure")]
struct LeafError;

#[derive(Debug, Error)]
enum EnumError {
    #[error("unit failure")]
    Unit,
    #[error("{code}")]
    Code {
        code:  u16,
        #[source]
        cause: LeafError
    },
    #[error(transparent)]
    Wrapped(#[from] LeafError)
}

#[app_error(...)] — AppError로의 변환

파생된 오류가 AppError/AppCode로 어떻게 변환되는지 기록합니다. 옵션: kind(필수), code(선택), message(플래그), no_source(플래그).

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("missing flag: {name}")]
#[app_error(kind = AppErrorKind::BadRequest, code = AppCode::BadRequest, message)]
struct MissingFlag {
    name: &'static str
}

let app: AppError = MissingFlag { name: "feature" }.into();
assert!(matches!(app.kind, AppErrorKind::BadRequest));

let code: AppCode = MissingFlag { name: "other" }.into();
assert_eq!(code, AppCode::BadRequest);
  • kind = ...AppErrorKind를 선택하며 From<T> for AppError를 생성합니다.
  • code = ...는 추가로 From<T> for AppCode를 생성합니다.
  • messageDisplay 출력을 공개 메시지로 전달합니다. 메시지를 내부용으로 유지하려면 생략하세요.
  • no_source는 소스 첨부를 생략하고 변환 시 도메인 오류를 폐기합니다(Send + Sync + 'static이 아닌 타입용).

생성된 From<T> for AppError는 원본 도메인 오류를 소스로 첨부합니다: downcast_ref로 다운캐스트할 수 있고 chain()/root_cause()에 나타나며, error_generic_member_access를 지원하는 툴체인에서는 #[provide] 데이터가 AppError를 통해 전달됩니다. 소스 첨부에는 T: Send + Sync + 'static이 필요합니다.

열거형은 변형별로 매핑을 선택하며, 파생은 여전히 단일 From<Enum> for AppError를 발행합니다.

#[provide(...)] — 타입 기반 텔레메트리

std::error::Request(nightly error_generic_member_access; 사용 가능할 때 자동으로 컴파일에 포함됨)를 통해 타입 기반 컨텍스트를 노출합니다. Option 필드는 값이 채워졌을 때만 프로바이더를 등록합니다:

use masterror::{AppCode, AppErrorKind, Error};

#[derive(Clone, Debug, PartialEq, Eq)]
struct TelemetrySnapshot {
    name:  &'static str,
    value: u64
}

#[derive(Debug, Error)]
#[error("structured telemetry {snapshot:?}")]
#[app_error(kind = AppErrorKind::Service, code = AppCode::Service)]
struct StructuredTelemetryError {
    #[provide(ref = TelemetrySnapshot, value = TelemetrySnapshot)]
    snapshot: TelemetrySnapshot
}

소비자는 도메인 오류 또는 변환된 AppError에 대해 std::error::request_ref::<TelemetrySnapshot>(&err)로 스냅샷을 추출합니다 — 변환은 첨부된 소스를 통해 프로바이더를 전달합니다.

#[derive(Masterror)] — 엔드투엔드 도메인 오류

#[derive(Masterror)]Display, std::error::Error, From<T> for masterror::Error 그리고 컴파일 타임 전송 매핑 테이블까지 생성하며, 모두 하나의 #[masterror(...)] 속성으로 구성합니다:

use masterror::{
    AppCode, AppErrorKind, Error, Masterror, MessageEditPolicy, mapping::HttpMapping
};

#[derive(Debug, Masterror)]
#[error("user {user_id} missing flag {flag}")]
#[masterror(
    code = AppCode::NotFound,
    category = AppErrorKind::NotFound,
    message,
    redact(message, fields("user_id" = hash)),
    telemetry(
        Some(masterror::field::str("user_id", user_id.clone())),
        attempt.map(|value| masterror::field::u64("attempt", value))
    ),
    map.grpc = 5,
    map.problem = "https://errors.example.com/not-found"
)]
struct MissingFlag {
    user_id: String,
    flag:    &'static str,
    attempt: Option<u64>,
    #[source]
    source:  Option<std::io::Error>
}

let err = MissingFlag {
    user_id: "alice".into(),
    flag: "beta",
    attempt: Some(2),
    source: None
};
let converted: Error = err.into();
assert_eq!(converted.code, AppCode::NotFound);
assert_eq!(converted.kind, AppErrorKind::NotFound);
assert_eq!(converted.edit_policy, MessageEditPolicy::Redact);
assert!(converted.metadata().get("user_id").is_some());
assert_eq!(
    MissingFlag::HTTP_MAPPING,
    HttpMapping::new(AppCode::NotFound, AppErrorKind::NotFound)
);

#[masterror(...)] 옵션

옵션의미
code = AppCode::...공개 기계 판독 가능 코드
category = AppErrorKind::...의미론적 범주 (HTTP 상태 결정)
message포매팅된 Display 출력을 안전한 공개 메시지로 노출
redact(message)전송에서 메시지를 제거하도록 MessageEditPolicy::Redact 설정
redact(fields("name" = hash, "card" = last4))필드별 메타데이터 정책 재정의: hash, last4, redact, none
telemetry(expr, ...)Option<masterror::Field>로 평가되는 표현식. 값이 있는 필드는 Metadata에 삽입됩니다. 없을 때는 telemetry() 사용
map.grpc = <i32>gRPC 상태 코드 (tonic::Code 판별값과 일치)
map.problem = "<uri>"RFC 7807 type URI

생성되는 매핑 테이블

구조체의 경우 파생은 연관 상수를 발행하고, 열거형의 경우 변형별 매핑을 집계하는 배열과 슬라이스를 발행합니다:

형태상수
구조체T::HTTP_MAPPING: HttpMapping, T::GRPC_MAPPING: Option<GrpcMapping>, T::PROBLEM_MAPPING: Option<ProblemMapping>
열거형T::HTTP_MAPPINGS: [HttpMapping; N], T::GRPC_MAPPINGS: &'static [GrpcMapping], T::PROBLEM_MAPPINGS: &'static [ProblemMapping]

디스크립터 타입은 masterror::mapping에 있습니다 (HttpMapping::status()는 종류에서 HTTP 코드를 파생하고, GrpcMapping::status()i32를 반환하며, ProblemMapping::type_uri()는 URI를 반환합니다).

#[from], #[source], #[backtrace]#[derive(Masterror)]에서도 계속 동작합니다. 소스와 캡처된 백트레이스는 결과 masterror::Error에 자동으로 첨부되며, Arc로 감싼 소스는 추가 복제 없이 재사용됩니다.

파생 선택 가이드

필요사용
thiserror 스타일의 Display + source + From#[derive(Error)]
AppError/AppCode로의 변환까지#[derive(Error)] + #[app_error(...)]
std::error::Request를 통한 타입 기반 컨텍스트#[provide(...)] 추가
메타데이터, 리덕션 정책, gRPC/problem+json 테이블#[derive(Masterror)] + #[masterror(...)]

함께 보기: 시작하기 · 오류 종류와 코드 · 컨텍스트와 메타데이터 · 마이그레이션

컨텍스트와 메타데이터

masterror는 문자열로 이어 붙인 컨텍스트(format!("failed to X: {e}"))를 세 가지 구조화된 메커니즘으로 대체합니다: Context 빌더, 타입 기반 Metadata 필드, 그리고 전송 경계에서 적용되는 리덕션 정책입니다.

ResultExt: 외부 오류 승격하기

ResultExtE: Error + Send + Sync + 'static인 모든 Result<T, E>에 구현되어 있으며 두 가지 메서드를 제공합니다:

.context(msg) — anyhow 스타일

오류를 메시지로 감싸고, 원본 오류는 소스가 됩니다:

use masterror::ResultExt;

fn read_config() -> Result<String, std::io::Error> {
    Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}

let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());

기저 오류가 이미 masterror::Error인 경우 .context()는 그 분류를 보존합니다. 종류, 코드, 메타데이터, 편집 정책, 재시도 힌트, 세부 정보가 이어지고, 메시지만 교체되며 원본 오류는 소스로 유지됩니다.

.ctx(|| Context) — 완전한 제어

use masterror::{AppErrorKind, Context, ResultExt, field};

fn validate() -> Result<(), std::io::Error> {
    Err(std::io::Error::other("boom"))
}

let err = validate()
    .ctx(|| {
        Context::new(AppErrorKind::Validation)
            .with(field::str("phase", "validate"))
            .redact(true)
            .track_caller()
    })
    .unwrap_err();

assert_eq!(err.kind, AppErrorKind::Validation);
assert!(err.metadata().get("phase").is_some());

클로저는 오류 경로에서만 평가됩니다.

Context 빌더

메서드효과
Context::new(kind)대상 범주. AppCode는 해당 종류의 표준 매핑으로 기본 설정됩니다
.code(AppCode)공개 코드 재정의
.category(kind)범주 변경. 코드가 재정의되지 않은 한 코드를 동기화 상태로 유지합니다
.with(field)메타데이터 Field 첨부
.redact(bool)메시지 리덕션 토글 (MessageEditPolicy::Redact / Preserve)
.redact_field(name, FieldRedaction)이름 있는 필드의 리덕션 정책 재정의
.track_caller()호출 지점을 caller.file, caller.line, caller.column 메타데이터로 기록

메타데이터 필드

Metadata는 정렬된 인라인 할당 방식의 타입 기반 필드 맵입니다(필드 0–4개는 스택에 유지됨). masterror::field 모듈로 필드를 만듭니다:

빌더FieldValue 변형
field::str("key", value)Str(Cow<'static, str>)
field::i64("key", -1)I64
field::u64("key", 42)U64
field::f64("key", 0.5)F64
field::bool("key", true)Bool
field::uuid("key", uuid)Uuid
field::duration("key", dur)Duration
field::ip("key", addr)Ip (v4 또는 v6)
field::json("key", json!({...}))Json (serde_json 기능 필요)

오류를 생성할 때나 Context를 통해 필드를 첨부하세요:

use core::time::Duration;
use masterror::{AppError, FieldValue, field};

let err = AppError::service("downstream degraded")
    .with_field(field::str("request_id", "abc123"))
    .with_field(field::duration("elapsed", Duration::from_millis(1500)))
    .with_field(field::u64("attempt", 2));

assert_eq!(err.metadata().len(), 3);
assert_eq!(err.metadata().get("attempt"), Some(&FieldValue::U64(2)));

for (name, value) in err.metadata().iter() {
    println!("{name}={value}");
}

with_fields(iter)는 이터레이터로부터 확장하고, with_metadata(meta)는 컨테이너를 교체하며, Metadata::insert는 키를 덮어쓸 때 이전 값을 반환합니다.

리덕션 정책

메시지 정책: MessageEditPolicy

Preserve(기본값)는 공개 메시지를 유지하고, Redact는 전송에서 메시지를 제거하도록 지시합니다. 오류에 .redactable(), Context.redact(true), 또는 #[masterror(...)]redact(message)로 설정합니다:

use masterror::{AppError, MessageEditPolicy, ProblemJson};

let err = AppError::internal("db-3 credentials rejected").redactable();
assert_eq!(err.edit_policy, MessageEditPolicy::Redact);

let problem = ProblemJson::from_app_error(err);
assert!(problem.detail.is_none());

필드 정책: FieldRedaction

각 필드는 메타데이터가 ProblemJson으로 직렬화될 때 적용되는 자체 정책을 지닙니다:

정책공개 페이로드에 미치는 효과
None값이 그대로 유지됨
Redact필드가 완전히 제거됨
Hash값이 SHA-256 다이제스트로 대체됨
Last4마지막 네 글자를 제외한 전부가 마스킹됨
use masterror::{AppError, FieldRedaction, field};

let err = AppError::internal("payment failed")
    .with_field(field::str("card_number", "4111111111111111"))
    .redact_field("card_number", FieldRedaction::Last4);

비밀 정보로 보이는 흔한 이름에는 필드 생성 시 안전한 기본값이 자동으로 적용됩니다. password, secret, authorization, cookie, session, jwt, bearer, otp, pin을 포함하는 이름은 기본적으로 Redact가 되고, 토큰/키 계열 이름(api_token, refresh_token, key, apikey)은 기본적으로 Hash가 되며, 카드/계좌 세그먼트와 숫자 계열 세그먼트가 결합된 이름(card_number, iban_no, account_id)은 기본적으로 Last4가 됩니다. 감지는 대소문자를 구분하지 않습니다. 명시적인 redact_field/with_redaction이 항상 우선합니다.

오류 체인

오류는 전체 인과 체인을 유지합니다. chain()은 오류 자신부터 근본 원인까지 반복하고, root_cause()는 가장 깊은 오류로 바로 건너뜁니다:

use masterror::AppError;

let io_err = std::io::Error::other("disk offline");
let app_err = AppError::internal("db down").with_context(io_err);

let chain: Vec<_> = app_err.chain().collect();
assert_eq!(chain.len(), 2);
assert_eq!(app_err.root_cause().to_string(), "disk offline");

업스트림 오류를 첨부할 때는 with_context(...)를 권장합니다. 소유된 오류나 공유 Arc<dyn Error + Send + Sync> 값을 받아 기존 할당을 재사용합니다. with_source(...) / with_source_arc(...)는 더 낮은 수준의 대응 메서드입니다.

다운캐스팅

첨부된 소스는 isdowncast_ref/downcast_mut로 검사합니다:

use masterror::AppError;

let io_err = std::io::Error::other("disk offline");
let err = AppError::internal("boom").with_context(io_err);

assert!(err.is::<std::io::Error>());

if let Some(io) = err.downcast_ref::<std::io::Error>() {
    assert_eq!(io.to_string(), "disk offline");
}
  • is::<E>() — 직접 소스가 타입 E일 때 true (전체 체인을 순회하지 않음).
  • downcast_ref::<E>() — 소스를 E로 대여.
  • downcast::<E>() / downcast_mut::<E>() — 현재는 스텁입니다 (downcast는 항상 Err(self)를, downcast_mut는 항상 None을 반환). downcast_ref를 권장합니다.

더 깊은 매칭이 필요하면 chain()을 순회하며 각 요소에 source.is::<E>() / source.downcast_ref::<E>()를 사용하세요.

백트레이스

backtrace 기능을 사용하면 err.backtrace()RUST_BACKTRACE를 존중하는 지연 캡처된 std::backtrace::Backtrace를 반환하고, with_backtrace(bt)는 명시적 캡처를 첨부합니다. 오류가 다시 감싸질 때 백트레이스는 Arc로 공유되므로 .context() 체인은 다시 캡처하지 않습니다.


함께 보기: 시작하기 · 오류 종류와 코드 · Derive 매크로 · 관측성 · 모범 사례

웹 프레임워크

masterror는 전송 경계에서 오류를 HTTP로 매핑합니다. 도메인 코드는 AppResult<T>를 반환하고, 프레임워크 어댑터가 오류를 RFC 7807 application/problem+json 응답으로 변환하며, 텔레메트리를 플러시하고 리덕션을 적용합니다. 크레이트에는 AppError에 대한 IntoResponse / ResponseError 구현이 정확히 하나만 존재하므로, 매핑을 직접 작성할 일이 없습니다.

기능 플래그

기능활성화 내용
axumAppError, ProblemJson, ErrorResponse에 대한 IntoResponse; serde_json을 함께 가져옴
actixAppError에 대한 ResponseError; ProblemJson, ErrorResponse에 대한 Responder
multipartError에 대한 From<axum::extract::multipart::MultipartError> (axum을 함께 활성화)
openapiErrorResponse에 대한 utoipa 스키마
[dependencies]
masterror = { version = "0.28", features = ["axum"] }   # or ["actix"]

와이어 형식

두 어댑터 모두 ProblemJson을 직렬화합니다:

필드타입비고
typestring URI정규 문제 클래스, 예: https://errors.masterror.rs/not-found
titlestringAppErrorKind에서 파생된 짧은 요약
statusnumberHTTP 상태 코드
detailstring?공개 메시지; 오류가 리덕션 가능하면 생략됨
detailsobject?구조화된 세부 정보 (serde_json 기능)
codestring안정적인 기계 판독 가능 AppCode, 예: NOT_FOUND
grpcobject?멀티 프로토콜 클라이언트를 위한 { name, value } gRPC 매핑
metadataobject?Metadata에서 정제된 필드; 리덕션 시 생략됨

전송 힌트는 본문 필드가 아니라 헤더가 됩니다:

  • AppError::with_retry_after_secs(n)Retry-After: n
  • AppError::with_www_authenticate(challenge)WWW-Authenticate: challenge

내부 소스(std::error::Error 체인)는 로그에만 기록되며 클라이언트에게 직렬화되지 않습니다.

Axum

axum 기능은 AppError, ProblemJsonErrorResponse에 대해 IntoResponse를 구현하고, 오류 종류에서 파생된 axum::http::StatusCode를 반환하는 고유 메서드 AppError::http_status()를 추가합니다. 응답으로 변환하면 텔레메트리(tracing 이벤트, 메트릭 카운터, 지연 백트레이스)가 플러시됩니다 — 관측성을 참조하세요.

use axum::{Router, routing::get};
use masterror::{AppError, AppResult};

async fn handler() -> AppResult<&'static str> {
    Err(AppError::forbidden("no access"))
}

let app: Router = Router::new().route("/demo", get(handler));

힌트가 포함된 401:

use masterror::AppError;

let err = AppError::unauthorized("missing token")
    .with_retry_after_secs(7)
    .with_www_authenticate("Bearer realm=\"api\"");

위 코드는 상태 401, 헤더 Retry-After: 7WWW-Authenticate: Bearer realm="api", 그리고 다음 본문을 생성합니다:

{
  "type": "https://errors.masterror.rs/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "missing token",
  "code": "UNAUTHORIZED",
  "grpc": { "name": "UNAUTHENTICATED", "value": 16 }
}

핸들러의 도메인 오류

examples/axum-rest-api의 패턴: 도메인 열거형을 파생하고, 한 번만 AppError로 변환한 뒤, 크레이트의 IntoResponse를 재사용합니다.

use axum::response::{IntoResponse, Response};
use masterror::{AppError, Error};

#[derive(Debug, Error, Clone)]
pub enum UserError {
    #[error("user not found")]
    NotFound,
    #[error("email already exists")]
    DuplicateEmail,
    #[error("invalid email format")]
    InvalidEmail
}

impl From<UserError> for AppError {
    fn from(err: UserError) -> Self {
        match err {
            UserError::NotFound => AppError::not_found(err.to_string()),
            UserError::DuplicateEmail => AppError::conflict(err.to_string()),
            UserError::InvalidEmail => AppError::validation(err.to_string())
        }
    }
}

impl IntoResponse for UserError {
    fn into_response(self) -> Response {
        AppError::from(self).into_response()
    }
}

파생에 #[app_error(kind = ..., code = ...)]를 붙이면 From<UserError> for AppError 구현이 자동으로 생성됩니다 — Derive 매크로를 참조하세요.

Actix Web

actix 기능은 AppError에 대해 actix_web::ResponseError를 구현하므로, AppResult<T>를 반환하는 핸들러가 즉시 동작합니다. error_response()는 텔레메트리를 발행하고 ProblemJson::from_ref를 통해 동일한 problem+json 페이로드를 빌드합니다.

use actix_web::{App, HttpServer, get};
use masterror::{AppError, AppResult};

#[get("/forbidden")]
async fn forbidden() -> AppResult<&'static str> {
    Err(AppError::forbidden("no access"))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(forbidden))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

클라이언트는 다음과 함께 403을 받습니다:

{
  "type": "https://errors.masterror.rs/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "no access",
  "code": "FORBIDDEN",
  "grpc": { "name": "PERMISSION_DENIED", "value": 7 }
}

ProblemJsonErrorResponseResponder를 구현하므로 핸들러가 이를 직접 반환할 수 있습니다. 상태 매핑은 Axum과 동일한 안정적인 AppErrorKind → StatusCode 테이블을 사용합니다.

Multipart

multipart(axum을 함께 활성화)는 axum::extract::multipart::MultipartError를 파서 메시지를 보존하면서 AppErrorKind::BadRequestError로 변환합니다:

use axum::extract::multipart::Multipart;
use masterror::{AppErrorKind, Error};

async fn upload(mut multipart: Multipart) -> Result<(), Error> {
    while let Some(field) = multipart.next_field().await? {
        let _ = field.bytes().await?;
    }
    Ok(())
}

잘못된 형식의 클라이언트 페이로드는 500 대신 400 Bad Request로 표면화됩니다.

응답 수동 빌드

테스트나 커스텀 전송을 위해 프레임워크 없이 페이로드를 구성할 수 있습니다:

use masterror::{AppError, ProblemJson};

let problem = ProblemJson::from_app_error(AppError::not_found("resource not found"));
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");

ProblemJson::from_ref(&err)는 소비하는 대신 빌려오고, ProblemJson::from_error_response(resp)는 레거시 ErrorResponse 와이어 타입을 업그레이드합니다.

함께 보기: 오류 종류와 코드 · 통합 · 관측성 · 기능 플래그

통합

선택적 기능 플래그는 널리 쓰이는 서드파티 오류 타입에서 masterror::Error로의 From<...> 변환을 추가하므로, 호출 지점의 ? 하나로 구조화된 메타데이터를 갖춘 분류된 오류가 생성됩니다. 모든 변환은 안정적인 AppErrorKind를 선택하고 관측성을 위한 텔레메트리 필드(비밀 정보는 절대 포함하지 않음)를 첨부합니다.

변환 매트릭스

기능소스 타입결과 AppErrorKind
sqlxsqlx_core::error::ErrorNotFound, Conflict, Validation, Timeout, DependencyUnavailable, Config, BadRequest, Serialization, Deserialization, Network, Database, Internal
sqlx-migratesqlx::migrate::MigrateErrorDatabase (마이그레이션 단계 메타데이터 포함)
redisredis::RedisErrorCache (기본값), Timeout, DependencyUnavailable
reqwestreqwest::ErrorTimeout, Network, RateLimited, DependencyUnavailable, ExternalApi
validatorvalidator::ValidationErrorsValidation
configconfig::ConfigErrorConfig (config.phase 메타데이터 포함)
tokiotokio::time::error::ElapsedTimeout
serde_jsonserde_json::ErrorSerialization (I/O), Deserialization (구문/데이터/EOF)
teloxideteloxide_core::RequestErrorExternalApi, Unauthorized, RateLimited, Network, Deserialization, Internal
init-datainit_data_rs::InitDataErrorTelegramAuth
tonicmasterror::Errortonic::Status아웃바운드 매핑, 아래 참조
multipartaxum::extract::multipart::MultipartErrorBadRequest (웹 프레임워크 참조)

sqlx 및 sqlx-migrate

sqlxsqlx-core에만 의존합니다(드라이버 없음, TLS 없음). 주요 매핑:

  • Error::RowNotFoundNotFound
  • 풀 타임아웃 → Timeout; 풀 종료 및 I/O 실패 → DependencyUnavailable; TLS 오류 → Network
  • 제약 조건 위반은 sqlx 오류 종류로 분류됩니다: 유니크 및 외래 키 위반 → Conflict, not-null / check 위반 → Validation, 그 외 → Database
  • 인코딩 → Serialization, 디코딩 → Deserialization

데이터베이스 오류는 SQLSTATE와 제약 조건 이름을 메타데이터로 캡처합니다. 알려진 SQLSTATE 코드는 공개 AppCode를 재정의합니다: 23505USER_ALREADY_EXISTS, 23503CONFLICT, 23502/23514VALIDATION. 일시적인 SQLSTATE(40001 직렬화 실패, 55P03 잠금 획득 불가)는 재시도 힌트를 첨부합니다.

use masterror::{AppErrorKind, Error};

async fn load_user(pool: &sqlx::PgPool, id: i64) -> Result<User, Error> {
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(pool)
        .await?;          // RowNotFound becomes AppErrorKind::NotFound
    Ok(user)
}

sqlx-migrate는 전체 sqlx(여전히 기본 기능 없이)를 가져오며 sqlx::migrate::MigrateErrorDatabase로 매핑하고, 마이그레이션 단계와 버전을 메타데이터에 기록합니다.

redis

모든 redis::RedisError 값은 기본적으로 Cache로 매핑됩니다. 타임아웃 계열 오류는 Timeout이 되고 연결 실패는 DependencyUnavailable이 됩니다. 오류 카테고리와 코드는 메타데이터로 보존됩니다.

reqwest

reqwest를 외부 HTTP API의 클라이언트로 취급합니다:

  • is_timeout()Timeout
  • is_connect() / is_request()Network
  • HTTP 상태 오류: 429RateLimited, 408Timeout, 5xxDependencyUnavailable, 그 외 → ExternalApi
  • 나머지 모든 경우 → ExternalApi

메타데이터는 엔드포인트, 상태 및 저수준 플래그를 기록합니다. URL은 공개 페이로드에서 해싱/리덕션 대상으로 표시됩니다.

validator

validator::ValidationErrorsValidation이며, 메타데이터에 집계 컨텍스트를 포함합니다: 실패한 필드 이름(validation.fields), 필드 및 오류 개수, 그리고 첫 번째 검증 코드(validation.codes):

use masterror::AppResult;
use validator::Validate;

#[derive(Validate)]
struct Payload {
    #[validate(length(min = 5))]
    name: String
}

fn check(p: &Payload) -> AppResult<()> {
    p.validate()?;      // ValidationErrors -> AppErrorKind::Validation
    Ok(())
}

config, tokio, serde_json

  • config::ConfigErrorConfig이며, 실패 단계를 식별하는 config.phase 메타데이터 필드(not_found, file_parse, type 등)를 포함합니다.
  • tokio::time::error::ElapsedTimeout이며 timeout.source = "tokio::time::timeout" 메타데이터 필드를 포함합니다. 이 오류는 커스텀 메시지를 갖지 않으므로 클라이언트는 해당 종류의 고정 제목 "Operation timed out"을 보게 됩니다.
  • serde_json::ErrorError::classify()를 통해 분류됩니다: I/O → Serialization; 구문, 데이터 및 EOF → Deserialization.

teloxide

teloxide_core::RequestError 매핑:

변형AppErrorKind
ApiExternalApi (유효하지 않은 토큰 → Unauthorized)
MigrateToChatIdExternalApi
RetryAfterRateLimited
NetworkNetwork
InvalidJsonDeserialization
IoInternal

init-data (Telegram Mini Apps)

모든 init_data_rs::InitDataError 변형(누락/유효하지 않은 해시, 만료된 페이로드, 서명 실패)은 TelegramAuth로 매핑되어 Mini App 인증 실패를 일반적인 잘못된 요청과 구분합니다.

tonic (아웃바운드 gRPC)

tonic은 반대 방향으로 변환합니다: From을 통해 masterror::Errortonic::Status. AppCode는 HTTP에 사용되는 것과 동일한 CODE_MAPPINGS 테이블을 통해 정규 tonic::Code로 매핑됩니다. 상태에는 메타데이터 항목 app-code, app-http-statusapp-problem-type이 포함되며, 존재할 경우 재시도 및 www-authenticate 힌트도 함께 전달됩니다. 리덕션 가능한 오류는 메시지가 종류 레이블로 대체되고 메타데이터가 제거됩니다.

use masterror::AppError;
use tonic::{Code, Status};

let status = Status::from(AppError::not_found("missing"));
assert_eq!(status.code(), Code::NotFound);

frontend (WASM / 브라우저)

frontend 기능은 wasm-bindgen을 기반으로 AppErrorErrorResponse에 대한 masterror::frontend::BrowserConsoleExt 트레이트를 추가합니다:

  • to_js_value() — 오류를 wasm_bindgen::JsValue로 직렬화
  • log_to_browser_console()console.error를 통해 발행

둘 다 wasm32 타깃에서 동작합니다. 네이티브 타깃에서는 BrowserConsoleError::UnsupportedTarget을 반환합니다. 실패 모드(콘솔 없음, console.error 호출 불가, 직렬화 실패)는 BrowserConsoleError 열거형으로 처리됩니다.

use masterror::{AppError, frontend::BrowserConsoleExt};

let err = AppError::not_found("user not found");
err.log_to_browser_console()?;

turnkey

turnkey 기능은 masterror::turnkey에 작고 안정적인 도메인 분류 체계를 노출합니다:

TurnkeyErrorKindAppErrorKind
UniqueLabelConflict
RateLimitedRateLimited
TimeoutTimeout
AuthUnauthorized
NetworkNetwork
ServiceTurnkey

TurnkeyError::new(kind, msg)는 도메인 오류를 빌드하고, From<TurnkeyError> for AppErrorFrom<TurnkeyErrorKind> for AppErrorKind가 매핑을 수행합니다. classify_turnkey_error(&str)는 원시 프로바이더 메시지를 휴리스틱하게 (대소문자 무시, 단어 경계 인식) TurnkeyErrorKind로 분류합니다:

use masterror::turnkey::{TurnkeyError, TurnkeyErrorKind, classify_turnkey_error};
use masterror::{AppError, AppErrorKind};

let kind = classify_turnkey_error("429 rate-limit reached");
assert_eq!(kind, TurnkeyErrorKind::RateLimited);

let app: AppError = TurnkeyError::new(kind, "quota exceeded").into();
assert_eq!(app.kind, AppErrorKind::RateLimited);

함께 보기: 기능 플래그 · 웹 프레임워크 · 오류 종류와 코드 · 관측성

관측성

masterror는 텔레메트리를 오류 수명 주기의 일부로 취급합니다. 각 AppError는 더티 플래그를 추적하며, 텔레메트리는 상태 변화당 한 번 발행됩니다 — 생성 시, 변경 후, 또는 오류가 전송 경계(Axum IntoResponse, Actix error_response(), tonic Status 변환)를 넘을 때. 수동으로 호출할 일은 거의 없습니다.

기능 플래그

기능추가 내용
tracing오류당 구조화된 tracing 이벤트, log-mdc를 통한 trace_id
metricsmetrics 크레이트를 통한 error_total{code,category} 카운터
backtraceRUST_BACKTRACE로 제어되는 지연 std::backtrace::Backtrace 캡처
coloredTTY 감지가 포함된 ANSI 컬러 터미널 스타일링
[dependencies]
masterror = { version = "0.28", features = ["tracing", "metrics", "backtrace"] }

Tracing

tracing이 활성화되면 각 오류는 타깃 masterror::error로 ERROR 레벨 이벤트를 하나 발행합니다:

필드내용
codeAppCode 문자열, 예: NOT_FOUND
categoryAppErrorKind 레이블, 예: Database
message공개 메시지 (있는 경우)
retry_seconds재시도 힌트 (설정된 경우)
redactable전송 경계에서 메시지가 리덕션되는지 여부
metadata_len첨부된 메타데이터 필드 수
www_authenticate인증 챌린지 (설정된 경우)
trace_idlog-mdc 컨텍스트 키 trace_id에서 가져옴 (존재하는 경우)

발행은 subscriber를 인식합니다: 해당 타깃의 ERROR 레벨 이벤트에 관심 있는 subscriber가 없으면 이벤트는 보류 상태로 남아 다음 플러시에서 재시도되므로, subscriber가 늦게 설치되어도 아무것도 손실되지 않습니다.

오류를 요청과 연관시키려면 요청 미들웨어에서 MDC에 trace ID를 저장하세요:

log_mdc::insert("trace_id", request_id);

키가 설정된 동안 생성된 모든 오류는 이벤트에 해당 값을 포함합니다.

메트릭

metrics가 활성화되면 새로 더티 상태가 된 각 오류는 다음을 증가시킵니다:

error_total{code="NOT_FOUND", category="NotFound"}

두 레이블 모두 안정적인 문자열(AppCode::as_str()AppErrorKind 레이블)이므로, 도메인 타입을 리팩터링해도 대시보드와 알림이 유지됩니다. metrics 레코더(Prometheus, StatsD 등)는 평소처럼 연결하면 됩니다. masterrormetrics::counter!만 사용합니다.

백트레이스

backtrace가 활성화되면 Backtrace 스냅샷은 텔레메트리가 플러시될 때 지연 캡처됩니다 — 생성할 때마다 캡처되지 않습니다. 캡처는 RUST_BACKTRACE로 제어됩니다: 미설정, 빈 값, 0, off, false는 비활성화하고, 그 외 값은 활성화합니다. 이 설정은 한 번 읽혀 프로세스별로 캐시됩니다.

#[cfg(feature = "backtrace")] {
use masterror::AppError;

let err = AppError::internal("db down");
if let Some(bt) = err.backtrace() {
    eprintln!("{bt}");
}
}

AppError::with_backtrace(backtrace)로 미리 캡처한 트레이스를 첨부할 수도 있으며, 이는 지연 캡처보다 우선합니다.

.log()를 통한 수동 플러시

생성자와 변환은 텔레메트리를 자동으로 발행합니다. 오류를 변경한 후(필드 추가, 재시도 힌트 변경) 재발행을 강제할 수 있습니다:

use masterror::{AppError, field};

let err = AppError::service("upstream degraded")
    .with_field(field::str("upstream", "billing"));
err.log();

log()는 상태별로 멱등합니다: 마지막 발행 이후 변경된 것이 없으면 아무것도 하지 않습니다. HTTP/gRPC 어댑터도 경계에서 같은 방식으로 플러시하므로, 생성되고 보강된 뒤 Axum 핸들러에서 반환되는 오류는 상태당 한 번 — 생성 시 한 번, 보강된 상태에 대해 경계에서 한 번 — 발행되며, 같은 상태에 대해 두 번 발행되는 일은 없습니다.

체인 검사

기능과 무관하게 AppError는 로그 파이프라인에 필요한 도구를 노출합니다:

#[cfg(feature = "std")] {
use std::io::Error as IoError;
use masterror::AppError;

let err = AppError::internal("db down").with_context(IoError::other("disk offline"));

assert_eq!(err.chain().count(), 2);
let _root = err.root_cause();
assert!(err.metadata().is_empty());
}

metadata().iter_with_redaction()(key, value, policy) 트리플을 산출하므로 로깅 계층이 필드 리덕션을 존중할 수 있습니다 — 컨텍스트와 메타데이터를 참조하세요.

컬러 터미널 출력

colored 기능은 CLI 도구를 위한 masterror::colored::style을 추가합니다. 색상은 stderr가 TTY이고, NO_COLOR가 설정되지 않았으며, TERMdumb가 아니고, 터미널이 ANSI를 지원할 때만 적용됩니다. 그렇지 않으면 텍스트가 그대로 통과합니다. 감지 결과는 프로세스별로 캐시됩니다.

함수스타일용도
error_kind_critical빨간색치명적 실패 종류
error_kind_warning노란색복구 가능/경고 종류
error_code청록색기계 코드
error_message밝은 흰색메인 메시지
source_context흐리게보조/소스 정보
metadata_key초록색구조화된 필드 이름
#[cfg(feature = "colored")] {
use masterror::colored::style;

eprintln!(
    "{}: {}",
    style::error_code("ERR_DB_001"),
    style::error_message("Database connection failed")
);
}

실행 가능한 데모는 examples/colored_cli.rs를 참조하세요.

함께 보기: 기능 플래그 · 컨텍스트와 메타데이터 · 웹 프레임워크 · 모범 사례

no_std 지원

masterror는 Rust 표준 라이브러리 없이 빌드됩니다. 크레이트 루트는 #![cfg_attr(not(feature = "std"), no_std)]를 선언하며, 기본 std 기능이 임베디드/WASM 친화적 빌드와 여러분 사이에 있는 유일한 장벽입니다:

[dependencies]
masterror = { version = "0.28", default-features = false }

alloc 필요

masterrorno_std이지만 no_alloc아닙니다. 크레이트는 무조건 extern crate alloc을 선언하고 메시지, 메타데이터 및 소스 체인에 Cow<'static, str>, String, ArcBTreeMap을 사용합니다. 타깃에 전역 할당자가 필요합니다. 순수 core 전용 환경은 지원되지 않습니다.

std 없이 동작하는 것

프레임워크에 독립적인 코어 전체:

영역no_std에서 사용 가능
코어 타입Error / AppError, AppResult, AppErrorKind, AppCode
메타데이터Metadata, Field, FieldValue, FieldRedaction, field::* 헬퍼
컨텍스트Context, ResultExt::{ctx, context}
제어 흐름ensure!, fail!
파생모든 속성을 지원하는 #[derive(Error)], #[derive(Masterror)]
와이어 타입ProblemJson, ErrorResponse, CODE_MAPPINGS, mapping_for_code
인트로스펙션chain(), root_cause(), is/downcast/downcast_ref/downcast_mut, render_message()
Serdealloc과 함께 serde (와이어 타입의 JSON 직렬화)

오류 소스는 **core::error::Error**를 통해 동작합니다: 크레이트는 std::error::Error 대신 core::error::Error(내부적으로 CoreError로 별칭됨)를 구현하고 소비하므로, with_source(...), 소스 체인 및 다운캐스팅이 no_std 빌드에서 완전히 동작합니다.

use masterror::{AppCode, AppError, AppErrorKind, field};

let err = AppError::new(AppErrorKind::Timeout, "deadline exceeded")
    .with_field(field::u64("attempt", 3));

assert_eq!(err.code, AppCode::Timeout);
assert_eq!(err.metadata().len(), 1);

std가 필요한 것

모든 런타임 통합은 기능 정의에서 명시적으로 std를 다시 활성화합니다. Cargo.toml 기준:

  • tracing, metrics, backtrace, colored
  • axum, actix, multipart, tonic, openapi
  • serde_json, redis, validator, config, tokio, reqwest, teloxide, init-data, frontend, turnkey

backtracestd::backtrace::Backtrace와 환경 변수 접근이 필요하고, colored는 TTY 감지가 필요하며, 웹 및 클라이언트 통합은 그 자체가 std 전용인 호스트 크레이트가 필요합니다.

CI 기능 매트릭스

no_std CI 잡(.github/workflows/ci.yml)은 모든 풀 리퀘스트와 main 푸시마다 다음 조합을 검사합니다:

명령검증 내용
barecargo check --no-default-features진정한 no_std + alloc 빌드
std-onlycargo check --features std기본 std 표면
tracingcargo check --no-default-features --features tracing단일 텔레메트리 기능의 독립 빌드
metricscargo check --no-default-features --features metricsmetrics도 동일
coloredcargo check --no-default-features --features coloredcolored도 동일
all-featurescargo check --all-features전체 기능 합집합

의미에 주의하세요: bare 잡만이 진정한 no_std 컴파일입니다. tracing = [..., "std"], metrics = [..., "std"]colored = [..., "std"]는 전이적으로 std를 다시 활성화하므로, 해당 잡들은 기본 기능이 꺼져 있을 때 각 텔레메트리 기능이 자족적인지 검증할 뿐 — 텔레메트리가 표준 라이브러리 없이 동작한다는 뜻은 아닙니다. 텔레메트리가 필요하면 std가 필요합니다.

실용적 구성

전송에 독립적이고 no_std 호환을 유지하려는 라이브러리 크레이트:

[dependencies]
masterror = { version = "0.28", default-features = false }

[features]
std = ["masterror/std"]

바이너리 또는 서비스 크레이트는 std와 필요한 통합을 함께 켭니다:

[dependencies]
masterror = { version = "0.28", features = ["axum", "tracing", "metrics"] }

AppErrorKind, AppCode 및 와이어 타입이 no_std 코어에 있기 때문에, 도메인 크레이트는 오류를 분류하고 ProblemJson 페이로드까지 빌드할 수 있으며, HTTP 매핑은 서비스 크레이트에서만 이루어집니다 — 모범 사례를 참조하세요.

툴체인

크레이트는 Cargo.toml에서 rust-version = "1.96"으로 에디션 2024를 대상으로 합니다. no_std 소스 체인의 기반인 core::error::Error는 Rust 1.81부터 안정화되었으므로 nightly 기능은 전혀 사용되지 않습니다.

함께 보기: 기능 플래그 · 시작하기 · 모범 사례

모범 사례

masterror 기반 서비스를 예측 가능하게 유지하는 패턴: 타입 기반 도메인 오류, 하나의 안정적인 코드 분류 체계, 경계에서의 전송 매핑, 그리고 내부 정보를 절대 누출하지 않는 공개 메시지.

도메인 오류를 파생하고 한 번만 매핑

각 바운디드 컨텍스트를 #[derive(Error)]가 붙은 열거형으로 모델링하고 #[app_error(...)]AppError 매핑을 인라인으로 선언하세요. 파생은 Display, 래핑된 소스에 대한 From<...>, 그리고 AppError/AppCode로의 변환을 생성합니다 — 호출 지점마다 손으로 match를 작성할 필요가 없습니다.

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
pub enum UserError {
    #[error("user {0} not found")]
    #[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
    NotFound(u64),

    #[error("email already registered")]
    #[app_error(kind = AppErrorKind::Conflict, code = AppCode::Conflict, message)]
    DuplicateEmail,

    #[error("storage failure")]
    #[app_error(kind = AppErrorKind::Database, code = AppCode::Database)]
    Storage(#[from] std::io::Error)
}

let app: AppError = UserError::DuplicateEmail.into();
assert_eq!(app.kind, AppErrorKind::Conflict);

messageDisplay 출력이 클라이언트에게 보여도 안전한 변형에만 포함하세요. (Storage처럼) 생략하면 텍스트는 내부용으로 유지되고 — 클라이언트는 해당 종류의 제목만 보게 됩니다. 전체 속성 레퍼런스: Derive 매크로.

서비스당 하나의 AppCode 분류 체계

AppCode는 공개 API 계약이며, 클라이언트는 이를 기준으로 분기합니다. 집합을 작게, 문서화된 상태로, 서비스별로 유지하세요:

  • 내장 코드(NOT_FOUND, CONFLICT, VALIDATION 등)를 우선 사용하세요 — 이미 정규 HTTP/gRPC/problem-type 매핑을 갖고 있습니다.
  • 커스텀 코드는 호출 지점에서 인라인으로 만들지 말고 중앙에서 발행하세요:
use masterror::AppCode;

pub const CODE_PLAN_LIMIT: AppCode = AppCode::new("PLAN_LIMIT_EXCEEDED");

AppCode::newconst이며 SCREAMING_SNAKE_CASE가 아닌 것은 컴파일 타임에 패닉합니다. 런타임 문자열에는 AppCode::try_new를 사용하세요. 코드 이름 변경은 파괴적 API 변경입니다 — 추가는 열거형 변형 추가처럼 취급하세요.

전송 매핑은 경계에서만

도메인 및 서비스 계층은 AppResult<T>를 반환하며 HTTP에 대해 아무것도 모릅니다. 크레이트의 단일 IntoResponse/ResponseError/Status 구현이 핸들러 계층에서 매핑을 수행합니다:

async fn get_user(id: u64, repo: &Repo) -> masterror::AppResult<User> {
    repo.find(id).await?          // sqlx::Error -> AppError::NotFound/Database
}

비즈니스 로직에서 상태 코드를 직접 만들지 말고, 두 번째 응답 변환을 구현하지도 마세요 — 웹 프레임워크의 안정적인 AppErrorKind → status 테이블이 유일한 진실의 원천입니다.

민감 데이터는 리덕션하고 텔레메트리는 유지

독립적인 두 가지 조절 수단이 있습니다:

  • 메시지 리덕션err.redactable()(또는 #[masterror(...)]redact(message))은 로그에는 남기면서 와이어 페이로드에서 detail을 숨깁니다.
  • 필드 리덕션 — 메타데이터 직렬화 시 적용되는 필드별 정책:
use masterror::{AppError, FieldRedaction, field};

let err = AppError::bad_request("Invalid credentials")
    .with_field(field::str("email", "user@example.com").with_redaction(FieldRedaction::Hash))
    .with_field(field::str("card", "4111111111111111").with_redaction(FieldRedaction::Last4))
    .with_field(field::str("ip", "192.168.1.100").with_redaction(FieldRedaction::Redact));

Hash는 원시 문자열을 노출하지 않으면서 상관 분석 가치(같은 입력 → 같은 다이제스트)를 유지하고, Last4는 카드/토큰 접미사에 적합하며, Redact는 값을 완전히 제거합니다. 기본값은 None입니다. 사용자를 식별할 수 있는 것은 기본적으로 리덕션하고, 의식적으로만 예외를 두세요 — 그 반대가 아니라요.

Context vs 파생

  • 파생은 오류 타입이 도메인 어휘의 일부일 때 사용하세요: 변형이 있고, 시그니처에 나타나며, 매핑이 정적인 경우입니다.
  • Context(ResultExt::ctx 사용)는 호출 지점에서 인프라 오류를 즉석에서 래핑하고 분류가 타입이 아니라 작업에 따라 달라질 때 사용하세요:
#[cfg(feature = "std")] {
use masterror::{AppErrorKind, Context, ResultExt, field};

fn read_state() -> masterror::AppResult<Vec<u8>> {
    std::fs::read("/var/lib/app/state.bin").ctx(|| {
        Context::new(AppErrorKind::Internal)
            .with(field::str("path", "/var/lib/app/state.bin"))
            .track_caller()
    })
}
}

ctx는 지연 평가됩니다 — 클로저는 오류 경로에서만 실행됩니다. 사람이 읽을 수 있는 메모만 필요하면 단순한 .context("message")를 사용하세요. 자세한 내용: 컨텍스트와 메타데이터.

문자열이 아니라 종류와 코드를 테스트

포맷된 메시지가 아니라 안정적인 분류 체계에 대해 단언하세요:

use masterror::{AppCode, AppError, AppErrorKind, ProblemJson};

let err = AppError::not_found("user 42 missing");
assert_eq!(err.kind, AppErrorKind::NotFound);
assert_eq!(err.code, AppCode::NotFound);

let problem = ProblemJson::from_ref(&err);
assert_eq!(problem.status, 404);
assert_eq!(problem.code.as_str(), "NOT_FOUND");
  • ProblemJson::from_ref를 사용하면 통합 테스트가 서버를 띄우지 않고도 정확한 와이어 계약을 단언할 수 있습니다.
  • mapping_for_code(&code)는 테이블 기반 테스트를 위한 정규 HTTP 상태, gRPC 코드 및 problem-type URI를 노출합니다.
  • 리덕션 테스트에서는 redactable() 오류에 대해 problem.detail.is_none()을 단언하고 metadata().iter_with_redaction() 정책을 확인하세요.

공개 메시지 vs 내부 텔레메트리

생성하는 모든 오류에 유용한 규칙:

채널내용
message / detail사람 지향적이고, 민감하지 않으며, 사용자에게 보여도 될 만큼 안정적
Metadata 필드ID, 시도 횟수, 엔드포인트, 소요 시간 — 리덕션 정책과 함께 로그/메트릭용
source 체인원시 하위 오류 — 로그에만 기록되며, 클라이언트에게 절대 직렬화되지 않음

masterror는 마지막 행을 강제합니다(소스는 와이어 페이로드에 절대 기록되지 않음). 하지만 첫 두 행은 여러분의 책임입니다: 문자열에 브라우저에 출력하지 않을 내용이 포함되어 있다면, 리덕션 정책과 함께 메타데이터에 넣거나 오류를 redactable()로 표시하세요.

함께 보기: Derive 매크로 · 컨텍스트와 메타데이터 · 오류 종류와 코드 · 마이그레이션

마이그레이션

masterrorthiserror(파생 구문)와 anyhow(사용 편의성) 모두의 대체재로 설계되었습니다. 대부분의 마이그레이션은 의존성 교체와 타입 기반 기능의 점진적 도입으로 이루어집니다. 실행 가능한 워크스루: examples/migrate_from_thiserror.rsexamples/migrate_from_anyhow.rs.

thiserror에서

1단계는 기계적입니다 — import만 변경하면 됩니다. 파생 구문은 호환됩니다:

-use thiserror::Error;
+use masterror::Error;

속성 호환성

thiserrormasterror비고
{field} 플레이스홀더가 포함된 #[error("...")]동일위치 기반 {0} 포함 1:1
포맷 스펙 :>8, :.3, :x, :p, :e동일TemplateFormatter가 thiserror의 포매터 감지를 미러링
#[error(transparent)]동일Display/source를 전달하는 단일 필드 래퍼 강제
#[from]동일From<...> 생성, 래퍼 형태 검증
#[source]동일source() 체인 연결
#[backtrace]동일필드에서 존중됨
#[app_error(kind = ..., code = ..., message)]추가: From<YourError> for AppError(및 AppCode) 생성; messageDisplay를 공개 메시지로 전달
#[provide(ref = T, value = T)]추가: std::error::Request를 통한 타입 기반 텔레메트리; Option<T> 필드는 Some일 때만 제공
#[derive(Masterror)] + #[masterror(...)]추가: category, redact(message, fields(...)), telemetry(...), map.grpc, map.problem을 포함한 전체 매핑

기존 열거형은 변경 없이 계속 컴파일됩니다. 어노테이션을 추가하면 얻는 것:

use masterror::{AppCode, AppError, AppErrorKind, Error};

#[derive(Debug, Error)]
#[error("user {user_id} not found")]
#[app_error(kind = AppErrorKind::NotFound, code = AppCode::NotFound, message)]
struct UserMissing {
    user_id: u64
}

let app: AppError = UserMissing { user_id: 42 }.into();
assert_eq!(app.kind, AppErrorKind::NotFound);

열거형은 변형별로 매핑됩니다 — 각 변형이 자체 #[app_error(...)]를 가지며, 파생은 단일 From<Enum> for AppError를 발행합니다.

권장 순서: (1) import 교체, (2) API 경계를 넘는 타입에 #[app_error] 추가, (3) 손으로 작성한 From<DomainError> for AppError 구현을 생성된 것으로 교체, (4) 리덕션이나 메타데이터가 필요한 곳에 #[masterror(...)] 도입.

anyhow에서

anyhowmasterror비고
anyhow::Result<T>masterror::AppResult<T>Result<T, AppError>의 별칭
anyhow::Errormasterror::AppError / Error불투명한 덩어리 대신 종류, 코드, 메타데이터를 전달
.context("msg").context("msg")masterror::ResultExt를 통해 동일
.with_context(|| ...).ctx(|| Context::new(kind)...)anyhow처럼 지연 평가되지만 타입 기반 Context(종류, 코드, 필드, 리덕션)를 빌드
bail!(err)fail!(err)포맷 장치 없이 타입 기반 오류 표현식을 받음
ensure!(cond, "msg {x}")ensure!(cond, AppError::...)조건 + 타입 기반 오류; 성공 경로에서 포매팅 없음
err.chain()err.chain()소스 체인에 대한 동일한 이터레이터
err.root_cause()err.root_cause()동일
err.is::<E>() / downcast / downcast_ref / downcast_mutAppError에서 동일한 이름다운캐스팅 동등성
#[from] 스타일 래핑기능 플래그 뒤의 From<...> 구현sqlx/redis/reqwest/… 가 사전 분류된 상태로 도착

.context()는 기대한 그대로 동작합니다:

use masterror::{AppResult, ResultExt};

fn read_config(path: &str) -> AppResult<String> {
    let content = std::fs::read_to_string(path).context("Failed to read config file")?;
    Ok(content)
}

ensure!/fail!은 anyhow의 문자열 포매팅을 타입 기반 오류와 맞바꿉니다:

use masterror::{AppError, AppResult, ensure, fail, field};

fn parse(content: &str, path: &str) -> AppResult<()> {
    ensure!(
        !content.is_empty(),
        AppError::bad_request("Config file is empty")
            .with_field(field::str("path", path.to_owned()))
    );
    if content.starts_with("invalid") {
        fail!(AppError::bad_request("Invalid config format"));
    }
    Ok(())
}

오류 표현식은 가드가 걸릴 때만 평가되므로 해피 패스는 할당이 없는 상태로 유지됩니다 — anyhow와 동일한 보장에 더해 기계 판독 가능한 코드까지 얻습니다.

anyhow에 대응물이 없는 것

마이그레이션하면 anyhow에 상응하는 것이 없는 기능들을 얻습니다:

  • 타입 기반 분류 체계 — 문자열 기반 컨텍스트 대신 AppErrorKind(내부)와 AppCode(공개, SCREAMING_SNAKE_CASE). 오류 종류와 코드를 참조하세요.
  • 전송 매핑 — 동일한 코드 테이블에서 파생되는 Axum/Actix용 RFC 7807 problem+json 및 gRPC용 tonic::Status. 웹 프레임워크를 참조하세요.
  • 텔레메트리 — 경계에서의 자동 tracing 이벤트, error_total{code,category} 메트릭 및 지연 백트레이스. 관측성을 참조하세요.
  • 리덕션 — 모든 전송에서 존중되는 redactable() 메시지 및 필드별 Hash/Last4/Redact 정책. 모범 사례를 참조하세요.
  • 구조화된 메타데이터 — 값을 메시지에 포매팅하는 대신 타입 기반 field::str/u64/duration/ip/....

주의할 점

  • anyhow의 ensure!(cond, "format {}", x) 포맷 메시지 형태에는 직접적인 대응물이 없습니다: 오류를 명시적으로 구성하거나 (AppError::bad_request(format!(...))) — 더 좋게는 — 정적 메시지에 메타데이터 필드를 더하세요.
  • anyhow::Error는 임의의 E: Error + Send + Sync를 받습니다. masterror에서는 래핑 시점에 종류를 선택합니다(Context::new(kind) 또는 From 변환) — 이 결정 지점은 마찰이 아니라 기능입니다: 바로 여기서 분류가 일어납니다.
  • ensure!fail!은 모두 return Err(...)로 확장되므로, 표현식이 이미 오류 타입인 Result<_, E>를 반환하는 모든 함수에서 동작합니다 — Into 변환은 삽입되지 않습니다.

함께 보기: 시작하기 · Derive 매크로 · 컨텍스트와 메타데이터 · 모범 사례