401 响应没有 x-frame-options:层的顺序反了
主线位置:两条主线开始之前的装配阶段。顺序一反,请求主线上谁先拿到请求、谁最后改响应就跟着反;拦截请求的那层在外面,里面的层根本见不到这个请求。
flowchart TB
src["同样写 A 再写 B"] --> q{用哪个 API}
q -->|ServiceBuilder| s1["A 在外:A 先看到请求"]
s1 --> s2[B 再看到]
q -->|"Router::layer"| r1["B 在外:B 先看到请求"]
r1 --> r2[A 再看到]
tower 的 ServiceBuilder 和 axum 的 Router::layer 都是「一个接一个地加 layer」,但规则相反:ServiceBuilder 先写的在最外层,Router::layer 后写的在最外层。 从一种写法搬到另一种,不改顺序,中间件的顺序就整个倒过来了。
复现
一个给所有响应加 x-frame-options 的层,一个没有 token 头就返回 401 的鉴权层。习惯了 ServiceBuilder 的人想让「加头」包住一切,会先写它:
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["full"] }
axum = "=0.8.9"
use axum::body::Body;
use axum::extract::Request;
use axum::http::{HeaderValue, StatusCode};
use axum::middleware::{from_fn, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::get;
use axum::Router;
use tower::util::MapResponseLayer;
use tower::{ServiceBuilder, ServiceExt};
// Rejects every request without a `token` header.
async fn require_token(req: Request, next: Next) -> Response {
if req.headers().contains_key("token") {
next.run(req).await
} else {
StatusCode::UNAUTHORIZED.into_response()
}
}
fn add_header(mut res: Response) -> Response {
res.headers_mut().insert("x-frame-options", HeaderValue::from_static("DENY"));
res
}
async fn show(name: &str, app: Router) {
let req = Request::get("/").body(Body::empty()).unwrap();
let res = app.oneshot(req).await.unwrap();
println!(
"{name}: {} x-frame-options={:?}",
res.status(),
res.headers().get("x-frame-options")
);
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Written "header first, auth second", as one would with ServiceBuilder.
let app = Router::new()
.route("/", get(|| async { "ok" }))
.layer(MapResponseLayer::new(add_header))
.layer(from_fn(require_token));
show("Router::layer, header first", app).await;
let app = Router::new()
.route("/", get(|| async { "ok" }))
.layer(from_fn(require_token))
.layer(MapResponseLayer::new(add_header));
show("Router::layer, header last ", app).await;
let app = Router::new().route("/", get(|| async { "ok" })).layer(
ServiceBuilder::new()
.layer(MapResponseLayer::new(add_header))
.layer(from_fn(require_token)),
);
show("ServiceBuilder, header first", app).await;
}
M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:
Router::layer, header first: 401 Unauthorized x-frame-options=None
Router::layer, header last : 401 Unauthorized x-frame-options=Some("DENY")
ServiceBuilder, header first: 401 Unauthorized x-frame-options=Some("DENY")
第一行就是这个坑:在 Router::layer 里先写加头,鉴权层到了外面,它直接返回 401,请求没往里走,响应也就没经过加头那层。正常请求的响应照样有头:给请求加上 token 头再跑,三行都是 200 OK x-frame-options=Some("DENY")。所以只看 200 的测试发现不了。
两种 API 各是什么顺序
把两个层换成只打印的 LogLayer,看它们各在什么时候见到请求和响应:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::body::Body;
use axum::http::Request;
use axum::routing::get;
use axum::Router;
use tower::{Layer, Service, ServiceBuilder, ServiceExt};
#[derive(Clone)]
struct LogLayer(&'static str);
#[derive(Clone)]
struct Log<S> {
name: &'static str,
inner: S,
}
impl<S> Layer<S> for LogLayer {
type Service = Log<S>;
fn layer(&self, inner: S) -> Log<S> {
Log { name: self.0, inner }
}
}
impl<S, R> Service<R> for Log<S>
where
S: Service<R>,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), S::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: R) -> Self::Future {
println!(" {} sees request", self.name);
let name = self.name;
let fut = self.inner.call(req);
Box::pin(async move {
let res = fut.await;
println!(" {} sees response", name);
res
})
}
}
async fn handler() -> &'static str {
println!(" handler");
"ok"
}
fn req() -> Request<Body> {
Request::get("/").body(Body::empty()).unwrap()
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("ServiceBuilder::new().layer(A).layer(B).service(..)");
let svc = ServiceBuilder::new()
.layer(LogLayer("A"))
.layer(LogLayer("B"))
.service(Router::new().route("/", get(handler)));
svc.oneshot(req()).await.unwrap();
println!("Router::new().route(..).layer(A).layer(B)");
let app = Router::new()
.route("/", get(handler))
.layer(LogLayer("A"))
.layer(LogLayer("B"));
app.oneshot(req()).await.unwrap();
println!("Router::new().route(..).layer(ServiceBuilder::new().layer(A).layer(B))");
let app = Router::new()
.route("/", get(handler))
.layer(ServiceBuilder::new().layer(LogLayer("A")).layer(LogLayer("B")));
app.oneshot(req()).await.unwrap();
}
Log 要派生 Clone、Future 要带 Send,是因为 Router::layer 要求包出来的服务 Clone + Send + Sync、future Send。依赖同上,连跑 3 次输出相同:
ServiceBuilder::new().layer(A).layer(B).service(..)
A sees request
B sees request
handler
B sees response
A sees response
Router::new().route(..).layer(A).layer(B)
B sees request
A sees request
handler
A sees response
B sees response
Router::new().route(..).layer(ServiceBuilder::new().layer(A).layer(B))
A sees request
B sees request
handler
B sees response
A sees response
| 写法 | 最外层 | 请求经过的顺序 |
|---|---|---|
ServiceBuilder::new().layer(A).layer(B) | A(先写的) | A → B → handler |
Router::new().layer(A).layer(B) | B(后写的) | B → A → handler |
Router::new().layer(ServiceBuilder::new().layer(A).layer(B)) | A(builder 内部按 builder 的规则) | A → B → handler |
为什么相反
tower 这边。 ServiceBuilder::layer 返回 ServiceBuilder<Stack<T, L>>:新 layer 放进 Stack 的 Inner 位置,之前攒下的整串放在 Outer 位置。Stack::layer 先用 Inner 包、再用 Outer 包,所以最后写的离服务最近。.service(svc) 之前没有服务,builder 只是在攒一串「待会儿怎么包」,细节见 ServiceBuilder 和 Layer 与 Stack。
axum 这边。 Router 里已经有服务了,每次 .layer(x) 立刻把 x 套在现有服务的外面。Router::layer 把 layer 交给 PathRouter::layer,它对每个路由的 endpoint 调用 layer,最后落到 Route::layer:
pub(crate) fn layer<L, NewError>(self, layer: L) -> Route<NewError>
where
// ...
{
let layer = (MapErrLayer::new(Into::into), layer);
Route::new(layer.layer(self))
}
self 是这个路由到目前为止的样子,已经包着之前所有 .layer 加的层;新 layer 包住它,所以后写的在外面。元组 (MapErrLayer, layer) 的第一个元素在最外层(元组的规则见 Layer 与 Stack),axum 只是在最外面再转一次错误类型。
axum 0.8.9 自己的文档 middleware.md 的 Ordering 一节 说的也是这两条:Router::layer 「generally speaking, this results in middleware being executed from bottom to top」;ServiceBuilder 「works by composing all layers into one such that they run top to bottom」,并且推荐用后者。
ServiceBuilder 是 #175 加入的(tower 0.1.0)。PR 描述里就写着「the order of services is actually opposite (I think) than what you would think」。评审时有人确认当时的语义:「Just checking, add means the new layer will wrap everything before it, right? So this ends up being RateLimit<InFlightLimit<Hyper>>, yea?」也就是今天 axum 的规则。后来一条列出待定事项的评论写道:「For the last one, I believe we should now reverse the order aka the FIFO instead of the current LIFO.」合并进去的版本是 Chain::new(layer, self.layer)(Chain 后来改名 Stack),即先写的在外,一直保持到 0.5.3。
修法
二选一,上面的实验都跑过:
- 在
Router::layer里按「从里往外」写:最先写的离 handler 最近,想包住一切的层(加安全头、记日志、统一错误)写在最后。 - 把一组中间件放进一个
ServiceBuilder,只调用一次Router::layer:builder 内部按「从外往里」读,和 tower 的其他代码一致,也是 axum 文档推荐的写法。
不管哪种,凡是「每个响应都要经过」的层,都要检查一遍被拒绝的响应(401、429、503)是不是也经过了它。本站的中间件栈就是这样排的,见 在本站中:中间件栈。
问: Router::layer 是把整个 Router 包一层吗?
答: 不是。PathRouter::layer 遍历已有的路由,给每一个分别套上;Router::layer 对 fallback 也各套一次。所以中间件在路由匹配之后才见到请求,而且只作用于调用 .layer 之前已经加进去的路由。.layer 之后再 .route(...) 的路由没有这层:把上面的程序改成 .route("/", ..).layer(LogLayer("A")).route("/late", ..),请求 / 会打印 A sees request,请求 /late 只打印 handler。
小结
ServiceBuilder先写的在最外层:.layer(x)把x放进Stack的Inner,服务要等.service()才传进来。- axum
Router::layer后写的在最外层:路由已经存在,Route::layer用新 layer 包住当前的路由。 - 症状通常出现在被拒绝的响应上:拦截请求的层跑到了外面,里面加头、记录的层见不到它。实验里 401 少了
x-frame-options。 - 修法:
Router::layer里把要包住一切的层写在最后,或者把整组放进一个ServiceBuilder再交给Router::layer。