Axumのタワースタックをリクエストが通過する旅を解き明かす
Emily Parker
Product Engineer · Leapcell

はじめに
現代のWeb開発のエコシステムにおいて、Webフレームワークが入ってくるリクエストをどのように処理するかを理解することは、堅牢でスケーラブル、かつ保守性の高いアプリケーションを構築する上で不可欠です。
パフォーマンスと安全性に重点を置くRustは、説得力のあるソリューションを提供しており、AxumはTokioと高度に拡張可能なTowerエコシステムの上に構築された、強力で人間工学に基づいたWebフレームワークとして際立っています。Axumは優れた開発者体験を提供しますが、真の魔法は、そのTowerサービススタックの舞台裏でしばしば起こっています。
パフォーマンスの最適化、複雑な問題のデバッグ、またはカスタムミドルウェアの実装を目指す開発者にとって、表面的な理解では不十分です。
この記事では、リクエストがAxumのTowerサービススタックを横断する完全なライフサイクルを深く掘り下げ、基盤となるメカニズムを解き明かし、その潜在能力を最大限に活用できるようにします。
リクエスト処理の旅を分解する
リクエストの旅を追跡する前に、Axumのリクエスト処理を支えるコア用語について共通の理解を確立しましょう。
コア用語
- Tower: モジュラーで再利用可能なネットワークサービスを構築するためのトレイトのセットを提供する基盤となるライブラリです。その中心には
Service
トレイト、Layer
トレイト、ServiceBuilder
があります。 Service<Request>
トレイト: Towerにおける基本的な構成要素です。リクエストを受け取り、レスポンスに解決されるFutureを返す非同期関数を表します。その主なメソッドはcall(&mut self, req: Request) -> Self::Future
です。Layer
トレイト: 既存のService
をラップして新しい動作を追加したり、リクエスト/レスポンスを変更したりするミドルウェアのような構造です。Layer
のservice
メソッドは、内部のService
を受け取り、それをラップする新しいService
を返します。ServiceBuilder
: 複数のLayer
を連鎖させることで、複雑なサービススタックを構築するのに役立つ型です。レイヤーを順次適用するための便利なAPIを提供します。- Axum: TokioとTower上に構築されたWebフレームワークです。ルーティング、リクエストからのデータ抽出、状態の処理、レスポンスの生成のための人間工学に基づいたユーティリティを提供し、すべてTowerのサービス処理を活用します。
- Handler: Axumでは、さまざまなエクストラクタを引数として受け取り、レスポンスに変換できる型を返す関数またはメソッドです。ハンドラは基本的に特殊化されたサービスです。
- Extractor:
FromRequestParts
またはFromRequest
トレイトを実装する型で、Axumが着信リクエストの特定のパート(パスパラメータ、クエリ文字列、リクエストボディなど)を解析し、ハンドラで利用できるようにします。
リクエストのオデッセイ
HTTPリクエストがAxumアプリケーションに到着すると、一連のサービスとレイヤーを通過する、予測可能で細心の注意を払って調整された旅に出ます。このプロセスをステップバイステップで分解しましょう。
1. サーバー受信とMakeService
最初に、Axumアプリケーションは通常、Hyperのようなサーバーを使用してTCPポートにバインドされます。Hyperまたは他のHTTPサーバーは、各着信接続に対して新しいService
を作成する方法を必要とします。ここでMakeService
トレイトが登場します。
AxumのRouter
は本質的にMakeService
を実装しており、Hyperが新しい接続ごとに新しいRouter
サービスをインスタンス化できるようになります。
// サーバーがMakeServiceをどのように使用するかを示す簡略化された例 use tower::make::MakeService; use tower::Service; use hyper::Request; // http Request型のためにHyperを想定 async fn serve_connection<M>(make_service: &M) where M: MakeService<(), hyper::Request<hyper::body::Incoming>>, { let mut service = make_service.make_service(()).await.unwrap(); // クライアントからの着信リクエストを想像する let request = Request::builder().uri("/").body(hyper::body::Incoming::empty()).unwrap(); let response = service.call(request).await.unwrap(); println!("Response: {:?}", response.status()); }
2. ルートRouter
サービス
Router
はAxumアプリケーションの中央オーケストレーターです。それは本質的に、内部にルーティングツリーを含む大きなService
です。Router
でservice.call(request)
が呼び出されると、まず着信リクエストのパスとメソッドを登録されたルートに一致させようとします。
3. Tower Layer
による事前処理
リクエストが特定のハンドラに到達する前に、通常はLayer
(ミドルウェア)のスタックを通過します。これらのレイヤーは、Axum Router
を設定する際にServiceBuilder
を使用して適用されます。
各Layer
は内部サービスをラップし、ロギング、認証、圧縮、エラー処理、状態管理などの機能を追加します。
ロギングと認証レイヤーの例を考えてみましょう。
use axum::{ routing::get, Router, response::IntoResponse, http::Request, extract::FromRef, }; use tower::{Layer, Service}; use std::task::{Poll, Context}; use std::future::Ready; use std::{pin::Pin, future::Future}; // シンプルな認証ミドルウェア #[derive(Clone)] struct AuthMiddleware<S> { inner: S, } impl<S, B> Service<Request<B>> for AuthMiddleware<S> where S: Service<Request<B>, Response = axum::response::Response> + Send + 'static, S::Future: Send + 'static, B: Send + 'static, { type Response = S::Response; type Error = S::Error; type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request<B>) -> Self::Future { let auth_header = req.headers().get("Authorization"); if let Some(header_value) = auth_header { if header_value == "Bearer mysecrettoken" { // 認証成功、内部サービスに渡す let fut = self.inner.call(req); Box::pin(async move { fut.await }) } else { // 無効なトークン Box::pin(async move { Ok(StatusCode::UNAUTHORIZED.into_response()) }) } } else { // 認証ヘッダーなし Box::pin(async move { Ok(StatusCode::UNAUTHORIZED.into_response()) }) } } } // AuthMiddlewareを作成するレイヤー struct AuthLayer; impl<S> Layer<S> for AuthLayer { type Service = AuthMiddleware<S>; fn service(&self, inner: S) -> Self::Service { AuthMiddleware { inner } } } async fn hello_world() -> String { "Hello, authorized world!".to_string() } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(hello_world)) .layer(AuthLayer); // カスタムAuthLayerを適用 let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap(); }
この例では、AuthLayer
はhello_world
ハンドラを明示的にラップしています。/
へのリクエストが来ると、まずAuthMiddleware
を通過します。認証が失敗した場合、ミドルウェアはすぐにUNAUTHORIZED
レスポンスを返し、リクエストがhello_world
に到達するのを防ぎます。成功した場合、AuthMiddleware
は内部サービス(この場合はhello_world
ハンドラ)を呼び出し、そのレスポンスが返されます。
4. ルートマッチングとハンドラ選択
リクエストがグローバルレイヤーをすべて通過したら、Router
はルーティングロジックを実行します。リクエストのパスとHTTPメソッドに一致するルートが見つかった場合、Router
は、そのルートに関連付けられた特定のハンドラ関数を識別します。
5. ハンドラのExtractorチェーン
ハンドラ関数が実際に実行される前に、Axumの強力なエクストラクタシステムが機能します。ハンドラのシグネチャ(例:Path
、Query
、Json
、State
)の各引数について、Axumは対応するエクストラクタを呼び出します。
各エクストラクタは本質的にService
であり、Request
を処理して必要なデータ型を生成します。このプロセスは、ハンドラで定義されている順序(左から右)で順次発生します。
use axum::{ extract::{Path, Query, Json, State}, response::{IntoResponse, Response}, routing::get, Router, }; use serde::Deserialize; use std::sync::Arc; async fn handler_with_extractors( Path(user_id): Path<u32>, Query(params): Query<QueryParams>, Json(payload): Json<UserPayload>, State(app_state): State<Arc<AppState>>, ) -> Response { println!("User ID: {}", user_id); println!("Query Params: {:?}", params); println!("Payload: {:?}", payload); println!("App State: {:?}", app_state); format!("Processed user {} with state {}", user_id, app_state.name).into_response() } #[derive(Deserialize, Debug)] struct QueryParams { name: String, age: u8, } #[derive(Deserialize, Debug)] struct UserPayload { email: String, } #[derive(Debug)] struct AppState { name: String, } #[tokio::main] async fn main() { let app_state = Arc::new(AppState { name: "My Awesome App".to_string(), }); let app = Router::new() .route("/users/:user_id", get(handler_with_extractors)) .with_state(app_state); // ... (サーバー設定は簡潔さのため省略、前の例と同様) }
handler_with_extractors
では、リクエストパートはこの順序で処理されます。
Path(user_id)
: URLパスからuser_id
を抽出します。Query(params)
: クエリパラメータをQueryParams
にデシリアライズします。Json(payload)
: リクエストボディ(存在し、有効なJSONの場合)をUserPayload
にデシリアライズします。State(app_state)
: 共有アプリケーション状態を取得します。
いずれかのエクストラクタが失敗した場合(例:JSONの形式が不正、パスセグメントの欠落)、Axumは自動的に適切なエラーレスポンス(例:400 Bad Request
、404 Not Found
)を生成し、リクエスト処理をショートサーキットして、ハンドラが呼び出されるのを防ぎます。このエラーは、通常、存在する場合は外側のエラー処理レイヤーによって処理されます。
6. ハンドラの実行とレスポンス生成
最後に、すべてエクストラクタが成功した場合、抽出された引数でハンドラ関数が呼び出されます。ハンドラはアプリケーション固有のロジックを実行し、データベースと対話し、他のサービスを呼び出し、最終的にIntoResponse
を実装する値を構築します。Axumはこの値を受け取り、完全なHTTPレスポンスに変換します。
7. Tower Layer
による後処理
ハンドラがレスポンスを生成した後、リクエストが通過したのと同じレイヤースタックを逆方向にレスポンスが移動します。各レイヤーは、レスポンスを検査または変更する機会を得ます。たとえば、ロギングレイヤーはレスポンスのステータスコードを記録したり、圧縮レイヤーはレスポンスボディを圧縮したりする可能性があります。
フローは、ネストされた一連の呼び出しとして視覚化できます。
+-------------------------------------------------+
| Server Connection |
| +---------------------------------------------+
| | Global Tower Layer 1 |
| | +-----------------------------------------+
| | | Global Tower Layer 2 |
| | | +-------------------------------------+
| | | | Axum Router Service |
| | | | +---------------------------------+
| | | | | Route Match & Handler Selection |
| | | | | +-----------------------------+
| | | | | | Extractor 1 Service | -- Request
| | | | | | +-------------------------+
| | | | | | | Extractor 2 Service | -- Request
| | | | | | | +---------------------+
| | | | | | | | ... | -- Request
| | | | | | | | +-----------------+
| | | | | | | | | Actual Handler | -- Request -> Response
| | | | | | | | +-----------------+
| | | | | | | | ... | <- Response
| | | | | | +-------------------------+
| | | | | | Extractor 2 Output | <- Response
| | | | +---------------------------------+
| | | | Extractor 1 Output | <- Response
| | | +-------------------------------------+
| | | Axum Router Service Output | <- Response
| | +-----------------------------------------+
| | Global Tower Layer 2 Output | <- Response
| +---------------------------------------------+
| Global Tower Layer 1 Output | <- Response
+-------------------------------------------------+
図の各「Service」は、呼び出されると、Requestを受け取り、Responseに解決されるFutureを返します。Layer
は「内部」サービスをラップすることで、順序を決定します。
結論
AxumアプリケーションのTowerサービススタックをリクエストが通過する旅は、モジュールコンポーネントの綿密なダンスです。初期のサーバー受信から最終的なレスポンスまで、各Layer
とService
がその一部を提供し、堅牢で柔軟な処理パイプラインを作成します。
この複雑なフローを理解することで、開発者はAxumアプリケーションを自信を持ってデバッグ、最適化、拡張するために必要な明瞭さを得ることができ、AxumのシンプルさがTowerの証明されたパターン上に構築された強力で高度に設定可能なエンジンを優雅に隠していることを認識します。非同期で構成可能なService
トレイトは、Axumの効率的で回復力のあるリクエスト処理の真の原動力です。