中间件没生效
主线位置:问题出在装配主线。
.layer(...)在调用的那一刻把 layer 套到当时已有的每个服务上,Router里没有任何地方记下「以后加进来的也要套」。请求主线上看到的只是结果:同一个路由器里,有的请求经过中间件,有的不经过。
flowchart TB newnode["Router::new()"] --> before["route / fallback / nest"] before --> lay["layer(mw)"] lay --> after["再 route / fallback / nest / merge"] before -.- w1["经过 mw"] after -.- w2["不经过 mw"]
这一页没有报错可抄:程序编译通过,请求也有响应,只是日志少了几行、鉴权漏了几条路径、CORS 头没出现。Router::layer 怎么把 layer 分给 RouterInner 的三个字段、route_layer 少套哪几处、三个层级谁在外层,layer 与 route_layer 已经讲过并做了实验。这里按症状把会漏掉中间件的写法列全,每一种对上源码里的原因。
复现
一个只记下「我被调用过」的中间件,挂在不同位置,每种挂法发一个请求,看中间件跑没跑:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::sync::atomic::{AtomicBool, Ordering};
use axum::body::Body;
use axum::extract::Request;
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;
static RAN: AtomicBool = AtomicBool::new(false);
// Records that it saw the request, then passes it on.
async fn mark(req: Request, next: Next) -> Response {
RAN.store(true, Ordering::SeqCst);
next.run(req).await
}
async fn check(case: &str, app: &Router, uri: &str) {
RAN.store(false, Ordering::SeqCst);
let res = app.clone().oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
let ran = if RAN.load(Ordering::SeqCst) { "ran" } else { "SKIPPED" };
println!("{case:<28} GET {uri:<9} {} middleware {ran}", res.status().as_u16());
}
fn h() -> axum::routing::MethodRouter {
get(|| async { "ok" })
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app = Router::new().route("/a", h()).layer(from_fn(mark)).route("/late", h());
check("route after layer", &app, "/late").await;
let app = Router::new()
.route("/a", h())
.layer(from_fn(mark))
.fallback(|| async { (axum::http::StatusCode::NOT_FOUND, "custom 404") });
check("fallback after layer", &app, "/nope").await;
let api = Router::new().route("/x", h());
let app = Router::new().route("/a", h()).layer(from_fn(mark)).nest("/api", api);
check("nest after layer", &app, "/api/x").await;
let other = Router::new().route("/m", h());
let app = Router::new().route("/a", h()).layer(from_fn(mark)).merge(other);
check("merge after layer", &app, "/m").await;
let api = Router::new().route("/x", h()).layer(from_fn(mark));
let app = Router::new().route("/a", h()).nest("/api", api);
check("layer inside nested router", &app, "/api/x").await;
check("layer inside nested router", &app, "/api/nope").await;
let api = Router::new()
.route("/x", h())
.fallback(|| async { (axum::http::StatusCode::NOT_FOUND, "api 404") })
.layer(from_fn(mark));
let app = Router::new().route("/a", h()).nest("/api", api);
check("nested with own fallback", &app, "/api/nope").await;
let app = Router::new()
.route("/a", h())
.fallback(|| async { (axum::http::StatusCode::NOT_FOUND, "custom 404") })
.nest("/api", Router::new().route("/x", h()))
.merge(Router::new().route("/m", h()))
.route("/late", h())
.layer(from_fn(mark));
for uri in ["/late", "/nope", "/api/x", "/api/nope", "/m"] {
check("layer last", &app, uri).await;
}
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
route after layer GET /late 200 middleware SKIPPED
fallback after layer GET /nope 404 middleware SKIPPED
nest after layer GET /api/x 200 middleware SKIPPED
merge after layer GET /m 200 middleware SKIPPED
layer inside nested router GET /api/x 200 middleware ran
layer inside nested router GET /api/nope 404 middleware SKIPPED
nested with own fallback GET /api/nope 404 middleware ran
layer last GET /late 200 middleware ran
layer last GET /nope 404 middleware ran
layer last GET /api/x 200 middleware ran
layer last GET /api/nope 404 middleware ran
layer last GET /m 200 middleware ran
前四行的状态码都对,只是中间件被跳过了。如果中间件是鉴权,这四条路径就是没上锁的门。
原因:layer 改的是已有的服务
Router::layer 对 path_router、fallback_router、catch_all_fallback 里现有的每个端点调一次 Layer::layer,然后把包好的端点放回去,layer 本身不保存。之后对 Router 的任何修改,都是往里放新的、没包过的端点,或者替换掉包过的端点:
后加的 route。 新端点直接插进 path_router,第一行。
后加的 fallback。 Router::fallback 不是在原来的 404 外面再加一层,而是整个换掉:
tap_inner!(self, mut this => {
this.catch_all_fallback =
Fallback::BoxedHandler(BoxedIntoRoute::from_handler(handler.clone()));
})
.fallback_endpoint(Endpoint::MethodRouter(any(handler)))
catch_all_fallback 被直接赋值,fallback_endpoint 再调 PathRouter::set_fallback,用 replace_endpoint 把 fallback_router 里 / 和 /{*__private__axum_fallback} 两条路由的端点换成新的 handler。Router::new() 时放进去、被 .layer 包过的那个 NotFound 就这样被扔掉了,第二行的 404 不经过中间件。只要中间件是做日志或计数的,404 就从统计里消失了。
后 nest、后 merge。 Router::nest 和 Router::merge 把另一个 Router 的 path_router 拆开,端点原样插进来(逐条注册的过程见 nest、merge 与 fallback)。那些端点只带着子路由器自己套过的 layer,父路由器之前的 .layer 与它们无关,第三、四行。
layer 挂在子路由器上,子路由器没有自己的 fallback。 第五行 /api/x 经过了中间件,第六行 /api/nope 没有。nest 里有这样一段:
tap_inner!(self, mut this => {
panic_on_err!(this.path_router.nest(path, path_router));
if !default_fallback {
panic_on_err!(this.fallback_router.nest(path, fallback_router));
}
})
子路由器的 fallback_router 只有在它设过自定义 fallback(default_fallback == false)时才会被 nest 进来。没设的话,那个被子路由器的 layer 包过的 NotFound 直接丢掉,/api/nope 在 Router::call_with_state 里一路落到父路由器的 fallback_router 和 catch_all_fallback,父路由器上没有这个中间件。给子路由器一个自己的 fallback,再 .layer,第七行就跑了。
axum 0.4.0 的 CHANGELOG 记着:「Router::nest will panic if the nested router has a fallback. Previously it would be silently discarded」(#529「Panic instead of silently discarding fallbacks」)。也就是说,最早子路由器的 fallback 被悄悄丢掉,0.4.0 起改成 panic,总之 /api 下面没匹配上的请求只能交给父路由器。#1521「Add fallback inheritance for nested routers」(首次发布于 axum-v0.6.0-rc.5,正式版 0.6.0)放开了这个限制,0.6.0 的 CHANGELOG 写的是「Nested routers are now allowed to have fallbacks」,并说明「The outer router's fallback will still apply if a nested router doesn't have its own fallback」。上面那段 if !default_fallback 就是这条规则:子路由器有自己的 fallback 才带进来,没有就继承父路由器的。中间件挂在子路由器上时,404 归谁,取决于子路由器有没有 fallback。
最后一组把 .layer 放到所有 route、fallback、nest、merge 之后,五个请求都经过了中间件。
挂的位置对,但范围不对
另外三种「没生效」不是因为调用顺序,而是挂的那一层本来就管不到。它们在 layer 与 route_layer 里各有实验,这里只列症状:
| 症状 | 原因 |
|---|---|
用 Router::route_layer 挂鉴权,路径不存在时是 404 而不是 401 | route_layer 不套 fallback_router 和 catch_all_fallback,这正是它的用途 |
用 Router::route_layer 挂鉴权,方法不对时是 401 而不是 405 | Router::route_layer 对每个 MethodRouter 调的是 MethodRouter::layer,方法级 fallback 也被套上;要让 405 不经过鉴权,用 MethodRouter::route_layer |
handler.layer(ConcurrencyLimitLayer::new(1)) 不限流 | Handler::layer 在每个请求的 call 里重新 layer 一次,每个请求一个新信号量 |
| 中间件改了 URI,路由却按旧 URI 匹配 | Router::layer 挂的中间件在路由之后执行;要在路由之前改,用 Layer::layer(router) 包住整个 Router |
最后一行在下一页 /foo/ 返回 404 里还会碰到:NormalizePathLayer 挂在 Router::layer 上就是这样失效的。
顺序:CORS 头没出现
挂的位置、范围都对,顺序反了,也会表现成「没生效」。常见的是 CORS 和鉴权。tower-http 的 Cors::call 看到 OPTIONS 就自己回预检响应,不调用内层;对其他请求,它调用内层,再把 CORS 头加到内层返回的响应上(细节见 其余常用中间件)。下面两种写法只差 .layer 的先后:
axum = "=0.8.9"
tower = { version = "=0.5.3", features = ["util"] }
tower-http = { version = "=0.7.1", features = ["cors"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, Method, StatusCode};
use axum::middleware::{from_fn, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use tower::ServiceExt;
use tower_http::cors::{Any, CorsLayer};
// Rejects every request without an `authorization` header.
async fn auth(req: Request, next: Next) -> Response {
if req.headers().contains_key(header::AUTHORIZATION) {
next.run(req).await
} else {
StatusCode::UNAUTHORIZED.into_response()
}
}
async fn show(name: &str, app: Router) {
println!("{name}");
let preflight = Request::builder()
.method(Method::OPTIONS)
.uri("/api")
.header(header::ORIGIN, "https://a.example")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "GET")
.header(header::ACCESS_CONTROL_REQUEST_HEADERS, "authorization")
.body(Body::empty())
.unwrap();
let get = Request::builder()
.uri("/api")
.header(header::ORIGIN, "https://a.example")
.body(Body::empty())
.unwrap();
for (label, req) in [("preflight OPTIONS", preflight), ("GET without auth", get)] {
let res = app.clone().oneshot(req).await.unwrap();
let acao = res.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN);
println!(" {label:<18} {} allow-origin={acao:?}", res.status());
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let cors = || CorsLayer::new().allow_origin(Any).allow_headers(Any);
let route = || Router::new().route("/api", get(|| async { "data" }));
show(".layer(cors).layer(auth)", route().layer(cors()).layer(from_fn(auth))).await;
show(".layer(auth).layer(cors)", route().layer(from_fn(auth)).layer(cors())).await;
}
access-control-allow-origin?M2 Max、rustc 1.99.0。答案在本页最后。修法
.layer写在最后:所有route、fallback、nest、merge都做完,再挂要覆盖全站的中间件。一组中间件用ServiceBuilder一次挂上,顺序更好读(层的顺序反了)。- 子路由器上的中间件要覆盖它自己的 404,就给子路由器设一个 fallback,并且
.fallback(...)写在.layer(...)之前。 - 必须在路由之前执行的(改 URI、规范化路径),不挂在
Router::layer上,而是用Layer::layer(router)包住整个Router。 - 需要在每个请求前判断、提前返回的(鉴权),放在 CORS 里面,也就是先
.layer(auth)再.layer(cors),让 CORS 在外层。 - 中间件漏没漏,不必等线上:像上面那样用
oneshot发请求、断言中间件留下的痕迹(不起服务器测路由),把「每条路径都经过鉴权」写成测试。
动脑筋答案
连跑 3 次输出相同:
.layer(cors).layer(auth)
preflight OPTIONS 401 Unauthorized allow-origin=None
GET without auth 401 Unauthorized allow-origin=None
.layer(auth).layer(cors)
preflight OPTIONS 200 OK allow-origin=Some("*")
GET without auth 401 Unauthorized allow-origin=Some("*")
- 第一种写法,预检是 401,没有 CORS 头。 后
.layer的在外层,auth在cors外面。浏览器的预检请求不带Authorization(只在Access-Control-Request-Headers里列出这个名字),auth直接回 401,请求根本没到Cors::call。浏览器看到预检失败,报的是 CORS 错误,真正的 GET 不会发出去。从前端看,像是「CORS 中间件没生效」。 - 第一种写法,带
Origin的 GET 被拒时也没有 CORS 头。 401 是auth造的,cors在里面,加不到这个响应上。前端 JS 读不到这个 401,只能看到一个网络错误。 - 第二种写法,预检 200。
cors在最外层,看到OPTIONS就自己回预检响应,auth没被调用。GET 仍然被拒成 401,但响应经过cors,带上了access-control-allow-origin: *,前端能读到这个 401。
小结
Router::layer只包调用那一刻已有的端点,不记录 layer;之后的route、fallback、nest、merge加进来的都不经过它。Router::fallback替换掉被包过的NotFound,fallback 写在.layer之后,404 就不经过中间件。- 子路由器上的 layer 管不到
/api下的 404,除非子路由器有自己的 fallback:没有的话nest不带它的fallback_router,404 由父路由器处理。 route_layer跳过 404、Handler::layer每请求重套、Router::layer在路由之后执行,这几种是范围问题,见 layer 与 route_layer。- 顺序也会造成「没生效」:鉴权在 CORS 外面时,预检请求先被 401 挡住,响应没有 CORS 头。