AxumとActix Webにおけるカスタムエクストラクタの作成
Min-jun Kim
Dev Intern · Leapcell

カスタムエクストラクタでリクエストデータを解き放つ
RustのWeb開発の世界では、受信したHTTPリクエストから特定の情報を抽出することは、基本的かつ頻繁に行われるタスクです。ユーザー認証トークン、解析されたJSONボディ、クエリパラメータのいずれであっても、このデータにクリーンかつ人間工学的にアクセスできる能力は、アプリケーションのハンドラ関数の可読性と保守性に大きな影響を与えます。AxumとActix Webは、Rustで人気のある2つのWebフレームワークであり、これを達成するための強力なメカニズムを提供しています。それがFromRequestトレイトです。このトレイトは、カスタムエクストラクタを構築するための基盤であり、開発者は複雑なデータ抽出ロジックを再利用可能で慣用的な形式にカプセル化できます。FromRequestを理解し活用することで、ボイラープレートを超えて、よりクリーンで堅牢なWeb APIを作成し、非常に表現力豊かで管理しやすいコードへと移行することができます。
FromRequestの力
独自のカスタムエクストラクタの作成に入る前に、いくつかのコアコンセプトを明確に理解しておきましょう。
- エクストラクタ: Webフレームワークのコンテキストにおいて、エクストラクタとは、特定のトレイト(
FromRequestなど)を実装する型であり、受信したHTTPリクエストから自動的に構築できるようにします。このプロセスは、ハンドラ関数が呼び出される前に行われます。 - 非同期コンテキスト: AxumとActix Webはどちらも非同期フレームワークです。これは、リクエストからデータを抽出する際には、(リクエストボディの読み取りなど)操作を
awaitする必要があることが多いことを意味します。したがって、FromRequestの実装は通常、それ自体がasync関数であり、Futureを返します。 - エラーハンドリング: エクストラクタは失敗する可能性があります。たとえば、必要なヘッダが存在しない場合や、JSONボディが不正な形式の場合などです。堅牢な
FromRequest実装は、これらの抽出の失敗がどのように処理され、クライアントにどのような種類のエラー応答が返されるかを定義する必要があります。
FromRequestトレイトは、AxumとActix Webがハンドラ関数に構造化されたデータをシームレスに提供する方法の中心にあります。リクエストが到着すると、フレームワークは,ハンドラ内の引数の型を検査します。引数の型がFromRequestを実装している場合、フレームワークはそのfrom_requestメソッドを呼び出し、受信したリクエストのコンテキストを渡します。このメソッドは、引数型のインスタンスを構築しようとします。成功した場合、インスタンスはハンドラに渡されます。失敗した場合、ハンドラが呼び出されることなく、適切なエラー応答がクライアントに返されます。このメカニズムは、関心の明確な分離を促進し、ハンドラを純粋なビジネスロジックに集中させます。
Axumでのカスタムエクストラクタの構築
すべてのリクエストから「Client-ID」ヘッダを抽出し、それが有効なUUIDであることを確認したいと想像してみましょう。カスタムClientIdエクストラクタを作成します。
// Axum Example use axum::{ async_trait, extract::{FromRequestParts, Request}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, Router, }; use std::fmt; use uuid::Uuid; // ClientId抽出の失敗のためのカスタムエラー型 #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid or missing Client-ID header") } } // エラーをHTTP応答に変換 impl IntoResponse for ClientIdError { fn into_response(self) -> Response { (StatusCode::BAD_REQUEST, "Missing or invalid Client-ID header").into_response() } } // カスタムエクストラクタ構造体 #[derive(Debug)] pub struct ClientId(pub Uuid); // ClientIdに対するFromRequestPartsの実装 // AxumのFromRequestPartsは、リクエストヘッダ、URIなどからのデータ抽出に使用されます。 // FromRequestは、ボディやリクエストを消費するその他の非同期操作からの抽出に使用されます。 #[async_trait] impl FromRequestParts for ClientId { type Rejection = ClientIdError; async fn from_request_parts(parts: &mut Parts, _state: &()) -> Result<Self, Self::Rejection> { let headers = &parts.headers; let client_id_header = headers .get("Client-ID") .ok_or(ClientIdError)? // ヘッダが存在しない場合はエラー .to_str() .map_err(|_| ClientIdError)?; // ヘッダが有効なUTF-8でない場合はエラー let client_id = Uuid::parse_str(client_id_header).map_err(|_| ClientIdError)?; // 有効なUUIDでない場合はエラー Ok(ClientId(client_id)) } } // カスタムエクストラクタを使用したハンドラの例 async fn handle_request_with_client_id(ClientId(client_id): ClientId) -> Json<String> { Json(format!("Hello from client: {}", client_id)) } #[tokio::main] async fn main() { let app = Router::new().route("/hello", axum::routing::get(handle_request_with_client_id)); println!("Axum server running on http://127.0.0.1:3000"); axum::Server::bind(&"0.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }
Axumの例では、リクエストのパーツ(ヘッダ)のみを見て、リクエストボディを消費していないため、FromRequestPartsを実装しています。JSONボディの解析を行う場合は、代わりにFromRequestを実装します。ClientIdErrorがIntoResponseを実装していることに注意してください。これにより、Axumは抽出の失敗を自動的に適切なHTTP応答に変換できます。
Actix Webでのカスタムエクストラクタの構築
次に、Actix Web用の同様のClientIdエクストラクタを実装しましょう。Actix WebのFromRequestトレイトは、ヘッダと非同期操作の両方を直接処理します。
// Actix Web Example use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, }; use futures::future::{ok, Ready}; use std::{fmt, ops::Deref}; use uuid::Uuid; // ClientId抽出の失敗のためのカスタムエラー型 #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Invalid or missing Client-ID header") } } // エラーをHTTP応答に変換 impl ResponseError for ClientIdError { fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) .body(self.to_string()) } fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } // カスタムエクストラクタ構造体 #[derive(Debug)] pub struct ClientId(pub Uuid); // 内部のUuidに簡単にアクセスできるようにDerefを実装 impl Deref for ClientId { type Target = Uuid; fn deref(&self) -> &Self::Target { &self.0 } } // ClientIdに対するFromRequestの実装 // Actix WebのFromRequestは、ヘッダとボディの抽出の両方に機能します。 impl FromRequest for ClientId { type Error = ClientIdError; type Future = Ready<Result<Self, Self::Error>>; type Config = (); fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let client_id_header = req.headers().get("Client-ID"); let result = match client_id_header { Some(header_value) => { header_value .to_str() .ok() .and_then(|s| Uuid::parse_str(s).ok()) .map(|uuid| ClientId(uuid)) .ok_or(ClientIdError) } None => Err(ClientIdError), }; ok(result) } } // カスタムエクストラクタを使用したハンドラの例 async fn handle_request_with_client_id(client_id: ClientId) -> HttpResponse { HttpResponse::Ok().body(format!("Hello from client: {}", client_id.0)) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(web::resource("/hello").to(handle_request_with_client_id)) }) .bind("127.0.0.1:8080")? .run() .await }
Actix Webの例では、FromRequestのfrom_requestメソッドはHttpRequest参照とミュータブルなPayloadを受け取ります。ClientIdエクストラクタはヘッダ情報のみを必要とするため、Payloadを操作する必要はありません。ClientIdのDeref実装は、client_id.0.to_string()の代わりに直接client_id.to_string()を使用できるようにする便利な機能です。
アプリケーションシナリオとベストプラクティス
カスタムエクストラクタは非常に用途が広く、数多くのシナリオに適用できます。
- 認証と認可: JWT、APIキー、またはセッショントークンを抽出し、検証します。
- テナントID抽出: マルチテナントアプリケーションで、ヘッダ、サブドメイン、またはパスからテナントIDを抽出します。
- 複雑なクエリパラメータ解析: クエリパラメータに、組み込みエクストラクタが提供するものを超える特定の検証または変換が必要な場合。
- ボディ前処理: ハンドラに到達する前にリクエストボディを復号化または解凍します。
- ユーザーコンテキスト: 認証されたIDに基づいてデータベースから
User構造体をロードし、ハンドラに注入します。
カスタムエクストラクタを設計する際は、これらのベストプラクティスを検討してください。
- 明確なエラーハンドリング: 必ずエクストラクタのために特定のエラー型を定義し、フレームワークのエラー変換トレイト(Axumの場合は
IntoResponse、Actix Webの場合はResponseError)を実装してください。これにより、クライアントに明確で実行可能なエラーメッセージが提供されます。 - モジュール性: エクストラクタを単一の責任に集中させてください。エクストラクタが複雑になりすぎる場合は、より小さく composable な単位に分割することを検討してください。
- パフォーマンス: エクストラクタ内のパフォーマンスが重要な操作、特にディスクI/Oやネットワークリクエストが関わる操作には注意してください。必要に応じて結果をキャッシュしてください。
- テスト容易性: エクストラクタを単独で容易にテストできるように設計してください。モックするのが難しい複雑なフレームワーク内部に依存しすぎないでください。
- ドキュメント: エクストラクタが期待するもの(例:必須ヘッダ、ボディ形式)と提供するものを明確に文書化してください。
Web開発の人間工学の向上
AxumとActix WebにおけるFromRequestトレイトに基づいたカスタムエクストラクタは、RustのWebアプリケーションの人間工学、モジュール性、テスト容易性を大幅に向上させる強力な機能です。リクエストデータ抽出および検証ロジックをカプセル化することで、ハンドラ関数をクリーンで簡潔に保ち、純粋なビジネスロジックに集中させることができます。これにより、より保守性と堅牢性の高いコードベースが実現します。

