49

I have one question regarding Spring Framework WebClient

In my application I need to do many similar API calls, sometimes I need to change headers in the calls (Authentication token). So the question arises, what would be better of the two options:

  1. To create one WebClient for all incoming requests to MyService.class, by making it private final field, like code below:
private final WebClient webClient = WebClient.builder()
        .baseUrl("https://another_host.com/api/get_inf")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
        .build();

Here arises another question: is WebClient thread-safe? (because service is used by many threads)

  1. To create new WebClient for each new request incoming to service class.

I want to provide maximum performance and to use it in the right way, but I don't know how WebClient works inside it, and how it expects to be used.

Thank you.

Sergey Luchko
  • 2,996
  • 3
  • 31
  • 51

2 Answers2

88

Two key things here about WebClient:

  1. Its HTTP resources (connections, caches, etc) are managed by the underlying library, referenced by the ClientHttpConnector that you can configure on the WebClient
  2. WebClient is immutable

With that in mind, you should try to reuse the same ClientHttpConnector across your application, because this will share the connection pool - this is arguably the most important thing for performance. This means you should try to derive all WebClient instances from the same WebClient.create() call. Spring Boot helps you with that by creating and configuring for you a WebClient.Builder bean that you can inject anywhere in your app.

Because WebClient is immutable it is thread-safe. WebClient is meant to be used in a reactive environment, where nothing is tied to a particular thread (this doesn't mean you cannot use in a traditional Servlet application).

If you'd like to change the way requests are made, there are several ways to achieve that:

configure things in the builder phase

WebClient baseClient = WebClient.create()
                                .baseUrl("https://example.org");

configure things on a per-request basis

Mono<ClientResponse> response = baseClient.get()
                                          .uri("/resource")
                                          .header("token", "secret")
                                          .exchange();

create a new client instance out of an existing one

// mutate() will *copy* the builder state and create a new one out of it
WebClient authClient = baseClient.mutate()
                                 .defaultHeaders(hdrs -> {hdrs.add("token", "secret");})
                                 .build();
Manuel Jordan
  • 15,253
  • 21
  • 95
  • 158
Brian Clozel
  • 56,583
  • 15
  • 167
  • 176
  • Thank you for full explanation. Could you also provide link on the doc? (Usually in doc you find examples of usage, but I'd like to read more about under hood part.) – Sergey Luchko Mar 05 '18 at 20:14
  • 1
    https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web-reactive.html#webflux-client – Brian Clozel Mar 05 '18 at 20:15
  • 1
    Why should you reuse one webclient across whole app? You said about connection pool and that's why I wouldn't share it - let's say that your application depends on two remote services that you connect with web client. If one service will be down and you have no circuit breaker or something that would prevent you from calling dead service then you can use all your connections for connecting to the dead service and the whole app is dead, because you didn't separate it. – ZZ 5 Oct 25 '18 at 07:48
  • 1
    I never said you should have only one webclient for the whole app. I believe Reactor Netty's connection pool is clever enough and is l managing connection on a per host basis, so I don't understand how this would happen. In the meantime things changed in Spring Framework in the way we're managing HTTP resources - so please ask a new question and feel free to point me to it. – Brian Clozel Oct 25 '18 at 07:59
  • 2
    WebClient in multi-threaded environment is overwriting my URI. WebClient is init at class level in following manner private WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector((HttpClientOptions.Builder builder) -> builder.disablePool())).build(); Had to mutate it per-request level. Let me know if this is the right approach. – Praveen Kamath Nov 05 '18 at 07:41
  • it doesn't look like it. Please ask a new question. – Brian Clozel Nov 05 '18 at 07:44
  • 1
    I have already https://stackoverflow.com/questions/53129956/springboot-v2-0-0-m6-webclient-making-multiple-duplicate-http-post-calls Please help! – Praveen Kamath Nov 05 '18 at 14:52
  • Even mutation of WebClient didn't work, it's overwriting my URL – Praveen Kamath Nov 07 '18 at 09:26
  • 1
    WebClient.Builder seems is not thread safe, then how to get WebClient? Here my questions if you would like to take a look https://stackoverflow.com/questions/54136085/spring-boot-webclient-builder-bean-usage-in-traditional-servlet-multi-threaded-a – P_M Jan 10 '19 at 20:02
  • Whether you create a separate WebClient or not, by default it will use shared resources to pool your connections together. Just look at the code for HttpClient.create - it calls HttpResources.get() to get the global resources. Read my answer for why I don't recommend this for external API calls. – rougou Mar 04 '20 at 01:06
12

From my experience, if you are calling an external API on a server you have no control over, don't use WebClient at all, or use it with the pooling mechanism turned off. Any performance gains from connection pooling are greatly overweighed by the assumptions built into the (default reactor-netty) library that will cause random errors on one API call when another was abruptly terminated by the remote host, etc. In some cases, you don't even know where the error occurred because the calls are all made from a shared worker thread.

I made the mistake of using WebClient because the doc for RestTemplate said it would be deprecated in the future. In hindsight, I would go with regular HttpClient or Apache Commons HttpClient, but if you are like me and already implemented with WebClient, you can turn off the pooling by creating your WebClient as follows:

private WebClient createWebClient(int timeout) {
    TcpClient tcpClient = TcpClient.newConnection();
    HttpClient httpClient = HttpClient.from(tcpClient)
        .tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout * 1000)
            .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(timeout))));

    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}

*** Creating a separate WebClient does not mean that WebClient will have a separate connection pool. Just look at the code for HttpClient.create - it calls HttpResources.get() to get the global resources. You could provide the pool settings manually but considering the errors that occur even with the default setup, I don't consider it worth the risk.

rougou
  • 956
  • 9
  • 14
  • Correct as stated in DOC, Httpclient.create() gives a pooled resource. But am not able to get why a shared pool causes errors. Of course, if you are subscribing to the corresponding flux stream, it would be the job of the flux to handle erros right? Why would ask people not to use? its actually great to make concurrent non blocking multiple requests. The default requests per connection pool would be 500 anyways. – Deekshith Anand Oct 21 '21 at 14:03
  • A random error cant happen just like that. Either the flux stream is not handled properly or some issue with your code. Unless specied what exactly causes it, this answer could be misleading. @rougou – Deekshith Anand Oct 21 '21 at 14:06
  • 2
    @Deekshith Anand Yes, I should clarify we are using blocking mode so not handling the flux stream in our code at all. The way the question was posed made me assume the OP also just wanted to make a blocking call from several request threads. In that case the only benefit you get is the connection pool, but if the destination server isn't expecting you to pool the connections you can end up with random errors, which is what we were seeing. – rougou Oct 25 '21 at 07:05
  • 1
    In a non-blocking application I agree WebClient is the preferred choice and what I use in a different project. – rougou Oct 25 '21 at 07:06