DEV Community

Cover image for Accessing the POST request body in Spring WebClient filters
Driptaroop Das
Driptaroop Das

Posted on • Originally published at blog.dripto.xyz on

Accessing the POST request body in Spring WebClient filters

Accessing the POST request body in Spring WebClient filters can be a bit tricky, as the request body is typically passed as a stream. In this blog post, we'll explore two solutions to this problem.

The problem

In some cases, you may need to access the POST request body in a Spring WebClient filter, in order to perform an operation such as signing the request with JWS header. However, because the request body is passed as a stream, it can be difficult to access and manipulate all at once.

Solution 1

Extending the ClientHttpRequestDecorator class

One approach to solving this problem is to extend the ClientHttpRequestDecorator class and override the writeWith method. This allows you to gain access to the DataBuffer publisher, which represents the stream of the request body. You can then concatenate and retrieve the original request by joining the published data buffers using DataBufferUtils.

class BufferingRequestDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {

    override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> =
        DataBufferUtils.join(body)
            .flatMap { db -> setJwsHeader(extractBytes(db)).then(super.writeWith(Mono.just(db))) }

    private fun setJwsHeader(data: ByteArray = byteArrayOf()): Mono<Void> {
        headers.add("JWS-header", createJws(data)) // or do whatever you want with the data
        return Mono.empty()
    }

    private fun extractBytes(data: DataBuffer): ByteArray {
        val bytes = ByteArray(data.readableByteCount())
        data.read(bytes)
        data.readPosition(0)
        return bytes
    }
}

Enter fullscreen mode Exit fullscreen mode

Once you have this class implemented, you can call it in the WebClient filter and deconstruct the old request and create a new one out of it.

class WebClientBufferingFilter : ExchangeFilterFunction {
    override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> =
        next.exchange(
            ClientRequest.from(request)
                .body { outputMessage, context ->
                    request.body().insert(
                        BufferingRequestDecorator(outputMessage),
                        context
                    )
                }.build()
        )
}

Enter fullscreen mode Exit fullscreen mode

Create a webclient with this filter

WebClient.builder()
        .filter(WebClientBufferingFilter())
        .build()

Enter fullscreen mode Exit fullscreen mode

Solution 2

Using a custom JSON Encoder

By default, WebClient uses Jackson to encode the data into JSON. When setting up the request, we use the utility BodyInserters.fromValue to create a body inserter for our data. The DefaultRequestBuilder for WebClient keeps this BodyInserter object at the time when the request will be sent, passing it all known message-writers and other relevant contextual information. However, we may need to access the request body before it is sent in order to perform additional actions such as signing or adding an authorization header.

Create a wrapper class around Jackson2JsonEncoder that allows us to intercept the encoded body. Specifically, we will be wrapping the encode method implementation from AbstractJackson2Encoder.

const val REQUEST_CONTEXT_KEY = "test"
class BodyProvidingJsonEncoder : Jackson2JsonEncoder() {
    override fun encode(
        inputStream: Publisher<out Any>,
        bufferFactory: DataBufferFactory,
        elementType: ResolvableType,
        mimeType: MimeType?,
        hints: MutableMap<String, Any>?
    ): Flux<DataBuffer> {
        return super.encode(inputStream, bufferFactory, elementType, mimeType, hints)
            .flatMap { db: DataBuffer ->
                Mono.deferContextual {
                    val clientHttpRequest = it.get<ClientHttpRequest>(REQUEST_CONTEXT_KEY)
                    db
                }
            }
    }

    private fun extractBytes(data: DataBuffer): ByteArray {
        val bytes = ByteArray(data.readableByteCount())
        data.read(bytes)
        data.readPosition(0)
        return bytes
    }
}

Enter fullscreen mode Exit fullscreen mode

Build a custom ReactorClientHttpConnector to put the request in the context.

class MessageSigningHttpConnector : ReactorClientHttpConnector() {
    override fun connect(
        method: HttpMethod,
        uri: URI,
        requestCallback: Function<in ClientHttpRequest, Mono<Void>>
    ): Mono<ClientHttpResponse> {
        // execute the super-class method as usual, but insert an interception into the requestCallback that can
        // capture the request to be saved for this thread.
        return super.connect(
            method, uri
        ) { incomingRequest: ClientHttpRequest ->
            requestCallback.apply(incomingRequest).contextWrite {
                it.put(
                    REQUEST_CONTEXT_KEY,
                    incomingRequest
                )
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Define a custom ExchangeFunction using the utility ExchangeFunctions.create() which accepts a custom HttpConnector. This connector has access to the function that makes the request. It is at this point that we can get a handle on the ClientHttpRequest and wait for the body to be serialized so that the header can be added.

val httpConnector = MessageSigningHttpConnector()
    val bodyProvidingJsonEncoder = BodyProvidingJsonEncoder()

    val client = WebClient.builder()
        .exchangeFunction(ExchangeFunctions.create(
            httpConnector,
            ExchangeStrategies
                .builder()
                .codecs { clientDefaultCodecsConfigurer: ClientCodecConfigurer ->
                    clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(bodyProvidingJsonEncoder)
                    clientDefaultCodecsConfigurer.defaultCodecs()
                        .jackson2JsonDecoder(Jackson2JsonDecoder(ObjectMapper(), MediaType.APPLICATION_JSON))
                }
                .build()
        ))
        .build()

Enter fullscreen mode Exit fullscreen mode

As always, be sure to test your codes thoroughly.

Solution 2 is originally found in https://andrew-flower.com/blog/Custom-HMAC-Auth-with-Spring-WebClient. I tinkered with it a bit before finding solution 1 which is somehow inspired by https://github.com/spring-projects/spring-framework/issues/26489. Finally, I decided to go with solution 1 as I found it to be a bit more simple and elegant.

originally published at: https://blog.dripto.xyz/accessing-the-post-request-body-in-spring-webclient-filters

Top comments (0)