Contents
Extractor の基本概念と axum における位置付け(Middleware と比較)
axum では、リクエストからハンドラが必要とするデータだけを 型安全に 取り出す仕組みとして Extractor が提供されています。
一方、Middleware はリクエスト全体やレスポンス全体を前後で加工することが主目的であり、ハンドラ内部まで直接的にデータを注入できません。この違いを正しく理解すれば、DI(Dependency Injection)的な設計とグローバルな前処理・後処理を自然に組み合わせられます。
- 呼び出しタイミング:
Routerがパスをマッチさせた直後、axum の内部パイプラインが各ハンドラ引数のFromRequest::from_requestを順次実行します。 - 組み合わせ方:全体的な前処理は
.layer()で Middleware を設定し、個別ルートでは.route(...).get(handler)にカスタム Extractor を入れるだけです。Middleware が先に走り、必要ならExtensionで共有状態を注入したうえで、Extractor がリクエストスコープのデータを取得します。
ポイント:Extractor はハンドラ引数として型安全にデータを提供し、Middleware は全体的な前後処理を担う。両者は相補的であり、
.layerと.routeの順序さえ守ればシームレスに併用できます。
FromRequest トレイトの現在のシグネチャと非同期実装
axum 0.7 系(2024 年リリース)以降、公式ドキュメントは次のシグネチャを推奨しています(https://docs.rs/axum/latest/axum/extract/trait.FromRequest.html)。
|
1 2 3 4 5 6 7 |
async fn from_request( request: Request<B>, ) -> Result<Self, Self::Rejection> where B: HttpBody + Send, Self: Sized; |
この形は impl Future<Output = Result<Self, Rejection>> + Send を返す async fn と等価であり、async_trait クレートを使う必要はありません。
その結果、ボックス化された Pin<Box<dyn Future>> が不要になるためコンパイルが速くなり、ランタイムオーバーヘッドも削減できます。
以下に、余計なインポートを除いた最小限の実装例を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
use axum::{ extract::FromRequest, http::{request::Parts, Request}, response::IntoResponse, }; use std::future::Future; #[derive(Debug)] pub struct MyExtractor { pub user_agent: String, } /// カスタムエラーは後述の `IntoResponse` 実装を持つ型 impl FromRequest for MyExtractor { type Rejection = MyExtractorError; fn from_request( req: Request<axum::body::Body>, ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send { async move { // ヘッダーだけ取り出すシンプルな例 let (parts, _) = req.into_parts(); let ua = parts .headers .get(axum::http::header::USER_AGENT) .and_then(|v| v.to_str().ok()) .unwrap_or("unknown") .to_string(); Ok(MyExtractor { user_agent: ua }) } } } |
- 必須要件:
Self::RejectionはIntoResponseを実装している必要があります。 - 非同期化のポイント:
async moveブロック内でawaitがあれば自動的に非同期になるので、手書きのFuture型は不要です。
ポイント:最新のシグネチャは
async fn from_request→impl Futureを返すだけ。async_trait不使用でシンプルかつ高速です。
ヘッダー・クエリパラメータ・Body からデータ取得する実装例
カスタム Extractor が実務上必要とする情報は、主に ヘッダー、クエリ文字列、そして JSON ボディ の3種です。ここではそれぞれを安全かつ idiomatic に取り出す方法を示します。
ヘッダー取得と認証トークンのバリデーション
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let auth_header = parts .headers .get("authorization") .and_then(|v| v.to_str().ok()) .ok_or(MyExtractorError::MissingHeader)?; if !auth_header.starts_with("Bearer ") { return Err(MyExtractorError::InvalidHeader); } // "Bearer <token>" 形式からトークン本体だけを抽出 let token = auth_header.trim_start_matches("Bearer ").trim(); // 任意の検証ロジック(例:設定された API キーと比較) if token != cfg.api_key { return Err(MyExtractorError::InvalidToken); } |
この実装は MissingHeader、InvalidHeader、InvalidToken といった具体的なエラーに分岐させることで、ハンドラ側が原因を容易に把握できるようにしています。
クエリパラメータの型安全パース
|
1 2 3 4 5 6 7 8 9 10 11 12 |
use serde::Deserialize; #[derive(Debug, Deserialize)] struct Params { page: Option<u32>, limit: Option<u32>, } let query_str = parts.uri.query().unwrap_or(""); let params: Params = serde_urlencoded::from_str(query_str) .map_err(|_| MyExtractorError::BadQuery)?; |
serde_urlencoded は URL エンコードされたクエリ文字列を構造体へ変換でき、型安全かつ エラー情報が明示的 です。
JSON Body の取得 ― 標準 Extractor を活用した例
手動で hyper::body::to_bytes を呼び出す代わりに、axum が提供する標準 Extractor Json<T> を組み合わせるとコードが大幅に簡潔になります。以下は MyExtractor の内部から Json<Value> を取得し、失敗時はカスタムエラーへ変換する例です。
|
1 2 3 4 5 6 7 8 9 |
use axum::extract::Json; use serde_json::Value; // 事前に `req.clone()` が必要なのは、`Json::from_request` が所有権を取るため let json: Value = Json::<Value>::from_request(req.clone()) .await .map_err(|_| MyExtractorError::InvalidJson)? .0; // `Json<T>` はタプル構造体なので .0 で中身へアクセス |
この書き方の利点は axum::extract::Json が自動的に Content-Type: application/json の検証とデシリアライズを行う ことです。手作業で hyper::body::to_bytes → serde_json::from_slice と書くより安全かつ保守性が高まります。
ポイント:ヘッダー・クエリ・Body の取得はそれぞれ専用のライブラリ(
serde_urlencoded,serde_json,axum::extract::Json)を活用するとコードが簡潔になる。失敗時は統一的なカスタムエラー型にマッピングしてハンドラ側のロジックをすっきりさせます。
DI(依存性注入)パターンと他 Extractor との組み合わせ
DI を実現する代表的な手段は Extension<T>、もしくは axum 0.7 系で新たに追加された .with_state(state) です。どちらも内部的には Extension に変換されるため、同等の使い勝手が得られます。
.with_state(state) のバージョン情報
- 利用可能バージョン:axum 0.7.0 以降(2024 年リリース)
- 動作概要:
Router::with_state(state)を呼び出すと、全てのルートに対して自動的にExtension(state.clone())が付与されます。従来は明示的に.layer(Extension(state))と書く必要がありました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
use axum::{Router, routing::get}; use std::sync::Arc; #[derive(Clone)] struct Config { api_key: String, } let app = Router::new() .route("/items", get(handler)) // 0.7 系からは with_state が推奨される .with_state(Arc::new(Config { api_key: "secret".into() })); |
カスタム Extractor 内で Extension と標準 Extractor を同時に利用
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
use axum::{ extract::{Extension, Json}, http::Request, }; use std::future::Future; use std::sync::Arc; impl FromRequest for MyExtractor { type Rejection = MyExtractorError; fn from_request( req: Request<axum::body::Body>, ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send { async move { // 共有設定(Extension)を取得 let Extension(cfg) = Extension::<Arc<Config>>::from_request(req.clone()) .await .map_err(|_| MyExtractorError::MissingState)?; // 標準 Json extractor を使ってリクエストボディをパース let payload: serde_json::Value = Json::<serde_json::Value>::from_request(req).await?.0; Ok(MyExtractor { user_agent: cfg.api_key.clone(), // 例示目的 // 他フィールドは省略(payload 等を利用可能) }) } } } |
- 循環依存の防止:
Arc<T>に包むことでCloneが軽量になり、所有権エラーを回避できます。 .with_stateの活用:上記例ではExtension::<Arc<Config>>を手動で取得していますが、Router::with_stateで自動的に注入されるためコードがさらにシンプルになります。
ポイント:
Extensionと標準 Extractor は自由に組み合わせられ、共有状態はArcで包むことで Clone コストを抑えつつ安全に DI が実現できます。axum 0.7 以降は.with_stateを使うと記述が一段階簡略化されます。
カスタムエラー型と統一的エラーハンドリング
Extractor が失敗した際に返す型は Rejection と呼ばれ、ハンドラ側へ Result<T, Rejection> の形で伝搬します。ここでは、HTTP ステータスコードと JSON ボディを同時に返すカスタムエラー MyExtractorError を実装し、全ての Extractor が共通して利用できるようにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
use axum::{ http::StatusCode, response::{IntoResponse, Json}, }; use serde_json::json; #[derive(Debug)] enum MyExtractorError { MissingHeader, InvalidHeader, InvalidToken, BadQuery, InvalidJson, MissingState, } impl IntoResponse for MyExtractorError { fn into_response(self) -> axum::response::Response { let (status, message) = match self { Self::MissingHeader => (StatusCode::BAD_REQUEST, "Missing required header"), Self::InvalidHeader => (StatusCode::UNAUTHORIZED, "Authorization header malformed"), Self::InvalidToken => (StatusCode::FORBIDDEN, "Invalid API token"), Self::BadQuery => (StatusCode::BAD_REQUEST, "Malformed query parameters"), Self::InvalidJson => (StatusCode::UNPROCESSABLE_ENTITY, "Invalid JSON payload"), Self::MissingState => (StatusCode::INTERNAL_SERVER_ERROR, "Application state not found"), }; let body = Json(json!({ "error": message })); (status, body).into_response() } } |
FromRequest::Rejection にこの型を指定すれば、ハンドラは Result<T, MyExtractorError> をそのまま返すだけで統一的なエラー応答が実現します。
ポイント:カスタムエラーに
IntoResponseを実装すると、Extractor の失敗が自動的に HTTP レスポンスへ変換され、ハンドラ側のコードはシンプルになるだけでなく、エラーメッセージやステータスコードを一元管理できるようになります。
Router への登録例・テストコード・ベストプラクティスとパフォーマンス考慮点
完全な Router 組み込み例(axum 0.7)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
use axum::{ routing::get, response::{IntoResponse, Json}, http::StatusCode, Router, }; use std::sync::Arc; #[derive(Clone)] struct Config { api_key: String, } async fn handler(extractor: MyExtractor) -> impl IntoResponse { // 正常系は JSON で返す例 let body = json!({ "user_agent": extractor.user_agent }); (StatusCode::OK, Json(body)) } let app = Router::new() .route("/items", get(handler)) .with_state(Arc::new(Config { api_key: "secret".into() })); // 0.7 系の推奨記法 |
ユニットテスト(tower::Service を利用)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#[tokio::test] async fn test_my_extractor_success() { use axum::{body::Body, http::{self, Request, StatusCode}, Router}; use tower::ServiceExt; // Service の call メソッドを async/await で使えるように let app = Router::new() .route("/test", get(handler)); // ヘッダー・クエリ・JSON ボディをすべて揃えたモックリクエスト let request = Request::builder() .method(http::Method::GET) .uri("/test?page=1&limit=20") .header("user-agent", "axum-test/0.1") .header("authorization", "Bearer secret") // 正しいトークン例 .header("content-type", "application/json") .body(Body::from(r#"{"key":"value"}"#)) .unwrap(); let response = app.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); } |
2024‑2026 年版ベストプラクティス(箇条書き)
-
async_traitを使わない
FromRequestはasync fn→impl Futureが公式推奨です。ボックス化されたトレイトオブジェクトは不要なので、コンパイル速度と実行時性能が向上します。 -
Pin<Box<dyn Future>>から脱却
async fnの戻り値型は自動的に匿名Futureになるため、手書きのボックス化は避けます。これによりヒープ割当が減ります。 -
共有状態は必ず
Arc<T>に包む
Extensionや.with_stateが要求する型はCloneが軽量であることが前提です。Arcを使うことで所有権の問題を回避しつつ、マルチスレッド環境でも安全に共有できます。 -
リクエストスコープキャッシュ
同一ハンドラ内でヘッダーやパース結果を複数回参照する場合は、一度取得した値をローカル変数に保持して再計算を防ぎます。特にserde_urlencoded::from_strは比較的重いのでキャッシュが有効です。 -
エラーは統一型
Rejectionに集約
カスタムエラーにIntoResponseを実装すれば、全ての Extractor が同じ形で失敗を伝搬でき、ハンドラ側のResult<T, E>パターンマッチがシンプルになります。 -
標準 Extractor の活用
Json<T>,Path<T>,Query<T>といった axum が提供する Extractor は内部でエラーハンドリングと型変換を行うため、手動実装より安全です。必要に応じて自前のロジックと組み合わせましょう。 -
with_stateの利用
axum 0.7 以降はRouter::with_state(state)が公式 API として提供されています。古いバージョンを対象にする場合は.layer(Extension(state))を併用してください。
まとめ:上記ベストプラクティスに沿って実装すれば、最新 axum (0.7 系) でも安全・高速なカスタム Extractor が構築でき、DI パターンやエラーハンドリングといった周辺機能ともシームレスに統合できます。
まとめ
- Extractor と Middleware の役割を正しく認識し、前者はハンドラ引数で型安全にデータ注入、後者は全体的な前処理・後処理を担う点を押さえておくことが重要です。
FromRequestの最新シグネチャはasync fn from_request -> impl Futureであり、async_traitは不要です。この形に統一すればコンパイル速度と実行時性能が向上します。- ヘッダー・クエリ・Body の取得は 標準 Extractor (
Json,Query) と外部ライブラリ(serde_urlencoded) を組み合わせ、失敗時はカスタムRejectionに集約してIntoResponseで統一的に返す設計がベストです。 - DI は
Extension<T>または axum 0.7 の.with_state(state)が推奨され、Arcに包むことで所有権問題とコピーコストを回避できます。 - テストコードは
tower::ServiceExtを用いたoneshot呼び出しで実装すれば、リクエスト全体の流れをそのまま検証でき、Extractor の正当性も保証されます。
これらの手順とベストプラクティスに沿ってコードを書くことで、2024‑2026 年版 axum において 安全・高速・保守しやすい カスタム Extractor を実装し、実際のサービスへシームレスに組み込むことが可能になります。