How to call struct method from axum server route? - rust

I want to instantiate a struct instance and then call a method of that instance in an api route. Here's an example of what I want, but it results in an error:
use axum::{http::StatusCode, routing::get, Router, Server};
#[derive(Clone)]
struct Api {
name: String
}
impl Api {
async fn hello(&self) -> Result<String, StatusCode> {
Ok(format!("Hello {}!", self.name))
}
}
#[tokio::main]
async fn main() {
let api = Api { name: "Alice".to_owned() };
let app = Router::new()
.route("/hello-user", get(api.hello));
Server::bind(&([127, 0, 0, 1], 3000).into())
.serve(app.into_make_service())
.await
.unwrap();
}
error[E0615]: attempted to take value of method `hello` on type `Api`
--> src\main.rs:19:39
|
19 | .route("/hello-user", get(api.hello));
| ^^^^^ method, not a field
I have attempted to get around this by defining a function which calls the instance method:
let hello_method = move || async {
match api.hello().await {
Ok(response) => response,
Err(_) => "error".to_owned(),
}
};
let app = Router::new()
.route("/hello-user", get(hello_method));
However, with this I get a "lifetime may not live long enough" error. How should I go about calling an instance method from an axum server route?

You can move api into the closure and then into the future:
let hello_method = move || async move {
match api.hello().await {
Ok(response) => response,
Err(_) => "error".to_owned(),
}
};
or with using of nightly feature #![feature(async_closure)]:
let hello_method = async move || {
match api.hello().await {
Ok(response) => response,
Err(_) => "error".to_owned(),
}
};

Related

Making parallel requests to Redis with Fred

I'm a complete Rust newbie and just tried to convert a simple microservice to Rust. It needs to do many parallel Redis requests for HTTP requests it gets and I'm a bit puzzled with the language syntax. I'm trying to do multiple Redis queries in parallel in an actix-web handler. I have the following type for the Redis GET function in Fred:
fn get<R, K>(&self, key: K) -> AsyncResult<R>
where
R: FromRedis + Unpin + Send,
K: Into<RedisKey>,
docs here: https://docs.rs/fred/5.2.0/fred/interfaces/trait.KeysInterface.html#method.get
In my own code I would then have a for-loop like this:
let resp_futures = vec!{};
for key in keys.iter() {
resp_futures.push(state.redis.get(key));
}
let resps = join_all(resp_futures).await;
Each Redis query should basically return an Option. However, this doesn't work due to some issues with type inference. Any ideas what's the correct way to send parallel Redis requests with the Fred Redis library? The complete server with some unnecessary stuff removed is the following:
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use actix_web::http::header::ContentType;
use serde::Deserialize;
use std::env;
use std::sync::Arc;
use fred::prelude::*;
use fred::pool::RedisPool;
use futures::future::join_all;
//
// Server configuration
//
fn get_redis_url() -> String {
match env::var("REDIS_URL") {
Err(_) => panic!("REDIS_URL environment variable not set."),
Ok(s) => s
}
}
struct AppState {
redis: Arc<RedisPool>
}
//
// Request parsing
//
fn get_redis_keys_from_accounts(accounts: Option<&String>) -> Vec<String> {
match accounts {
None => vec!{},
Some(s) => s.split(",").map(|s| "sid:".to_owned() + s).collect()
}
}
fn get_redis_keys_from_app_accounts(accounts: Option<&String>) -> Vec<String> {
match accounts {
None => vec!{},
Some(s) => s.split(",").map(|s| "aid:".to_owned() + s).collect()
}
}
fn get_redis_keys(req: web::Form<RouteRequest>) -> Vec<String> {
let ids = get_redis_keys_from_accounts(req.ids.as_ref());
let accounts = get_redis_keys_from_app_accounts(req.accounts.as_ref());
let aliases = get_redis_keys_from_app_accounts(req.aliases.as_ref());
ids.into_iter().chain(accounts.into_iter()).chain(aliases.into_iter()).collect()
}
//
// Request handling
//
#[derive(Debug, Deserialize)]
struct RouteRequest {
ids: Option<String>,
accounts: Option<String>,
aliases: Option<String>
}
#[post("/v1/route")]
async fn route(state: web::Data<AppState>, req: web::Form<RouteRequest>) -> impl Responder {
let keys = get_redis_keys(req);
// TODO: Fix this!
let resp_futures = vec!{};
for key in keys.iter() {
resp_futures.push(state.redis.get(key));
}
let resps = join_all(resp_futures).await;
HttpResponse::Ok().content_type(ContentType::json()).body(r#"{"status": "ok"}"#)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Connecting to Redis backend");
let url = get_redis_url();
let config = match RedisConfig::from_url(&url) {
Ok(x) => x,
Err(_) => panic!("Invalid redis URL")
};
let policy = ReconnectPolicy::default();
let redis = Arc::new(match RedisPool::new(config, 5) {
Ok(x) => x,
Err(_) => panic!("Unable to create Redis connection pool")
});
let _ = redis.connect(Some(policy));
println!("Starting HTTP server");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState {redis: redis.clone()}))
.service(route)
})
.bind(("0.0.0.0", 8080))?
.run()
.await
}
Output of cargo check is :
error[E0698]: type inside `async fn` body must be known in this context
--> src/main.rs:76:39
|
76 | resp_futures.push(state.redis.get(key));
| ^^^ cannot infer type for type parameter `R` declared on the associated function `get`
|
note: the type is part of the `async fn` body because of this `await`
--> src/main.rs:78:39
|
78 | let resps = join_all(resp_futures).await;
| ^^^^^^
At line 70 you have to give a hint about the type which should be used. For a String the following line should be used:
resp_futures.push(state.redis.get::<String, _>(key));

