31

I'm trying to set timeout on my WebClient, here is the current code :

SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();

ClientHttpConnector httpConnector = new ReactorClientHttpConnector(opt -> {
    opt.sslContext(sslContext);
    HttpClientOptions option = HttpClientOptions.builder().build();
    opt.from(option);
});
return WebClient.builder().clientConnector(httpConnector).defaultHeader("Authorization", xxxx)
                .baseUrl(this.opusConfig.getBaseURL()).build();

I need to add timeout and also pooling strategy, I was thinking of something like that :

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(this.applicationConfig.getHttpClientMaxPoolSize());
cm.setDefaultMaxPerRoute(this.applicationConfig.getHttpClientMaxPoolSize());
cm.closeIdleConnections(this.applicationConfig.getServerIdleTimeout(), TimeUnit.MILLISECONDS);

RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(this.applicationConfig.getHttpClientSocketTimeout())
        .setConnectTimeout(this.applicationConfig.getHttpClientConnectTimeout())
        .setConnectionRequestTimeout(this.applicationConfig.getHttpClientRequestTimeout()).build();

CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(cm).build();

But I can't figure out how to set the httpClient in my webclient

Serhii Povísenko
  • 3,352
  • 1
  • 30
  • 48
Seb
  • 3,602
  • 8
  • 36
  • 52

10 Answers10

26

To set the read and connect timeout I use the method below, because the SO_TIMEOUT option is not available for channels using NIO (and giving the warning Unknown channel option 'SO_TIMEOUT' for channel '[id: 0xa716fcb2]')

ReactorClientHttpConnector connector = new ReactorClientHttpConnector(
          options -> options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                            .compression(true)
                            .afterNettyContextInit(ctx -> {
                                ctx.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS));
                            }));
return WebClient.builder()
                .clientConnector(connector)
                .build();
joshiste
  • 2,638
  • 1
  • 14
  • 19
24

ReactorClientHttpConnector API changed in version Spring WebFlux 5.1.

So I do the following (Kotlin syntax, based on @joshiste example):

val tcpClient = TcpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000)
    .doOnConnected { connection ->
        connection.addHandlerLast(ReadTimeoutHandler(10))
            .addHandlerLast(WriteTimeoutHandler(10))
    }

val myWebClient = webClientBuilder
    .clientConnector(ReactorClientHttpConnector(HttpClient.from(tcpClient)))
    .baseUrl(myEndPoint)
    .build()

UPDATE 2021

HttpClient.from in deprecated in last version of Reactive Netty. It was copying the configuration of the tcpClient. Now we can configure httpClient directly.

val httpClient = HttpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000)
    .doOnConnected { connection ->
        connection.addHandlerLast(ReadTimeoutHandler(10))
            .addHandlerLast(WriteTimeoutHandler(10))
    }

val myWebClient = webClientBuilder
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .baseUrl(myEndPoint)
    .build()
mcoolive
  • 3,805
  • 1
  • 27
  • 32
  • 3
    When using your solution I face```the method from(TcpClient) from the type HttpClient is deprecated```. Seems like the ```from(TcpClient)``` is deprecated too. – CtrlAltElite Jan 14 '21 at 09:29
16

As Spring Webflux was updated, here is a solution that works for Java (based on the answer for Kotlin):

TcpClient timeoutClient = TcpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, SECONDS*1000)
    .doOnConnected(
        c -> c.addHandlerLast(new ReadTimeoutHandler(SECONDS))
              .addHandlerLast(new WriteTimeoutHandler(SECONDS)));
return webClientBuilder.baseUrl(YOUR_URL)
       .clientConnector(new ReactorClientHttpConnector(HttpClient.from(timeoutClient)))
       .build();

UPDATE 2021

since HttpClient.from(TcpClient) is deprectaed now it's even easier:

return WebClient.builder()
                .baseUrl(YOUR_URL)
                .clientConnector(new ReactorClientHttpConnector(HttpClient.create()
                                                                          .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, SECONDS * 1000)
                                                                          .doOnConnected(c -> c.addHandlerLast(new ReadTimeoutHandler(SECONDS))
                                                                                               .addHandlerLast(new WriteTimeoutHandler(SECONDS)))))
                .build();
