有时候我们的后端API会对其他地方发送http请求来获取数据。如果请求体过大(如上传文件)或响应体过大(如下载文件),就需要使用流式传输以节约后端服务器内存(否则后端服务器可能会先把整个请求下载到自己的内存里面然后再发送)。
转发reqwest client的响应
可以使用流对reqwest的响应进行转发。官方例子:axum/examples/reqwest-response/src/main.rs at main · tokio-rs/axum · GitHub。
转发请求到client
这里的原理是使用hyper库的Body trait进行转发。Body trait是hyper给出的面向流式传输的东西,使得基于hyper的http库的请求和响应都是可以随意转发的。然而,axum依赖hyper v1.2
,但reqwest依赖hyper v0.14
(提供的是Body struct而不是Body trait),不兼容,因此不得不找较新的hyper client。
参考了axum反向代理的例子reverse-proxy,使用较底层的hyper客户端hyper_util::client::legacy::Client<HttpConnector, Body>
来发起请求。可以随意调整uri和headers。
client的返回值类型是Response<Incoming>
,不能直接读取。将其into_response
直接返回给前端的话,是可以被前端读出来的。若想在后端直接读取的话,GPT4给出的0.14
版本的hyper的解决方案是:
use hyper::Client;
use hyper::client::HttpConnector;
use axum::body::Body;
use serde_json::from_slice;
use futures::TryStreamExt; // 提供了 stream 的扩展方法,如 and_then
use hyper::{Response, StatusCode};
async fn fetch_ipfs_add_response() -> Result<IpfsAddResponse, Box<dyn std::error::Error>> {
let client = Client::new();
let uri = "http://localhost:5001/api/v0/add".parse()?;
let response = client.get(uri).await?;
match response.status() {
StatusCode::OK => {
// 将响应体聚合成一个完整的bytes
let bytes = hyper::body::to_bytes(response.into_body()).await?;
// 将bytes解析为IpfsAddResponse
let ipfs_response = from_slice::<IpfsAddResponse>(&bytes)?;
Ok(ipfs_response)
},
_ => Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Failed to fetch"))))
}
}
但在hyper的1.x
版本里,有些东西被改掉了,个人摸索出来的解决方案是:
// read body
let res = raw_hyper_client
.request(req)
.await.map_err(|_| http::StatusCode::BAD_REQUEST)?;
let body = res.into_body().collect();
let body = body.await.unwrap();
let body = body.to_bytes();
let body: dtos::IpfsAddFileResponse = serde_json::from_slice(body.as_ref()).unwrap();
info!("Add file succeed. {:?}", body);