spring-bootspring-webfluxspring-webclient

Spring WebClient.Builder timeout defaults and overrides for runtimes


I'm using Spring Boot 3.0.4 with Java 17. The Spring WebClient documentation says to use the injected WebClient.Builder:

Spring Boot creates and pre-configures a WebClient.Builder for you. It is strongly advised to inject it in your components and use it to create WebClient instances. Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones …, and more.

The documentation also says:

Spring Boot will auto-detect which ClientHttpConnector to use to drive WebClient, depending on the libraries available on the application classpath. For now, Reactor Netty, Jetty ReactiveStream client, Apache HttpClient, and the JDK’s HttpClient are supported.

This is a bit unclear to me. I had read in books and articles that Spring Boot will use Netty automatically for WebClient. But does this mean that without further configuration, the latest Spring Boot will use the JDK HttpClient? Note that I have included spring-boot-starter-web and spring-boot-starter-webflux in my project, but nothing specifically relating to Netty.

Furthermore the Spring Reactor documentation tells me that I can configure a connection timeout like this if I am using the Netty runtime:

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

But what is the timeout default already, if I don't add this code? And if I don't like the default and want to use this code, how do I override the default WebClient.Builder (mentioned above) without building one from scratch (and possibly negating all the other benefits)?

So let me summarize my doubts, based upon all this slightly ambiguous documentation:

  1. If I only specify spring-boot-starter-web and spring-boot-starter-webflux, is Spring WebClient using Netty or JDK HttpClient. (If it's using Netty by default, why does the documentation even mention JDK HttpClient? How would I force JDK HttpClient?)
  2. What are the default HTTP connection timeouts with the preconfigured WebClient.Builder, and where is this documented (or how can I find this out in the source code)?
  3. How can I override just the connection timeout for the preconfigured WebClient.Builder which is injected into the Spring context automatically?

Solution

    1. HoaPhan has already pointed out the code that checks for the presence of different HttpClient classes in the classpath. Based on those checks, the initialization of a ClientHttpConnector object happens in this method. As we can see in the linked code(included below), a JdkClientHttpConnector gets initialized if none of the other libraries are present in the classpath
    
        private ClientHttpConnector initConnector() {
            if (reactorNettyClientPresent) {
                return new ReactorClientHttpConnector();
            }
            else if (reactorNetty2ClientPresent) {
                return new ReactorNetty2ClientHttpConnector();
            }
            else if (jettyClientPresent) {
                return new JettyClientHttpConnector();
            }
            else if (httpComponentsClientPresent) {
                return new HttpComponentsClientHttpConnector();
            }
            else {
                return new JdkClientHttpConnector();
            }
        }
    

    Looks like netty gets added as a transient dependency when using spring-boot-starter-webflux. So to force the code to use the JDK HttpClient, you can do either what HoaPhan suggested or exclude netty dependencies from the classpath, which on gradle would look something like below:

        implementation(group: 'org.springframework.boot', name: 'spring-boot-starter-webflux') {
            exclude group: 'io.projectreactor.netty'
        }
    

    With the above exclusion and with none of the other HttpClient libraries present in the classpath, a WebClient bean like below should use the JDK HttpClient:

        @Bean
        public WebClient webClient(WebClient.Builder builder) {
            return builder.build();
        }
    
    1. The default connect timeout, if using the netty client, is 30 seconds. The timeouts are documented here

    2. Overriding the timeout in the preconfigured WebClient.Builder bean can be done using the same code you have included in the question, substituting WebClient.builder() with the injected WebClient.Builder bean. Something like below:

    package io.github.devatherock.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.client.reactive.ReactorClientHttpConnector;
    import org.springframework.web.reactive.function.client.WebClient;
    
    import io.netty.channel.ChannelOption;
    import reactor.netty.http.client.HttpClient;
    
    @Configuration
    public class WebClientConfig {
    
        @Bean
        public WebClient webClient(WebClient.Builder builder) {
            HttpClient httpClient = HttpClient.create()
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
    
            return builder
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .build();
        }
    }
    

    UPDATE:

    To create WebClient objects with different configurations from the single pre-configured WebClient.Builder bean, we'll need to clone the builder bean first

        @Bean
        public WebClient accountsClient(WebClient.Builder builder) {
            HttpClient httpClient = HttpClient.create()
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
    
            return builder.clone()
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .build();
        }
        
        @Bean
        public WebClient payrollClient(WebClient.Builder builder) {
            HttpClient httpClient = HttpClient.create()
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
    
            return builder.clone()
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .build();
        }
    

    UPDATE 2:

    To set timeouts and other customizations to the pre-configured WebClient.Builder bean, the simplest way would be to provide a custom WebClientCustomizer bean, like HoaPhan pointed out, as spring-boot applies all customizers to the WebClient.Builder bean when it is created. Then the customized WebClient.Builder bean can used to create as many WebClient objects as required. Sample below:

    package io.github.devatherock.config;
    
    import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.client.reactive.ReactorClientHttpConnector;
    import org.springframework.web.reactive.function.client.WebClient;
    
    import io.netty.channel.ChannelOption;
    import lombok.Getter;
    import lombok.RequiredArgsConstructor;
    import reactor.netty.http.client.HttpClient;
    
    @Configuration
    public class WebClientConfig {
        
        @Bean
        public WebClientCustomizer timeoutCustomizer() {
            return builder -> {
                HttpClient httpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
                builder.clientConnector(new ReactorClientHttpConnector(httpClient));
            };
        }
    
        @Bean
        public GoogleService googleService(WebClient.Builder builder) {
            WebClient googleClient = builder
                    .baseUrl("https://www.google.com")
                    .build();
    
            return new GoogleService(googleClient);
        }
    
        @Bean
        public BingService bingService(WebClient.Builder builder) {
            WebClient bingClient = builder
                    .baseUrl("https://www.bing.com")
                    .build();
    
            return new BingService(bingClient);
        }
    }