iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Rust

Rust 後端入門系列 第 11

Day 11 Tower與中間件(Middleware)

  • 分享至 

  • xImage
  •  

什麼是中間件?為什麼重要?

中間件(middleware)在 Web 框架中扮演「介於請求與處理器之間」的軟體層,負責跨切關注點(cross-cutting concerns):驗證、授權、日誌、限流、快取、跨域設定、壓縮、超時等。

  • 把共用邏輯抽離到單一層次,降低 handler 重複程式碼,提升可測試性與可維護性。
  • 可以在請求進入真正業務邏輯前完成驗證或注入必要資源,或在回應送出前做統一處理(例如壓縮、加入標頭)。
  • 中間件是有方向性的:某些中間件需在其他中間件之前執行(例如 CORS 應該能在 preflight 快速回應,限流應在早期短路)。
  • 若順序錯誤,可能會造成安全漏洞(在未驗證前就記錄或快取敏感資料)或性能浪費(在未限流前做昂貴的驗證)。

中間件與請求生命週期

  1. HTTP 連線到來(TCP/TLS)
  2. Web 框架解析 HTTP 請求,建立 Request 物件
  3. 最外層的中間件(Layer/Service)先執行
  4. 請求經過每層中間件(可檢查/修改請求、短路回應或將請求傳遞下去)
  5. Handler(處理器)執行並產生回應
  6. 回應沿著中間件向上回傳(中間件可修改回應)
  7. Web 框架將回應序列化成 HTTP 回傳給 client

Axum 與 Tower 的關係

Axum 的中間件概念建立在 Tower 生態上。Tower 定義了可組合的 middleware 抽象(Layer 與 Service)。在 Axum:

  • Router 可以 .layer(...) 或 .route(...).layer(...) 來套用 Layer
  • Layer 是用來包裝 Service 的「工廠」,可建立可串接的中間件組合
  • 了解 Tower 的抽象可以讓你寫出高彈性、可重用的中間件
  • Axum 的中間件大多是 Tower-compatible;若要撰寫自訂中間件,就會用到 Tower 的 trait(Layer、Service)。
  • Tower 生態(例如 tower-http)已提供大量穩定中間件可直接使用,加速開發。

Tower 生態系統介紹

Tower 是一組 Rust 的抽象與工具套件。 Tower 可以讓你:

  • 使用大量社群維護的中間件(如 tower-http)
  • 寫出高效能且可組合的系統
  • 在不同網路框架間 reuse 中間件(只要相容於 tower traits)

重要元件

  • Service trait:核心,表示可處理 Request 的物件
  • Layer trait:中間件工廠,用來包裝 Service
  • tower-http:為 HTTP 堆疊提供大量現成中間件,如 TraceLayer、CorsLayer、CompressionLayer、TimeoutLayer、NormalizePath 等
  • tower::ServiceBuilder:方便串接多個 layer(與 tower-http 組合很順手)

建議

  • 優先使用 tower-http 提供的成熟中間件,減少自己寫的量。
  • 若需跨服務追蹤,選擇與 OpenTelemetry 兼容的 tracing/otel 中間件。tower-http 與 tracing 有良好整合。
  • 使用 ServiceBuilder 在啟動時清楚定義中間件順序,並把不同責任分層(例如:global-limit -> auth -> tracing -> handler)。

在開始前我們先在cargo.toml加上

tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "compression-br", "timeout", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

啟用相關依賴套件


使用內建與常用中間件:CORS、Compression、Timeout

  • CORS:瀏覽器的跨域請求需要正確回應預檢(preflight)與允許來源,否則前端無法呼叫 API。
  • Compression(壓縮):減少帶寬與回應大小,對於大回應(例如 JSON 列表、靜態資源)有明顯效益。
  • Timeout:避免 handler 長時間佔用資源,確保系統穩定性,防止資源耗盡。

範例程式碼:基本 API 加上 CORS、Compression、Timeout(使用 tower-http 提供的中間件)

use axum::{
    routing::get,
    Router, Json, response::IntoResponse,
};
use std::time::Duration;
use tower_http::cors::{CorsLayer, Any};
use tower_http::compression::CompressionLayer;
use tower_http::timeout::TimeoutLayer;
use serde::Serialize;

#[derive(Serialize)]
struct Health { 
    status: &'static str 
}

async fn health() -> impl IntoResponse {
    Json(Health { status: "ok" })
}

async fn big_json() -> impl IntoResponse {
    let data: Vec<_> = (0..1000).map(|i| format!("item-{}", i)).collect();
    Json(data)
}

#[tokio::main]
async fn main() {
    // CORS:允許所有來源與方法
    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);  // 建議也加上 headers

    // Compression:自動根據 Accept-Encoding 壓縮回應
    let compression = CompressionLayer::new();

    // Timeout:每個請求最大 10 秒
    let timeout = TimeoutLayer::new(Duration::from_secs(10));

    let app = Router::new()
        .route("/health", get(health))
        .route("/big", get(big_json))
        // 中間件套在整個 router(注意順序:由外到內執行)
        .layer(timeout)     // 最外層:超時控制
        .layer(compression) // 中間層:壓縮
        .layer(cors);      // 最內層:CORS

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    println!("Server running on http://127.0.0.1:3000");
    println!("Try:");
    println!("   GET http://127.0.0.1:3000/health");
    println!("   GET http://127.0.0.1:3000/big");
    
    axum::serve(listener, app)
        .await
        .unwrap();
}

