英文:
Get access to the request headers from Axum IntoResponse
问题
I have a custom IntoResponse
type that needs to return different data to the client depending on the value of the Accept
header in the request. 我有一个自定义的 IntoResponse
类型,根据请求中的 Accept
头的值,需要向客户端返回不同的数据。
I would prefer, if possible, to have this be seamless (i.e. I don't have to pass the header value into the IntoResponse
type's constructor). 如果可能的话,我希望这个过程是无缝的(即我不必将头部的值传递到 IntoResponse
类型的构造函数中)。
I thought the most straightforward way to accomplish this would be to write a layer using Axum's middleware system that all functions returning that type are required to be wrapped with, not have the type implement IntoResponse
, and instead perform the conversion inside the middleware so that it could access the request headers, but it seems that Axum's middleware system requires the service function to return an IntoResponse
. 我认为实现这个的最直接方法是使用 Axum 的中间件系统编写一个层,要求所有返回该类型的函数都必须包装在其中,而不是让类型实现 IntoResponse
,而是在中间件内部执行转换,以便它可以访问请求头,但似乎 Axum 的中间件系统要求服务函数返回一个 IntoResponse
。
Could I whip something up with a tower::Layer
that does the same thing? 我可以使用 tower::Layer
来实现相同的功能吗?
英文:
I have a custom IntoResponse
type that needs to return different data to the client depending on the value of the Accept
header in the request. I would prefer, if possible, to have this be seamless (i.e. I don't have to pass the header value into the IntoResponse
type's constructor). I thought the most straightforward way to accomplish this would be to write a layer using Axum's middleware system that all functions returning that type are required to be wrapped with, not have the type implement IntoResponse
, and instead perform the conversion inside the middleware so that it could access the request headers, but it seems that Axum's middleware system requires the service function to return an IntoResponse
. Could I whip something up with a tower::Layer
that does the same thing?
答案1
得分: 4
Here is the translated content without the code:
首先,让我们澄清一件事,没有内置机制来支持Accept
头部对响应的影响。请参考这个简短的讨论。
您可能已经注意到,处理程序必须产生的IntoResponse
特性并不提供对原始请求的访问:
pub trait IntoResponse {
fn into_response(self) -> Response<UnsyncBoxBody<Bytes, Error>>;
}
因此,您必须从处理程序本身或某个中间件中获取头部信息。
更糟糕的是,它要求响应体必须是一系列字节(这是有道理的,但丢失了类型信息)。因此,中间件必须从编码的响应体中解码数据,以便为不同的媒体类型进行修改。如果您的数据非常简单,那么这可能会起作用,但对于许多情况来说,这可能是一种令人讨厌且不必要的开销。这种数据流还会影响底层的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(),
_ => { /* 返回原始的 501 响应 */ }
}
}
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();
}
请注意,这是代码的翻译,但我已删除了代码部分,只提供了相关文本。如果需要代码的翻译,请提出具体的代码部分。
英文:
Firstly, lets get it out of the way that there is not a built-in mechanism to support the Accept
header impacting the response. See this brief discussion.
You've probably noticed the IntoResponse
trait that handlers must yield does not provide access to the original request:
pub trait IntoResponse {
fn into_response(self) -> Response<UnsyncBoxBody<Bytes, Error>>;
}
So you'd have to get the header from either the handler itself or some middleware.
Even worse, it requires the body be a sequence of bytes (which makes sense but loses type information). So a middleware would have to decode data from the encoded response body to modify it for a different mime type. This could probably work if your data is really simple, but for a lot of cases this would probably be an annoying and unnecessary cost. This data flow also impacts how underlying tower
services can affect the response, so the distinction doesn't help.
Fortunately, there is another way: you can provide any data to your middleware via an Extension
. Through .extensions()
/.extensions_mut()
on a response, you can store arbitrary data, so your IntoResponse
implementation can store itself in an extension that a middeware could then pull out and re-format as necessary.
Here's a mockup of how that could work:
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();
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论