Axum文档 ~ 6.中间件层(middleware)
axum中间件四个核心要点:①集成:无独立中间件系统,集成tower复用生态;②应用与顺序:可用于路由器、方法路由或单处理器,多中间件建议使用ServiceBuilder(顺序从上到下),普通layer顺序相反;③编写方式:提供from_fn(兼容async/await)、from_extractor(兼提取器)、tower组合子(简单操作)及实现tower::Service(适合发布)等方法;④
概述(Intro)
axum 的特别之处在于,它没有自己的定制中间件系统,而是与 tower 集成。这意味着 tower 和 tower-http 的中间件生态系统都可与 axum 兼容使用。
虽然编写或使用 axum 中间件并不需要完全理解 tower,但建议至少掌握 tower 的基础概念。如需全面了解,可参考 tower 的官方指南;同时也推荐阅读tower::ServiceBuilder
的文档。
应用中间件(Applying middleware)
axum 几乎允许你在任何位置添加中间件:
- 通过
Router::layer
和Router::route_layer
,为整个路由器(Router)添加。 - 通过
MethodRouter::layer
和MethodRouter::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
:用于自动压缩响应内容。RequestIdLayer
与PropagateRequestIdLayer
:用于设置和传递请求 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)
具体执行流程如下:
- 首先,
layer_three
接收请求; - 随后它执行自身逻辑,并将请求传递给
layer_two
; layer_two
执行逻辑后,将请求传递给layer_one
;layer_one
执行逻辑后,将请求传递给处理器(handler),处理器生成响应;- 响应先传递给
layer_one
,由其处理; - 接着响应传递给
layer_two
,由其处理; - 最后响应传递给
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)” 难以兼容。理想状态下,你希望在调用服务前确保它已准备好接收请求;但要确定调用哪个服务,又必须先获取请求 —— 这就形成了矛盾。
解决该矛盾的方案主要有两种:
- 等待所有服务就绪:仅当所有目标服务都准备就绪后,才认为路由器服务(router service)自身已就绪。
tower::steer::Steer
采用的就是这种方案。 - 默认认为服务就绪:在
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();
更多推荐
所有评论(0)