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.
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());
}