How to intercept Spring WebFlux WebClient request and response with headers and bodies?
Asked Answered
B

1

6

I'm working on a Spring Boot application that uses Spring WebFlux, and I'm trying to intercept WebClient requests and responses. Specifically, I need to capture both the headers and the request/response bodies to compute and verify an HMAC.

I've explored the use of ExchangeFilterFunction, which allows me to access ClientHttpRequest and ClientHttpResponse and retrieve the headers. However, I'm facing an issue with capturing the request/response bodies using this approach.

On a ExchangeFilterFuction, the request.body() is a BodyInsert, is there any way to extract the body from there? Also on the filter, the response.bodyToMono(String.class) doesn't retrieve the response body.

On the other hand, I've looked into using Encoder and Decoder, which can help me retrieve the body but do not provide access to the Request/Response objects, making it challenging to obtain the header information.

Have anyone manage to achieve anything related to this?

Bonar answered 20/10, 2023 at 9:10 Comment(2)
I have exactly the same problem...Gifford
you can try to use a BodyExtractor as in response.body(BodyExtractor.toMono(Foobar.class)) and no i dont have a code sample. docs.spring.io/spring-framework/docs/current/javadoc-api/org/…Drying
O
0

You can solve it in following manner. Create a filter, that intercepts body with BufferingDecorator that reads body when it's being sent and calculates required header. Be aware that this code is called just before sending the request, so if you use some header loggers may not see the calculated header if they are executed before.

For the response it's easier, you just need to use BodyExtractor.

Feel free to ask, if you have further questions.

public class HmacFilter implements ExchangeFilterFunction {
    @Override
    public @NonNull Mono<ClientResponse> filter(@NonNull ClientRequest request, ExchangeFunction next) {
        ClientRequest modifiedRequest = ClientRequest.from(request)
                .body((outputMessage, context) ->
                        request.body().insert(
                                new BufferingDecorator(outputMessage),
                                context
                        ))
                .build();
        return next.exchange(modifiedRequest)
                  .flatMap(this::appendResponseHeader);
    }
private static final class BufferingDecorator extends ClientHttpRequestDecorator {

        private BufferingDecorator(ClientHttpRequest delegate) {
            super(delegate);
        }

        @Override
        public  @NonNull Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            return DataBufferUtils.join(body).flatMap(buffer -> {
                HttpHeaders headers = getHeaders();
                headers.set("Hmac", calculateHmac(getDelegate(), readBytes(buffer)));
                return super.writeWith(Mono.just(buffer));
            });
        }

        /*
        writeWith is not called for GET with empty body, in such cases header needs to be added in setComplete()
         */
        @Override
        public @NonNull Mono<Void> setComplete() {
            HttpHeaders headers = getHeaders();
            headers.putIfAbsent("Hmac", Collections.singletonList(calculateHmac(getDelegate(), new byte[0])));
            return super.setComplete();
        }
        private String calculateHmac(ClientHttpRequest request, byte[] payload) {
        // calculate HMAC using request body
        }
        private String calculateHmac(byte[] responseBody) {
        // calculate HMAC using response body
        }


        public byte[] readBytes(DataBuffer dataBuffer) {
            ByteBuffer byteBuffer = dataBuffer.asByteBuffer();
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
            return bytes;
        }
    private Mono<ClientResponse> appendResponseHeader(ClientResponse response) {
        HttpHeaders headers = response.headers().asHttpHeaders();
        Flux<DataBuffer> body = response.body(BodyExtractors.toDataBuffers())
                .map(dataBuffer -> {
                    ByteBuffer byteBuffer = dataBuffer.asByteBuffer();
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
                    headers.putIfAbsent("Hmac-Response", Collections.singletonList(calculateHmac(bytes)));
                    return dataBuffer;
                });
        response.mutate().body(body);
        return Mono.just(response);
    }
}
Odont answered 6/9 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.