layer 与 route_layer
主线位置:两条主线都在。装配主线上,
.layer(...)在调用的那一刻把 towerLayer套到路由器里已有的每个服务上;请求主线上,这些中间件在路由匹配之后、提取器之前执行。route_layer和layer的区别只在装配时少套了哪几个服务,请求时的结果却差在状态码上。
flowchart TB
req["请求"] --> pr{"PathRouter 匹配路径"}
pr -->|命中| mr{"MethodRouter 选方法"}
pr -->|没命中| fb["fallback:404"]
mr -->|有这个方法| slot["方法槽 Route"]
mr -->|没有| mf["方法级 fallback:405"]
slot -.- a1["layer、两种 route_layer 都套"]
mf -.- a2["layer、Router::route_layer 套"]
fb -.- a3["只有 layer 套"]
Service、Layer 本身和 ServiceBuilder 的顺序规则在 tower 卷讲过(Layer 与 Stack、层的顺序反了)。这一页只讲 axum 自己的部分:Router、MethodRouter、Handler 三处的 layer 各套在哪、什么时候套、对错误类型有什么要求。
Router::layer:套在已有的每个服务上
Router::layer 不包整个路由器,而是把同一个 layer 分给 RouterInner 的三个字段:
pub fn layer<L>(self, layer: L) -> Router<S>
where
L: Layer<Route> + Clone + Send + Sync + 'static,
L::Service: Service<Request> + Clone + Send + Sync + 'static,
<L::Service as Service<Request>>::Response: IntoResponse + 'static,
<L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
<L::Service as Service<Request>>::Future: Send + 'static,
{
map_inner!(self, this => RouterInner {
path_router: this.path_router.layer(layer.clone()),
fallback_router: this.fallback_router.layer(layer.clone()),
default_fallback: this.default_fallback,
catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)),
})
}
PathRouter::layer 遍历 self.routes 这个 HashMap<RouteId, Endpoint<S>>,对每个端点调用 Endpoint::layer:是 MethodRouter 就交给 MethodRouter::layer,是 Route(route_service 注册的)就交给 Route::layer。layer 不记在 Router 上(只有还欠状态的 handler 槽会把它存进闭包,等 with_state 时再套),所以之后再 .route(...) 加的路由不会有它。fallback_router 是 Router::new() 时就注册好的两条 NotFound 路由,catch_all_fallback 是兜底的 NotFound,两者都被套上,所以 404 也经过中间件。这几处各套几次、with_state 之前的 handler 为什么要推迟到 with_state 才套,见 一个请求穿过 axum;RouterInner 各字段的含义见 Router 的内部结构。
下面的程序在 .layer 之后再加一条 /late,中间件打印它见到的请求和 MatchedPath:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::{MatchedPath, Request};
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;
// Logs every request it sees, then passes it on.
async fn log(req: Request, next: Next) -> Response {
let matched = req.extensions().get::<MatchedPath>().map(|m| m.as_str().to_owned());
println!(" log sees {} {} matched={matched:?}", req.method(), req.uri());
next.run(req).await
}
async fn send(app: &Router, method: &str, uri: &str) {
let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
println!(" {:<10} -> {}", format!("{method} {uri}"), res.status());
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app = Router::new()
.route("/a", get(|| async { "a" }))
.layer(from_fn(log))
// added after .layer(): not wrapped
.route("/late", get(|| async { "late" }));
for (m, u) in [("GET", "/a"), ("GET", "/late"), ("POST", "/a"), ("GET", "/nope")] {
send(&app, m, u).await;
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
log sees GET /a matched=Some("/a")
GET /a -> 200 OK
GET /late -> 200 OK
log sees POST /a matched=Some("/a")
POST /a -> 405 Method Not Allowed
log sees GET /nope matched=None
GET /nope -> 404 Not Found
/late返回 200,但中间件没打印:它在.layer之后才加进来。POST /a的 405 和GET /nope的 404 都经过了中间件。405 来自/a这个MethodRouter的方法级 fallback(MethodRouter),404 来自fallback_router。- 中间件看到
/a时MatchedPath已经有值:路由在中间件之前做完了。404 的那次是None。
route_layer:少套哪两处
Router::route_layer 和 layer 的约束完全一样,区别在函数体:
map_inner!(self, this => RouterInner {
path_router: this.path_router.route_layer(layer),
fallback_router: this.fallback_router,
default_fallback: this.default_fallback,
catch_all_fallback: this.catch_all_fallback,
})
fallback_router 和 catch_all_fallback 原样保留,没匹配到任何路径的请求不经过中间件。PathRouter::route_layer 先检查有没有路由,没有就 panic:「Adding a route_layer before any routes is a no-op. Add the routes you want the layer to apply to first.」然后和 PathRouter::layer 一样,对每个端点调 endpoint.layer(...)。
关键在这一步:端点是 MethodRouter 时,调用的是 MethodRouter::layer,不是 MethodRouter::route_layer。MethodRouter::layer 套的是全部方法槽加上方法级 fallback:
let layer_fn = move |route: Route<E>| route.layer(layer.clone());
MethodRouter {
get: self.get.map(layer_fn.clone()),
head: self.head.map(layer_fn.clone()),
// ...
connect: self.connect.map(layer_fn.clone()),
fallback: self.fallback.map(layer_fn),
allow_header: self.allow_header,
}
而 MethodRouter::route_layer 只套九个方法槽,fallback 不动:
let layer_fn = move |svc| Route::new(layer.layer(svc));
self.get = self.get.map(layer_fn.clone());
self.head = self.head.map(layer_fn.clone());
// ...
self.connect = self.connect.map(layer_fn);
self
所以两个 route_layer 管的范围不一样:Router::route_layer 跳过的是「路径没匹配上」,MethodRouter::route_layer 跳过的是「路径匹配上了但方法不对」。两份文档的说法也对应:Router 的那份说它防止把「404 Not Found 变成 401 Unauthorized」,MethodRouter 的那份说的是「405 Method Not Allowed 变成 401 Unauthorized」。
layer_on_matching_routeRouter::route_layer 是 #474 加的,随 axum 0.3.2(tag v0.3.2)发布。PR 描述了当时的问题:用 .layer(RequireAuthorizationLayer::bearer("password")) 保护 /foo 之后,「calling some unknown route like GET /not-found would also return 401 Unauthorized」,原因是「the 404 you'd otherwise see is generated by a fallback service stored on Router. When adding a layer to the router the layer would also be applied to the fallback」。作者认为大多数中间件就该对所有请求生效(「you don't wanna skip logging for unmatched requests」),所以没改 layer,而是另加一个方法,PR 里起的名字是 layer_on_matching_route,合并时叫 route_layer。今天 Router::route_layer 的函数体就是这个思路:fallback_router 和 catch_all_fallback 不套。
下面用一个没有 token 头就返回 401 的中间件,比较四种挂法。每种挂法发三个请求:匹配的 GET /a、方法不对的 POST /a、路径不存在的 GET /nope,都不带 token:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::{from_fn, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;
// Rejects every request without a `token` header.
async fn auth(req: Request, next: Next) -> Response {
if req.headers().contains_key("token") {
next.run(req).await
} else {
StatusCode::UNAUTHORIZED.into_response()
}
}
async fn row(name: &str, app: Router) {
let mut cells = Vec::new();
for (method, uri) in [("GET", "/a"), ("POST", "/a"), ("GET", "/nope")] {
let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
let res = app.clone().oneshot(req).await.unwrap();
cells.push(format!("{method} {uri} {}", res.status().as_u16()));
}
println!("{name:<26} {}", cells.join(" | "));
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let h = || async { "a" };
row("Router::layer", Router::new().route("/a", get(h)).layer(from_fn(auth))).await;
row("Router::route_layer", Router::new().route("/a", get(h)).route_layer(from_fn(auth))).await;
row(
"MethodRouter::route_layer",
Router::new().route("/a", get(h).route_layer(from_fn(auth))),
)
.await;
row(
"fallback + route_layer",
Router::new()
.route("/a", get(h))
.fallback(|| async { (StatusCode::IM_A_TEAPOT, "fallback") })
.route_layer(from_fn(auth)),
)
.await;
}
Router::route_layer 那一行,POST /a 的客户端看到 401 还是 405?M2 Max、rustc 1.99.0。答案在本页最后。程序里最后一种挂法(先 .fallback(...) 再 .route_layer(...))是照着文档写的。0.8.9 里 Router::route_layer 的文档(docs/routing/route_layer.md)仍写着「first add your routes (and / or fallback) and then call route_layer」,读起来像是 fallback 也会被套。issue #3421 指出了这一点,随后的 #3567(axum 0.8.8,CHANGELOG 记作「Clarify documentation for Router::route_layer」)删掉了「(and / or fallback)」,但改动的文件是 docs/method_routing/route_layer.md,Router 那份没动。以函数体为准:Router::route_layer 不碰 fallback_router 和 catch_all_fallback。
三个层级与执行顺序
layer 有三个挂点:Handler::layer 只包一个 handler,MethodRouter::layer 包一条路径的所有方法(连同 405),Router::layer 包所有已有路由和 fallback。同一层级上多次 .layer,后写的在外层;三个层级之间,Router 在最外,Handler 在最里。下面的程序用同一个中间件函数,靠 from_fn_with_state 传进来的名字区分各层(from_fn_with_state 见 from_fn 与它的兄弟),最后两段留给下一节:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::{MatchedPath, Request, State};
use axum::handler::Handler;
use axum::middleware::{from_fn_with_state, map_request, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::{Layer, ServiceExt};
// One middleware function, named by its state.
async fn log(State(name): State<&'static str>, req: Request, next: Next) -> Response {
let matched = req.extensions().get::<MatchedPath>().is_some();
println!(" {name} sees request (matched={matched})");
let res = next.run(req).await;
println!(" {name} sees response");
res
}
async fn handler() -> &'static str {
println!(" handler");
"ok"
}
fn req(uri: &str) -> Request {
Request::get(uri).body(Body::empty()).unwrap()
}
fn rewrite(mut req: Request) -> Request {
if req.uri() == "/old" {
*req.uri_mut() = "/".parse().unwrap();
}
req
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!(".layer(A).layer(B).layer(C)");
let app = Router::new()
.route("/", get(handler))
.layer(from_fn_with_state("A", log))
.layer(from_fn_with_state("B", log))
.layer(from_fn_with_state("C", log));
app.oneshot(req("/")).await.unwrap();
println!("Handler::layer + MethodRouter::layer + Router::layer");
let app = Router::new()
.route(
"/",
get(handler.layer(from_fn_with_state("handler-level", log)))
.layer(from_fn_with_state("method-level", log)),
)
.layer(from_fn_with_state("router-level", log));
app.oneshot(req("/")).await.unwrap();
println!("whole Router wrapped: outer.layer(router)");
let app = Router::new()
.route("/", get(handler))
.layer(from_fn_with_state("inner", log));
let wrapped = from_fn_with_state("outer", log).layer(app);
wrapped.oneshot(req("/")).await.unwrap();
println!("rewrite /old -> / with Router::layer");
let app = Router::new().route("/", get(handler)).layer(map_request(rewrite_async));
let res = app.oneshot(req("/old")).await.unwrap();
println!(" status {}", res.status());
println!("rewrite /old -> / around the whole Router");
let app = Router::new().route("/", get(handler));
let res = tower::util::MapRequestLayer::new(rewrite).layer(app).oneshot(req("/old")).await.unwrap();
println!(" status {}", res.status());
}
async fn rewrite_async(req: Request) -> Request {
rewrite(req)
}
M2 Max、rustc 1.99.0,连跑 3 次输出相同。前两段:
.layer(A).layer(B).layer(C)
C sees request (matched=true)
B sees request (matched=true)
A sees request (matched=true)
handler
A sees response
B sees response
C sees response
Handler::layer + MethodRouter::layer + Router::layer
router-level sees request (matched=true)
method-level sees request (matched=true)
handler-level sees request (matched=true)
handler
handler-level sees response
method-level sees response
router-level sees response
原因都在 Route::layer:Route::new((MapErrLayer::new(Into::into), layer).layer(self)),新 layer 包住到目前为止的整个 Route。tower 卷的 层的顺序反了 拿这一点和 ServiceBuilder(先写的在外)做了对比,那里也有用 ServiceBuilder 把一组 layer 一次交给 Router::layer 的写法。
三个层级还有一处不同:Layer::layer 在什么时候被调用。
| 挂法 | Layer::layer 何时调用 | 中间件的错误类型 |
|---|---|---|
Router::layer | 调用 .layer 时;欠状态的 handler 推迟到 with_state | 要 Into<Infallible> |
MethodRouter::layer | 同上 | 任意 NewError,得到 MethodRouter<S, NewError> |
Handler::layer | 每个请求一次 | 要 Infallible |
Handler::layer 返回的 Layered 只存下 layer 和 handler,套的动作在它的 Handler::call 里:
fn call(self, req: Request, state: S) -> Self::Future {
use futures_util::future::{FutureExt, Map};
let svc = self.handler.with_state(state);
let svc = self.layer.layer(svc);
// ...
> = svc.oneshot(req).map(|result| match result {
Ok(res) => res.into_response(),
Err(err) => match err {},
});
Handler::call 每个请求调用一次,于是 layer 每个请求套一遍,套出来的服务用完就扔。对无状态的 layer 没有区别;对 ConcurrencyLimitLayer 这种在 layer 时新建信号量的,每个请求拿到的都是一个新信号量。用一个只计数的 Counted 包住 ConcurrencyLimitLayer::new(1),三种挂法各同时发 3 个请求,handler 睡 100ms:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["limit", "util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "time"] }
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use axum::body::Body;
use axum::extract::Request;
use axum::handler::Handler;
use axum::routing::get;
use axum::Router;
use tower::limit::ConcurrencyLimitLayer;
use tower::{Layer, ServiceExt};
static LAYERS: AtomicUsize = AtomicUsize::new(0);
// Counts Layer::layer calls, delegates to the wrapped layer.
#[derive(Clone)]
struct Counted<L>(L);
impl<L: Layer<I>, I> Layer<I> for Counted<L> {
type Service = L::Service;
fn layer(&self, inner: I) -> Self::Service {
LAYERS.fetch_add(1, Ordering::SeqCst);
self.0.layer(inner)
}
}
async fn slow() {
tokio::time::sleep(Duration::from_millis(100)).await;
}
async fn run(name: &str, app: Router) {
let app = app.with_state(());
let before = LAYERS.load(Ordering::SeqCst);
let start = Instant::now();
let tasks: Vec<_> = (0..3)
.map(|_| {
let app = app.clone();
tokio::spawn(async move {
app.oneshot(Request::get("/").body(Body::empty()).unwrap()).await.unwrap();
start.elapsed().as_millis() / 10 * 10
})
})
.collect();
let mut done = Vec::new();
for t in tasks {
done.push(t.await.unwrap());
}
done.sort();
println!(
"{name:<20} Layer::layer during 3 requests = {}, done at ~{done:?} ms",
LAYERS.load(Ordering::SeqCst) - before
);
}
#[tokio::main]
async fn main() {
let limit = || Counted(ConcurrencyLimitLayer::new(1));
run("Handler::layer", Router::new().route("/", get(slow.layer(limit())))).await;
run("MethodRouter::layer", Router::new().route("/", get(slow).layer(limit()))).await;
run("Router::layer", Router::new().route("/", get(slow)).layer(limit())).await;
}
连跑 3 次输出相同(毫秒数向下取整到 10,完成时间排了序):
Handler::layer Layer::layer during 3 requests = 3, done at ~[100, 100, 100] ms
MethodRouter::layer Layer::layer during 3 requests = 0, done at ~[100, 200, 300] ms
Router::layer Layer::layer during 3 requests = 0, done at ~[100, 200, 300] ms
Handler::layer 那一行:3 个请求调了 3 次 Layer::layer,上限 1 没起作用,三个请求同时完成。另外两行在 with_state(()) 时就套好了,请求时 0 次,三个请求排队。Handler::layer 自己的文档例子正是 handler.layer(ConcurrencyLimitLayer::new(64));要限流就挂在 MethodRouter 或 Router 上,或者用 GlobalConcurrencyLimitLayer(信号量在 Arc 里,套几遍都是同一个,见 ConcurrencyLimit)。
0.5 的 Handler::layer 是 Layered::new(layer.layer(self.into_service())):调用 layer 的当下就把 handler 变成服务、套上 layer,之后每个请求复用。#1155(引入 State,首次发布于 axum-v0.6.0-rc.1,正式版 0.6.0)给 handler 加上了状态类型参数,PR 的说法是「MethodRouter, FromRequest, RequestParts, and Handler then all have a new type parameter for the state」。从此 handler 要拿到状态才能变成服务,而状态只在 Handler::call(self, req, state) 里才有,这个 PR 于是把 Layered 改成存下 layer,在 call 里先 self.handler.with_state(state) 再 self.layer.layer(svc),就是上面引用的那几行。Router、MethodRouter 直接交给 axum::serve 时每个请求重套 layer 的问题,0.7.5 由 #2586 修掉了(见 一个请求穿过 axum 的冷知识);Handler::layer 的套法在 Layered::call 里,到 0.8.9 还是每个请求一次。
路由之前执行:把整个 Router 包起来
中间件在路由之后执行,所以它改请求的 URI 不会影响路由结果。axum 文档 middleware.md 的「Rewriting request URI in middleware」 给的办法是反过来:Router<()> 本身是 Service,用 Layer::layer(router) 把整个路由器包住。上一节程序的后两段就是这个:日志中间件分别放在里外两层,同一个改写函数(把 /old 改成 /)用两种方式挂上。输出:
whole Router wrapped: outer.layer(router)
outer sees request (matched=false)
inner sees request (matched=true)
handler
inner sees response
outer sees response
rewrite /old -> / with Router::layer
status 404 Not Found
rewrite /old -> / around the whole Router
handler
status 200 OK
包在外面的 outer 先于路由执行,MatchedPath 还没有;Router::layer 挂上的 inner 在路由之后。map_request 挂在 Router::layer 上时,路由已经按 /old 匹配失败,进入的是 404 fallback,改 URI 已经晚了。
代价是包完之后的类型不再是 Router,不能再 .route、.with_state;交给 axum::serve 时,文档的例子用 axum::ServiceExt::into_make_service 把它变成 make service。
错误类型:Into<Infallible> 与 HandleErrorLayer
Router::layer 的约束里有一条 Error: Into<Infallible>:中间件不能产生错误,只能产生响应。axum 的 middleware.md 解释了原因:「If hyper receives an error the connection will be closed without sending a response. Thus axum requires those errors to be handled gracefully」。不少 tower 中间件会产生错误,比如 tower 的 TimeoutLayer,超时时返回 Err(Elapsed),错误类型是 BoxError(Timeout)。直接挂上去:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["timeout"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::time::Duration;
use axum::{Router, routing::get};
use tower::timeout::TimeoutLayer;
fn main() {
let _app: Router = Router::new()
.route("/", get(|| async { "ok" }))
.layer(TimeoutLayer::new(Duration::from_secs(1)));
}
rustc 1.99.0 的报错(只去掉了路径前缀):
error[E0277]: the trait bound `Infallible: From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not satisfied
--> src/bin/timeout_bad.rs:9:12
|
9 | .layer(TimeoutLayer::new(Duration::from_secs(1)));
| ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not implemented for `Infallible`
| |
| required by a bound introduced by this call
|
help: the trait `From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not implemented for `Infallible`
but trait `From<!>` is implemented for it
--> core/src/convert/mod.rs:990:1
|
990 | const impl From<!> for Infallible {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: for that trait implementation, expected `!`, found `Box<(dyn std::error::Error + Send + Sync + 'static)>`
= note: required for `Box<(dyn std::error::Error + Send + Sync + 'static)>` to implement `Into<Infallible>`
note: required by a bound in `Router::<S>::layer`
--> axum-0.8.9/src/routing/mod.rs:308:50
|
303 | pub fn layer<L>(self, layer: L) -> Router<S>
| ----- required by a bound in this associated function
...
308 | <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
| ^^^^^^^^^^^^^^^^ required by this bound in `Router::<S>::layer`
报错里没有出现 Timeout,只说 Box<dyn Error + Send + Sync> 转不成 Infallible,指向 Router::layer 的第 308 行。看到 Into<Infallible> 就是这个原因。
约束写成 Into<Infallible> 而不是 = Infallible,Route::layer 里的 MapErrLayer::new(Into::into) 再把错误转成 Infallible。这是 issue #922 要求的:一个组合两个服务的中间件,用枚举 MyError<Infallible, Infallible> 表示错误,它同样不可能有值,却不是 Infallible 这个类型。issue 的原话是「axum really just cares that the error isn't possible rather than needing it to be the specific type Infallible」。改动在 axum 0.6.0 发布(#948)。
修法是在 TimeoutLayer 外面套一层 HandleErrorLayer,它的 Service 实现 把内层的 Err 交给你的函数变成响应,自己的 Error 是 Infallible:
type Response = Response;
type Error = Infallible;
type Future = future::HandleErrorFuture;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let f = self.f.clone();
let clone = self.inner.clone();
let inner = std::mem::replace(&mut self.inner, clone);
let future = Box::pin(async move {
match inner.oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Ok(f(err).await.into_response()),
}
});
future::HandleErrorFuture { future }
}
poll_ready 永远就绪,等内层就绪的事放进了 oneshot。这个函数前面还可以放最多 16 个 FromRequestParts<()> 提取器(impl_service! 从 T1 展开到 T1..T16),错误放最后。两种挂法:
use std::time::Duration;
use axum::body::Body;
use axum::error_handling::HandleErrorLayer;
use axum::extract::Request;
use axum::http::{Method, StatusCode, Uri};
use axum::routing::get;
use axum::{BoxError, Router};
use tower::timeout::{TimeoutLayer, error::Elapsed};
use tower::{ServiceBuilder, ServiceExt};
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_secs(1)).await;
"done"
}
// Extractors (FromRequestParts<()>) may come before the error.
async fn on_error(method: Method, uri: Uri, err: BoxError) -> (StatusCode, String) {
if err.is::<Elapsed>() {
(StatusCode::REQUEST_TIMEOUT, format!("{method} {uri} timed out"))
} else {
(StatusCode::INTERNAL_SERVER_ERROR, format!("unhandled: {err}"))
}
}
async fn show(name: &str, app: Router) {
let res = app.oneshot(Request::get("/slow").body(Body::empty()).unwrap()).await.unwrap();
let status = res.status();
let body = axum::body::to_bytes(res.into_body(), usize::MAX).await.unwrap();
println!("{name}: {status} {body:?}");
}
#[tokio::main]
async fn main() {
// One Router::layer call, HandleErrorLayer outside TimeoutLayer.
let app = Router::new().route("/slow", get(slow)).layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(on_error))
.layer(TimeoutLayer::new(Duration::from_millis(50))),
);
show("ServiceBuilder", app).await;
// MethodRouter::layer may change the error type; handle_error brings it back.
let app = Router::new().route(
"/slow",
get(slow)
.layer(TimeoutLayer::new(Duration::from_millis(50)))
.handle_error(on_error),
);
show("MethodRouter ", app).await;
}
依赖同上,连跑 3 次输出相同:
ServiceBuilder: 408 Request Timeout b"GET /slow timed out"
MethodRouter : 408 Request Timeout b"GET /slow timed out"
- 在
Router上,HandleErrorLayer和TimeoutLayer必须放进同一次.layer调用,ServiceBuilder里先写HandleErrorLayer(外层)。拆成.layer(TimeoutLayer::new(..)).layer(HandleErrorLayer::new(..))两次调用,第一次就因为同样的Into<Infallible>编译不过;Router里每个服务都是Route(Route<Infallible>),没有地方暂存一个带错误的中间状态。 MethodRouter可以:MethodRouter::layer的约束是Error: Into<NewError>,TimeoutLayer之后类型变成MethodRouter<S, BoxError>,再用handle_error(就是self.layer(HandleErrorLayer::new(f)))变回MethodRouter<S, Infallible>,Router::route才肯收。
tower-http 的 TimeoutLayer 不需要这一步:它超时时直接返回一个响应,错误类型不变,见 tower-http 的其余常用中间件。
动脑筋答案
连跑 3 次输出相同:
Router::layer GET /a 401 | POST /a 401 | GET /nope 401
Router::route_layer GET /a 401 | POST /a 401 | GET /nope 404
MethodRouter::route_layer GET /a 401 | POST /a 405 | GET /nope 404
fallback + route_layer GET /a 401 | POST /a 401 | GET /nope 418
Router::route_layer下POST /a是 401。PathRouter::route_layer对/a的MethodRouter调的是MethodRouter::layer,方法级 fallback 也被套上了,鉴权中间件先拒绝,405 永远出不来。客户端看到的是「没登录」,而不是「这个路径不支持 POST」。GET /nope是 404:fallback_router和catch_all_fallback没套,鉴权中间件根本没被调用。这正是route_layer想解决的问题。MethodRouter::route_layer三格各不相同:只套方法槽,405 和 404 都原样返回。想让 401 只出现在「路径和方法都对」的请求上,要用这一种。- 自定义 fallback 加
Router::route_layer,GET /nope是 418:Router::fallback设置的 fallback 在fallback_router/catch_all_fallback里,route_layer不套它,和调用顺序无关。要让 fallback 也经过鉴权,只能用layer。
小结
Router::layer把 layer 交给每个已有路由的端点、fallback_router和catch_all_fallback,layer 本身不保存,之后加的路由不受影响。- 中间件在路由之后执行,能看到
MatchedPath,改 URI 不影响路由;要在路由之前执行,用Layer::layer(router)包住整个Router。 Router::route_layer只跳过两个 fallback(404),每条路径的方法级 fallback 仍被套上,鉴权中间件会把 405 变成 401;MethodRouter::route_layer才连 405 一起跳过。- 后
.layer的在外层;三个层级Router在外、MethodRouter居中、Handler在内。 Handler::layer在每个请求的Handler::call里重新layer一次,ConcurrencyLimitLayer挂在这里不起作用。Router::layer要求中间件Error: Into<Infallible>;产生错误的 tower 中间件要和HandleErrorLayer放进同一次.layer,或者先挂在MethodRouter上再handle_error。