actix / actix-web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
https://actix.rs
Apache License 2.0
21.6k stars 1.67k forks source link

accessing request body and response body from middleware #1457

Closed wyanlord closed 4 years ago

wyanlord commented 4 years ago

I cannot view RequestBody and ResponseBody in the log middleware. These are business logs, which are necessary for troubleshooting the submission and return of data.

use std::pin::Pin;
use std::task::{Context, Poll};

use actix_service::{Service, Transform};
use actix_web::{Error, web};
use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse};
use futures::Future;
use futures::future::{ok, Ready};

pub mod user;
pub mod asset;

pub fn api_routes() -> impl HttpServiceFactory {
    web::scope("/api")
        .route("/user/login", web::post().to(user::login))
}

pub fn static_routes() -> impl HttpServiceFactory {
    web::scope("/static")
        .route("/index.html", web::get().to(asset::index))
}

// custom request log middleware
pub struct AccessLog;

impl<S, B> Transform<S> for AccessLog
    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 Transform = AccessLogMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

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

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

impl<S, B> Service for AccessLogMiddleware<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 = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        let begin = std::time::SystemTime::now();

        // request information
        let path = req.path().to_string();
        let method = req.method().as_str().to_string();
        let ip_addr = req.connection_info().remote().unwrap().to_string();
        let queries = req.query_string().to_string();

        // Todo: Request body is necessary.

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;

            // Todo: Response body is necessary.

            let duration = begin.elapsed().unwrap().as_millis();

            log::info!("path: {}, method: {}, ip: {}, queries: {}, duration: {}ms",
                     path,
                     method,
                     ip_addr,
                     queries,
                     duration
            );
            Ok(res)
        })
    }
}
robjtede commented 4 years ago

check out

might have some methods you can use for accessing the req/res body

wyanlord commented 4 years ago

If I take the req body from service request stream, how do I put the body buffer into the service request again. Because the request body needs to be parsed into json later again. And the function call of middleware is not async, so I can not execute the await of the stream at the function call. Do I say more detail?

jbrookins13 commented 4 years ago

@wyanlord If I am understanding correctly I am currently doing something similar. I am doing HMAC verification, thus I needed the request body as well. Since I needed to put the body back into the request after inspecting it here is a snippet of the code I am using in the call() function, simplified a bit:

Box::pin(async move {
    let body_result: Result<BytesMut, Error> = get_request_body(&mut req).await;
    match body_result {
        Ok(bytes) => {
            // get the payload body for further use
            let req_body = String::from_utf8(bytes.to_vec());

            // put the payload back into the ServiceRequest
            let mut payload = actix_http::h1::Payload::empty();
            payload.unread_data(bytes.freeze());
            req.set_payload(payload.into());

            match req_body {...}
        }
        Err(e) => ...
    }
})

Not saying this is the best way to do it, but this is what I had to do to get it to work for me. If anyone else has any suggestions or critiques I would love to hear them.