Borrowed value does not live long enough when using async/await

I am trying to make two async api call and eventually get error E0597.
Here is a code:
async fn make_request() -> Result<()> {
.........
.........
.........
let mut result = client.get(uri).await?;
let some_key = result.headers().get("some_key");
let next_url = match some_key {
Some(url) => {
let some_result = client.get(Uri::from_static(url.to_str().unwrap())).await?
}
None => println!("....")
};
Ok(())
}
When I run this code the error "borrowed value does not live long enough argument requires that result is borrowed for `'static"
I have created a compile-able example based on your snipped to reproduce the error in the playground, and if you are able to do something like this in your question (for future reference), it usually helps you get more specific answers.
The Request passed into the function has no lifetime guarantees, so this will fail with the error you mentioned:
use http::{Request, Uri};
async fn make_request(result: &Request<()>) -> std::io::Result<()> {
match result.headers().get("some_key") {
// `url` is a reference to the string in the "some_key" header
Some(url) => {
let some_result = Uri::from_static(url.to_str().unwrap());
}
None => println!("....")
};
Ok(())
}
You can add that lifetime requirement, but that probably isn't what you need, and will likely give you the same error message, just in a different place:
async fn make_request_static(result: &'static Request<()>) -> std::io::Result<()> {
match result.headers().get("some_key") {
// because the request is static, so can be `url`
Some(url) => {
let some_result = Uri::from_static(url.to_str().unwrap());
}
None => println!("....")
};
Ok(())
}
Uri implements the FromStr trait, though, so you would be best off using that. There is no longer a lifetime requirement, so it can work with any string you pass in, even one which is currently borrowed:
// need to import the trait to use its methods
use std::str::FromStr;
async fn make_request_3(result: &Request<()>) -> std::io::Result<()> {
match result.headers().get("some_key") {
// because the request is static, so can be `url`
Some(url) => {
let some_result = Uri::from_str(url.to_str().unwrap());
}
None => println!("....")
};
Ok(())
}

Why does match will not release the mutable borrow until end of it's expression?

