Axum 路由尾部斜杠 404 问题:成因、SEO 影响与 NormalizePathLayer 解决方案

最近网站采用 Leptos 后,在 SEO 检查中出现一个奇怪现象:访问 /2025/post​ 能正常返回 200 状态,但访问带尾部斜杠的 /2025/post/​ 却返回了 404 错误,尽管页面内容在浏览器中正常渲染。这种情况经常出现在使用 Axum 定义类似于 /post/:slug​ 路由时,由于 Axum 默认会将带尾部斜杠的 URL 视作不同的路由,未定义对应的路由则会导致返回 404。这种情况虽未影响用户实际访问页面内容,但搜索引擎却会识别为“软 404”,对 SEO 非常不利。解决此问题的方法是使用 Axum 提供的 NormalizePathLayer​ 中间件自动去除 URL 尾部斜杠,从而确保无论 URL 是否带斜杠都能正常匹配到定义的路由,保证页面状态码一致,提升网站 SEO 效果。

为什么 Axum 会为带尾部斜杠的 URL 返回 404?

默认情况下,Axum 会将带尾部斜杠和不带尾部斜杠的 URL 视为两个不同的路由。如果你定义了 /post/:slug​ 而未定义 /post/:slug/​,那么带尾部斜杠的请求将无法匹配任何已注册的路由,触发 Axum 的回退逻辑(通常是返回 404 错误)。在 Axum 0.6 版本之前,框架会自动处理这些情况,但从 0.6 版本开始,为避免混乱,默认不再自动修正尾部斜杠。

这种严格的路由匹配是有意设计的,防止隐含或未预期的路由出现。因此 /foo​ 和 /foo/​ 是完全不同的路由,若只定义了 /foo​,请求 /foo/​ 就会返回 404 错误。

SSR 的迷惑现象:页面渲染但状态仍为 404

如果你使用 Leptos 这样的 SSR 框架,你可能会发现,即使 Axum 日志中显示 URL 以 404 返回,但页面内容却仍在浏览器中成功渲染。这是因为:

这种现象被称为 “软 404” ——对用户而言页面看起来正常,但搜索引擎却收到 404 错误状态。

尾部斜杠引起 404 对 SEO 的影响

软 404 状态对网站的 SEO 有害。搜索引擎需要正确的 HTTP 状态码以进行索引。具体而言,问题包括:

简言之,让 /post/your-article/​ 返回 404,即使页面可见,也对 SEO 不利。我们希望 /post/your-article​ 和 /post/your-article/​ 都返回正常的 200 OK 状态。

解决方法:使用 NormalizePathLayer 统一路径

Axum 提供了通过 tower_http​ 中的 NormalizePathLayer​ 中间件解决此问题的方法。该中间件能在请求到达路由定义之前自动去除 URL 尾部的斜杠,使请求的 /post/your-article/​ 自动规范化为 /post/your-article​,从而匹配预定义路由。

注意:要确保该中间件正确生效,必须将其包裹在整个路由(Router)外层,而不是在单独的路由上使用 Router::layer​,否则路由匹配前路径将不会被及时调整。

NormalizePathLayer 使用示例代码

use axum::{Router, routing::get};
use tower_http::normalize_path::NormalizePathLayer;

async fn post_handler(axum::extract::Path(slug): axum::extract::Path<String>) -> String {
    format!("Blog post: {}", slug)
}

async fn home_handler() -> &'static str {
    "Welcome to the blog"
}

fn main() {
    let router = Router::new()
        .route("/posts/:slug", get(post_handler))
        .route("/", get(home_handler));
    
    let app = NormalizePathLayer::trim_trailing_slash().layer(router);

    // 运行 Axum 服务器的标准配置 (axum::Server::bind(...).serve(app))
    // ...
}

在这个设置中,NormalizePathLayer 会在路由匹配前自动去除尾部斜杠,从而保证 /posts/rust-is-awesome​ 和 /posts/rust-is-awesome/​ 都能正确路由到 post_handler​,并返回 200 OK 状态。

确保 NormalizePathLayer 的正确放置

一定要确保中间件放置位置正确。若错误地写成如下形式:

let app = Router::new()
    .route("/posts/:slug", get(post_handler))
    .layer(NormalizePathLayer::trim_trailing_slash());

这种方式将不会起作用,因为中间件执行在路由匹配之后。正确方法是将中间件包裹整个路由(如上述正确示例所示)。

你可能也感兴趣