refactor(middleware): 抽出 ssr_generation/version_headers 中间件到 middleware.rs

main.rs 的 serve() 闭包内有两个内联 async fn 中间件
(ssr_generation_middleware、version_headers_middleware),与已抽出到
middleware.rs 的 add_cache_control/admin_guard 同类却散落在入口,无法独立测试。

将其移入 middleware.rs 作为 pub(crate) 函数,serve() 闭包改用全路径
crate::middleware::xxx 引用(与现有中间件一致),行为不变。

签名归一:原内联用 axum::http::Request<Body>,迁移后改用
axum::extract::Request(与 add_cache_control/admin_guard 一致;二者是同一
类型的别名,from_fn 接受不变)。

挂载点说明(版本头置于最外层合并 router)留在 main.rs 调用处注释,描述的是
挂载决策而非函数本身。
This commit is contained in:
xfy 2026-07-23 13:56:51 +08:00
parent b25abcb9d8
commit 67406f661a
2 changed files with 59 additions and 60 deletions

View File

@ -238,64 +238,6 @@ fn main() {
"版本响应头开关(Server / X-Yggdrasil-Version / X-Yggdrasil-Git)"
);
// SSR 世代号中间件:把当前全局世代号注入请求扩展,并对 GET 请求的
// 响应附加 `X-SSR-Generation` 头。这是为未来 Dioxus 支持自定义 SSR 缓存键
// 预留的钩子;目前主要提供可观测性,不会实际失效 SSR 缓存。
async fn ssr_generation_middleware(
req: axum::http::Request<axum::body::Body>,
next: axum::middleware::Next,
) -> axum::response::Response {
let generation = crate::ssr_cache::current_global_generation();
let is_get = req.method() == axum::http::Method::GET;
let (mut parts, body) = req.into_parts();
parts
.extensions
.insert(crate::ssr_cache::SsrGeneration(generation));
let mut response = next.run(axum::http::Request::from_parts(parts, body)).await;
if is_get {
response.headers_mut().insert(
axum::http::header::HeaderName::from_static("x-ssr-generation"),
axum::http::HeaderValue::from_str(&generation.to_string())
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("0")),
);
}
response
}
// 版本头中间件:为所有响应附加 Server / X-Yggdrasil-Version / X-Yggdrasil-Git
// 数据源与启动日志 log_build_info() 同源crate::build_info::BUILD_INFO
// 挂在最终合并 router 的最外层(见下方 Ok(router) 前),因此连 /healthz、
// /uploads/*、被 CSRF 拒(403)/超时/admin_guard 重定向的响应都会带头,
// 探测价值最大。受 EXPOSE_VERSION_HEADERS 控制(默认 true
async fn version_headers_middleware(
req: axum::http::Request<axum::body::Body>,
next: axum::middleware::Next,
) -> axum::response::Response {
let mut response = next.run(req).await;
let h = response.headers_mut();
// Server 头:产品名/版本,遵循 "Server: product/version" 习惯。
h.insert(
axum::http::header::SERVER,
axum::http::HeaderValue::from_str(&format!(
"yggdrasil/{}",
crate::build_info::BUILD_INFO.version
))
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("yggdrasil")),
);
// X-Yggdrasil-VersionCargo.toml 版本号。
h.insert(
axum::http::header::HeaderName::from_static("x-yggdrasil-version"),
axum::http::HeaderValue::from_static(crate::build_info::BUILD_INFO.version),
);
// X-Yggdrasil-Gitgit describe版本+提交数+短hash+脏标记)。
h.insert(
axum::http::header::HeaderName::from_static("x-yggdrasil-git"),
axum::http::HeaderValue::from_str(crate::build_info::BUILD_INFO.git_describe)
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("unknown")),
);
response
}
// 自定义 API 路由:图片上传(大文件,需要更长超时)
// CSRF 校验置于最外层,先拦截非法来源再做超时/限体。
let upload_route = axum::Router::new()
@ -348,7 +290,9 @@ fn main() {
// 合并 Dioxus + CSRF/世代号/缓存头/可选压缩/30s 超时中间件
// layer 顺序后加的最外层先执行。CSRF 最外层先拦截非法来源。
let mut app_routes = dioxus_app
.layer(axum::middleware::from_fn(ssr_generation_middleware))
.layer(axum::middleware::from_fn(
crate::middleware::ssr_generation_middleware,
))
.layer(axum::middleware::from_fn(
crate::middleware::add_cache_control,
))
@ -394,7 +338,9 @@ fn main() {
// 版本头中间件置于最终合并 router 的最外层:所有端点(含 /healthz、/uploads/*、
// 被 CSRF 拒/超时/admin_guard 重定向的响应)都会带上版本头。受 EXPOSE_VERSION_HEADERS 控制。
let router = if expose_version_headers {
router.layer(axum::middleware::from_fn(version_headers_middleware))
router.layer(axum::middleware::from_fn(
crate::middleware::version_headers_middleware,
))
} else {
router
};

View File

@ -226,6 +226,59 @@ pub(crate) async fn admin_guard(
}
}
/// Axum 中间件:把当前 SSR 全局世代号注入请求扩展,并对 GET 请求的响应附加
/// `X-SSR-Generation` 头。这是为未来 Dioxus 支持自定义 SSR 缓存键预留的钩子;
/// 目前主要提供可观测性,不会实际失效 SSR 缓存。
pub(crate) async fn ssr_generation_middleware(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let generation = crate::ssr_cache::current_global_generation();
let is_get = req.method() == axum::http::Method::GET;
let (mut parts, body) = req.into_parts();
parts
.extensions
.insert(crate::ssr_cache::SsrGeneration(generation));
let mut response = next.run(axum::http::Request::from_parts(parts, body)).await;
if is_get {
response.headers_mut().insert(
axum::http::header::HeaderName::from_static("x-ssr-generation"),
axum::http::HeaderValue::from_str(&generation.to_string())
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("0")),
);
}
response
}
/// Axum 中间件:为所有响应附加 Server / X-Yggdrasil-Version / X-Yggdrasil-Git 头。
/// 数据源与启动日志 `log_build_info()` 同源(`crate::build_info::BUILD_INFO`)。
/// 受 `EXPOSE_VERSION_HEADERS` 控制——该开关在 `main.rs` 决定是否挂载本层。
pub(crate) async fn version_headers_middleware(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let mut response = next.run(req).await;
let h = response.headers_mut();
h.insert(
axum::http::header::SERVER,
axum::http::HeaderValue::from_str(&format!(
"yggdrasil/{}",
crate::build_info::BUILD_INFO.version
))
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("yggdrasil")),
);
h.insert(
axum::http::header::HeaderName::from_static("x-yggdrasil-version"),
axum::http::HeaderValue::from_static(crate::build_info::BUILD_INFO.version),
);
h.insert(
axum::http::header::HeaderName::from_static("x-yggdrasil-git"),
axum::http::HeaderValue::from_str(crate::build_info::BUILD_INFO.git_describe)
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("unknown")),
);
response
}
#[cfg(test)]
mod tests {
use super::{cache_control_for_path, parse_compression_algorithms, CompressionAlgorithms};