4

I'd like to modify the response body in Actix-Web. I've implemented the v1.0.0 middleware but I've not been successful in changing the response body so far.

I've tried two basic approaches: return a new ServiceResponse and use the method chains on ServiceResponse to attempt to set a new body. Both approaches have resulted in varying kinds of compiler errors and move violations that I haven't been able to resolve. While I'm new to Rust, other members of my team are more experienced and weren't able to work though these challenges either. I have not been able to find any examples that are on point.

Here is my implementation of call().

fn call(&mut self, req: ServiceRequest) -> Self::Future {
    let content_type = req.get_header_value("content-type");

    println!(
        "Request Started: {}; content type: {};",
        req.path(),
        content_type
    );

    Box::new(self.service.call(req).and_then(move |mut res| {
        match res.response().error() {
            Some(e) => {
                println!(
                    "Response: Error Code: {}; content type: {};",
                    res.status(),
                    content_type
                );

                dbg!(e);

                // I'd really like to modify the response body here.

                return Ok(res);
            }
            None => {
                println!("Middleware detected the response is not an error. ");
                return Ok(res);
            }
        }
    }))
}

I'd like to modify the response body to be the text "fredbob". If I can get that far, I can accomplish the rest of what I want to do.

Complete code:

use actix_service::{Service, Transform};
use actix_web::http::StatusCode;
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error, HttpResponse};
use futures::future::{ok, FutureResult};
use futures::{Future, Poll};

use serde::Serialize;
use serde_json;

pub trait Headers {
    fn get_header_value(&self, name: &str) -> String;
}

impl Headers for ServiceRequest {
    fn get_header_value(&self, name: &str) -> String {
        self.headers()
            .get(name)
            .unwrap()
            .to_str()
            .unwrap()
            .to_string()
    }
}

// There are two step in middleware processing.
// 1. Middleware initialization, middleware factory get called with
//    next service in chain as parameter.
// 2. Middleware's call method get called with normal request.
pub struct SayHi;

// Middleware factory is `Transform` trait from actix-service crate
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S> for SayHi
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = SayHiMiddleware<S>;
    type Future = FutureResult<Self::Transform, Self::InitError>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(SayHiMiddleware { service })
    }
}

pub struct SayHiMiddleware<S> {
    service: S,
}

fn is_error(status: StatusCode) -> bool {
    status.as_u16() >= 400
}

impl<S, B> Service for SayHiMiddleware<S>
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Box<Future<Item = Self::Response, Error = Self::Error>>;

    fn poll_ready(&mut self) -> Poll<(), Self::Error> {
        self.service.poll_ready()
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        let content_type = req.get_header_value("content-type");

        println!(
            "Request Started: {}; content type: {};",
            req.path(),
            content_type
        );

        Box::new(self.service.call(req).and_then(move |mut res| {
            match res.response().error() {
                Some(e) => {
                    println!(
                        "Response: Error Code: {}; content type: {};",
                        res.status(),
                        content_type
                    );

                    dbg!(e);

                    // I'd really like to modify the response body here.

                    return Ok(res);
                }
                None => {
                    println!("Middleware detected the response is not an error. ");
                    return Ok(res);
                }
            }
        }))
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Chris McKenzie
  • 3,681
  • 3
  • 27
  • 36

2 Answers2

3

The posted solutions above no longer work in later versions of Actix. In 4.0.0-beta.14, in your middleware you can capture the Request via:

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));

And you can capture the response via:

let _body_data =
    match str::from_utf8(&body::to_bytes(res.into_body()).await?){
    Ok(str) => {
        str
    }
    Err(_) => {
        "Unknown"
    }
};

Note: pulling the Request/Response will imply that you consume the Request/Response, so you need to alter it and create a new Request/Response.

You can then return a new response:

let new_request = res.request().clone();

let new_response = HttpResponseBuilder::new(StatusCode::BAD_REQUEST)
          .insert_header((header::CONTENT_TYPE, "application/json"))
          .body("New body data");

Ok(ServiceResponse::new(
    new_request,
    new_response
))

I posted a full example here:

How to print a response body in actix_web middleware?

Flair
  • 2,609
  • 1
  • 29
  • 41
Chris Andrew
  • 103
  • 6
2

You can try one of these two approaches. Both of them work for me:

  1. Use map_body method from ServiceResponse:
let new_res = res.map_body(|_head, _body| {
    ResponseBody::Other(Body::Message(Box::new("fredbob")))
});

return Ok(new_res);
  1. Access to the HttpRequest, clone it, and build a new ServiceResponse:
let new_res = ServiceResponse::new(
    res.request().clone(),
    HttpResponse::Ok().body("fredbob").into_body(),
);

return Ok(new_res);

Hope it helps!

robertohuertasm
  • 846
  • 9
  • 17
  • In solution no.2, you don't add dev enough to ServiceResponse. It should be dev :: ServiceResponse :: new (      res.request (). clone (),      HttpResponse :: Ok (). Body ("fredbob"). Into_body (), ); – Wahyu Kodar Jan 12 '20 at 17:01