Spring boot WebClient

In this article, we will take a deep dive into Spring boot WebClient and how to send HTTP requests and get response using it with examples.
We will also learn how to set request headers and configure timeouts.
Below are the topics covered

What is WebClient
WebClient is a client or an object for performing HTTP requests.
It a is reactive, non-blocking client. The terms

Reactive means, that it reacts to events such as server event when data is available.
Non-Blocking means, that it does not block the executing thread and executes request asynchronously.

WebClient is introduced in Spring 5. It supports both synchronous and asynchronous(non-blocking) API calls but it should not be used synchronously, which shall defeat its purpose.

WebClient is built on top of reactive libraries and it uses Reactor Netty by default.

WebClient is an interface that resides in org.springframework.web.reactive.function.client package. Notice the term reactive in package name.
WebClient has a single implementation class DefaultWebClient in the same package.
WebClient Gradle dependency
WebClient is a part of Spring WebFlux module. To use WebClient, we need to have following gradle dependency of Spring boot WebFlux

implementation 'org.springframework.boot:spring-boot-starter-webflux:2.7.0'

WebClient Maven dependency
Below is the maven dependency for Spring WebFlux module

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.7.0</version>
</dependency>

Spring WebClient – Send HTTP Request
Sending HTTP request with Spring WebClient involves following steps:

Creating WebClient
There are following ways to create an instance of Spring WebClient.
A. create()
WebClient contains a static interface method create(), which returns its instance.
Actual type of instance is DefaultWebClient. Example,

WebClient webClient = WebClient.create();

B. create(String)
WebClient contains an overloaded create() method which takes a string argument representing the URL of the endpoint where request will be sent.

WebClient webClient = WebClient.create(
                            "https://jsonplaceholder.typicode.com");

C. Using builder
WebClient has a static builder() method. Calling build() on return value returns an instance of WebClient. Example,

WebClient webClient = WebClient.builder().build();

Defining Request URL
There are multiple ways to configure the URL at which HTTP request will be sent with WebClient.
A. In create() method
WebClient has a create() method which takes a string URL as argument. We have already seen this above.

B. In baseUrl() method
If you create WebClient using builder() method, then there is an option to specify URL using its baseURL() method as shown below

WebClient webClient = WebClient.
               builder().
               baseUrl("request-URL").
               build();

Remember that when URL is specified using a builder, then it applies to all the requests sent using this WebClient.
C. uri() method
Request URL can be specified after uri() method. There are multiple overloaded versions of uri() method.

(I). As a string

webClient.
get().
uri("request-URL")

(II) As a URI

webClient.
get().
uri(URI.create("request-URL"))

Note that uri() method is available in an object of type RequestBodyUriSpec, which is obtained when we call request methods on WebClient such as get() or post().
Thus, above lines of code can be expanded to

RequestBodyUriSpec uriSpec = w.post();
uriSpec.uri("request-URL");

Defining request type
Till this point, we have learnt how to create a WebClient object and define the request URL.
Now, we are ready to send HTTP request to an endpoint URL.

To send an HTTP request, call the corresponding method on WebClient instance. Such as get() for HTTP GET request, post() for POST request and so on.
Another method is to call method() on WebClient, supplying it the request type as argument.
Both the methods are shown below

WebClient w = WebClient.create("request-URL");
// GET request
w.get();
// POST request
w.post()
// PUT request
w.put()

// GET request: other way
w.method(HttpMethod.GET);

Actual request is not sent merely by calling any of the above methods.
Sending request and Handling response
WebClient provides two methods to send the request and get response back. These are

1. retrieve()
This method sends the request and extracts the response.
Spring WebClient returns the response in the form of Mono and Flux objects that belong to project reactor on which WebFlux is created.

Mono represents a stream which emits at most 1 element.
Flux represents a stream which can emit 0 to N elements.

To get response with retrieve() method, invoke toEntity() method, if you want a ResponseEntity object.
A ResponseEntity contains the actual response and other information such as response status and status code.
Or invoke bodyToMono(), if you are only interested in the body of response.

Both toEntity() and bodyToMono() accept the class type of response expected. Example,

WebClient webClient = WebClient.
                  create(
                   "https://jsonplaceholder.typicode.com/posts/1");
// get response body
Mono<Post> mono = webClient.
                     get().
                     retrieve().
                     bodyToMono(Post.class);

// get response entity
Mono<ResponseEntity<Post>> mono = webClient.
                                     get().
                                     retrieve().
                                     toEntity(Post.class);

Here, Post is a simple java class having fields that match the keys of below JSON response and their getter/setter methods.

{
  id: 101,
  title: 'foo',
  body: 'bar',
  userId: 1
}

2. exchange()
exchange() method provides access to response status and headers apart from response body. Example,

Mono<Post> res = webClient.
                   get().
                   exchange().flatMap(r -> {
                   // get status code
                   HttpStatus statusCode = r.statusCode();
                   // get response
                   return r.bodyToMono(Post.class)
    });

exchange() has been deprecated as of Spring 5.3. Use exchangeToMono() and exchangeToFlux() instead as shown below.

Mono<ResponseEntity<Post>> res = webClient.
                                   get().
                                   exchangeToMono(r -> r.toEntity(Post.class));

