AxumとActix Webにおけるカスタムエクストラクタによるハンドラーの効率化
Olivia Novak
Dev Intern · Leapcell

RustでのWeb開発の世界では、AxumやActix Webのようなフレームワークが、そのパフォーマンス、安全性、簡潔さから大きな注目を集めています。アプリケーションが複雑になるにつれて、リクエストハンドラーはヘッダーの解析、クエリパラメータの検証、リクエストボディのデシリアライズなどの定型コードで散らかってしまうことがよくあります。これにより、コアとなるビジネスロジックが不明瞭になり、ハンドラーの可読性、テスト、保守が困難になります。幸いなことに、AxumとActix Webはどちらも、この繰り返し作業を抽象化するための強力なメカニズムを提供しています。それがカスタムリクエストエクストラクタです。これらを活用することで、ハンドラーロジックをその純粋なエッセンスにまで凝縮させ、よりクリーンで、保守しやすく、最終的にはより楽しいコードベースにつながります。この記事では、カスタムエクストラクタを作成する理由と方法を掘り下げ、Webアプリケーションハンドラーを簡素化する上でのその有用性を実証します。
カスタムエクストラクタの理解
実装に飛び込む前に、Webフレームワークにおけるカスタムエクストラクタに関連するいくつかの重要な用語を明確にしましょう。
リクエストハンドラー: Webフレームワークにおいて、ハンドラーは受信したHTTPリクエストを処理し、HTTPレスポンスを返す責任を持つ関数です。アプリケーションのロジックが主に存在する場所です。
エクストラクタ: エクストラクタは、構造化され再利用可能な方法で受信HTTPリクエストからデータを「抽出」できるようにするメカニズムです。各ハンドラー内でリクエストオブジェクトを手動で検査するのではなく、エクストラクタはこのロジックをカプセル化し、ハンドラー引数として直接利用可能なデータ型を提供します。一般的な組み込みエクストラクタには、Json
、Query
、Path
、HeaderMap
、State
などがあります。
ミドルウェア: 関連はしていますが、ミドルウェア関数は異なるレイヤーで動作します。これらはハンドラーの前または後に実行でき、リクエストまたはレスポンスを変更したり、ロギングや認証のような横断的懸念事項を実行したりする可能性があります。一方、エクストラクタは、ハンドラーにデータを解析して提供するために特別に設計されています。
カスタムエクストラクタの基本的な考え方は、開発者がハンドラーシグネチャに直接注入できる独自の型を定義できるようにすることです。これにより、モジュール性、テスト容易性が促進され、コードの重複が削減されます。ハンドラーが呼び出されると、フレームワークは受信したRequest
から必要な情報を抽出することにより、これらのカスタム型を自動的にインスタンス化します。
カスタムエクストラクタの原則
Axum
Axumでは、エクストラクタはFromRequestParts
またはFromRequest
トレイトを実装する任意の型です。
FromRequestParts
は、エクストラクタがリクエストパート(ヘッダー、メソッド、URIなど)への不変アクセスのみを必要とし、リクエストボディを消費しない場合に使用されます。FromRequest
は、エクストラクタがリクエストボディを消費したり、リクエストを変更したりする必要がある場合に使用されます。FromRequest
を実装する場合、パートのみが必要な場合は通常FromRequestParts
に委譲するか、request.into_body()
と直接やり取りします。
どちらのトレイトも、抽出が失敗した場合に返される関連するError
型と、非同期メソッドであるfrom_request_parts
またはfrom_request
を必要とします。
Actix Web
Actix Webでは、カスタムエクストラクタは、カスタム型に対してFromRequest
トレイトを実装することによって作成されます。このトレイトは、HttpRequest
とPayload
を引数として受け取るfrom_request
非同期メソッドを提供します。Result<Self, Self::Error>
を返します。ここでSelf::Error
はカスタムエラー型です。
実用的な応用: 認証済みユーザーエクストラクタ
一般的なシナリオ、つまり通常JWTを含むAuthorization
ヘッダーから認証済みユーザーを抽出する例で説明しましょう。
Axumの実装
まず、User
構造体と簡単なJWT検証関数があると仮定しましょう。
// src/models.rs #[derive(Debug, Clone)] pub struct User { pub id: u32, pub username: String, } // src/auth.rs (例として簡略化) pub async fn validate_jwt_and_get_user(token: &str) -> Option<User> { // 実際のアプリケーションでは、これにはJWTデコード、署名検証、 // おそらくデータベース検索が含まれます。 // 簡単にするために、「valid_token」であるかどうかだけをチェックしましょう if token == "valid_token_abc" { Some(User { id: 1, username: "john_doe".to_string() }) } else { None } }
次に、カスタムAuthUser
エクストラクタを定義しましょう。
// src/extractors.rs use axum::{ async_trait, extract::{FromRequestParts, TypedHeader}, headers::{authorization::{Bearer, Authorization}, Header}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, }; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUser(pub User); #[async_trait] impl FromRequestParts for AuthUser { type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, _state: &Self::State) -> Result<Self, Self::Rejection> { let TypedHeader(Authorization(bearer)) = parts .extract::<TypedHeader<Authorization<Bearer>>>() .await .map_err(|_| AuthError::InvalidToken)?; let token = bearer.token(); let user = validate_jwt_and_get_user(token) .await .ok_or(AuthError::InvalidToken)?; Ok(AuthUser(user)) } } pub enum AuthError { InvalidToken, // 他の関連する認証エラーを追加 } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, error_message) = match self { AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), // 他のエラーを処理 }; (status, Json(json!({"error": error_message}))).into_response() } }
そして、ハンドラーがそれを使用する方法は次のとおりです。
// src/main.rs use axum::{routing::get, Router}; use std::net::SocketAddr; use crate::extractors::AuthUser; use crate::models::User; mod auth; mod extractors; mod models; async fn protected_handler(AuthUser(user): AuthUser) -> String { format!("Welcome, {} (ID: {})!", user.username, user.id) } #[tokio::main] async fn main() { let app = Router::new() .route("/protected", get(protected_handler)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
Authorization: Bearer valid_token_abc
ヘッダーを持つ/protected
へのリクエストを送信すると、「Welcome, john_doe (ID: 1)!」が返されます。トークンが無効または欠落している場合、AxumはAuthError::into_response
で定義されたJSONエラーメッセージとともに401 Unauthorized
を自動的に返します。
Actix Webの実装
同様にActix Webの場合、同じUser
構造体とvalidate_jwt_and_get_user
関数を使用します。
// src/extractors.rs use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web::Bytes, FromRequest, HttpRequest, HttpResponse, }; use futures::future::{ready, Ready}; use serde::Serialize; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUserActix(pub User); // 認証失敗のためのカスタムエラー #[derive(Debug, Serialize)] pub enum AuthErrorActix { MissingToken, InvalidToken, } impl std::fmt::Display for AuthErrorActix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl ResponseError for AuthErrorActix { fn error_response(&self) -> HttpResponse { let (status, message) = match self { AuthErrorActix::MissingToken => (StatusCode::UNAUTHORIZED, "Authorization token not found"), AuthErrorActix::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), }; HttpResponse::build(status) .json(json!({"error": message})) } } impl FromRequest for AuthUserActix { type Error = AuthErrorActix; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let auth_header = req.headers().get(header::AUTHORIZATION); let user_future = async move { let token = auth_header .ok_or(AuthErrorActix::MissingToken)? .to_str() .map_err(|_| AuthErrorActix::InvalidToken)? // Malformed header value .strip_prefix("Bearer ") .ok_or(AuthErrorActix::InvalidToken)?; let user = validate_jwt_and_get_user(token) .await .ok_or(AuthErrorActix::InvalidToken)?; Ok(AuthUserActix(user)) }; // Actix FromRequest は非同期ではないため、非同期ブロックをFutureに変換し、 // 即座にポーリングします(または、同期操作のために利用可能なヘルパーを使用します)。 // `from_request`内で実際の非同期作業を行う場合、通常はタスクをスポーンするか`web::block`を使用します。 // ただし、`validate_jwt_and_get_user`が非同期であり、awaitされることを意味します。 // 一般的なパターンは、同期抽出のために`ready(block(move || ...))`を使用するか、直接`Future`を使用することです。 // この例の`Ready`要件を簡素化するために、`validate_jwt_and_get_user`が同期であるか、すでに結果が解決されているかのように扱います。 // 真に非同期の抽出のためには、`Self::Future`の型も変更する必要があるでしょう。 // この例では、`Ready`と一致させるために、あたかも同期しているか、すでに解決されているかのようにパターンを維持します。 // 正しい非同期`FromRequest`のためには、`Self::Future`は`Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>`になるでしょう。 // よりシンプルにするために: Box::pin(async move { user_future.await }).into() // これは、 ポーリングによって、ボックス化されたFutureをReady<Result<Self, Self::Error>>に変換します。 // これは、`FromRequest::Future`が複雑なFutureになり得るため、一般的な混乱のポイントです。 // このブログ投稿の簡潔さのため、デモンストレーションのために、即座の結果を持つ`ready`は許容されます。 // ただし、`from_request`内の真に非同期抽出の含意に注意してください。 } }
そしてActix Webハンドラー:
// src/main.rs use actix_web::{get, App, HttpResponse, HttpServer, Responder}; use crate::extractors::AuthUserActix; mod auth; mod extractors; mod models; #[get("/protected")] async fn protected_handler_actix(user: AuthUserActix) -> impl Responder { format!("Welcome, {} (ID: {})!", user.0.username, user.0.id) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(protected_handler_actix) }) .bind(("127.0.0.1", 8080))? .run() .await }
Actix WebにAuthorization: Bearer valid_token_abc
ヘッダーを持つ/protected
へのリクエストを送信すると、「Welcome, john_doe (ID: 1)!」が生成されます。無効または欠落しているトークンは、JSONエラーメッセージとともに401 Unauthorized
レスポンスを生成します。
利点とユースケース
上記の例は、カスタムエクストラクタのいくつかの重要な利点を強調しています。
- クリーンなハンドラー: ハンドラーのシグネチャは直接
User
オブジェクトを受け取り、ハンドラーの目的をすぐに明確にし、関数本体内の定型コードを削減します。 - コードの再利用性:
AuthUser
(またはAuthUserActix
)ロジックは一度定義され、アプリケーション内の任意の保護されたハンドラーで使用できます。 - テスト容易性の向上: 抽出ロジックは
FromRequestParts
/FromRequest
実装内に分離されています。これにより、ハンドラーとは独立して抽出ロジックの単体テストを容易に記述できます。 - エラーハンドリング: カスタムエラーを定義し、適切なHTTPレスポンスに自動的に変換でき、特定の問題に対するエラーハンドリングを一元化できます。
- カプセル化: (JWT検証のような)複雑なロジックはカプセル化され、ハンドラー関数への漏洩を防ぎます。
認証以外にも、カスタムエクストラクタは非常に役立ちます。
- テナントID抽出: マルチテナントアプリケーションの場合、エクストラクタは
X-Tenant-ID
ヘッダーまたはサブドメインを解析して、テナントコンテキストを提供できます。 - 権限/ロールチェック: ユーザーロールを抽出し、ハンドラーが実行される前に特定のエンドポイントに必要な権限を持っているかどうかを確認します。
- 複雑なクエリパラメータ解析: 多くの関連するクエリパラメータが論理的な単位を形成する場合(例:ページネーションフィルター
page
、limit
、sort_by
)、これらQuery
パラメータを受け取り、単一のPaginationParams
構造体を構築するエクストラクタを作成できます。 - セッション管理: セッションデータの取得または更新。
- APIキー検証: ヘッダーでの有効なAPIキーのチェック。
結論
カスタムリクエストエクストラクタは、AxumとActix Webにおける開発者体験を大幅に向上させます。これにより、繰り返し発生する、問題固有のロジックをハンドラー関数から移動させることができます。FromRequestParts
/ FromRequest
トレイトを実装することにより、生の要求データを構造化された、すぐに使用できる型に変換する再利用可能な強力なコンポーネントを構築できます。これにより、集中的で、読みやすく、楽に保守可能なハンドラーが実現し、RustでのWeb開発プロセスを効率化します。カスタムエクストラクタの活用は、堅牢でよく設計されたWebアプリケーションを構築するための重要なステップです。