How to modify the response from inside an actix web custom middleware

I would like to modify the res variable's body before returning, but since it is of type B (a generic) the compiler won't allow me to convert it to Bytes and use things like String::from_utf8(body.to_vec()). How can I modify the value of the response's body? Again it is an unconstrained Generic of type B.

impl<S, B> Service<ServiceRequest> for ReqAppenderMiddlewareExecutor<S> 
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(next_service);

    fn call(&self, mut req: ServiceRequest) -> Self::Future {  
        let fut = self.next_service.clone();

        Box::pin(async move {
            let body_original = req.extract::<Bytes>().await.unwrap();

            if req.content_type() == ContentType::json().0 {                               
                let body_str = String::from_utf8(body_original.to_vec());
                match body_str {
                    Ok(str) => {
                        let req_body_json: Result<RequestBodyJson, serde_json::Error> = serde_json::from_str(&str);
                        match req_body_json {
                            Ok(mut rbj) => {
                                rbj.msg = format!("{}. how are you?", rbj.msg);
                                let new_rbj_result = serde_json::to_string(&rbj);
                                let new_rbj_str = new_rbj_result.unwrap().clone();
                                let body_final = Bytes::from(new_rbj_str.clone());
                                req.set_payload(bytes_to_payload(body_final));
                            },
                            Err(_) => println!("Not of type RequestBodyJson, continuing")
                        };
                    },
                    Err(_) => println!("Payload not string, continuing")
                };
            }            
            
            let res = fut.call(req).await?;
            Ok(res)
        })
    }
}

If you need to return a response with a different body type than that returned by the service you call in your middleware, actix provides the EitherBody enum for that. See i.e. this topic:

In the implementation you can I see I extracted the req as a Bytes and then got the utf8 string from it in order to modify that string.
I would like to do something similar to the res variable's body before returning it. In other words I need access to res' body to change it.

You need to call into_parts on the ServiceResponse to have access to the response's body: ServiceResponse in actix_web::dev - Rust.

Hmm, what happens if you change the generic B to a concrete type, like Bytes?

Actix web's middleware requires a secondary Trait impl called Transform which uses the other impl and that one will fail since it expects an unconstrained B not a bound B of B: Bytes.

According to App::wrap, the only constraint put on B is that it implements MessageBody, which is true for Bytes, String, etc. Transform itself does not have a generic parameter that would match B per se, it is only part of its Response type, i.e. Response = ServiceResponse<B>, so Transform can't require anything from B. The only constraint on the generic parameter B is coming from App::wrap, which requires that Transform::Response = ServiceResponse<B>, where B: MessageBody.

1 Like

By adding B: 'static + MessageBody + Clone to the impl for Transform and Service I was able to get this code in without the squiggles (needed to clone body so that res would not be taken and I could use it later to call res.response_mut). However after adding Clone, now my App instance wrap is complaining.

error[E0277]: the trait bound BoxBody: Clone is not satisfied
--> section_1/finish/middleware/src/main.rs:15:19
|
15 | .wrap(ReqAppenderMiddlewareBuilder)
| ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait Clone is not implemented for BoxBody
| |
| required by a bound introduced by this call
|
= help: the trait Transform<S, ServiceRequest> is implemented for ReqAppenderMiddlewareBuilder
note: required for ReqAppenderMiddlewareBuilder to implement Transform<actix_web::app_service::AppRouting, ServiceRequest>
--> section_1/finish/middleware/src/req_middleware.rs:20:12