I want to return client, In this function in any circumstances the code will not continue after match so rust should allow returning client.
pub async fn call_query2(mut client: Client<Compat<TcpStream>>, query:&str) -> Result<(Vec<tiberius::Row>,Client<Compat<TcpStream>>),(tiberius::error::Error,Client<Compat<TcpStream>>)> {
match client.query(query, &[]).await {
Ok(stream) =>{
match stream.into_first_result().await {
Ok(rows) => Ok((rows,client)),
Err(e) => Err((e,client))
}
},
Err(e) => Err((e,client))
}
}
but the compiler return this error message:
match client.query(query, &[]).await {
| ------------------------------
| |
| borrow of `client` occurs here
| a temporary with access to the borrow is created here ...
...
102 | Err(e) => Err((e,client))
| ^^^^^^ move out of `client` occurs here
103 | }
104 | }
| - ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Result<QueryStream<'_>, tiberius::error::Error>`
It seems that match will not release the mutable borrow until end of it's expression, because this code will work but I'm not able to return client:
pub async fn call_query(mut client: Client<Compat<TcpStream>>, query:&str) -> Result<(Vec<tiberius::Row>,Client<Compat<TcpStream>>),tiberius::error::Error> {
let stream = client.query(query, &[]).await?;
return Ok((stream.into_first_result().await?, client));
}
Any idea?
The main thing is that the temporary value that is created with the match expression is not dropped (through the Drop trait) until after the match-expression. In a sense you can think of the code being something like this:
pub async fn call_query2(mut client: Client<Compat<TcpStream>>, query:&str) -> Result<(Vec<tiberius::Row>,Client<Compat<TcpStream>>), (tiberius::error::Error,Client<Compat<TcpStream>>)> {
let result;
let tmp = client.query(query, &[]).await;
match tmp {
Ok(stream) => {
match stream.into_first_result().await {
Ok(rows) => result = Ok((rows,client)),
Err(e) => result = Err((e,client))
}
},
Err(e) => result = Err((e,client))
}
// tmp.drop(); happens here implicitly
return result;
}
Note that the implicit call tmp.drop() here theoretically might need access to client from the borrow checker's perspective.
Your other example works because you're basically dropping result before the return statement. Conceptually something like this:
pub async fn call_query(mut client: Client<Compat<TcpStream>>, query:&str) -> Result<(Vec<tiberius::Row>,Client<Compat<TcpStream>>),tiberius::error::Error> {
let result = client.query(query, &[]).await;
if let Err(e) = result {
return Err( e );
}
let stream = result.unwrap();
return Ok((stream.into_first_result().await?, client));
}
Note that you couldn't return an Err( (e,client) ) inside the if here either or you'd get again the same error from the borrow checker, since result hasn't been dropped yet.
That being said -- why would you want to return client in the first place? Probably because you want to use client in the calling code again. But then your function shouldn't require the caller to give up ownership to client in the first place. Just change mut client: ... in the signature of your function to client: &mut ... and remove Client from the return value type like this:
pub async fn call_query2(client: &mut Client<Compat<TcpStream>>, query:&str) -> Result<Vec<tiberius::Row>, tiberius::error::Error> {
// same code as before, but changing the result so that
// it doesn't return client anymore, i.e,
// Ok( (rows,client) ) => Ok(rows) and same for Err
}
Now your calling code can still refer to client without needing it "passed back" from you function. I.e., you go from
if let Ok( (rows, old_new_client) ) = call_query(client, query) {
old_new_client.whatever();
}
to a much nicer
if let Ok(rows) = call_query2(&mut client, query) {
client.whatever();
}

What lifetimes and bounds are needed to generalize this async code? [duplicate]

