All Articles

Interceptors for Spring WebClient

HTTP
Sometimes, you need information from other sources through http

Previously, we’ve talked about making http calls through WebClient, and also how to test. Sometimes however, you’ll want information about the requests and responses in the actual usage, so that you can fix bugs in production.

In this article, we’ll cover intercepting filters that we can use to print such useful information.

Methods

Let’s first determine what we want from the interceptors. Having the full request may be a bit much at times, as it would produce way too much information. The headers will already give us plenty of information for any call that we could then afterwards reproduce.

// TODO: 26.06.22 replace println with logger
    private fun logRequest(): ExchangeFilterFunction{
        return ExchangeFilterFunction.ofRequestProcessor { clientRequest: ClientRequest ->
            println("Request: ${clientRequest.method()}  ${clientRequest.url()}")
            clientRequest.headers()
                .forEach { name: String?, values: List<String?> ->
                    values.forEach(
                        Consumer { value: String? ->
                            println(
                                "${name}=${value}"
                            )
                        })
                }
            Mono.just(clientRequest)
        }
    }

    private fun logResponse(): ExchangeFilterFunction {
        return ExchangeFilterFunction.ofResponseProcessor { clientResponse: ClientResponse ->
            println("Response status: ${clientResponse.statusCode()}")
            clientResponse.headers().asHttpHeaders()
                .forEach { name: String?, values: List<String?> ->
                    values.forEach(
                        Consumer { value: String? ->
                            println(
                                "${name}=${value}"
                            )
                        })
                }
            Mono.just(clientResponse)
        }
    }

As you can see, these methods would take the headers, and print them. Simple as that, but already really powerful.

Inclusion in the WebClient

Now, to use the methods above, we need to add them to the WebClient. The previous bean has now two additional lines.

    @Bean
    fun coinGeckoWebClient(@Value("\${service.coingecko.base-url}") baseUrl: String): WebClient {
        return WebClient.builder()
            .baseUrl(baseUrl)
            .filter(logRequest())  // logging the request headers
            .filter(logResponse())  // logging the response headers
            .clientConnector(ReactorClientHttpConnector(httpClient()))
            .codecs { configurer ->
                configurer
                    .defaultCodecs()
                    .maxInMemorySize(16 * 1024 * 1024)
            }
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.ACCEPT, "${MediaType.APPLICATION_JSON}")
            .defaultHeader(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.toString())
            .build()
    }

Usage

Now, let’s run the same call we’ve been running before and check out what the result is.

Logged meta information
We can see the status and other useful headers already in our request

There you go! Now you have all the information, and you can read through your logs if you’ve received any 4XX or 5XX responses from any of the servers you’ve tried calling. You can make alerts based off this, or anything else you may fancy!

In a future article, I may decide to delve a bit deeper into logging the bodies as well. However, this is usually simply leading to bloated logs and should be used very sparingly, therefore I will need some time to decide whether I’ll even attempt it myself.