Spring WebClient add body
Body is required when sending a POST request using WebClient.
To insert request body, use body() method.
There are two ways to add a body with WebClient.

1. Directly supplying the object to be sent in request body.

Example,

// create a mono
Mono<Post> postMono = Mono.just(post);
// send POST request
Mono<ResponseEntity<Post>> response = webClient.
                                       post().
                                       body(postMono, Post.class).
                                       retrieve().
                                       toEntity(Post.class);

where post is an object which needs to be sent in request body.

First argument of body() is of type Publisher, which is an interface. Both Mono and Flux implement it, so we can pass either of these.
Second argument is the class type of the Publisher supplied as first argument.

2.
Using BodyInserters class to insert body.

Example,

Mono<Post> postMono = Mono.just(post);
Mono<Post> exchangeToMono = webClient.
                            post().
                            body(BodyInserters.fromPublisher(postMono, Post.class)).
                            exchangeToMono(response -> response.bodyToMono(Post.class));

BodyInserters has a static method fromPublisher() which accepts two arguments, a Publisher and the class type of Publisher.

To get the response, we can use exchange() or retrieve() methods, as explained in the previous sections.
Spring WebClient headers
There are two ways to set request headers in Spring WebClient.
1. Setting default headers
This is done while building WebClient with WebClient.builder() as shown below

WebClient w = WebClient.
              builder().
              defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML.toString());

This header will be sent with every request sent using this WebClient instance.
2. Set request headers
Chaining header() method after calling the respective request method as shown below

WebClient w = WebClient.create();
w.get().header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML.toString());

This header will be sent only with this request.
Spring WebClient handle 404 error
With exchangeToMono() or exchangeToFlux() methods, you can gain access to response status and status code.
To handle 404 error, use statusCode() method from response object as shown below

Mono<Post> mono = webClient.
                     get().
                     exchangeToMono(res -> {
                	if(res.statusCode() == HttpStatus.NOT_FOUND) {
                          return res.createException().flatMap(Mono::error);
                        } else {
                          return res.bodyToMono(Post.class);
                        }
                   });

This method checks for a specific 404 error. To check for general 4xx errors, use is4xxClientError() method as shown below

Mono<Post> mono = webClient.
                    get().
                    exchangeToMono(res -> { 
                      if(res.statusCode().is4xxClientError()) { 
                        return res.createException().flatMap(Mono::error); 
                      } else { 
                        return res.bodyToMono(Post.class); 
                      } 
                  });

Spring WebClient timeout
There is no direct way to provide timeout in WebClient.
You have to define a connector using clientConnector() method. It accepts an object of type ReactorClientHttpConnector, which in turn, requires an HttpClient.
It is on this client object that we can configure timeout.

To create an object of HttpClient, use its static create() method followed by option() method to specify timeouts.

In below example, we have configured three different timeouts
1. Connection timeout, which is the time it will wait for making a connection.
2. Read timeout, which is the time it will wait to read response.
3. Write timeout, which is the time it will wait to write response.

Note that read and write timeout only make sense when a connection is made. Therefore, they are added inside doOnConnected() method.

HttpClient client = HttpClient.
                       create().
                       option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000).
                       doOnConnected(c -> c.addHandler(new ReadTimeoutHandler(10)).
                                          addHandler(new WriteTimeoutHandler(5))).
                       responseTimeout(Duration.ofSeconds(5));
WebClient w = WebClient.
                 builder().
                 clientConnector(new ReactorClientHttpConnector(client)).
                 build();

In some articles, you will find timeout added using tcpConfiguration() method as shown below

HttpClient.
create().
tcpConfiguration(c -> c.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

tcpConfiguration() method has been deprecated and will be removed in HttpClient v1.1.0.
Summary
Below is the summary of using Spring WebClient to send HTTP requests.

1. Spring WebClient can be created using any of the below methods

WebClient w  = WebClient.create();
WebClient w1 = WebClient.builder().baseUrl("request-URL").build();
WebClient w3 = WebClient.create("request-URL");

2. To send get() request, call get() method after WebClient or method() with HttpMethod.GET as

// 1st method
webClient.get();
// 2nd method
webClient.method(HttpMethod.GET);

Similar ways can be used to other HTTP request types.
3. To get response, use retrieve() or exchangeToMono() or exchangeToFlux() methods.
With retrieve(), you can only access response body.  With exchange methods, response body, status and status codes can also be accessed.
4. If you want response as ResponseEntity, use toEntity() method.
To get response wrapped with Mono or Flux objects, use bodyToMono() and bodyToFlux() methods.
5. To add body to a POST request, use body() method.
6. body() method directly accepts the object to be sent in request and its class type as arguments or using BodyInserters as shown below

// 1st method
webClient. 
post(). 
body(postMono, Post.class);

// 2nd method
webClient. 
post(). 
body(BodyInserters.fromPublisher(postMono, Post.class));

7. There are two ways to add headers to HTTP request sent with WebClient.

// set default headers
WebClient. 
builder(). 
defaultHeader(headerName, headerValue);

// set request specific headers
webClient.
get().
header(headerName, headerValue);

That is all on Spring WebClient. Feel free to refer to WebClient documentation for further clarity.
Hope the article was useful.