Spring Boot - How to use WebClient

We will examine here how to use Spring WebClient to make calls to a service by sending request.

What is Spring WebClient?

WebClient provides a common interface for making web requests in a non-blocking way. It is part of the Spring Web Reactive module and will replace the well-known RestTemplate. We will see below the dependencies we need, how to create a web client, and some more configurations that we can use with Spring WebClient.

What dependencies do you need to use Spring WebClient?

The first thing we need is to define the dependencies.

  • spring-boot-starter-webflux is the necessary dependency for the webclient
  • mockserver-netty we will use it to mock the web server in the unit tests for this example
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.mock-server</groupId>
        <artifactId>mockserver-netty</artifactId>
        <version>5.11.1</version>
    </dependency>
</dependencies>

The structure of this example looks like

Spring WebClient

How to create a WebClient?

To use WebClient we will use the builder provided by WebClient WebClient.builder()

This way we will create a WebClient instance with a base url.

WebClient webClient = WebClient.builder()
        .baseUrl("http://localhost:8899")
        .build();

How to add default headers to the WebClient?

Commonly, when it comes to calls with a body Json, the header is added indicating that it is an application/json call.

We can indicate headers by default.

WebClient webClient = WebClient.builder()
        .baseUrl("https://run.mocky.io")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
        .build();

How to send a request and receive a response with WebClient

Once we have our instance of WebClient we can send a request.

In this example, we have our request that we have defined as ClientRequest and our response in ClientResponse.

We will be sending a POST to an uri through webClient.post().uri(…)

The retrieve method executes the HTTP request and returns the response.

The bodyToMono method takes the body from the retrieve and converts it to our response class that we specify.

ClientResponse response = webClient.post()
        .uri("/accounts")
        .body(Mono.just(request), ClientRequest.class)
        .retrieve()
        .bodyToMono(ClientResponse.class)
        .block();

How to configure a timeout in the WebClient?

We can configure the timeout of our WebClient by indicating a duration in seconds or any other duration provided by the Duration class.

ClientResponse response = webClient.post()
        .uri("/accounts")
        .body(Mono.just(request), ClientRequest.class)
        .retrieve()
        .bodyToMono(ClientResponse.class)
        .timeout(Duration.ofSeconds(3))  // timeout
        .block();

Another way to specify a timeout is through the configuration of a TcpClient where we can be more specific.

Next we see the specification of a timeout for the connection and read/write of the WebClient.

// tcp client timeout
TcpClient tcpClient = TcpClient.create()
      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
      .doOnConnected(connection ->
              connection.addHandlerLast(new ReadTimeoutHandler(3))
                      .addHandlerLast(new WriteTimeoutHandler(3)));

WebClient webClient = WebClient.builder()
      .baseUrl("http://localhost:8899")
      .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
      .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))  // timeout
      .build();

ClientResponse response = webClient.post()
      .uri("/accounts")
      .body(Mono.just(request), ClientRequest.class)
      .retrieve()
      .bodyToMono(ClientResponse.class)
      .block();

return response;

Manage http status in WebClient

We can know and manage any http status in our WebClient using the onStatus method.

For example, here in case of an error 500 we log in and throw an error using onStatus(…) throwing an exception our ApiWebClientException.

Let’s see what we do next, there is a lot here:

  1. We create our webclient.
  2. We indicate that it will do a POST.
  3. It will receive a request from our ClientRequest class.
  4. It will do a retrieve executing the HTTP request
  5. If an HttpStatus 500 error occurs, it will evaluate it in onStatus and decide what to do with that error.
  6. Convert the response to our ClientResponse class.
  7. Wait 3 seconds maximum for the connection.
ClientResponse response = webClient.post()
    .uri("/accounts")
    .body(Mono.just(request), ClientRequest.class)
    .retrieve()

    // handle status
    .onStatus(HttpStatus::is5xxServerError, clientResponse -> {
        logger.error("Error endpoint with status code {}",  clientResponse.statusCode());
        throw new ApiWebClientException("HTTP Status 500 error");  // throw custom exception
    })

    .bodyToMono(ClientResponse.class)
    .timeout(Duration.ofSeconds(3))  // timeout
    .block();

What does our finished web client look like?

Adding up everything we have seen before, our web client looks like this.

@Component
public class ApiWebClient {

    private static final Logger logger = LoggerFactory.getLogger(ApiWebClient.class);

