웹 프레임워크
masterror는 전송 경계에서 오류를 HTTP로 매핑합니다. 도메인 코드는
AppResult<T>를 반환하고, 프레임워크 어댑터가 오류를
RFC 7807 application/problem+json 응답으로 변환하며, 텔레메트리를 플러시하고
리덕션을 적용합니다. 크레이트에는 AppError에 대한 IntoResponse /
ResponseError 구현이 정확히 하나만 존재하므로, 매핑을 직접 작성할 일이
없습니다.
기능 플래그
| 기능 | 활성화 내용 |
|---|---|
axum | AppError, ProblemJson, ErrorResponse에 대한 IntoResponse; serde_json을 함께 가져옴 |
actix | AppError에 대한 ResponseError; ProblemJson, ErrorResponse에 대한 Responder |
multipart | Error에 대한 From<axum::extract::multipart::MultipartError> (axum을 함께 활성화) |
openapi | ErrorResponse에 대한 utoipa 스키마 |
[dependencies]
masterror = { version = "0.28", features = ["axum"] } # or ["actix"]
와이어 형식
두 어댑터 모두 ProblemJson을 직렬화합니다:
| 필드 | 타입 | 비고 |
|---|---|---|
type | string URI | 정규 문제 클래스, 예: https://errors.masterror.rs/not-found |
title | string | AppErrorKind에서 파생된 짧은 요약 |
status | number | HTTP 상태 코드 |
detail | string? | 공개 메시지; 오류가 리덕션 가능하면 생략됨 |
details | object? | 구조화된 세부 정보 (serde_json 기능) |
code | string | 안정적인 기계 판독 가능 AppCode, 예: NOT_FOUND |
grpc | object? | 멀티 프로토콜 클라이언트를 위한 { name, value } gRPC 매핑 |
metadata | object? | Metadata에서 정제된 필드; 리덕션 시 생략됨 |
전송 힌트는 본문 필드가 아니라 헤더가 됩니다:
AppError::with_retry_after_secs(n)→Retry-After: nAppError::with_www_authenticate(challenge)→WWW-Authenticate: challenge
내부 소스(std::error::Error 체인)는 로그에만 기록되며 클라이언트에게
직렬화되지 않습니다.
Axum
axum 기능은 AppError, ProblemJson 및 ErrorResponse에 대해
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: 7 및
WWW-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 }
}
ProblemJson과 ErrorResponse도 Responder를 구현하므로 핸들러가 이를
직접 반환할 수 있습니다. 상태 매핑은 Axum과 동일한 안정적인
AppErrorKind → StatusCode 테이블을 사용합니다.
Multipart
multipart(axum을 함께 활성화)는
axum::extract::multipart::MultipartError를 파서 메시지를 보존하면서
AppErrorKind::BadRequest의 Error로 변환합니다:
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 와이어
타입을 업그레이드합니다.