概述(Intro)

axum 的特别之处在于,它没有自己的定制中间件系统,而是与 tower 集成。这意味着 tower 和 tower-http 的中间件生态系统都可与 axum 兼容使用。

虽然编写或使用 axum 中间件并不需要完全理解 tower,但建议至少掌握 tower 的基础概念。如需全面了解,可参考 tower 的官方指南;同时也推荐阅读tower::ServiceBuilder的文档。

应用中间件(Applying middleware)

axum 几乎允许你在任何位置添加中间件:

  • 通过Router::layerRouter::route_layer,为整个路由器(Router)添加。
  • 通过MethodRouter::layerMethodRouter::route_layer,为方法路由器(MethodRouter)添加。
  • 通过Handler::layer,为单个处理器(handler)添加。

应用多个中间件(Applying multiple middleware)

建议使用tower::ServiceBuilder一次性应用多个中间件,而非重复调用layer(或route_layer):

use axum::{
    routing::get,
    Extension,
    Router,
};
use tower_http::{trace::TraceLayer};
use tower::ServiceBuilder;

async fn handler() {}

#[derive(Clone)]
struct State {}

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(Extension(State {}))
    );

常用中间件(Commonly used middleware)

一些常用的中间件包括:

  • TraceLayer:用于高级追踪 / 日志记录。
  • CorsLayer:用于处理跨域资源共享(CORS)。
  • CompressionLayer:用于自动压缩响应内容。
  • RequestIdLayerPropagateRequestIdLayer:用于设置和传递请求 ID。
  • TimeoutLayer:用于设置请求超时。

执行顺序(Ordering)

当你通过Router::layer(或类似方法)添加中间件时,所有之前添加的路由都会被该中间件包裹。一般来说,这会导致中间件按照 “从下到上” 的顺序执行。

例如,若你编写如下代码:

use axum::{routing::get, Router};

async fn handler() {}

let app = Router::new()
    .route("/", get(handler))
    .layer(layer_one)
    .layer(layer_two)
    .layer(layer_three);

可将中间件想象成洋葱的分层结构,每新增一层都会包裹之前所有的层:

        请求(requests)
           |
           v
+----- layer_three -----+
| +---- layer_two ----+ |
| | +-- layer_one --+ | |
| | |               | | |
| | |处理器(handler) | | |
| | |               | | |
| | +-- layer_one --+ | |
| +---- layer_two ----+ |
+----- layer_three -----+
           |
           v
        响应(responses)

具体执行流程如下:

  1. 首先,layer_three接收请求;
  2. 随后它执行自身逻辑,并将请求传递给layer_two
  3. layer_two执行逻辑后,将请求传递给layer_one
  4. layer_one执行逻辑后,将请求传递给处理器(handler),处理器生成响应;
  5. 响应先传递给layer_one,由其处理;
  6. 接着响应传递给layer_two,由其处理;
  7. 最后响应传递给layer_three,由其处理后从应用中返回。

实际情况会稍复杂一些:任何中间件都可以提前返回而不调用下一层(例如请求未通过授权时),但上述 “洋葱模型” 仍是理解中间件执行顺序的有用框架。

如前所述,建议使用tower::ServiceBuilder添加多个中间件,但这会改变执行顺序:

use tower::ServiceBuilder;
use axum::{routing::get, Router};

async fn handler() {}

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(layer_one)
            .layer(layer_two)
            .layer(layer_three),
    );

ServiceBuilder的工作方式是将所有层组合成一个整体,使其按照 “从上到下” 的顺序执行。因此,上述代码的执行流程为:

  • 请求顺序:layer_one → layer_two → layer_three → 处理器;
  • 响应顺序:处理器 → layer_three → layer_two → layer_one

中间件按 “从上到下” 的顺序执行通常更易于理解和梳理逻辑,这也是推荐使用ServiceBuilder的原因之一。

编写中间件(Writing middleware)

axum 提供了多种编写中间件的方式,这些方式处于不同的抽象层级,各有优缺点。

1. axum::middleware::from_fn

满足以下情况时,可使用axum::middleware::from_fn编写中间件:

  • 你不熟悉如何实现自定义Future,更倾向于使用熟悉的async/await语法;
  • 你不打算将中间件打包成 crate 供他人使用(这种方式编写的中间件仅与 axum 兼容)。

2. axum::middleware::from_extractor

满足以下情况时,可使用axum::middleware::from_extractor编写中间件:

  • 你有一个类型,既想将其用作提取器(extractor),又想用作中间件;
  • 若你仅需将该类型用作中间件,则优先选择middleware::from_fn

3. tower 的组合子(tower’s combinators)

tower 提供了多个实用的组合子(combinator),可用于对请求或响应进行简单修改。最常用的包括:

  • ServiceBuilder::map_request
  • ServiceBuilder::map_response
  • ServiceBuilder::then
  • ServiceBuilder::and_then

满足以下情况时,可使用这些组合子:

你需要执行小型临时操作(例如添加请求头);

  • 你不打算将中间件打包成 crate 供他人使用。

