Can I access the request/response body on an ExchangeFilterFunction?
Asked Answered
W

1

8

Given an exchange using WebClient, filtered by a custom ExchangeFilterFunction:

@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
    return next.exchange(request)
        .doOnSuccess(response -> {
            // ...
        });
}

Trying to access the response body more than once using response.bodyToMono() will cause the underlying HTTP client connector to complain that only one receiver is allowed. AFAIK, there's no way to access the body's Publisher in order to cache() its signals (and I'm not sure it'd be a good idea, resource-wise), as well as no way to mutate or decorate the response object in a manner that allows access to its body (like it's possible with ServerWebExchange on the server side).

That makes sense, but I am wondering if there are any ways I could subscribe to the response body's publisher from a form of filter such as this one. My goal is to log the request/response being sent/received by a given WebClient instance.

I am new to reactive programming, so if there are any obvious no-nos here, please do explain :)

Wellgroomed answered 4/1, 2018 at 20:56 Comment(3)
The short answer would be - no. What you want to do goes against the reactive stack. The best approach here would be to log a request/response body explicitly in reactive chain e.g. .doOnNext(body -> log.info(JsonUtil.objectAsString(body))) for more details please pay attention to this question #45240505Penetrating
Possible duplicate of How to log request and response bodies in Spring WebFluxPenetrating
At least it's good to know it can't be done. Sadly for me, the WebClient reactive chain is not my code (it's an OAuth library) - I just get to add filters. I ended up consuming the body to log it only when the status code is an error, and let the downstream code think the response was empty. It doesn't need really the body if it was an error anyway.Horatia
S
2

Only for logging you could add a wiretap to the HttpClient as desribed in this answer.

However, your question is also interesting in a more general sense outside of logging.

One possible way is to create a duplicate of the ClientResponse instance with a copy of the previous request body. This might go against reactive principles, but it got the job done for me and I don't see big downsides given the small size of the response bodies in my client.

In my case, I needed to do so because the server sending the request (outside of my control) uses the HTTP status 200 Ok even if requests fail. Therefore, I need to peek into the response body in order to find out if anything went wrong and what the cause was. In my case I evict a session cookie in the request headers from the cache if the error message indicates that the session expired.

These are the steps:

  1. Get the response body as a Mono of a String (cf (1)).
  2. Return a Mono.Error in case an error is detected (cf (2)).
  3. Use the String of the response body to build a copy of the original response (cf (3)).

You could also use a dependency on the ObjectMapper to parse the String into an object for analysis.

Note that I wrote this in Kotlin but it should be easy enough to adapt to Java.

@Component
class PeekIntoResponseBodyExchangeFilterFunction : ExchangeFilterFunction {
    override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
        return next.exchange(request)
            .flatMap { response ->
                // (1)
                response.bodyToMono<String>()
                    .flatMap { responseBody ->
                        if (responseBody.contains("Error message")) {
                            // (2)
                            Mono.error(RuntimeException("Response contains an error"))
                        } else {
                            // (3)
                            val clonedResponse = response.mutate().body(responseBody).build()
                            Mono.just(clonedResponse)
                        }
                    }
            }
    }
}
Stannary answered 6/7, 2022 at 10:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.