6

I want to read out the body in a middleware in actix-web 1.0. I'm using the closure-style middleware using wrap_fn.

My basic setup is this:

let mut server = HttpServer::new(move || {
    ActixApp::new()
        .wrap_fn(|req, srv| {
            srv.call(req).map(|res| {
                let req_ = res.request();
                let body = req_.magical_body_read_function();
                dbg!(body);
                res
            })
        })
});

I need that magical_body_read_function() which sadly doesn't exist.

I hacked together something that looks like it could work by reading the examples and using take_payload() but it didn't work, sadly:

let mut server = HttpServer::new(move || {
    ActixApp::new()
        .wrap_fn(|req, srv| {
            srv.call(req).map(|res| {
                let req_ = res.request();
                req_.take_payload()
                    .fold(BytesMut::new(), move |mut body, chunk| {
                        body.extend_from_slice(&chunk);
                        Ok::<_, PayloadError>(body)
                    })
                    .and_then(|bytes| {
                        info!("request body: {:?}", bytes);
                    });
                res
            })
        })
});

Gives me

error[E0599]: no method named `fold` found for type `actix_http::payload::Payload<()>` in the current scope    --> src/main.rs:209:26
    | 209 |                         .fold(BytesMut::new(), move |mut body, chunk| {
    |                          ^^^^
    |
    = note: the method `fold` exists but the following trait bounds were not satisfied:
            `&mut actix_http::payload::Payload<()> : std::iter::Iterator`

I then tried an approach using the full middleware:

pub struct Logging;

impl<S, B> Transform<S> for Logging
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 = LoggingMiddleware<S>;
    type Future = FutureResult<Self::Transform, Self::InitError>;

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

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

impl<S, B> Service for LoggingMiddleware<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<dyn 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 {
        Box::new(self.service.call(req).and_then(|res| {
            let req_ = res.request();
            req_.take_payload()
                .fold(BytesMut::new(), move |mut body, chunk| {
                    body.extend_from_slice(&chunk);
                    Ok::<_, PayloadError>(body)
                })
                .and_then(|bytes| {
                    info!("request body: {:?}", bytes);
                });
            Ok(res)
        }))
    }
}

which sadly also resulted in the very similar looking error:

error[E0599]: no method named `fold` found for type `actix_http::payload::Payload<()>` in the current scope
   --> src/main.rs:204:18
    |
204 |                 .fold(BytesMut::new(), move |mut body, chunk| {
    |                  ^^^^
    |
    = note: the method `fold` exists but the following trait bounds were not satisfied:
            `&mut actix_http::payload::Payload<()> : futures::stream::Stream`
            `&mut actix_http::payload::Payload<()> : std::iter::Iterator`
            `actix_http::payload::Payload<()> : futures::stream::Stream`
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
svenstaro
  • 1,783
  • 2
  • 21
  • 31

3 Answers3

4

With the help of the fine people in the actix-web Gitter channel, I came to this solution which I also made a PR for.

Full solution is:

pub struct Logging;

impl<S: 'static, B> Transform<S> for Logging
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 = LoggingMiddleware<S>;
    type Future = FutureResult<Self::Transform, Self::InitError>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(LoggingMiddleware {
            service: Rc::new(RefCell::new(service)),
        })
    }
}

pub struct LoggingMiddleware<S> {
    // This is special: We need this to avoid lifetime issues.
    service: Rc<RefCell<S>>,
}

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

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

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

        Box::new(
            req.take_payload()
                .fold(BytesMut::new(), move |mut body, chunk| {
                    body.extend_from_slice(&chunk);
                    Ok::<_, PayloadError>(body)
                })
                .map_err(|e| e.into())
                .and_then(move |bytes| {
                    println!("request body: {:?}", bytes);
                    svc.call(req).and_then(|res| Ok(res))
                }),
        )
    }
}
svenstaro
  • 1,783
  • 2
  • 21
  • 31
  • I tried this but it seems like the body gets consumed by the middleware. Do you know if it is possible to only read the body in the middleware while letting it pass through to the service? – evading Sep 23 '19 at 05:31
  • I believe this is not possible due to the way actix-web is designed. Basically, in order to read the body, you have to get it moved into you. You could try putting it back into the Request somehow. – svenstaro Sep 23 '19 at 09:36
  • You can rebuild the payload and add it to the ServiceRequest – LoganSmith Oct 01 '19 at 22:13
4

Building on svenstaro's solution you can do something like the following to rebuild the request after cloning the stripped bytes.

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

        Box::new(
            req.take_payload()
                .fold(BytesMut::new(), move |mut body, chunk| {
                    body.extend_from_slice(&chunk);
                    Ok::<_, PayloadError>(body)
                })
                .map_err(|e| e.into())
                .and_then(move |bytes| {
                    println!("request body: {:?}", bytes);

                    let mut payload = actix_http::h1::Payload::empty();
                    payload.unread_data(bytes.into());
                    req.set_payload(payload.into());

                    svc.call(req).and_then(|res| Ok(res))
                }),
        )
    }                      
LoganSmith
  • 106
  • 2
1

Payload implements Stream<Item = Bytes, Error = _>, so there is no reason you cannot use the same trick as for other frameworks:

req_
    .take_payload().concat2()
    .and_then(|bytes| {
         info!("request body: {:?}", bytes);
    });

That is, if you had a proper Payload from a POST/PUT request. Since you've used wrap_fn(), you've effectively set up a middleware. Those run across all requests, and do not allow you access to the Payload (partly because you can only take it once).

As such, you're out of luck, I think.

Sébastien Renauld
  • 19,203
  • 2
  • 46
  • 66
  • No dice, sadly: error[E0599]: no method named `concat2` found for type `actix_http::payload::Payload<()>` in the current scope --> src/main.rs:209:26 | 209 | .concat2() | ^^^^^^^ – svenstaro Sep 10 '19 at 12:21
  • The error doesn't even tell you about importing a trait below it? – Sébastien Renauld Sep 10 '19 at 12:22
  • Nope, I know those already and the compiler is helpful there. Sadly, I think the type that comes out of `take_payload()` is not, as I'd expect, `Payload`. – svenstaro Sep 10 '19 at 12:25
  • @Svenstaro Yeah, I thought `wrap_fn` would do more, but all it does is create a middleware and since the lowest common denominator for payload in that is `()`, you're stuck with `Payload<()>`. You need to explicitly define your service :-( – Sébastien Renauld Sep 10 '19 at 12:37
  • _Payload implements Stream,_ `Payload` implements `Stream` only if `S` implements `Stream`, in the case for OP `S` is `()` , `()` is not an implementor of `Stream`, [please see](https://docs.rs/futures/0.2.0/futures/stream/trait.Stream.html#implementors) – Ömer Erden Sep 10 '19 at 12:41
  • Sadly, using the full middleware results in roughly the same error. Please check the updated answer. Thank you for your help so far. – svenstaro Sep 10 '19 at 13:15
  • Checking in, I have not forgotten about your question and may have a solution, but it's a lot more complex due to the requirements on middlewares :-( – Sébastien Renauld Sep 11 '19 at 08:19
  • I believe I've found a good fix. I'll post an answer for this and also create a new examples for the actix-examples repo so that other people may benefit. – svenstaro Sep 11 '19 at 11:16
  • Please do :-) I'm still struggling to see how you could do it without destructuring `ServiceRequest` and reforming it after – Sébastien Renauld Sep 11 '19 at 11:17