Axum에서 오류 처리
Source: Dev.to
Axum은 Tokio와 Hyper 위에 구축된 Rust용 웹 프레임워크입니다.
그 오류‑처리 모델은 단순하고 예측 가능합니다: 모든 오류는 결국 HTTP 응답이 되어야 합니다. 이를 가능하게 하는 핵심 트레이트는 IntoResponse입니다. IntoResponse를 구현하는 모든 것은 핸들러에서 반환될 수 있으며, Axum은 이를 HTTP 응답으로 변환합니다.
1. StatusCode를 직접 반환하기
오류를 표시하는 가장 쉬운 방법은 핸들러에서 StatusCode를 반환하는 것입니다.
use axum::{
extract::Path,
http::StatusCode,
routing::get,
Router,
};
async fn get_user(Path(id): Path) -> Result {
if id == 0 {
return Err(StatusCode::NOT_FOUND);
}
Ok(format!("User #{}", id))
}
#[tokio::main]
async fn main() {
// ✅ Use `{id}` in the route pattern
let app = Router::new().route("/users/{id}", get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
- 핸들러가
Ok를 반환하면 Axum은 200 OK를 전송합니다. Err(StatusCode)를 반환하면 Axum은 전달된 상태 코드를 전송합니다.
단순히 StatusCode만 반환하면 왜 실패했는지에 대한 설명이 부족하므로, 대부분의 경우 더 많은 정보를 함께 반환하는 것이 좋습니다.
2. (StatusCode, String) 튜플 반환하기
상태 코드와 일반 텍스트 메시지를 포함하는 튜플을 반환할 수 있습니다. 이 튜플은 자동으로 IntoResponse를 구현합니다.
use axum::{
extract::Path,
http::StatusCode,
routing::post,
Json,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
async fn create_user(
Json(payload): Json,
) -> Result {
if payload.username.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
"Username cannot be empty".to_string(),
));
}
Ok(format!("Created user: {}", payload.username))
}
3. 전체 애플리케이션을 위한 단일 오류 enum
실제 프로젝트에서는 보통 모든 가능한 실패 모드를 포괄하는 하나의 오류 타입을 정의하고, 그에 대해 IntoResponse를 구현합니다.
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
/// 애플리케이션 전역 오류 타입
enum AppError {
NotFound(String),
Unauthorized,
InternalError(String),
}
/// `AppError`를 HTTP 응답으로 변환
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::Unauthorized => (
StatusCode::UNAUTHORIZED,
"You are not authorized".to_string(),
),
AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
};
(status, message).into_response()
}
}
// 커스텀 오류 타입을 사용하는 예시 핸들러
async fn get_document(Path(id): Path) -> Result {
if id == 99 {
return Err(AppError::Unauthorized);
}
if id > 100 {
return Err(AppError::NotFound(format!("Document {} not found", id)));
}
Ok(format!("Document #{}", id))
}
Pattern: 하나의 오류 타입, 응답으로 변환되는 방식을 정의하는 한 곳.
4. JSON error bodies
APIs는 일반 텍스트 대신 JSON 오류 페이로드를 반환하는 경우가 많습니다. 아래는 이를 구현하는 일반적인 방법입니다.
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
#[derive(Serialize)]
struct ErrorBody {
error: String,
code: u16,
}
enum ApiError {
NotFound(String),
BadRequest(String),
Internal,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
ApiError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
"Something went wrong".to_string(),
),
};
let body = Json(ErrorBody {
error: message,
code: status.as_u16(),
});
(status, body).into_response()
}
}
Resulting response example
{
"error": "Document 999 not found",
"code": 404
}
5. ?와 From을 이용한 오류 전파
실제 코드에서는 하위 레벨 라이브러리에서 발생한 오류를 ? 연산자를 사용해 상위로 전파하고 싶을 때가 많습니다. AppError에 대해 From을 구현하면 변환이 자동으로 이루어집니다.
use std::num::ParseIntError;
enum AppError {
BadRequest(String),
Internal(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
// Same implementation as before (omitted for brevity)
unimplemented!()
}
}
// Convert `ParseIntError` into `AppError`
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::BadRequest(format!("Invalid number: {}", err))
}
}
async fn parse_id(Path(raw): Path) -> Result {
// `?` calls the `From` impl on failure
let id: u32 = raw.parse()?;
Ok(format!("Parsed id: {}", id))
}
raw.parse() 뒤의 ?는 ParseIntError를 자동으로 AppError::BadRequest로 변환하므로, 별도의 수동 변환이 필요하지 않습니다.
6. 빠른 프로토타이핑을 위한 anyhow 사용
세밀한 오류 타입이 필요하지 않을 때, anyhow 크레이트는 편리한 범용 오류 타입을 제공합니다.
use anyhow::Context;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
/// Wrapper that implements `IntoResponse` for `anyhow::Error`
struct AnyhowError(anyhow::Error);
impl IntoResponse for AnyhowError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal error: {}", self.0),
)
.into_response()
}
}
// Allow any error that can be turned into `anyhow::Error` to be converted
impl<E> From<E> for AnyhowError {
fn from(err: E) -> Self {
AnyhowError(err.into())
}
}
async fn risky_handler() -> Result {
let data = std::fs::read_to_string("config.txt")
.context("Failed to read config file")?;
Ok(data)
}
이제 E가 Into를 구현하는 모든 Result에 ?를 사용할 수 있습니다.
Source: …
7. HandleError 로 Tower 서비스 오류 변환하기
Tower 서비스나 미들웨어가 자체 오류 타입을 반환하는 경우, Axum의 HandleError 레이어를 사용하면 해당 오류를 응답으로 매핑할 수 있습니다.
use axum::{
error_handling::HandleError,
http::StatusCode,
response::IntoResponse,
routing::get,
Router,
};
use std::convert::Infallible;
use std::time::Duration;
use tower::timeout::TimeoutLayer;
// Example service that may time out
async fn timeout_handler() -> Result {
Ok("All good".to_string())
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route(
"/",
get(
timeout_handler
.layer(TimeoutLayer::new(Duration::from_secs(1)))
.handle_error(|err| async move {
// Convert the timeout error into a response
(
StatusCode::REQUEST_TIMEOUT,
format!("Request timed out: {}", err),
)
.into_response()
}),
),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
HandleError 를 사용하면 서비스의 오류 타입을 IntoResponse 구현으로 변환하는 사용자 정의 로직을 정의할 수 있어, 나머지 코드를 깔끔하게 유지할 수 있습니다.
요약
- **
IntoResponse**는 Rust 오류와 HTTP 응답을 연결하는 다리입니다. StatusCode또는(StatusCode, String)와 같이 간단하게 시작하세요.- 규모가 큰 애플리케이션에서는 단일 오류 열거형을 정의하고
IntoResponse를 한 번만 구현합니다. - API 사용자를 위해 JSON 본문을 반환합니다.
From구현을 사용하면?연산자가 오류를 자동으로 전파하도록 할 수 있습니다.anyhow는 상세한 오류 계층 구조가 필요 없는 프로토타입에 편리합니다.HandleError는 Tower 서비스나 미들웨어에서 발생한 오류를 변환하는 데 도움을 줍니다.
이러한 패턴을 사용하면 어떤 Axum 애플리케이션에서도 견고하고 사용하기 쉬운 오류 처리를 구축할 수 있습니다.
Tower & Axum 오류 처리 예제
use axum::{
http::StatusCode,
response::IntoResponse,
routing::get,
Router,
};
use std::convert::Infallible;
// A function that may fail
async fn might_fail() -> Result {
Err("Something broke".to_string())
}
// Wrap the service to handle its errors
let service = tower::service_fn(|_req| async {
Ok::("ok")
});
let handled = HandleError::new(service, |err: String| async move {
(StatusCode::INTERNAL_SERVER_ERROR, err)
});
이것은 더 고급이며 주로 서드파티 Tower 컴포넌트를 통합할 때 관련됩니다.
오류 처리 패턴
| 패턴 | 적합한 경우 |
|---|---|
Result | 간단한, 코드 전용 오류 |
Result with (StatusCode, String) | 메시지가 포함된 오류 |
Custom enum + impl IntoResponse | 타입이 지정된 오류가 필요한 프로덕션 앱 |
impl IntoResponse returning Json | JSON 오류 본문을 반환하는 API |
impl From for AppError | 라이브러리 오류와 ? 사용 |
anyhow wrapper | 빠른 프로토타입이나 스크립트 |
대부분의 앱에 권장되는 접근 방식
-
중심 오류 타입 정의
#[derive(Debug)] enum AppError { NotFound, InvalidInput(String), Internal(String), // …other variants } -
IntoResponse구현 (보통 JSON 반환이 가장 좋음)impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { let (status, body) = match self { AppError::NotFound => ( StatusCode::NOT_FOUND, json!({ "error": "resource not found" }) ), AppError::InvalidInput(msg) => ( StatusCode::BAD_REQUEST, json!({ "error": msg }) ), AppError::Internal(msg) => ( StatusCode::INTERNAL_SERVER_ERROR, json!({ "error": msg }) ), }; (status, Json(body)).into_response() } } -
서드파티 오류 변환
impl From<LibError> for AppError { fn from(err: LibError) -> Self { // map `LibError` variants to `AppError` variants AppError::Internal(err.to_string()) } } -
핸들러에서
Result사용async fn handler() -> Result { let data = some_lib_call().await?; // `?` works because of `From` Ok(Json(data)) }
그게 전부입니다. 숨겨진 panic 없이 명시적이고 타입이 지정된 오류 경로만 있습니다.