4. tower::Service 与 Pin<Box<dyn Future>>

若需获得最大控制权(并使用更底层的 API),你可以通过实现tower::Service来编写自定义中间件。

满足以下情况时,可结合Pin<Box<dyn Future>>使用tower::Service编写中间件:

  • 你的中间件需要支持配置(例如通过tower::Layer上的构建器方法进行配置,如tower_http::trace::TraceLayer);
  • 你打算将中间件打包成 crate 供他人使用;
  • 你不熟悉如何实现自定义Future

此类中间件的通用模板如下:

use axum::{
    response::Response,
    body::Body,
    extract::Request,
};
use futures_util::future::BoxFuture;
use tower::{Service, Layer};
use std::task::{Context, Poll};

#[derive(Clone)]
struct MyLayer;

impl<S> Layer<S> for MyLayer {
    type Service = MyMiddleware<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MyMiddleware { inner }
    }
}

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

impl<S> Service<Request> for MyMiddleware<S>
where
    S: Service<Request, Response = Response> + Send + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    // `BoxFuture` 是 `Pin<Box<dyn Future + Send + 'a>>` 的类型别名
    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, request: Request) -> Self::Future {
        let future = self.inner.call(request);
        Box::pin(async move {
            let response: Response = future.await?;
            Ok(response)
        })
    }
}

注意:将错误类型定义为S::Error意味着你的中间件通常不返回错误。作为原则,应始终尝试返回响应,而非通过自定义错误类型提前退出。例如,若你的中间件中使用的第三方库返回了其特有的错误类型,应尝试将其转换为合理的响应(如 400 Bad Request),并通过Ok返回该响应。

若你选择实现自定义错误类型(例如type Error = BoxError,即装箱的不透明错误),或任何非Infallible的错误类型,则必须使用HandleErrorLayer。以下是使用ServiceBuilder的示例:

// 注意:此代码为示例,可能无法直接编译
ServiceBuilder::new()
        .layer(HandleErrorLayer::new(|_: BoxError| async {
            // 由于axum使用无错误(infallible)类型,此处必须处理中间件返回的自定义错误类型
            StatusCode::BAD_REQUEST
        }))
        .layer(
             // <此处放置你实际会返回错误的层>
        );

5. tower::Service 与自定义 Future

若你熟悉如何实现自定义Future(或希望学习相关知识),且需要尽可能多的控制权,则可使用不依赖装箱Future(boxed futures)的tower::Service

满足以下情况时,可结合手动实现的Future使用tower::Service编写中间件:

  • 你希望中间件的开销尽可能低;
  • 你的中间件需要支持配置(例如通过tower::Layer上的构建器方法进行配置,如tower_http::trace::TraceLayer);
  • 你打算将中间件打包成 crate 供他人使用(可能作为tower-http的一部分);
  • 你熟悉如何实现自定义Future,或希望了解异步 Rust 底层的工作原理。

tower 的《从零开始构建中间件》(“Building a middleware from scratch”)指南是学习相关方法的优质资源。

中间件的错误处理(Error handling for middleware)

axum 的错误处理模型要求处理器(handler)始终返回响应。但中间件是可能向应用引入错误的环节之一:若hyper接收到错误,会直接关闭连接而不发送响应。因此,axum 要求这些错误必须被妥善处理:

use axum::{
    routing::get,
    error_handling::HandleErrorLayer,
    http::StatusCode,
    BoxError,
    Router,
};
use tower::{ServiceBuilder, timeout::TimeoutLayer};
use std::time::Duration;

async fn handler() {}

let app = Router::new()
    .route("/", get(handler))
    .layer(
        ServiceBuilder::new()
            // 该中间件需放在`TimeoutLayer`之上,因为它要接收`TimeoutLayer`返回的错误
            .layer(HandleErrorLayer::new(|_: BoxError| async {
                StatusCode::REQUEST_TIMEOUT
            }))
            .layer(TimeoutLayer::new(Duration::from_secs(10)))
    );

有关 axum 错误处理模型的更多细节,请参阅error_handling文档。

路由到服务 / 中间件与背压(Routing to services/middleware and backpressure)

通常情况下,“路由到多个服务中的一个” 与 “背压(backpressure)” 难以兼容。理想状态下,你希望在调用服务前确保它已准备好接收请求;但要确定调用哪个服务,又必须先获取请求 —— 这就形成了矛盾。

解决该矛盾的方案主要有两种:

  1. 等待所有服务就绪:仅当所有目标服务都准备就绪后,才认为路由器服务(router service)自身已就绪。tower::steer::Steer采用的就是这种方案。
  2. 默认认为服务就绪:在Service::poll_ready中始终返回Poll::Ready(Ok(())),并在Service::call返回的响应Future内部实际处理服务就绪状态。若你的服务本身不关注背压且始终处于就绪状态,这种方案会很有效。

axum 默认假设应用中使用的所有服务都不关注背压,因此采用了上述第二种方案。但这意味着你应避免路由到关注背压的服务(或使用这类中间件);若无法避免,至少应实现负载丢弃(load shed)逻辑,确保请求能被快速丢弃而不堆积。