impl<S, B> Service<ServiceRequest> for ReqAppenderMiddlewareExecutor<S> 
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static + MessageBody + Clone
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(next_service);

    fn call(&self, mut req: ServiceRequest) -> Self::Future {  
        let fut = self.next_service.clone();

        Box::pin(async move {
            let body_original = req.extract::<Bytes>().await.unwrap();

            if req.content_type() == ContentType::json().0 {                               
                let body_str = String::from_utf8(body_original.to_vec());
                match body_str {
                    Ok(str) => {
                        let req_body_json: Result<RequestMessage, serde_json::Error> = serde_json::from_str(&str);
                        match req_body_json {
                            Ok(mut rbj) => {
                                rbj.msg = format!("{}. I modified the request.", rbj.msg);
                                let new_rbj_result = serde_json::to_string(&rbj);
                                let new_rbj_str = new_rbj_result.unwrap();
                                let body_final = Bytes::from(new_rbj_str);
                                req.set_payload(bytes_to_payload(body_final));
                            },
                            Err(_) => println!("Not of type RequestMessage, continuing")
                        };
                    },
                    Err(_) => println!("Payload not string, continuing")
                };
            }            
            
            let mut res = fut.call(req).await?;
            if res.headers().contains_key("content-type") {
                let body = res.response().body().clone();
                let body_bytes_result = body.try_into_bytes();             
                match body_bytes_result {
                    Ok(body_bytes) => {
                        let body_str = String::from_utf8(body_bytes.to_vec());
                        match body_str {
                            Ok(str) => {
                                let body_obj: Result<ResponseMessage, serde_json::Error> = serde_json::from_str(&str);
                                match body_obj {
                                    Ok(mut body_obj) => {
                                        body_obj.msg = format!("{}. I modified the response.", body_obj.msg);
                                        let new_body_obj_result = serde_json::to_string(&body_obj);
                                        let new_body_obj_str = new_body_obj_result.unwrap();
                                        let body_final = Bytes::from(new_body_obj_str);
                                        res.response_mut().set_body(body_final);
                                    },
                                    Err(_) => println!("Not of type ResponseMessage, continuing")
                                };
                            },
                            Err(_) => println!("To string failed")  
                        }                          
                    },
                    Err(_) => println!("Payload not string, continuing")
                };
            }
            Ok(res)
        })
    }
}

As BoxBody does not implement Clone, I think you have to consume res and construct a new ServiceResponse from res's headers and the newly created body.

1 Like

Thanks for your help. I'm almost there but having one more issue. When I build a new ServiceResponse it has to be of type B, but when I build it I get ServiceResponse. This is my error.

body_obj.msg = format!("{}. I modified the response.", body_obj.msg);
let new_body_obj_result = serde_json::to_string(&body_obj);
let new_body_obj_str = new_body_obj_result.unwrap();
let body_final = Bytes::from(new_body_obj_str); // also tried getting rid of this Bytes conversion and 
// making my body_final object type impl MessageBody but that did nothing
let resp = HttpResponse::build(status)
.content_type("application/json")
.body(body_final);                                        
Ok(req.into_response(resp)) // req.into_response returns ServiceResponse<BoxBody>

Thing that I don't get here is in my function signature B is constrained as B: 'static + MessageBody. And I can see in the docs that BoxBody does an impl of MessageBody. So why would BoxBody not be accepted as B?

You can pass a generic argument to into_response to get the correct expected type:

Ok(req.into_response::<B>(resp))

into_response takes two generics. I tried <B, HttpResponse> and <B, ServiceResponse> but neither work.

Ah, sorry. I was looking at the into_response method for ServiceResponse.

Did you try with Ok(req.into_response::<B, ServiceResponse<B>>(resp))?

Just tried it and got
mismatched types
expected struct ServiceResponse<B>
found struct HttpResponse

This associated type is the reason why you need to return ServiceResponse<B>. Try changing it to ServiceResponse<BoxBody>, as you only ever return ServiceResponse<BoxBody> AFAICT. If you return either ServiceResponse<B> or ServiceResponse<BoxBody>, try returning ServiceResponse<EitherBody<B>> instead.

If this won't work and you still need help, would you mind posting a minimal reproducible example (including your Transform implementation)?

1 Like

I like that EitherBody idea. I'll give that a shot thanks.

I couldn't figure it out. Here's a small repo with the code. Please give it a shot and thank you! GitHub - jsoneaday/actix-middleware

1 Like

Thanks, I'll try to look into it tomorrow or on Monday.

The concrete types for generic parameters are chosen by the caller, and it could choose e.g. B=Vec<u8>, and of course BoxBody is different than Vec<u8>.