Rust in Everything
基于 axum 0.8.9

中间件没生效

主线位置:问题出在装配主线。.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,第七行就跑了。

📝NOTE
冷知识:子路由器曾经不许有 fallback

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 而不是 401route_layer 不套 fallback_router 和 catch_all_fallback,这正是它的用途
用 Router::route_layer 挂鉴权,方法不对时是 401 而不是 405Router::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;
}
❗IMPORTANT
动脑筋:第一种写法下,浏览器发出的预检请求得到什么状态码?响应里有没有 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 头。

💬 关联讨论

发起讨论
加载中...