Contents
Axumにおけるエラーハンドリングの基礎概念
Axumでのエラーハンドリングは、「tower::Service」のError型とInfallibleトレイトの関係性に深く関わっています。Webサーバーが予期せぬエラーを返さないためには、フレームワークレベルでエラータイプを統一的に管理する仕組みが必要です。このセクションでは、その基礎的な流れと設計思想を解説します。
tower::ServiceのError型とInfallibleの役割
Axumは、Rustのtowerライブラリに依存して非同期処理を実装しています。これにより、ハンドラ関数内で発生するエラーが「tower::Service::Error」として統一的に扱われます。
Infallibleトレイトとは
- Rust標準で定義された
std::convert::Infallibleトレイトは、「実行時にパニックを起こさない型」を表します。 - これは、Rustの安全性担保に深く関係しており、
Result<T, Infallible>という型定義は「成功するか永続的に失敗しない」ことを意味します(例:Ok(())のみが存在)。 - towerライブラリでは、Serviceトレイトのエラー戻り値として
Infallibleをデフォルトで使用しており、これによりサーバーのクラッシュを防ぎます。
| 項目 | 内容 | 補足 |
|---|---|---|
| tower::Service::Error | サービスが返す可能性のあるエラー型 | 多くはBox<dyn std::error::Error + Send + Sync>など |
| Infallible | ネットワーク通信など、「絶対にエラーにならない」場面で使用 | Rust標準トレイト |
エラー処理のフロー概要
Axumでは、ハンドラ関数内で発生したエラーが自動的にIntoResponseトレイトを実装する型に変換されます。この流れには以下のようなステップがあります。
- ハンドラ内でエラーが発生 →
Result<T, E>型として返される HandleErrorマクロでエラー型を指定 → カスタムレスポンスを設定- Axumが
IntoResponseトレイトを使ってHTTP応答に変換
注意点: 未処理のエラーは、サーバー側でクラッシュする可能性があります。必ず
HandleErrorやカスタム型で捕まえる必要があります。
HandleErrorマクロによるエラーレスポンスカスタマイズ
AxumのHandleErrorマクロは、特定のエラー型に応じて自動的にカスタムなHTTPレスポンスを生成する仕組みです。これにより、一貫したエラー表現を実現できます。
基本構文と動作原理
HandleErrorマクロの基本的な使用例は以下の通りです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use axum::response::{IntoResponse, Response}; use std::fmt; #[derive(Debug)] enum CustomError { NotFound, } impl IntoResponse for CustomError { fn into_response(self) -> Response { match self { Self::NotFound => (StatusCode::NOT_FOUND, "Resource not found").into_response(), } } } |
このコードでは、CustomErrorが発生した場合に自動的に404 Not Foundレスポンスを返すようにしています。
- ポイント:
HandleErrorは、impl IntoResponse for Eが実装された型に対してのみ効果を持ちます。 - 理由: Axumは、エラーを
IntoResponseトレイトを使ってHTTP応答に変換する必要があります。
複数のエラー型への対応方法
複数のエラー型を扱う場合、HandleErrorマクロ内でパターンマッチを行い、それぞれに応じたレスポンスを作成します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use axum::response::{IntoResponse, Response}; use std::fmt; #[derive(Debug)] enum ApiError { DbError(sqlx::Error), InvalidInput, } impl IntoResponse for ApiError { fn into_response(self) -> Response { match self { Self::DbError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Self::InvalidInput => (StatusCode::BAD_REQUEST, "Invalid input data").into_response(), } } } |
例として、
sqlx::Errorや独自のエラー型を扱う方法が確認できます。このように分岐処理することで、ユーザーに適切なフィードバックを提供できます。
カスタムエラー型の定義とanyhow::Errorの活用
ビジネスロジックに応じたカスタムエラー型は、「共通のインターフェースでエラーレスポンスを作成しやすくなる」という利点があります。anyhow::Errorを使うことで、エラー診断情報を柔軟に保持可能です。
エラータイプの設計方針
カスタムエラー型を定義する際は以下の2つのポイントが重要です。
- エラーの種類と状態を明確に分ける(例: データベースエラー、入力不正など)
anyhow::Errorと連携させる(診断情報を保持しやすい)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
use anyhow::{Context, Error}; use std::fmt; #[derive(Debug)] struct CustomError { message: String, source: Option<Box<dyn fmt::Display + Send + Sync>>, } impl fmt::Display for CustomError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.message) } } impl From<sqlx::Error> for CustomError { fn from(e: sqlx::Error) -> Self { Self { message: "Database error".to_string(), source: Some(Box::new(e)), } } } |
anyhow::Errorは、エラーの原因をトレースできるため、デバッグに役立ちます。カスタム型でラップすることで、診断情報も一緒に伝えることができます。
IntoResponseトレイトの実装と独自型設計
Axumでは、ハンドラ関数が返す値はIntoResponseトレイトを実装する必要があります。このトレイトを独自型に適用することで、カスタムなHTTP応答形式を作成できます。
標準型への変換ロジック
IntoResponseトレイトの実装例(基本的な構造)は以下の通りです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
use axum::response::{IntoResponse, Response}; use std::fmt; struct CustomError { status: u16, message: String, } impl IntoResponse for CustomError { fn into_response(self) -> Response { let json = serde_json::to_string(&self).unwrap(); (self.status, json).into_response() } } |
statusフィールドは、HTTPステータスコードを指定messageフィールドは、エラーメッセージを保持
ステータスコードとボディのカスタマイズ
カスタム型に応じてステータスコードやJSONボディを変更する例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
use axum::response::{IntoResponse, Response}; use serde_json::json; use std::fmt; struct ErrorBody { code: u16, message: String, } impl IntoResponse for ErrorBody { fn into_response(self) -> Response { let body = json!({ "code": self.code, "message": self.message }).to_string(); (self.code, body).into_response() } } |
| エラー種類 | ステータスコード | レスポンスボディ |
|---|---|---|
| NotFound | 404 | {"code": 404, "message": "Resource not found"} |
| InvalidInput | 400 | {"code": 400, "message": "Invalid data format"} |
注意事項: JSON形式で返す場合、
serde_jsonライブラリが必須です。エラーメッセージには具体的な情報を含めることが重要です。
データベースクレートとの連携時におけるエラーハンドリング
SQLクエリなどで発生したエラーは、Axumのカスタムエラー型に変換することで統一的なレスポンスを実現できます。特にsqlxなどのDBクレートと組み合わせる場合に注意が必要です。
sqlxのError型とAxumの統合
sqlx::Errorは、IntoResponseトレイトを実装していないため、独自のカスタムエラー型にラップする必要があります。以下がその例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use axum::{response::IntoResponse, http::StatusCode}; use sqlx::Error; #[derive(Debug)] struct DbError { original: Error, } impl From<sqlx::Error> for DbError { fn from(e: sqlx::Error) -> Self { Self { original: e } } } impl IntoResponse for DbError { fn into_response(self) -> axum::response::Response { (StatusCode::INTERNAL_SERVER_ERROR, self.original.to_string()).into_response() } } |
上記コードでは、
sqlx::ErrorをDbErrorに変換し、Axumのエラーレスポンスに統合しています。
サブリクエスト時のエラー伝搬
サブリクエスト(例: サードパーティAPIへのリクエスト)が失敗した場合、エラーを適切にキャッチする必要があります。HandleErrorマクロと組み合わせて以下のように処理できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
use axum::response::{IntoResponse, Response}; use std::fmt; async fn fetch_external_data() -> Result<String, ExternalApiError> { // サードパーティAPI呼び出し... } #[derive(Debug)] enum ExternalApiError { Timeout, } impl IntoResponse for ExternalApiError { fn into_response(self) -> Response { match self { Self::Timeout => (StatusCode::GATEWAY_TIMEOUT, "External service timeout").into_response(), } } } |
まとめ
- Axumでのエラーハンドリングは、
tower::Service::ErrorとInfallibleの理解が不可欠 HandleErrorマクロを使って、複数のエラー型に応じたレスポンスを設定anyhow::Errorをラップしたカスタム型で診断情報を保持IntoResponseトレイトを実装することで、独自型をHTTP応答に変換- DBクレートと連携する際には、エラー型を統一的に処理
このように設計を行うことで、Rust開発者はAxumの強力なエラーハンドリング機能を最大限活用できます。記事で解説した方法を基に、自身のAxumプロジェクトにおけるエラーハンドリング構造を再設計してみてください。