Contents
Axum のミドルウェアシステム概要と Tower の Layer / Service の関係
Axum は内部で Tower ライブラリを利用しており、HTTP リクエストは Layer が生成した Service を順に通過します。この仕組みを理解すると、リクエストの前処理・後処理を安全かつ効率的に差し込めるようになります。以下では概念図と公式ガイドへのリンクを示しながら、ミドルウェアがどのように構築されているかを解説します。
- Layer:ミドルウェア本体を組み立てるファクトリ的役割です。
- Service:
call(request)で実際の処理を行うオブジェクトです。 - リクエストは外側の
Layer→ 内側のハンドラ の順に流れます。
このパイプラインを把握すれば、任意のロジックを安全に差し込むことが可能です。詳しい解説は公式ガイドをご参照ください【Axum と Tower のミドルウェア入門】。
カスタムミドルウェアの実装基礎
カスタムミドルウェアを作る際は、まず Layer と Service をそれぞれ実装します。この節では、最小構成のコード例と必要な型制約について初心者でも分かりやすいように解説します。
Layer 構造体の定義と tower::layer::Layer の実装
以下はロギング用ミドルウェアのベースとなる LoggingLayer と LoggingService の実装例です。BoxBody が未インポートだった点を修正し、必要なクレートも明示しています。
|
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::body::BoxBody; use axum::http::{Request, Response}; use std::task::{Context, Poll}; use tower::{Layer, Service}; #[derive(Clone)] pub struct LoggingLayer; /// Layer の実装は `layer` メソッドで内部ハンドラを包む Service を返すだけです。 impl<S> Layer<S> for LoggingLayer { type Service = LoggingService<S>; fn layer(&self, inner: S) -> Self::Service { LoggingService { inner } } } #[derive(Clone)] pub struct LoggingService<S> { inner: S, } |
型制約のポイント
Sは 任意の Service で構いませんが、後述するcall実装ではResponse = Response<BoxBody>が必要です。Clone,Send,'staticの要件は非同期環境で安全に所有権を移すために必須です。
Service トレイトの call メソッドで前処理・後処理を差し込む例(リクエストロギング)
Service を実装するときに必要になる型制約と、非同期処理を扱うための 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 35 36 37 38 39 40 41 42 43 44 45 |
use axum::body::BoxBody; use axum::http::{Request, Response}; use std::future::Future; use std::pin::Pin; use tower::Service; /// LoggingService が実際にリクエストを処理する部分です。 impl<S, ReqBody> Service<Request<ReqBody>> for LoggingService<S> where // inner は BoxBody を返す Service であることを要求します。 S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + 'static, S::Error: Into<axum::BoxError>, S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; // Box に入れた Future を返すことで型の肥大化を防ぎます。 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>> { // inner の準備状態に委譲します。 self.inner.poll_ready(cx) } fn call(&mut self, req: Request<ReqBody>) -> Self::Future { let method = req.method().clone(); let uri = req.uri().clone(); // 前処理:リクエスト情報をログに出す tracing::info!("incoming {} {}", method, uri); // inner の非同期呼び出しを取得 let fut = self.inner.call(req); Box::pin(async move { // 後処理:レスポンスのステータスコードをログに出す let resp = fut.await?; tracing::info!("response {}", resp.status()); Ok(resp) }) } } |
async_trait が必要になるケース
上記は 手書き の Future を返すパターンですが、async fn をそのまま実装したい場合は async_trait クレートが便利です。
|
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 |
use async_trait::async_trait; #[async_trait] impl<S, ReqBody> Service<Request<ReqBody>> for LoggingService<S> where S: Service<Request<ReqBody>, Response = Response<BoxBody>> + Clone + Send + Sync + 'static, S::Error: Into<axum::BoxError>, { 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) } async fn call(&mut self, req: Request<ReqBody>) -> Result<Self::Response, Self::Error> { // ここに非同期ロジックを書くだけで済みます tracing::info!("incoming {} {}", req.method(), req.uri()); let resp = self.inner.call(req).await?; tracing::info!("response {}", resp.status()); Ok(resp) } } |
async_trait を使用するとコードがシンプルになりますが、コンパイル時に追加のボックス化が入るため パフォーマンス要件が厳しい場合 は手書き Future の方が好まれます。
代表的ミドルウェアのステップバイステップ実装
この章では、実務で頻出する「リクエストロギング」と「JWT 認証」の二つのミドルウェアを完成形まで作ります。コード例はすべて Axum 0.7 系 に合わせています。
リクエストロギングミドルウェアの完全実装
まずは Cargo.toml に必要な依存関係を追加します。
|
1 2 3 4 5 6 |
[dependencies] axum = "0.7" tower = "0.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt"] } |
次に、先ほど定義した LoggingLayer と LoggingService をモジュール化し、アプリ起動時にロガーを初期化します。
|
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 |
use axum::{routing::get, Router}; mod middleware { // ここに LoggingLayer / LoggingService の実装を入れる(上記参照) pub use super::*; } #[tokio::main] async fn main() { // 構造化ログの初期化 tracing_subscriber::fmt() .with_target(false) .with_level(true) .init(); let app = Router::new() .route("/", get(root_handler)) // ミドルウェアは `.layer` で差し込みます .layer(middleware::LoggingLayer); axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) .serve(app.into_make_service()) .await .expect("server failed"); } async fn root_handler() -> &'static str { "Hello, Axum!" } |
この構成で、全リクエストの入出力が標準出力に記録されます。ポイントは「LoggingLayer を Router::layer の最外層に置く」ことです。
JWT 認証ミドルウェア(jsonwebtoken クレート使用)
重要な安全上の注意
本サンプルでは dangerous_insecure_decode フィーチャを有効化していますが、これはトークンの検証を行わないデコード手法であり、本番環境では絶対に使用しないでください。代替として decode に適切な Validation オプションとシークレットキーを渡す方法を推奨します。
Cargo.toml の設定(安全版)
|
1 2 3 4 |
jsonwebtoken = { version = "9", default-features = false, features = ["serde"] } serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } # 時刻計算に必要 |
ペイロード構造体
|
1 2 3 4 5 6 7 8 |
use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, exp: usize, } |
トークン生成関数(参考実装)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fn create_jwt(sub: &str, secret: &[u8]) -> Result<String, jsonwebtoken::errors::Error> { let expiration = chrono::Utc::now() .checked_add_signed(chrono::Duration::hours(24)) .expect("valid timestamp") .timestamp() as usize; let claims = Claims { sub: sub.into(), exp: expiration, }; jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, secret) } |
JWT 認証ミドルウェアの実装
|
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
use axum::{ body::BoxBody, http::{header, HeaderMap, Request, Response, StatusCode}, }; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; use tower::{Layer, Service}; #[derive(Clone)] pub struct JwtAuthLayer { secret: Vec<u8>, } impl<S> Layer<S> for JwtAuthLayer { type Service = JwtAuthService<S>; fn layer(&self, inner: S) -> Self::Service { JwtAuthService { inner, secret: self.secret.clone(), } } } #[derive(Clone)] pub struct JwtAuthService<S> { inner: S, secret: Vec<u8>, } /// Service の実装は Logging と同様ですが、トークンの検証ロジックが追加されています。 impl<S, B> Service<Request<B>> for JwtAuthService<S> where S: Service<Request<B>, Response = Response<BoxBody>> + Clone + Send + 'static, S::Error: Into<axum::BoxError>, S::Future: 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 { // 所有権を移すために clone します(軽量な Vec のコピーです)。 let secret = self.secret.clone(); let mut inner = self.inner.clone(); Box::pin(async move { // Authorization ヘッダーからトークンを抽出 let token_opt = extract_token(req.headers()); match token_opt { Some(token) => { // 安全に検証(dangerous_insecure_decode は使用しません) let validation = jsonwebtoken::Validation::default(); jsonwebtoken::decode::<Claims>(&token, &secret.into(), &validation) .map_err(|e| { tracing::warn!("invalid jwt: {}", e); // カスタムエラー型に変換して返すと後続で統一的にハンドリングできます (StatusCode::UNAUTHORIZED, "Invalid token") })?; // 検証が通ったら内部ハンドラへ委譲 inner.call(req).await.map_err(Into::into) } None => Err((StatusCode::UNAUTHORIZED, "Missing token").into()), } }) } } /// Authorization: Bearer <token> 形式のヘッダーを取得するユーティリティ関数です。 fn extract_token(headers: &HeaderMap) -> Option<String> { headers .get(header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) .and_then(|s| s.strip_prefix("Bearer ")) .map(|s| s.to_string()) } |
ルータへの適用例
|
1 2 3 4 5 6 7 8 |
let secret = b"my-very-secret-key".to_vec(); let app = Router::new() .route("/protected", get(protected_handler)) // ロギング → JWT 認証 の順でレイヤーを積み重ねます .layer(LoggingLayer) .layer(JwtAuthLayer { secret }); |
注意:上記コードは 検証のみ を行う安全な実装です。dangerous_insecure_decode フィーチャは Cargo.toml から除外してください。
ミドルウェアの合成と Router への適用
複数のミドルウェアを組み合わせる場合、Router::layer をチェーンしていくことで実装できます。この節ではレイヤーの順序がどのようにリクエストフローに影響するかを具体例とともに解説します。
合成の基本パターン
|
1 2 3 4 5 6 7 8 9 |
let app = Router::new() .route("/", get(root_handler)) // 1️⃣ ロギングは最外層に置くことで全リクエストを捕捉 .layer(LoggingLayer) // 2️⃣ 認証はロギング後に実行したいので次に配置 .layer(JwtAuthLayer { secret }) // 必要ならキャッシュ、レートリミットなどを続けて追加 ; |
- 外側の Layer が先に
poll_readyとcallを受け取り、内部へ委譲します。 - 逆に 内側の Service はハンドラ直前で実行されるため、認可やキャッシュ処理に適しています。
型安全性とコンパイルエラーの防止
Tower の設計は「レイヤー合成時にすべての型が決定」になる点が特徴です。Router::layer を連続して呼び出すだけで、ビルド時にミスマッチが検出 されます。そのため、実行時エラーを減らしやすくなります。
テスト・エラーハンドリング・デプロイベストプラクティス
この章では、開発フロー全体で品質を保つためのテスト手法、統一的なエラーレスポンス設計、そして本番環境へのデプロイ時に留意すべき設定項目をまとめます。
ユニットテストと統合テストの書き方
Service 単体のユニットテスト(tower::ServiceExt を活用)
|
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 |
#[tokio::test] async fn logging_middleware_works() { use axum::{body::Body, http::Response}; use tower::ServiceExt; // oneshot が使用できるようにインポート let layer = LoggingLayer; // ダミー Service を作成:リクエストを受け取ったら 200 OK を返す let dummy_service = tower::service_fn(|_req| async { Ok::<_, axum::BoxError>(Response::new(Body::empty())) }); // Layer → Service のチェーンを構築 let service = layer.layer(dummy_service); let request = axum::http::Request::builder() .uri("/test") .method("GET") .body(Body::empty()) .unwrap(); // oneshot で一度だけ呼び出す let response = service.oneshot(request).await.unwrap(); assert_eq!(response.status(), axum::http::StatusCode::OK); } |
統合テスト(Router 全体を起動)
|
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 |
#[tokio::test(flavor = "multi_thread")] async fn protected_route_requires_jwt() { use axum::{body::Body, http::Request}; use hyper::client::Client; use tower::ServiceExt; let secret = b"my-secret".to_vec(); // アプリ全体を組み立て let app = Router::new() .route("/protected", get(|| async { "OK" })) .layer(LoggingLayer) .layer(JwtAuthLayer { secret }); // テスト用サーバーをバックグラウンドで起動 let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 0)); let server = axum::Server::bind(&addr).serve(app.into_make_service()); let (tx, rx) = tokio::sync::oneshot::channel(); tokio::spawn(async move { server.with_graceful_shutdown(async { rx.await.ok(); }).await.unwrap(); }); // 実際のリクエストは hyper client で送信 let client = Client::new(); let uri = format!("http://{}/protected", server.local_addr()); let resp = client.get(uri.parse().unwrap()).await.unwrap(); assert_eq!(resp.status(), axum::http::StatusCode::UNAUTHORIZED); // テスト完了後にサーバー停止 let _ = tx.send(()); } |
エラーハンドリングとレスポンス変換のベストプラクティス
エラーは 一元管理 し、IntoResponse を実装した独自型で返すとコードがシンプルになります。
|
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::{ response::{IntoResponse, Response}, Json, }; use http::StatusCode; use serde_json::json; #[derive(Debug)] enum AppError { Unauthorized, BadRequest(String), Internal, } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, body) = match self { AppError::Unauthorized => ( StatusCode::UNAUTHORIZED, Json(json!({ "error": "unauthorized" })), ), AppError::BadRequest(msg) => ( StatusCode::BAD_REQUEST, Json(json!({ "error": msg })), ), AppError::Internal => ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "internal server error" })), ), }; (status, body).into_response() } } |
ミドルウェア内では次のように使用します。
|
1 2 3 4 |
if token_missing { return Err(AppError::Unauthorized.into()); } |
本番環境向けデプロイ設定ポイント
| 項目 | 推奨設定 | 理由 |
|---|---|---|
| Tokio runtime | #[tokio::main(flavor = "multi_thread", worker_threads = 4)] |
I/O と CPU バランスを最適化し、スケーラビリティを確保 |
| リクエストタイムアウト | tower::timeout::TimeoutLayer::new(Duration::from_secs(30)) を最外層に配置 |
長時間ブロックされるリクエストを自動で切断し、サービス全体の安定性向上 |
| 構造化ログ出力 | tracing_subscriber::fmt().json().with_writer(std::io::stdout).init(); |
JSON 形式はクラウドロギング(CloudWatch, Loki 等)への転送が容易 |
| Graceful shutdown | signal::ctrl_c() を監視し、server.with_graceful_shutdown(shutdown_signal()) を使用 |
デプロイ時や障害復旧時に接続を安全に閉じられる |
| ヘルスチェックエンドポイント | /healthz で簡易 200 OK を返すハンドラを実装 |
Kubernetes 等のオーケストレーターが正常性を判定できる |
Dockerfile の例(抜粋):
|
1 2 3 4 5 6 7 8 9 10 |
FROM rust:1.78-slim AS builder WORKDIR /app COPY . . RUN cargo build --release FROM debian:buster-slim COPY --from=builder /app/target/release/my_service /usr/local/bin/ EXPOSE 3000 CMD ["my_service"] |
本稿の要点まとめ
- Axum のミドルウェアは Tower の Layer/Service に基づく。Layer が Service を生成し、リクエストは外側から内側へ流れる。
- カスタム Layer と Service の実装手順を具体例(ロギング)で示した。型制約や
async_traitの使い分けも解説したので、初心者でも安全に実装できる。 - JWT 認証ミドルウェアでは、危険な
dangerous_insecure_decodeを使用しない安全な実装を提示し、必ずシークレットで検証するよう注意喚起した。 - ミドルウェアの合成方法は
Router::layerのチェーン化で行い、順序が処理フローに直接影響する点を強調した。 - テスト・エラーハンドリング・デプロイのベストプラクティスを網羅し、実運用で必要となる設定例も提供した。
これらを参考に、自社サービスに最適なミドルウェアを設計・実装し、本番環境でも安定して動作させてください。