reqwest-middleware icon indicating copy to clipboard operation
reqwest-middleware copied to clipboard

How to Get Response Body in middleware

Open caibirdme opened this issue 1 year ago • 4 comments

I want to impl a middleware that can log request and response body

#[async_trait]
impl Middleware for LoggingMiddlware {
    async fn handle(
        &self,
        req: Request,
        extensions: &mut Extensions,
        next: Next<'_>,
    ) -> ReqResult<Response> {
        req.body().clone()
			.and_then(|b| b.as_bytes())
			.and_then(|v| {
				println!("Request: {:?}", std::str::from_utf8(v).unwrap());
				Some(())
			});
        let res = next.run(req, extensions).await;
        // since response is a future
        // how can I get its response body without changing it?
        res
    }
}

caibirdme avatar Jul 31 '24 03:07 caibirdme

Any updates on this one? I had the same scenario, trying to log the response body within the logger middleware but the current design just doesn't support this. In the end, I needed to pull out my logging logic out of the middleware and wrap around HTTP client's execute method in order to invoke the .bytes() method.

I guess the culprit is that body streaming methods such as .bytes() are consuming the whole Response object (self) so it becomes unusable after it is invoked.

urosjovanovic avatar Oct 10 '24 22:10 urosjovanovic

I am interested in adding a body trace to my OTLP span but that seems even more difficult as fn on_request_end ofReqwestOtelSpanBackend is not even async.

https://docs.rs/reqwest-tracing/latest/reqwest_tracing/

lcmgh avatar Oct 17 '24 20:10 lcmgh

Did anyone find a workaround for this?

craigdub avatar Aug 07 '25 02:08 craigdub

Hi, I managed to get body by consuming the response and rebuilding it back

Note: This code seems to work for me but I can't guarantee correctness in all scenarios

async fn collect_body(body: reqwest::Body) -> Result<Bytes, reqwest::Error> {
    // impl from `reqwest::Response::bytes()`
    // allows to collect without consuming whole response (w/ parts)
    use http_body_util::BodyExt;

    BodyExt::collect(body).await.map(|buf| buf.to_bytes())
}

fn insert_response_url_extension(parts: &mut http::response::Parts, resp_url: Url) {
    // while https://github.com/seanmonstar/reqwest/pull/2798 is not merged
    // do this hack/fix manually
    use reqwest::ResponseBuilderExt;

    let extensions_with_response_url = http::response::Builder::new()
        .url(resp_url)
        .extensions_ref()
        .expect("New builder + infallible operation")
        .clone();
    parts.extensions.extend(extensions_with_response_url);
}

async fn clone_reqwest_response(resp: reqwest::Response) -> Result<(reqwest::Response, reqwest::Response), reqwest::Error> {
    let resp_url = resp.url().clone();
    let resp = http::Response::<reqwest::Body>::from(resp);
    let (mut parts, body) = resp.into_parts();
    // prevent losing response url while executing `reqwest::Response::from`
    insert_response_url_extension(&mut parts, resp_url);
    let b: Bytes = collect_body(body).await?;
    let reqwest_1 = reqwest::Response::from(http::Response::from_parts(parts.clone(), b.clone()));
    let reqwest_2 = reqwest::Response::from(http::Response::from_parts(parts, b));
    Ok((reqwest_1, reqwest_2))
}

Also had to make insert_response_url_extension hack because response url is for some reason overridden from extensions when converting back to reqwest::Response and it's not fixed yet.

Depending on your needs you can avoid cloning the whole response, but this was my use case :)

bragov4ik avatar Sep 16 '25 16:09 bragov4ik