Axum/Actix Web で IntoResponse を用いたエレガントなエラーハンドリング
James Reed
Infrastructure Engineer · Leapcell

はじめに
Webサービスの世界では、堅牢なエラーハンドリングが最重要です。不正なリクエスト、データベースの問題、または内部サーバー障害など、問題が発生した場合、サーバーはそれをクライアントに明確かつ効果的に伝える必要があります。これには通常、適切なHTTPステータスコードと説明的なエラーメッセージを返すことが含まれます。型安全性とエラー処理能力で知られるRustという言語では、Result enum はエラー伝播の基本的な構成要素です。しかし、Axum や Actix Web のような Web フレームワークのハンドラ関数から単に Result を返すだけでは、ユーザーフレンドリーな HTTP レスポンスには直接変換されません。ここで IntoResponse トレイトが不可欠になります。これは、アプリケーションの Result 型、特に Err バリアントを、構造化された HTTP エラーレスポンスにシームレスにマッピングすることを可能にし、開発者エクスペリエンスと API の明確さの両方を向上させます。この強力なメカニズムが、Web サービスの error handling を向上させるのにどのように機能するかを詳しく見ていきましょう。
コアコンセプトの理解
詳細に入る前に、議論の中心となるいくつかの重要な用語を明確にしましょう。
Result<T, E>: これは、成功または失敗する可能性のある操作のためのRustの標準enumです。Ok(T)(値Tでの成功を表す) とErr(E)(エラー値Eでの失敗を表す) の2つのバリアントがあります。IntoResponseトレイト: Axum と Actix Web の両方で提供されるこのトレイト(Axum ではIntoResponse、Actix Web ではResponderというわずかに異なる名前ですが、類似の目的を果たします)は、型が HTTP レスポンスにどのように変換できるかを定義します。IntoResponseを実装する任意の型は、Web ハンドラから直接返すことができます。- HTTP ステータスコード: HTTP リクエストの結果を示す標準的な 3 桁の数字です。エラーの場合、一般的なコードには
400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、500 Internal Server Errorなどがあります。 - JSON (JavaScript Object Notation): Web サービスからクライアントに構造化されたエラーメッセージを送信するためによく使用される、軽量なデータ交換フォーマットです。
エレガントなエラー変換の原則
コアアイデアは、カスタムエラー型に対して IntoResponse トレイトを実装することです。ハンドラ関数が Result<T, E> を返し、それが Err(e) として評価された場合、Web フレームワークは E 型の IntoResponse 実装を探します。見つかった場合、その実装を使用してエラーを適切な HTTP レスポンスに変換します。これにより、エラーから HTTP レスポンスへのマッピングロジックを一元化し、ハンドラ関数をクリーンに保ち、ビジネスロジックに集中させることができます。
Axum と Actix Web の両方の例でこれを説明しましょう。
Axum の実装
Axum は、エラーハンドリングのために IntoResponse トレイトを効果的に活用します。カスタムエラーenumを定義し、次にそれに対して IntoResponse を実装します。
use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde::Serialize; use thiserror::Error; // エラー型を導出するための人気のクレート // 1. カスタムエラー enum を定義する #[derive(Error, Debug)] pub enum AppError { #[error("無効な入力データ: {0}")] ValidationError(String), #[error("リソースが見つかりません: {0}")] NotFound(String), #[error("データベースエラー: {0}")] DatabaseError(#[from] sqlx::Error), // DB エラーとの統合例 #[error("内部サーバーエラー")] InternalServerError, } // 2. 標準化された HTTP エラーボディ用の struct を定義する #[derive(Serialize)] struct ErrorResponse { code: u16, message: String, details: Option<String>, } // 3. カスタムエラー enum に対して IntoResponse を実装する impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match self { AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), AppError::DatabaseError(err) => { eprintln!("データベースエラー: {:?}", err); // 内部エラーをログに記録 (StatusCode::INTERNAL_SERVER_ERROR, "データベース操作に失敗しました".to_string()) }, AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "予期しないエラーが発生しました".to_string()), }; let body = Json(ErrorResponse { code: status.as_u16(), message: error_message, details: None, // 必要に応じて詳細を追加できます }); (status, body).into_response() } } // Axum ハンドラ例 async fn create_user() -> Result<Json<String>, AppError> { // 検証ロジックをシミュレートする let is_valid = false; if !is_valid { return Err(AppError::ValidationError("ユーザー名は空にできません".to_string())); } // データベース操作をシミュレートする let db_success = false; if !db_success { // 実際のアプリでは、これは実際の sqlx::Error になります return Err(AppError::DatabaseError(sqlx::Error::RowNotFound)); } Ok(Json("ユーザーが正常に作成されました".to_string())) } // これを実行するには: // async fn main() { // let app = axum::Router::new().route("/users", axum::routing::post(create_user)); // let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); // axum::serve(listener, app).await.unwrap(); // }
この Axum の例では、
AppErrorを定義して、さまざまなアプリケーション固有のエラーをカプセル化します。ErrorResponseは、クライアントに送信されるエラーメッセージの一貫した構造を提供します。impl IntoResponse for AppErrorブロックにはコアロジックが含まれています。各AppErrorバリアントは、適切なStatusCodeとエラーメッセージにマッピングされ、JSON にシリアライズされて HTTP レスポンスボディとして返されます。create_userハンドラは、単にResult<_, AppError>を返すことができます。Err(AppError::...)が返された場合、Axum は自動的にそれにinto_responseを呼び出します。
Actix Web の実装
Actix Web は、同様の機能のために Responder トレイトを使用します。
use actix_web::{ dev::HttpResponseBuilder, // カスタムレスポンスの構築用 http::StatusCode, web::Json, HttpResponse, ResponseError, // 実装するトレイト }; use serde::Serialize; use thiserror::Error; // 1. カスタムエラー enum を定義する #[derive(Error, Debug)] pub enum ServiceError { #[error("検証に失敗しました: {0}")] ValidationFailed(String), #[error("認証が必要です") ] Unauthorized, #[error("データベースの問題: {0}")] DbError(#[from] sqlx::Error), #[error("何かがうまくいきませんでした") ] InternalError, } // 2. 標準化された HTTP エラーボディ用の struct を定義する #[derive(Serialize)] struct ApiError { status: u16, message: String, } // 3. カスタムエラー enum に対して ResponseError を実装する impl ResponseError for ServiceError { fn status_code(&self) -> StatusCode { match *self { ServiceError::ValidationFailed(_) => StatusCode::BAD_REQUEST, ServiceError::Unauthorized => StatusCode::UNAUTHORIZED, ServiceError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_response(&self) -> HttpResponse { let status = self.status_code(); let error_message = match self { ServiceError::ValidationFailed(msg) => msg.clone(), ServiceError::Unauthorized => "認証に失敗しました".to_string(), ServiceError::DbError(err) => { eprintln!("Actix DB エラー: {:?}", err); // 内部エラーをログに記録 "データベースエラーが発生しました".to_string() }, ServiceError::InternalError => "予期しないエラーが発生しました".to_string(), }; HttpResponseBuilder::new(status).json(ApiError { status: status.as_u16(), message: error_message, }) } } // Actix Web ハンドラ例 async fn get_item() -> Result<Json<String>, ServiceError> { let item_id_exists = false; // アイテムが見つからないことをシミュレートする if !item_id_exists { return Err(ServiceError::ValidationFailed( "アイテム ID が欠落しているか無効です".to_string(), )); } // 認可チェックをシミュレートする let is_authorized = false; if !is_authorized { return Err(ServiceError::Unauthorized); } Ok(Json("アイテムの詳細".to_string())) } // これを実行するには: // #[actix_web::main] // async fn main() -> std::io::Result<()> { // use actix_web::{web, App, HttpServer}; // HttpServer::new(|| { // App::new().route("/items", web::get().to(get_item)) // }) // .bind("127.0.0.1:8080")? // .run() // .await // }
Actix Web の例では、
ResponseErrorを実装するServiceErrorを定義します。ResponseErrorトレイトは、status_codeとerror_responseメソッドを必要とします。status_codeは HTTP ステータスを提供し、error_responseは JSON ボディを含む完全なHttpResponseオブジェクトを構築します。get_itemのようなハンドラ関数はResult<_, ServiceError>を返すことができ、actix-webはServiceErrorをHttpResponseに自動的に変換します。
アプリケーションシナリオとベストプラクティス
- 一元化されたエラーハンドリング: このパターンは、さまざまなアプリケーションエラーが HTTP レスポンスにどのように変換されるかについて、単一の信頼できる情報源を促進します。これにより、API はより一貫性があり、クライアントにとって理解しやすくなります。
- 可読性: ハンドラ関数は
Resultを返すだけでクリーンに保たれます。マッピングロジックはIntoResponse/ResponseError実装内にカプセル化されます。 - コンテキストに応じたロギング:
DatabaseErrorやDbErrorのケースで示されているように、クライアントに表示するメッセージをより一般的で安全なものにしながら、根本的な内部エラーの詳細(スタックトレース、特定のデータベースエラーメッセージなど)をログに記録できます。これは、機密情報を公開せずにデバッグするために重要です。 - カスタムエラーペイロード: エラーレスポンスの JSON 構造を完全に制御できるため、クライアントが簡単に解析できる、リッチで説明的なエラーメッセージを提供できます。
- エラー集約:
thiserrorを使用して、さまざまなモジュールやサードパーティ製クレートのエラー(#[from]を使用)を包含する複雑なエラー enum を簡単に作成し、エラーハンドリングをさらに一元化します。
結論
カスタムエラー型に対して IntoResponse (Axum) または ResponseError (Actix Web) トレイトを実装することにより、Rust Web アプリケーションは非常にエレガントで保守性の高いエラーハンドリングを実現できます。このパターンにより、ハンドラ関数から返される Result 型が、意味のある HTTP エラーレスポンスに適切に変換されることが保証され、クライアントに一貫した API エクスペリエンスを提供しながら、アプリケーションロジックをクリーンで集中したものに保ちます。これは、Rust で堅牢で開発者に優しい Web サービスを構築するための基本的なプラクティスです。