Serhii Povísenko
  • 3,352
  • 1
  • 30
  • 48
15

The WebFlux WebClient doesn't use Apache Commons HTTP Client. Although you might be able to implement one solution via custom ClientHttpConnector. The existing ReactorClientHttpConnector is based on the Netty. So, consider to use Netty options to configure the client, e.g.:

ReactorClientHttpConnector connector =
            new ReactorClientHttpConnector(options ->
                    options.option(ChannelOption.SO_TIMEOUT, this.applicationConfig.getHttpClientConnectTimeout()));

or

.onChannelInit(channel -> channel.config().setConnectTimeoutMillis(this.applicationConfig.getHttpClientConnectTimeout()))

UPDATE

We also can use ReadTimeoutHandler:

.onChannelInit(channel -> 
        channel.pipeline()
           .addLast(new ReadTimeoutHandler(this.applicationConfig.getHttpClientConnectTimeout())))
Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • 1
    This seems to be what I'm looking for, just a quick question is that the connect timeout or the request timeout. Any idea how I could set up the connection pool size ? thanks for your help – Seb Sep 18 '17 at 06:53
  • 2
    There is `poolResources()` in the `reactor.ipc.netty.options.ClientOptions.Builder` The `requestTimeout` is indeed equal to the `ChannelOption.SO_TIMEOUT`. The `connectTimeoutMillis` is definitely about connection. – Artem Bilan Sep 18 '17 at 13:12
  • Yeah I saw the poolResources(), I have to admit I have no idea how to use it :/ any idea ? – Seb Sep 18 '17 at 14:02
  • I tried : options.poolResources(PoolResources.fixed("myPool", this.applicationConfig.getHttpClientMaxPoolSize())); Is that the correct way ? – Seb Sep 18 '17 at 14:13
  • When using the SO_TIMEOUT option, I'm getting following warnings logged: `Unknown channel option 'SO_TIMEOUT' for channel '[id: 0xa716fcb2]'` Any hints on this @ArtemBilan ? – joshiste Nov 05 '17 at 13:20
  • You might use some incompatible dependencies. Be sure just rely on what Reactor-Netty provides – Artem Bilan Nov 05 '17 at 13:25
  • hmm just using boot 2.0 – joshiste Nov 05 '17 at 13:42
  • Well, that sounds like an independent SO question, please – Artem Bilan Nov 05 '17 at 13:43
  • with which netty / reacter version do you have tested this? – joshiste Nov 05 '17 at 14:35
  • This doesn't work for NIO channels and is imho therefore not an correct answer – joshiste Jan 23 '18 at 19:34
  • Try `CONNECT_TIMEOUT_MILLIS` instead then – Artem Bilan Jan 23 '18 at 19:40
  • 3
    The api seems to have changed, here's how it works now https://stackoverflow.com/a/53781016/3993662 – baao Jan 15 '19 at 20:36
4

With Spring Webflux 5.1.8 I ran into problems yielding the error messages below using the answer from mcoolive when executing multiple subsequent tests that uses the WebClient.

Force-closing a channel whose registration task was not accepted by an event loop
Failed to submit a listener notification task. Event loop shut down?

Adding a connection provider and loop resources solved my problem:

final ConnectionProvider theTcpClientPool = ConnectionProvider.elastic("tcp-client-pool");
final LoopResources theTcpClientLoopResources = LoopResources.create("tcp-client-loop");

final TcpClient theTcpClient = TcpClient
    .create(theTcpClientPool)
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
    .runOn(theTcpClientLoopResources)
    .doOnConnected(theConnection -> {
        theConnection.addHandlerLast(new ReadTimeoutHandler(mTimeoutInMillisec, TimeUnit.MILLISECONDS));
        theConnection.addHandlerLast(new WriteTimeoutHandler(mTimeoutInMillisec, TimeUnit.MILLISECONDS));
    });