This question already has answers here:
How to fix lifetime error when function returns a serde Deserialize type?
(2 answers)
Why do Rust lifetimes matter when I move values into a spawned Tokio task?
(1 answer)
Closed 1 year ago.
I have this websocket code that uses tokio and serde here:
use async_once::AsyncOnce;
use common_wasm::models::status::{CommandMessage, StatusMessage};
use futures_util::{SinkExt, StreamExt};
use lazy_static::lazy_static;
use std::{collections::VecDeque, net::SocketAddr};
use tokio::{
net::{TcpListener, TcpStream}, sync::{broadcast, mpsc}
};
use tokio_tungstenite::{
accept_async, tungstenite::{Error, Message, Result}
};
use tracing::*;
// https://stackoverflow.com/questions/67650879/rust-lazy-static-with-async-await
lazy_static! {
pub static ref STATUS_REPORTER: AsyncOnce<StatusWs> = AsyncOnce::new(async {
info!("Init lazy static WS");
let server = StatusWs::init("ws://localhost:44444").await;
server
});
}
use StatusMessage as SenderType;
use CommandMessage as ReceiveType;
pub struct StatusWs {
buf: VecDeque<ReceiveType>,
rx_client_msg: mpsc::Receiver<ReceiveType>,
tx_server_msg: broadcast::Sender<SenderType>,
}
impl StatusWs {
pub async fn init(addr: &str) -> StatusWs {
info!("Init Status WS on {}", addr);
let listener = TcpListener::bind(&addr).await.expect("Can't listen");
// Clients producting to server, they use the tx to send and server uses the rx to read
let (tx_client_msg, rx_client_msg) = mpsc::channel::<ReceiveType>(32);
// spmc for server to broadcast status to listeners. Server uses tx to send and client uses rx to read
let (tx_server_msg, _rx_server_msg) = broadcast::channel::<SenderType>(10);
let tx_server_2 = tx_server_msg.clone();
tokio::spawn(async move {
while let Ok((stream, peer)) = listener.accept().await {
info!("Peer address connected: {}", peer);
let tx_client = tx_client_msg.clone();
let rx_server = tx_server_msg.subscribe();
tokio::spawn(async move {
accept_connection(peer, stream, tx_client, rx_server).await;
});
}
});
StatusWs { buf: VecDeque::new(), rx_client_msg, tx_server_msg: tx_server_2 }
}
pub async fn reportinfo(&self, msg: &SenderType) {
let my_msg = msg.clone();
match &self.tx_server_msg.send(my_msg) {
Ok(_size) => {
//trace!("Server Sending OK {}", size)
},
Err(_err) => {
//trace!("Server Sending ERR {:?}", err)
},
}
}
pub async fn next(&mut self) -> Result<Option<ReceiveType>> {
loop {
// If buffer contains data, we can directly return it.
if let Some(data) = self.buf.pop_front() {
return Ok(Some(data));
}
// Fetch new response if buffer is empty.
let response = self.next_response().await?;
// Handle the response, possibly adding to the buffer
self.handle_response(response)?;
}
}
async fn next_response(&mut self) -> Result<ReceiveType> {
loop {
tokio::select! { // TODO don't need select if there's only one thing?
Some(msg) = self.rx_client_msg.recv() => {
return Ok(msg)
},
}
}
}
fn handle_response(&mut self, response: ReceiveType) -> Result<()> {
self.buf.push_back(response);
Ok(())
}
}
async fn accept_connection(peer: SocketAddr, stream: TcpStream, tx_client: mpsc::Sender<ReceiveType>, rx_server: broadcast::Receiver<SenderType>) {
info!("Accepting connection from {}", peer);
if let Err(e) = handle_connection(peer, stream, tx_client, rx_server).await {
match e {
Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8 => error!("Connection closed"),
err => error!("Error processing connection: {}", err),
}
}
}
async fn handle_connection(
_peer: SocketAddr, stream: TcpStream, tx_client: mpsc::Sender<ReceiveType>, mut rx_server: broadcast::Receiver<SenderType>,
) -> Result<()> {
let ws_stream = accept_async(stream).await.expect("Failed to accept");
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
loop {
tokio::select! {
remote_msg = ws_receiver.next() => {
match remote_msg {
Some(msg) => {
let msg = msg?;
match msg {
Message::Text(resptxt) => {
match serde_json::from_str::<ReceiveType>(&resptxt) {
Ok(cmd) => { let _ = tx_client.send(cmd).await; },
Err(err) => error!("Error deserializing: {}", err),
}
},
Message::Close(_) => break,
_ => { },
}
}
None => break,
}
}
Ok(msg) = rx_server.recv() => {
match serde_json::to_string(&msg) {
Ok(txt) => ws_sender.send(Message::Text(txt)).await?,
Err(_) => todo!(),
}
}
}
}
Ok(())
}
The sender and receiver types are simple (simple types all the way down):
use std::{collections::BTreeMap, fmt::Debug};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct StatusMessage {
pub name: String,
pub entries: BTreeMap<i32, GuiEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommandMessage {
pub sender: String,
pub entryid: i32,
pub command: GuiValue,
}
Now I want to generalize the code so that I can create a struct that takes some other kind of Sender and Receiver type. Yes, I could just change the aliases, but I want to be able to use the generic type arguments rather than duplicate the whole file. The problem is as I follow the suggestions from the compiler, I end up in a place where I don't know what to do next. It's telling me resptext does not live long enough:
`resptxt` does not live long enough
borrowed value does not live long enoughrust cE0597
status_ws.rs(133, 29): `resptxt` dropped here while still borrowed
status_ws.rs(115, 28): lifetime `'a` defined here
status_ws.rs(129, 39): argument requires that `resptxt` is borrowed for `'a`
Here's what I have thus far:
use async_once::AsyncOnce;
use common_wasm::models::status::{CommandMessage, StatusMessage};
use futures_util::{SinkExt, StreamExt};
use lazy_static::lazy_static;
use serde::{Serialize, Deserialize};
use std::{collections::VecDeque, net::SocketAddr};
use tokio::{
net::{TcpListener, TcpStream}, sync::{broadcast, mpsc}
};
use tokio_tungstenite::{
accept_async, tungstenite::{Error, Message, Result}
};
use tracing::*;
// https://stackoverflow.com/questions/67650879/rust-lazy-static-with-async-await
lazy_static! {
pub static ref STATUS_REPORTER: AsyncOnce<StatusWs<CommandMessage, StatusMessage>> = AsyncOnce::new(async {
info!("Init lazy static WS");
let server = StatusWs::init("ws://localhost:44444").await;
server
});
}
// use StatusMessage as SenderType;
// use CommandMessage as ReceiveType;
pub struct StatusWs<ReceiveType, SenderType> {
buf: VecDeque<ReceiveType>,
rx_client_msg: mpsc::Receiver<ReceiveType>,
tx_server_msg: broadcast::Sender<SenderType>,
}
impl <'a, ReceiveType: Deserialize<'a> + Send, SenderType: Serialize + Clone + Send + Sync> StatusWs <ReceiveType, SenderType> {
pub async fn init(addr: &str) -> StatusWs<ReceiveType, SenderType> {
info!("Init Status WS on {}", addr);
let listener = TcpListener::bind(&addr).await.expect("Can't listen");
// Clients producting to server, they use the tx to send and server uses the rx to read
let (tx_client_msg, rx_client_msg) = mpsc::channel::<ReceiveType>(32);
// spmc for server to broadcast status to listeners. Server uses tx to send and client uses rx to read
let (tx_server_msg, _rx_server_msg) = broadcast::channel::<SenderType>(10);
let tx_server_2 = tx_server_msg.clone();
tokio::spawn(async move {
while let Ok((stream, peer)) = listener.accept().await {
info!("Peer address connected: {}", peer);
let tx_client = tx_client_msg.clone();
let rx_server = tx_server_msg.subscribe();
tokio::spawn(async move {
accept_connection(peer, stream, tx_client, rx_server).await;
});
}
});
StatusWs { buf: VecDeque::new(), rx_client_msg, tx_server_msg: tx_server_2 }
}
pub async fn reportinfo(&self, msg: &SenderType) {
let my_msg = msg.clone();
match &self.tx_server_msg.send(my_msg) {
Ok(_size) => {
//trace!("Server Sending OK {}", size)
},
Err(_err) => {
//trace!("Server Sending ERR {:?}", err)
},
}
}
pub async fn next(&mut self) -> Result<Option<ReceiveType>> {
loop {
// If buffer contains data, we can directly return it.
if let Some(data) = self.buf.pop_front() {
return Ok(Some(data));
}
// Fetch new response if buffer is empty.
let response = self.next_response().await?;
// Handle the response, possibly adding to the buffer
self.handle_response(response)?;
}
}
async fn next_response(&mut self) -> Result<ReceiveType> {
loop {
tokio::select! { // TODO don't need select if there's only one thing?
Some(msg) = self.rx_client_msg.recv() => {
return Ok(msg)
},
}
}
}
fn handle_response(&mut self, response: ReceiveType) -> Result<()> {
self.buf.push_back(response);
Ok(())
}
}
async fn accept_connection<'a, ReceiveType: Deserialize<'a>, SenderType: Clone + Serialize>(peer: SocketAddr, stream: TcpStream, tx_client: mpsc::Sender<ReceiveType>, rx_server: broadcast::Receiver<SenderType>) {
info!("Accepting connection from {}", peer);
if let Err(e) = handle_connection(peer, stream, tx_client, rx_server).await {
match e {
Error::ConnectionClosed | Error::Protocol(_) | Error::Utf8 => error!("Connection closed"),
err => error!("Error processing connection: {}", err),
}
}
}
async fn handle_connection<'a, ReceiveType: Deserialize<'a>, SenderType: Clone + Serialize>(
_peer: SocketAddr, stream: TcpStream, tx_client: mpsc::Sender<ReceiveType>, mut rx_server: broadcast::Receiver<SenderType>,
) -> Result<()> {
let ws_stream = accept_async(stream).await.expect("Failed to accept");
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
loop {
tokio::select! {
remote_msg = ws_receiver.next() => {
match remote_msg {
Some(msg) => {
let msg = msg?;
match msg {
Message::Text(resptxt) => {
match serde_json::from_str::<ReceiveType>(&resptxt) {
Ok(cmd) => { let _ = tx_client.send(cmd).await; },
Err(err) => error!("Error deserializing: {}", err),
}
},
Message::Close(_) => break,
_ => { },
}
}
None => break,
}
}
Ok(msg) = rx_server.recv() => {
match serde_json::to_string(&msg) {
Ok(txt) => ws_sender.send(Message::Text(txt)).await?,
Err(_) => todo!(),
}
}
}
}
Ok(())
}
I think there's some confusion about the necessary lifetimes and bounds, in particular the lifetime on the Deserializer from Serde and the Send/Sync auto trait markers on the message types.
In any case, it seems a bit brute force to just copy the whole original file and change out the aliases, which would definitely work, when it seems there's some sort of useful lesson here.
You should use serde::de::DeserializeOwned instead of Deserialize<'a>.
The Deserialize trait takes a lifetime parameter to support zero-cost deserialization, but you can't take advantage of that since the source, resptxt, is a transient value that isn't persisted anywhere. The DeserializeOwned trait can be used to constrain that the deserialized type does not keep references to the source and can therefore be used beyond it.
After fixing that, you'll get errors that ReceiveType and SenderType must be 'static to be used in a tokio::spawn'd task. Adding that constraint finally makes your code compile.
See the full compiling code on the playground for brevity.

How to print a response body in actix_web middleware?

I'd like to write a very simple middleware using actix_web framework but it's so far beating me on every front.
I have a skeleton like this:
let result = actix_web::HttpServer::new(move || {
actix_web::App::new()
.wrap_fn(move |req, srv| {
srv.call(req).map(move|res| {
println!("Got response");
// let s = res.unwrap().response().body();
// ???
res
})
})
})
.bind("0.0.0.0:8080")?
.run()
.await;
and I can access ResponseBody type via res.unwrap().response().body() but I don't know what can I do with this.
Any ideas?
This is an example of how I was able to accomplish this with 4.0.0-beta.14:
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::collections::HashMap;
use std::str;
use erp_contrib::{actix_http, actix_web, futures, serde_json};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{HttpMessage, body, http::StatusCode, error::Error ,HttpResponseBuilder};
use actix_http::{h1::Payload, header};
use actix_web::web::{BytesMut};
use futures::future::{ok, Future, Ready};
use futures::task::{Context, Poll};
use futures::StreamExt;
use crate::response::ErrorResponse;
pub struct UnhandledErrorResponse;
impl<S: 'static> Transform<S, ServiceRequest> for UnhandledErrorResponse
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Transform = UnhandledErrorResponseMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(UnhandledErrorResponseMiddleware { service: Rc::new(RefCell::new(service)), })
}
}
pub struct UnhandledErrorResponseMiddleware<S> {
service: Rc<RefCell<S>>,
}
impl<S> Service<ServiceRequest> for UnhandledErrorResponseMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
S::Future: 'static,
{
type Response = ServiceResponse;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let svc = self.service.clone();
Box::pin(async move {
/* EXTRACT THE BODY OF REQUEST */
let mut request_body = BytesMut::new();
while let Some(chunk) = req.take_payload().next().await {
request_body.extend_from_slice(&chunk?);
}
let mut orig_payload = Payload::empty();
orig_payload.unread_data(request_body.freeze());
req.set_payload(actix_http::Payload::from(orig_payload));
/* now process the response */
let res: ServiceResponse = svc.call(req).await?;
let content_type = match res.headers().get("content-type") {
None => { "unknown"}
Some(header) => {
match header.to_str() {
Ok(value) => {value}
Err(_) => { "unknown"}
}
}
};
return match res.response().error() {
None => {
Ok(res)
}
Some(error) => {
if content_type.to_uppercase().contains("APPLICATION/JSON") {
Ok(res)
} else {
let error = error.to_string();
let new_request = res.request().clone();
/* EXTRACT THE BODY OF RESPONSE */
let _body_data =
match str::from_utf8(&body::to_bytes(res.into_body()).await?){
Ok(str) => {
str
}
Err(_) => {
"Unknown"
}
};
let mut errors = HashMap::new();
errors.insert("general".to_string(), vec![error]);
let new_response = match ErrorResponse::new(&false, errors) {
Ok(response) => {
HttpResponseBuilder::new(StatusCode::BAD_REQUEST)
.insert_header((header::CONTENT_TYPE, "application/json"))
.body(serde_json::to_string(&response).unwrap())
}
Err(_error) => {
HttpResponseBuilder::new(StatusCode::BAD_REQUEST)
.insert_header((header::CONTENT_TYPE, "application/json"))
.body("An unknown error occurred.")
}
};
Ok(ServiceResponse::new(
new_request,
new_response
))
}
}
}
})
}
}
The extraction of the Request Body is straightforward and similar to how Actix example's illustrate. However, with the update to version Beta 14, pulling the bytes directly from AnyBody has changed with the introduction of BoxedBody. Fortunately, I found a utility function body::to_bytes (use actix_web::body::to_bytes) which does a good job. It's current implementation looks like this:
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
which I believe should be fine to extract the body in this way, as the body is extracted from the body stream by to_bytes().
If someone has a better way, let me know, but it was a little bit of pain, and I only had recently determined how to do it in Beta 13 when it switched to Beta 14.
This particular example intercepts errors and rewrites them to JSON format if they're not already json format. This would be the case, as an example, if an error occurs outside of a handler, such as parsing JSON in the handler function itself _data: web::Json<Request<'a, LoginRequest>> and not in the handler body. Extracting the Request Body and Response Body is not necessary to accomplish the goal, and is just here for illustration.

Resources