我有一个定制的IntoResponse类型,它需要根据请求中Accept头的值向客户端返回不同的数据.如果可能的话,我更希望这是无缝的(即,我不必将头值传递到IntoResponse类型的构造函数中).我认为实现这一点最直接的方法是使用Axum的中间件系统编写一个层,所有返回该类型的函数都需要用它来包装,而不是让类型实现IntoResponse,而是在中间件内执行转换,以便它可以访问请求头,但似乎Axum的中间件系统要求服务函数返回IntoResponse.我可以用tower::Layer做同样的事情吗?

推荐答案

首先,让我们摆脱没有内置机制来支持影响响应的Accept头的问题.请参见this brief discussion.


您可能已经注意到,处理程序必须提供的IntoResponse特征并不提供对原始请求的访问:

pub trait IntoResponse {
    fn into_response(self) -> Response<UnsyncBoxBody<Bytes, Error>>;
}

因此,您必须从处理程序本身或某个中间件获取标头.

更糟糕的是,它要求主体是一个字节序列(这是有意义的,但会丢失类型信息).因此,中间件必须对编码的响应正文中的数据进行解码,以针对不同的MIME类型对其进行修改.如果您的数据真的很简单,这可能会起作用,但在许多情况下,这可能会是一种恼人的、不必要的成本.这种数据流还会影响底层的tower个服务如何影响响应,因此这种区别无济于事.


幸运的是,还有另一种方法:您可以通过Extension向您的中间件提供任何数据.通过响应上的.extensions()/.extensions_mut(),您可以存储任意数据,因此您的IntoResponse实现可以将自身存储在一个扩展中,然后中间件可以取出该扩展并根据需要重新格式化.

以下是它的工作原理的一个模型:

use axum::http::header::ACCEPT;
use axum::http::{Request, StatusCode};
use axum::middleware::{from_fn, Next};
use axum::response::{Html, IntoResponse, Json, Response};
use axum::{routing::get, Router};
use serde::Serialize;

#[derive(Serialize)]
struct MyData {
    data: String,
}

impl IntoResponse for MyData {
    fn into_response(self) -> Response {
        let mut response = StatusCode::NOT_IMPLEMENTED.into_response();
        response.extensions_mut().insert(self);
        response
    }
}

async fn get_accept_aware() -> MyData {
    MyData {
        data: "test".to_owned(),
    }
}

async fn my_data_applicator<B>(request: Request<B>, next: Next<B>) -> Response {
    let accept_header = request
        .headers()
        .get(&ACCEPT)
        .map(|value| value.as_ref().to_owned());

    let mut response = next.run(request).await;

    if let Some(my_data) = response.extensions_mut().remove::<MyData>() {
        match accept_header.as_deref() {
            Some(b"application/json") => return Json(my_data).into_response(),
            Some(b"html") => return Html(format!("<body>{}</body>", my_data.data)).into_response(),
            _ => { /* yield original 501 response */ }
        }
    }

    response
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(get_accept_aware))
        .layer(from_fn(my_data_applicator));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Rust相关问答推荐

使用nom将任何空白、制表符、白线等序列替换为单个空白

Tauri tauri—apps/plugin—store + zustand

在一个tauri协议处理程序中调用一个rectuc函数的推荐技术是什么?

是否可以为`T:Copy`执行`T. clone`的测试

如何装箱生命周期相关联的两个对象?

这种获取-释放关系是如何运作的?

将数组转换为HashMap的更简单方法

integer cast as pointer是什么意思

使用铁 rust S还原对多个数组执行顺序kronecker积

不能在一个代码分支中具有不变的自身borrow ,而在另一个代码分支中具有可变的self borrow

如何将生存期参数添加到框<>;具有dyn类型别名

确保参数是编译时定义的字符串文字

信号量释放后 Rust 输出挂起线程

可选包装枚举的反序列化

为什么这段 Rust 代码会在没有递归或循环的情况下导致堆栈溢出?

打印 `format_args!` 时borrow 时临时值丢失

是否可以预测堆栈溢出?

将文件的第一行分别读取到文件的其余部分的最有效方法是什么?

为什么分配对变量的引用使我无法返回它

如何将 while 循环内的用户输入添加到 Rust 中的向量?