WebClient theWebClient = WebClient.builder()
    .baseUrl(mVfwsServerBaseUrl)
    .clientConnector(new ReactorClientHttpConnector(HttpClient.from(theTcpClient)))
    .build();
  • When using your solution I face```the method from(TcpClient) from the type HttpClient is deprecated```. Seems like the ```from(TcpClient)``` is deprecated too – CtrlAltElite Jan 14 '21 at 09:29
  • I suggest taking a look at the documentation (JavaDoc) of the deprecated method. There is an example on how to replace the deprecated from() method in there. –  Jan 15 '21 at 13:05
3

Here's how I did it (thanks to @Artem)

SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();

        ClientHttpConnector httpConnector = new ReactorClientHttpConnector(options -> {
            options.sslContext(sslContext);
            options.option(ChannelOption.SO_TIMEOUT, this.applicationConfig.getHttpClientRequestTimeout());
            options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.applicationConfig.getHttpClientConnectTimeout());
            options.poolResources(PoolResources.fixed("myPool", this.applicationConfig.getHttpClientMaxPoolSize()));
        });

        return WebClient.builder().clientConnector(httpConnector).defaultHeader("Authorization", "xxxx")
                .baseUrl(this.config.getBaseURL()).build();
Seb
  • 3,602
  • 8
  • 36
  • 52
2

Based on the above comment if you want to add a Socket Timeout just add it as another option in the same timeoutClient.

TcpClient timeoutClient = TcpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, SECONDS*10) //Connect Timeout
    .option(ChannelOption.SO_TIMEOUT,1000) // Socket Timeout
    .doOnConnected(
        c -> c.addHandlerLast(new ReadTimeoutHandler(SECONDS))
              .addHandlerLast(new WriteTimeoutHandler(SECONDS)));
return webClientBuilder.baseUrl(YOUR_URL)
       .clientConnector(new ReactorClientHttpConnector(HttpClient.from(timeoutClient)))
       .build();
  • Are you recommending both setting `SO_TIMEOUT` and adding `ReadTimeoutHandler`? – Jin Kwon Jul 30 '19 at 02:51
  • When using your solution I face```the method from(TcpClient) from the type HttpClient is deprecated```. Seems like the ```from(TcpClient)``` is deprecated too – CtrlAltElite Jan 14 '21 at 09:31
2

Rather than creating your own WebClient.Builder, you can provide a custom ReactorNettyHttpClientMapper which is going to be applied to the default WebClient.Builder:

@Configuration
class MyAppConfiguration {

    @Bean
    fun reactorNettyHttpClientMapper(): ReactorNettyHttpClientMapper {
        return ReactorNettyHttpClientMapper { httpClient ->
            httpClient.tcpConfiguration { tcpClient ->
                tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30_000)
                    .doOnConnected { connection ->
                        connection.addHandlerLast(ReadTimeoutHandler(60))
                            .addHandlerLast(WriteTimeoutHandler(60))
                    }
            }
        }
    }
}
walrus03
  • 151
  • 1
  • 6
2

You can use overloaded block() method which accepts a timeout on Mono object. Or there is a timeout() method directly available on Mono object.

WebClient webClient = WebClient.builder()
              .baseUrl( "https://test.com" )
              .defaultHeader( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE )
              .build(); 

webClient.post()
              .uri( "/services/some/uri" )
              .body( Mono.just( YourEntityClassObject ), YourEntityClass.class )
              .retrieve()
              .bodyToMono( String.class )
              .timeout(Duration.ofMillis( 5000 )) // option 1
              .block(Duration.ofMillis( 5000 )); // option 2
Siddu
  • 99
  • 1
  • 8
0

Kotlin Syntax!!

webClient
.post()
.body(BodyInserters.fromObject(body))
.headers(headersSetter)
.retrieve()
.bodyToMono<SomeClass>()
.timeout(Duration.ofSeconds(30)) /// specify timeout duration
.doOnNext {
    logger.info{ "log something"}
}
.onErrorMap { throwable ->
    logger.error{"do some transformation"}
    throwable
}
neshant sharma
  • 174
  • 2
  • 4