javaspring-webfluxreactive-programminginterceptor

How to capture request body in ClientRequest when implementing ExchangeFilterFunction


I am implementing a class that implements the ExchangeFilterFunction interface in a Spring WebFlux application to intercept and log HTTP requests and responses when using a reactive WebClient.

I am able to log request and response headers from ClientRequest and ClientResponse. Additionally, I can capture and log the response body from ClientResponse. However, I am struggling to capture the request body from ClientRequest.

Here's a simplified version of my current implementation:

@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
    return next.exchange(request)
        .flatMap(response ->
            response.bodyToMono(String.class)
                .flatMap(responseBody -> {
                    ClientResponse clientResponse = ClientResponse.create(response.statusCode())
                        .headers(httpResponse -> httpResponse.addAll(response.headers().asHttpHeaders()))
                        .body(responseBody)
                        .build();

                    log.atInfo()
                        .addKeyValue("requestHeaders", request.headers())
                        .addKeyValue("responseHeaders", response.headers().asHttpHeaders())
                        .addKeyValue("requestBody", "") // TODO: How to capture this?
                        .addKeyValue("responseBody", parseResponseBody(responseBody))
                        .log();

                    return Mono.just(clientResponse);
                })
        );
}

private Object parseResponseBody(String json) {
    try {
        return objectMapper.readValue(json, Object.class);
    } catch (Exception e) {
        log.error("Failed to parse JSON", e);
        return json;
    }
}

I found a potential solution here, which involves overriding certain classes and methods to access the request body. While this approach works, I was wondering if there is a simpler or more direct way to capture the request body without the need to override and create additional classes or methods.


Solution

  • I managed to find a way to capture the request body by Overriding HttpServiceArgumentResolver's resolve method and creating a Utility class called RequestContext to store the value of the request body.

    The resolve method now checks if there is a RequestBody annotation and stores it in RequestContext. It returns false since no modification is done to the request.

    RequestBodyResolver

    @Component
    @RequiredArgsConstructor
    public class RequestBodyResolver implements HttpServiceArgumentResolver {
    
        @Override
        public boolean resolve(@Nullable Object argument, MethodParameter parameter,
                               HttpRequestValues.@NonNull Builder requestValues) {
            Object requestBody = parameter.hasParameterAnnotation(RequestBody.class) ? argument : new Object();
            RequestContext.setRequestBody(requestBody);
            return false;
        }
    }
    

    RequestContext

    @UtilityClass
    @Value
    public class RequestContext {
        private static final ThreadLocal<Object> requestBodyHolder = new ThreadLocal<>();
    
        public void setRequestBody(Object requestBody) {
            requestBodyHolder.set(requestBody);
        }
    
        public Object getRequestBody() {
            return requestBodyHolder.get();
        }
    
        public void clear() {
            requestBodyHolder.remove();
        }
    }
    

    Also, register this as a custom argument resolver in HttpServiceProxyFactory.

    @Bean
    public HttpServiceProxyFactory httpServiceProxyFactory(
        HttpServiceProxyFactory.Builder httpServiceProxyFactoryBuilder) {
        httpServiceProxyFactoryBuilder.customArgumentResolver(requestBodyResolver);
        return httpServiceProxyFactoryBuilder.build();
    }
    

    I can now call RequestContext.getRequestBody() in my LoggingInterceptor's filter method to log my request body.

    @Override
    public @NonNull Mono<ClientResponse> filter(@NonNull ClientRequest request, ExchangeFunction next) {
        return next.exchange(request)
            .flatMap(response -> response.bodyToMono(String.class)
                .flatMap(responseBody -> {
                    log.atInfo()
                        .addKeyValue("requestHeaders", request.headers())
                        .addKeyValue("responseHeaders", response.headers().asHttpHeaders())
                        .addKeyValue("requestBody", RequestContext.getRequestBody())
                        .addKeyValue("responseBody", parseResponseBody(responseBody))
                        .log();
    
                    return Mono.just(ClientResponse.create(response.statusCode())
                        .headers(httpResponse -> httpResponse.addAll(response.headers().asHttpHeaders()))
                        .body(responseBody)
                        .build());
                })
            )
            .doFinally(signalType -> RequestContext.clear());
    }