注意事項

  • CORS:在實際專案中避免使用 Any。建議列明允許的 origin 與 header,以避免安全風險。
  • Compression:壓縮會增加 CPU 負擔,對小回應可能反而不划算。可以根據回應大小條件性啟用。
  • Timeout:TimeoutLayer 會在 Service 層級取消 future,但內部非協作式(non-cooperative)阻塞仍可導致實際資源未釋放。盡量在 handler 中使用 async I/O 並配合 cancel-friendly 的設計。

日誌中間件與請求追蹤(Tracing)

日誌與追蹤有助於觀察系統行為、除錯與性能分析。建議用 tracing/ tracing-subscriber 搭配 tower-http 的 TraceLayer。Tracing 支援結構化日誌、span 與層級說明,比傳統 println 更適合生產環境。

為什麼要用 tracing

  • 結構化日誌方便在 log aggregator(如 ELK、Grafana Loki)做查詢與聚合。

範例程式碼:使用 tracing 與 TraceLayer

use axum::{Router, routing::get, response::Json};
use serde::Serialize;
use tracing::{info, Level};
use tracing_subscriber::{FmtSubscriber, layer::SubscriberExt, EnvFilter};
use tower_http::trace::TraceLayer;
use std::time::Instant;

#[derive(Serialize)]
struct Ping { pong: &'static str }

async fn ping() -> Json<Ping> {
    // 也可以在 handler 裡建立 span 或 log
    info!("handler ping 被呼叫");
    Json(Ping { pong: "pong" })
}

#[tokio::main]
async fn main() {
    // 建立 subscriber,從環境變數控制等級
    let subscriber = FmtSubscriber::builder()
        .with_env_filter(EnvFilter::from_default_env())
        .finish();

    tracing::subscriber::set_global_default(subscriber)
        .expect("設定 tracing subscriber 失敗");

    let app = Router::new()
        .route("/ping", get(ping))
        .layer(TraceLayer::new_for_http()); // tower-http 的 TraceLayer

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    info!("Server running on http://127.0.0.1:3000");
    
    axum::serve(listener, app)
        .await
        .unwrap();
}

在執行cargo run前,請輸入set RUST_LOG=trace,這樣就能看到最詳細的紀錄。

2025-09-25T04:08:49.413528Z  INFO my_first_axum: Server running on http://127.0.0.1:3000
2025-09-25T04:08:55.167886Z TRACE axum::serve: connection 127.0.0.1:56183 accepted
2025-09-25T04:08:55.168903Z DEBUG request{method=GET uri=/ping version=HTTP/1.1}: tower_http::trace::on_request: started processing request
2025-09-25T04:08:55.169268Z  INFO request{method=GET uri=/ping version=HTTP/1.1}: my_first_axum: handler ping 被呼叫
2025-09-25T04:08:55.169710Z DEBUG request{method=GET uri=/ping version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=200

自訂中間件開發

當內建或第三方中間件無法滿足需求時,必須撰寫自訂 Layer/Service。

為什麼要寫自訂中間件

  • 公司業務或觀察需求常常是特製化的(例如加密 header、依組織規則調整 response、動態限流);
  • 自訂中間件能把這些需求封裝為可重用元件,且可套用在 router 的不同層級。

範例:開發自訂HTTP 的響應時間監控中間件,用於測量和記錄每個 API 請求的處理時間,並在回應中加上自訂標頭 X-Response-Time

use axum::{body::Body, http::{Request, Response, header::HeaderName, HeaderValue}, Router};
use tower::{Layer, Service};
use std::task::{Context, Poll};
use std::pin::Pin;
use futures::future::BoxFuture;
use std::time::Instant;

#[derive(Clone)]
struct ResponseTimeLayer;

#[derive(Clone)]
struct ResponseTimeMiddleware<S> { inner: S }

impl<S> Layer<S> for ResponseTimeLayer {
    type Service = ResponseTimeMiddleware<S>;
    fn layer(&self, inner: S) -> Self::Service { ResponseTimeMiddleware { inner } }
}

impl<S, ReqBody> Service<Request<ReqBody>> for ResponseTimeMiddleware<S>
where
    S: Service<Request<ReqBody>, Response = Response<Body>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    ReqBody: Send + 'static,
{
    type Response = Response<Body>;
    type Error = S::Error;
    type 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<ReqBody>) -> Self::Future {
        let mut inner = self.inner.clone();
        let started = Instant::now();
        Box::pin(async move {
            let res = inner.call(req).await?;
            let elapsed = started.elapsed();
            // 計算耗時
            let mill = elapsed.as_millis().to_string();
            let mut res = res;
            res.headers_mut().insert(
                HeaderName::from_static("x-response-time-ms"),
                HeaderValue::from_str(&mill).unwrap(),
            );
            Ok(res)
        })
    }
}

async fn hello() -> &'static str {
    "Hello, World!"
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", axum::routing::get(hello))
        .layer(ResponseTimeLayer);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();

    axum::serve(listener, app)
        .await
        .unwrap();
}

脫離了tower-http的懷抱,從零開發自訂中間件就是這麼複雜。

測試

curl -i http://127.0.0.1:3000/
# 輸出內容
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
x-response-time-ms: 0
content-length: 13
date: Thu, 25 Sep 2025 04:25:10 GMT

可以看到我們增加的x-response-time-ms,因為是內網幾乎是0

說明與注意事項

  • Service 的泛型與類別依賴眾多,初期寫起來會覺得繁瑣。建議先使用 tower-http 等現有 layer 的實作方式。
  • 中間件應儘量保持單一職責(SRP),若有多個關注點,拆成多個 layer 以便測試與重用。


上一篇
Day10 Axum 請求處理全攻略:從提取器到資料驗證
下一篇
Day 12 Axum 狀態管理與資料共享
系列文
Rust 後端入門12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言