这还意味着:若poll_ready返回错误,该错误会在call返回的响应Future中抛出,而非直接从poll_ready抛出。在这种情况下,底层服务不会被丢弃,仍会用于后续请求。因此,那些期望在poll_ready失败时被丢弃的服务不应与 axum 一同使用。

兼容关注背压的中间件的方案

一种可行的方案是:仅在整个应用外围应用关注背压的中间件。这之所以可行,是因为 axum 应用本身也是一种服务(service):

use axum::{
    routing::get,
    Router,
};
use tower::ServiceBuilder;

async fn handler() { /* ... */ }

let app = Router::new().route("/", get(handler));

let app = ServiceBuilder::new()
    .layer(some_backpressure_sensitive_middleware)
    .service(app);

但以这种方式在应用外围应用中间件时,你必须确保错误仍能被妥善处理。

另需注意:由异步函数创建的处理器(handler)不关注背压且始终处于就绪状态。因此,若你未使用任何 Tower 中间件,则无需担心上述背压相关问题。

在中间件中访问状态(Accessing state in middleware)

如何让中间件能够访问状态(state),取决于中间件的编写方式。

在 axum::middleware::from_fn 中访问状态

使用axum::middleware::from_fn_with_state即可。

在自定义 tower::Layer 中访问状态

use axum::{
    Router,
    routing::get,
    middleware::{self, Next},
    response::Response,
    extract::{State, Request},
};
use tower::{Layer, Service};
use std::task::{Context, Poll};

#[derive(Clone)]
struct AppState {}

#[derive(Clone)]
struct MyLayer {
    state: AppState,
}

impl<S> Layer<S> for MyLayer {
    type Service = MyService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        MyService {
            inner,
            state: self.state.clone(),
        }
    }
}

#[derive(Clone)]
struct MyService<S> {
    inner: S,
    state: AppState,
}

impl<S, B> Service<Request<B>> for MyService<S>
where
    S: Service<Request<B>>,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = S::Future;

    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 {
        // 对`self.state`执行相关操作
        // 有关如何直接从`Request`运行提取器(extractor),请参阅`axum::RequestExt`的文档

        self.inner.call(req)
    }
}

async fn handler(_: State<AppState>) {}

let state = AppState {};

let app = Router::new()
    .route("/", get(handler))
    .layer(MyLayer { state: state.clone() })
    .with_state(state);

从中间件向处理器传递状态(Passing state from middleware to handlers)

可通过请求扩展(request extensions)将状态从中间件传递给处理器:

use axum::{
    Router,
    http::StatusCode,
    routing::get,
    response::{IntoResponse, Response},
    middleware::{self, Next},
    extract::{Request, Extension},
};

#[derive(Clone)]
struct CurrentUser { /* ... */ }

async fn auth(mut req: Request, next: Next) -> Result<Response, StatusCode> {
    let auth_header = req.headers()
        .get(http::header::AUTHORIZATION)
        .and_then(|header| header.to_str().ok());

    let auth_header = if let Some(auth_header) = auth_header {
        auth_header
    } else {
        return Err(StatusCode::UNAUTHORIZED);
    };

    if let Some(current_user) = authorize_current_user(auth_header).await {
        // 将当前用户(current_user)插入请求扩展中,以便处理器能提取该状态
        req.extensions_mut().insert(current_user);
        Ok(next.run(req).await)
    } else {
        Err(StatusCode::UNAUTHORIZED)
    }
}

async fn authorize_current_user(auth_token: &str) -> Option<CurrentUser> {
    // ... 授权逻辑 ...
}

async fn handler(
    // 提取由中间件设置的当前用户(current_user)
    Extension(current_user): Extension<CurrentUser>,
) {
    // ... 处理器逻辑 ...
}

let app = Router::new()
    .route("/", get(handler))
    .route_layer(middleware::from_fn(auth));

也可使用响应扩展(response extensions),但需注意:请求扩展不会自动迁移到响应扩展中,你需要手动将所需的扩展迁移过去。

在中间件中重写请求 URI(Rewriting request URI in middleware)

通过Router::layer添加的中间件会在路由(routing)完成后运行,这意味着此类中间件无法用于重写请求 URI—— 当中间件执行时,路由已经完成。

解决方案是将中间件包裹在整个路由器(Router)外围(这之所以可行,是因为 Router 实现了Service trait):

use tower::Layer;
use axum::{
    Router,
    ServiceExt, // 用于`into_make_service`方法
    response::Response,
    middleware::Next,
    extract::Request,
};

fn rewrite_request_uri<B>(req: Request<B>) -> Request<B> {
    // ... 重写URI的逻辑 ...
}

// 此处可使用任意`tower::Layer`类型
let middleware = tower::util::MapRequestLayer::new(rewrite_request_uri);

let app = Router::new();

// 将该层包裹在整个`Router`外围
// 这样中间件会在`Router`接收请求前执行
let app_with_middleware = middleware.layer(app);

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app_with_middleware.into_make_service()).await.unwrap();

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