You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Minijinja模板中全局获取Axum请求路径(含错误页)

问题

我正在用Axum开发Web应用,想给Minijinja模板里的活跃链接添加属性和样式(比如aria-current),这需要所有模板(包括错误页模板)都能获取到请求路径。

目前普通页面中,我通过OriginalUri提取器将请求路径传入模板上下文;但错误页的情况特殊——我给应用错误类型实现了IntoResponse,这个实现无法访问请求对象。我曾考虑在中间件里更新minijinja::Environment,但它是作为应用状态传入的,属于只读资源。

另外,我没法在IntoResponse里复用应用状态中的环境,所以错误响应是重新创建的环境,这应该不是正确做法。我的AppError定义如下:

enum AppError {
    UnexpectedError(anyhow::Error),
}

impl From<anyhow::Error> for AppError {
    fn from(e: anyhow::Error) -> Self {
        Self::UnexpectedError(e)
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            Self::UnexpectedError(_) => {
                let mut env = Environment::new();
                env.set_loader(path_loader("templates"));
                match env
                    .get_template("500.html.j2")
                    .and_then(|tmpl| tmpl.render(context! {}))
                {
                    Ok(content) => {
                        (StatusCode::INTERNAL_SERVER_ERROR, Html(content)).into_response()
                    }
                    Err(_) => {
                        (StatusCode::INTERNAL_SERVER_ERROR, "We're experiencing unexpected technical difficulties. Please try again later. Thank you for your patience.").into_response()
                    }
                }
            }
        }
    }
}
解决方案

方法一:将请求路径附加到AppError中

既然IntoResponse本身无法获取请求,那就在错误产生时把请求路径一起存入AppError:

  1. 修改AppError定义,新增存储请求路径的字段:
use axum::http::Uri;

enum AppError {
    UnexpectedError(anyhow::Error, Option<Uri>),
}

// 调整From实现,默认路径为None
impl From<anyhow::Error> for AppError {
    fn from(e: anyhow::Error) -> Self {
        Self::UnexpectedError(e, None)
    }
}

// 提供方法手动附加请求路径
impl AppError {
    pub fn with_uri(self, uri: Uri) -> Self {
        match self {
            Self::UnexpectedError(e, _) => Self::UnexpectedError(e, Some(uri)),
        }
    }
}
  1. 在handler中提取路径并附加到错误:
async fn some_handler(uri: OriginalUri) -> Result<Html<String>, AppError> {
    // 业务逻辑出错时,将路径附加到错误
    Err(anyhow::anyhow!("something went wrong").into()).with_uri(uri.0)
}
  1. 修改IntoResponse实现,将路径传入模板上下文:
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            Self::UnexpectedError(_, uri) => {
                let mut env = Environment::new();
                env.set_loader(path_loader("templates"));
                let ctx = context! {
                    request_path => uri.map(|u| u.path().to_string())
                };
                match env
                    .get_template("500.html.j2")
                    .and_then(|tmpl| tmpl.render(ctx))
                {
                    Ok(content) => {
                        (StatusCode::INTERNAL_SERVER_ERROR, Html(content)).into_response()
                    }
                    Err(_) => {
                        (StatusCode::INTERNAL_SERVER_ERROR, "我们遇到了意外的技术问题,请稍后重试。感谢您的耐心。").into_response()
                    }
                }
            }
        }
    }
}

方法二:用中间件存路径到请求扩展,通过错误处理层获取

利用Axum的Extensions存储请求临时数据,再通过统一的错误处理层获取路径:

  1. 编写中间件存储请求路径:
use axum::{
    http::Request,
    middleware::Next,
    response::Response,
};

async fn store_request_path<B>(mut req: Request<B>, next: Next<B>) -> Result<Response, AppError> {
    let uri = req.uri().clone();
    req.extensions_mut().insert(uri);
    Ok(next.run(req).await)
}
  1. 移除AppError的IntoResponse实现,改用错误处理层:
use axum::extract::Extension;

async fn handle_app_error(
    err: AppError,
    Extension(uri): Extension<Uri>,
    Extension(env): Extension<Environment>,
) -> impl IntoResponse {
    match err {
        AppError::UnexpectedError(_) => {
            let ctx = context! {
                request_path => uri.path().to_string()
            };
            match env
                .get_template("500.html.j2")
                .and_then(|tmpl| tmpl.render(ctx))
            {
                Ok(content) => (StatusCode::INTERNAL_SERVER_ERROR, Html(content)),
                Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "我们遇到了意外的技术问题,请稍后重试。感谢您的耐心。"),
            }
        }
    }
}
  1. 注册中间件和错误处理层到路由:
use axum::routing::get;
use axum::Router;

let app = Router::new()
    .route("/", get(some_handler))
    .layer(axum::middleware::from_fn(store_request_path))
    .layer(axum::error_handling::HandleErrorLayer::new(handle_app_error))
    .with_state(Environment::new().set_loader(path_loader("templates")));

这种方法能复用应用状态中的Environment,无需每次错误都重新创建。

方法三:封装模板渲染工具,自动注入路径

编写统一的模板渲染函数,自动从请求扩展中获取路径并注入上下文:

use minijinja::context;
use axum::http::Request;

pub async fn render_template(
    env: &Environment,
    template_name: &str,
    req: &Request<()>,
    extra_ctx: Option<minijinja::Value>,
) -> Result<String, minijinja::Error> {
    let uri = req.extensions().get::<Uri>().cloned().unwrap_or_default();
    let mut ctx = context! {
        request_path => uri.path().to_string()
    };
    // 合并额外上下文
    if let Some(extra) = extra_ctx {
        ctx.extend(extra);
    }
    env.get_template(template_name)?.render(ctx)
}

之后不管是普通页面还是错误页,都调用这个函数渲染模板,确保路径被自动注入。


内容的提问来源于stack exchange,提问作者Eva

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 02:31:11