Contents
tower-httpとAxumミドルウェア開発の概要
Rust言語でWebアプリケーションを開発する際、Axumフレームワークとtower-httpライブラリを組み合わせることで、柔軟なミドルウェア構築が可能になります。特に中級以上のエンジニアであれば、リクエストの前処理や後処理をカスタマイズする機会が多く、今回の記事では具体的な実装手順とパターンに焦点を当てて解説します。tower-httpは圧縮やCORSなど標準的な機能を提供し、カスタムミドルウェアも簡単に作成可能です。本記事で学ぶ内容により、Axumアプリケーションの拡張性が飛躍的に向上します。
TowerのService/Layerトレイトの基本動作原理
Axumはtower::Serviceとtower::Layerという2つのトレイトを基盤としてミドルウェア処理を実現しています。これらの仕組みを理解することで、カスタムロジックや既存ミドルウェアの連携がスムーズになります。
Serviceトレイトの役割と実装例
Serviceトレイトは、リクエストを受け取り、応答を生成する処理単位です。Axumでは、各ルーティングハンドラやミドルウェアがこのトレイトを実装しています。
|
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 tower::Service; use std::task::{Context, Poll}; use futures::future; use axum::http::{Request, Response}; // ← 追加 use http_body::Body; // ← 追加 struct MyService; impl Service<Request> for MyService { type Response = Response; type Error = Infallible; type Future = future::Ready<Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) } fn call(&mut self, req: Request) -> Self::Future { // リクエスト処理ロジックをここに記述 future::ok(Response::new(Body::from("Hello, world!"))) } } |
Layerトレイトによるミドルウェアのラッピングメカニズム
Layerトレイトは、Serviceに対して前処理や後処理を追加するためのインターフェースです。これにより、ミドルウェアがServiceのライフサイクルに自動的に組み込まれます。
|
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 35 36 37 38 39 40 41 42 43 44 |
use tower::layer::Layer; struct MyMiddleware; impl<S> Layer<S> for MyMiddleware { type Service = MiddlewareService<S>; fn layer(&self, inner: S) -> Self::Service { MiddlewareService { inner } } } struct MiddlewareService<S> { inner: S, } impl<S> Service<Request> for MiddlewareService<S> where S: Service<Request>, { type Response = S::Response; type Error = S::Error; type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { // ミドルウェアの前処理 let start = std::time::Instant::now(); println!("Request started: {}", req.uri()); let future = self.inner.call(req); Box::pin(async move { let res = future.await?; let duration = start.elapsed(); println!("Response completed in {:?}", duration); Ok(res) }) } } |
tower-httpで提供される標準ミドルウェア一覧と使い方
tower-httpには、圧縮・CORS・トレースID挿入などの実用的なミドルウェアが多数用意されています。以下に代表的なものを紹介します。
圧縮ミドルウェア(CompressionLayer)の適用方法
HTTP通信を効率化するため、レスポンスデータの圧縮は必須です。CompressionLayerはこの処理を自動で行います。
|
1 2 3 4 |
use tower_http::compression::CompressionLayer; let compression = CompressionLayer::new(); |
注意:
CompressionLayerはクライアントがAcceptヘッダに応じて動的に圧縮方式を選択します。ただし、Gzip/Brotliに対応したクライアント向けに最適化されます。
CORS対応ミドルウェアの設定例
クロスドメインリクエストを許可するには、CORSミドルウェアを使用します。
|
1 2 3 4 5 6 7 8 |
use tower_http::cors::{Any, CorsLayer}; use http::header; let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(vec!["GET", "POST"]) .allow_headers(vec![header::ACCEPT]); |
トレースID挿入ミドルウェアの実装
デバッグやロギングに有用なリクエストごとのトレースIDを自動生成するミドルウェアも用意されています。
|
1 2 3 4 |
use tower_http::trace::TraceLayer; let trace_layer = TraceLayer::new_for_http(); |
カスタムミドルウェアの作成手順とライフサイクル
独自のロジックを実装するには、Layerトレイトを実装したカスタムミドルウェアを作成します。以下は基本的な構成です。
Layerトレイト実装時のコールバック処理
on_requestやon_responseイベントでリクエスト/応答の前後処理を制御できます。
|
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 35 36 37 38 39 40 41 42 43 44 45 |
use tower::layer::Layer; use std::task::{Context, Poll}; use futures::future; struct LoggingMiddleware; impl<S> Layer<S> for LoggingMiddleware { type Service = LogService<S>; fn layer(&self, inner: S) -> Self::Service { LogService { inner } } } struct LogService<S> { inner: S, } impl<S> Service<Request> for LogService<S> where S: Service<Request>, { type Response = S::Response; type Error = S::Error; type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { self.inner.poll_ready(cx) } fn call(&mut self, req: Request) -> Self::Future { let start = std::time::Instant::now(); println!("Request started: {}", req.uri()); let future = self.inner.call(req); Box::pin(async move { let res = future.await?; let duration = start.elapsed(); println!("Response completed in {:?}", duration); Ok(res) }) } } |
複数ミドルウェアの合成方法
複数のミドルウェアを組み合わせるには、tower::util::Eitherやチェーン式スタック構造を使用します。
tower::util::Eitherによる条件分岐型合成
特定の条件に応じてミドルウェアを選択的に適用できます。
|
1 2 3 4 5 6 7 8 9 |
use tower::util::Either; let middleware1 = CompressionLayer::new(); let middleware2 = LoggingMiddleware; // 条件によって選択する let handler = Either::left(middleware1.layer(handler)) .or(Either::right(middleware2.layer(handler))); |
チェーン式ミドルウェアスタックの構築
順番に適用する場合は、layer().layer()の連続呼び出しが可能です。
|
1 2 |
let handler = cors_layer.layer(tracing_layer.layer(router)); |
Axumアプリケーションへのミドルウェア適用例
Axumにはルーターレベルでのミドルウェア設定と全体的なスタック構成の2つの方法があります。
ルーターlevelでのミドルウェア設定
特定のルートにのみミドルウェアを適用できます。
|
1 2 3 4 5 6 |
use axum::{routing::get, Router}; let app = Router::new() .route("/api/data", get(|_| async { "data" })) .layer(CompressionLayer::new()); // 全ルーティングに適用 |
全体的なミドルウェアスタックの構築
アプリケーション全体に共通するミドルウェアを定義します。
|
1 2 3 4 5 6 7 8 9 10 |
use axum::{Router, routing::get}; let app = Router::new() .route("/", get(|| async { "Hello, world!" })) .layer( tower::ServiceBuilder::new() .layer(CompressionLayer::new()) .layer(LoggingMiddleware), ); |
ミドルウェアの比較と選択指針
| ミドルウェア | 主な用途 | 特徴 | 参考ドキュメント |
|---|---|---|---|
| CompressionLayer | レスポンス圧縮 | クライアントのAcceptヘッダに応じて自動選択 | tower_http::compression::CompressionLayer |
| CorsLayer | CORS対応 | ドメイン指定・メソッド指定など柔軟に設定可能 | tower_http::cors::CorsLayer |
| TraceLayer | トレースID挿入 | リクエスト毎の識別子を自動生成 | tower_http::trace::TraceLayer |
終わりに
本記事では、Axumとtower-httpによるミドルウェア開発の基本的な流れと実装例を解説しました。実際のプロジェクトにおいては、Service・Layerトレイトの理解が不可欠であり、カスタムロジックを組み込む際には公式ドキュメントと連携しながら設計を行うことが重要です。今後の記事では、さらに高度なミドルウェアパターンやパフォーマンスチューニングについても解説していきます。