    public ClientResponse createClient(ClientRequest request) {

        // tcp client timeout
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                .doOnConnected(connection ->
                        connection.addHandlerLast(new ReadTimeoutHandler(3))
                                .addHandlerLast(new WriteTimeoutHandler(3)));

        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:8899")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))  // timeout
                .build();

        ClientResponse response = webClient.post()
                .uri("/accounts")
                .body(Mono.just(request), ClientRequest.class)
                .retrieve()

                // handle status
                .onStatus(HttpStatus::is5xxServerError, clientResponse -> {
                    logger.error("Error endpoint with status code {}", clientResponse.statusCode());
                    throw new ApiWebClientException("HTTP Status 500 error");  // throw custom exception
                })

                .bodyToMono(ClientResponse.class)
                .block();

        return response;
    }

    public ClientResponse createClientWithDuration(ClientRequest request) {

        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:8899")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .build();

        ClientResponse response = webClient.post()
                .uri("/accounts")
                .body(Mono.just(request), ClientRequest.class)
                .retrieve()

                // handle status
                .onStatus(HttpStatus::is5xxServerError, clientResponse -> {
                    logger.error("Error endpoint with status code {}", clientResponse.statusCode());
                    throw new ApiWebClientException("HTTP Status 500 error");  // throw custom exception
                })

                .bodyToMono(ClientResponse.class)
                .timeout(Duration.ofSeconds(3))  // timeout
                .block();

        return response;
    }

}

How to create a test to test Spring WebClient

First, we define a Spring test in which we will use MockServer .

For each test, we raise a server on port 8899 and at the end of each test, we stop it.

We are going to create several tests to try different cases. We will try:

  • A correct connection.
  • A connection with an expired timeout.
  • A connection that throws an error 500 and returns an error ApiWebClientException.

Notice below that we lift the mock from the server and then stop it.

ClientAndServer mockServer = ClientAndServer.startClientAndServer(8899);
mockServer.stop();

Then we set the expectations we want for the server.

In this example, we indicate that when a POST occurs for the path /account, it should respond with the body that we indicated, the header and with a delay time.

mockServer.when(HttpRequest.request().withMethod("POST")
    .withPath("/accounts")).
    respond(HttpResponse.response()
            .withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
            .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .withDelay(TimeUnit.MILLISECONDS, 5000));

Let’s see how the unit tests look for different cases.

@SpringBootTest
public class ClientServiceTest {

    private static final Logger logger = LoggerFactory.getLogger(ClientServiceTest.class);

    @Autowired
    private ApiWebClient webClient;

    private ClientAndServer mockServer;

    @BeforeEach
    public void startServer() {
        mockServer = ClientAndServer.startClientAndServer(8899);
    }

    @AfterEach
    public void stopServer() {
        mockServer.stop();
    }

    @Test
    void createShouldReturnTheResponse() {

        // set up mock server with a delay of 1 seconds
        mockServer.when(HttpRequest.request().withMethod("POST")
                .withPath("/accounts")).
                respond(HttpResponse.response()
                        .withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withDelay(TimeUnit.MILLISECONDS, 1000));

        ClientRequest request = new ClientRequest();
        request.setName("Frank");
        request.setName("frank@mail.com");

        ClientResponse response = webClient.createClient(request);

        assertThat(response).isNotNull();
        assertThat(response.getName()).isEqualTo("Frank");
        assertThat(response.getEmail()).isEqualTo("frank@mail.com");

    }

    @Test
    void createWithTimeoutShouldThrowReadTimeOut() {

        // set up mock server with a delay of 5 seconds
        mockServer.when(HttpRequest.request().withMethod("POST")
                .withPath("/accounts")).
                respond(HttpResponse.response()
                        .withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withDelay(TimeUnit.MILLISECONDS, 5000));

        ClientRequest request = new ClientRequest();
        request.setName("Frank");
        request.setName("frank@mail.com");

        assertThrows(ReadTimeoutException.class, () -> webClient.createClient(request));
    }

    @Test
    void createWithStatusShouldThrowACustomException() {

        // set up mock server with a http status 500
        mockServer.when(HttpRequest.request().withMethod("POST")
                .withPath("/accounts"))
                .respond(HttpResponse.response().withStatusCode(500)
                        .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .withDelay(TimeUnit.MILLISECONDS, 1000));

        ClientRequest request = new ClientRequest();
        request.setName("Frank");
        request.setName("frank@mail.com");

        ApiWebClientException apiWebClientException = assertThrows(ApiWebClientException.class, () -> webClient.createClient(request));
        assertTrue(apiWebClientException.getMessage().contains("HTTP Status 500 error"));

    }
}

Conclusion

We saw how to simply create a web client using Spring WebClient. We examined how to configure the client, send a request and, receive the response.

You can see this code at:
https://github.com/gustavopeiretti/spring-boot-examples

Hi! If you find my posts helpful, please support me by inviting me for a coffee :)

See also