Spring Boot WebClient Tutorial

In this tutorial, we will learn how to use WebClient to consume the REST APIs, how to handle errors using WebClient, how to call REST APIs reactively using WebClient, and how to use basic authentication with WebClient.

Spring WebClient Overview

Spring's WebClient is a modern, non-blocking, and reactive client for HTTP requests. It was introduced in Spring 5 as part of the reactive stack web framework and is intended to replace the RestTemplate with a more modern, flexible, and powerful tool.

Here are some key points to understand when working with WebClient: 

Reactive Programming: WebClient is part of the Spring WebFlux module and follows the reactive programming paradigm, which makes it suitable for asynchronous and non-blocking applications. 

Non-Blocking IO: It is designed to support non-blocking IO operations, which means it can handle concurrent operations without thread blocking, leading to more scalable applications. 

Back Pressure: It integrates with Project Reactor and supports backpressure, which allows it to handle streaming scenarios where data is produced and consumed at different rates. 

Flexible Request Building: It provides a fluent API to build and execute HTTP requests. You can easily add headers, and query parameters, and set the request body. 

Error Handling: WebClient provides mechanisms for handling client and server errors cleanly through status code checks and the onStatus method. 

Response Processing: It supports a variety of ways to process responses, such as fetching a single object (Mono), streaming a sequence of objects (Flux), or directly into Java objects using codecs. 

Header and Cookie Management: WebClient allows you to manipulate headers and cookies for each request easily. You can set these per request or globally during WebClient creation. 

Authentication and Authorization: It supports various authentication mechanisms like Basic Auth, Bearer Token, and more sophisticated OAuth2 client credentials. 

Client Configuration: You can customize the underlying client configuration, such as connection timeout, read/write timeout, response buffer size, and SSL details. 

Filters: You can add filters to the client to manipulate the request and response or to add cross-cutting concerns like logging. 

Testing Support: Spring Boot provides WebTestClient, which can be used to test WebClient interactions or your entire WebFlux application without a running server. 

Interoperability: While WebClient is part of the reactive stack, it can also be used in a more traditional, synchronous way by blocking on the result. However, this should be done with caution as it negates the benefits of the reactive approach. 

Global and Local Configuration: You can configure WebClient instances globally when defining the bean or on a per-request basis, providing flexibility in how different requests are handled. 

Thread Model: It operates on a different threading model than servlet-based RestTemplate, utilizing fewer threads and achieving higher scalability with event-driven architecture. 

Remember, while WebClient is part of the Spring WebFlux library, it can be used in any Spring Boot application, even those that don’t use the full reactive stack. This makes WebClient a versatile tool for RESTful interactions in modern Spring applications.

Pre-requisites

We are going to use WebClient to consume the REST APIs hence first we need to expose the REST APIs right. Refer to the below tutorial to create and expose CRUD REST APIs for the User resource:

Add Dependencies 

To use WebClient, you need to add the spring-boot-starter-webflux dependency to your project's build file. 

For Maven, add to pom.xml:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
For Gradle, add to build.gradle:
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

Create a WebClient Instance 

You can configure and create a WebClient instance as a Spring bean for reuse:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        return builder.baseUrl("http://localhost:8080").build();
    }
}

Using WebClient as REST Client 

Assuming as prerequisites, you have created and exposed CRUD REST APIs. Next, let’s create a REST Client service to interact with these CRUD REST APIs.

Let's create a UserServiceClient class and add the following code to it:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@Service
public class UserServiceClient {

    private final WebClient webClient;

    @Autowired
    public UserServiceClient(WebClient webClient) {
        this.webClient = webClient;
    }

    // Create a new User
    public Mono<User> createUser(User user) {
        return webClient.post()
                .bodyValue(user)
                .retrieve()
                .bodyToMono(User.class);
    }

    // Get a User by ID
    public Mono<User> getUserById(Long userId) {
        return webClient.get()
                .uri("/{id}", userId)
                .retrieve()
                .bodyToMono(User.class);
    }

    // Get all Users
    public Flux<User> getAllUsers() {
        return webClient.get()
                .retrieve()
                .bodyToFlux(User.class);
    }

    // Update a User
    public Mono<User> updateUser(Long userId, User user) {
        return webClient.put()
                .uri("/{id}", userId)
                .bodyValue(user)
                .retrieve()
                .bodyToMono(User.class);
    }

    // Delete a User
    public Mono<String> deleteUser(Long userId) {
        return webClient.delete()
                .uri("/{id}", userId)
                .retrieve()
                .bodyToMono(String.class);
    }
}
In this service:
First, we have injected WebClient to make REST API requests:
    private final WebClient webClient;

    @Autowired
    public UserServiceClient(WebClient webClient) {
        this.webClient = webClient;
    }

Next, we performed below CRUD REST API calls:
createUser(User user): This method sends a POST request to create a new user.

getUserById(Long userId): This method sends a GET request to retrieve a user by their ID.

getAllUsers(): This method sends a GET request to fetch all users.

updateUser(Long userId, User user): This method sends a PUT request to update an existing user.

deleteUser(Long userId): This method sends a DELETE request to remove a user by ID.

Error Handling using WebClient

Handling errors with WebClient is an important aspect of building a robust client. You can handle client and server errors using WebClient. Let's refine the UserServiceClient service with proper error handling and reactive operations.

Let's refine the UserServiceClient service with proper error handling and reactive operations.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@Service
public class UserServiceClient {

    private final WebClient webClient;

    @Autowired
    public UserServiceClient(WebClient webClient) {
        this.webClient = webClient;
    }

    // Create a new User
    public Mono<User> createUser(User user) {
        return webClient.post()
                .bodyValue(user)
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new RuntimeException("Client Error")))
                .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                        Mono.error(new RuntimeException("Server Error")))
                .bodyToMono(User.class)
                .onErrorResume(WebClientResponseException.class, e ->
                        Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
    }

    // Get a User by ID
    public Mono<User> getUserById(Long userId) {
        return webClient.get()
                .uri("/{id}", userId)
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new RuntimeException("Not Found")))
                .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                        Mono.error(new RuntimeException("Server Error")))
                .bodyToMono(User.class)
                .onErrorResume(WebClientResponseException.class, e ->
                        Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
    }

    // Get all Users
    public Flux<User> getAllUsers() {
        return webClient.get()
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new RuntimeException("Client Error")))
                .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                        Mono.error(new RuntimeException("Server Error")))
                .bodyToFlux(User.class)
                .onErrorResume(WebClientResponseException.class, e ->
                        Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
    }

    // Update a User
    public Mono<User> updateUser(Long userId, User user) {
        return webClient.put()
                .uri("/{id}", userId)
                .bodyValue(user)
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new RuntimeException("Not Found")))
                .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                        Mono.error(new RuntimeException("Server Error")))
                .bodyToMono(User.class)
                .onErrorResume(WebClientResponseException.class, e ->
                        Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
    }

    // Delete a User
    public Mono<String> deleteUser(Long userId) {
        return webClient.delete()
                .uri("/{id}", userId)
                .retrieve()
                .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                        Mono.error(new RuntimeException("Not Found")))
                .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                        Mono.error(new RuntimeException("Server Error")))
                .bodyToMono(String.class)
                .onErrorResume(WebClientResponseException.class, e ->
                        Mono.error(new RuntimeException("Error: " + e.getResponseBodyAsString())));
    }
}



In the updated UserServiceClient, we have added proper error handling for different scenarios: 
4xx Client Errors: These typically indicate that there was an error in the request sent from the client. We throw a RuntimeException with a custom message. 

5xx Server Errors: These indicate errors on the server side. We again throw a RuntimeException, which could be handled or logged accordingly higher up in the call stack. 

Error Resume: If a WebClientResponseException is thrown, we resume with a Mono.error(), containing more details from the exception's response body.

Reactively Consuming the API using WebClient

To reactively consume RESTFUL web services, you would generally use the subscribe method on the publisher (either Mono or Flux). In a web application, you might not subscribe directly; instead, you would return the Mono or Flux from your controller methods for the framework to handle. For background processes or during application startup, you might use the subscribe method as follows:
public void useClientService() {
    // Create User
    createUser(new User("Jane", "Doe"))
        .subscribe(
            user -> System.out.println("Created user: " + user),
            error -> System.err.println("Error during user creation: " + error.getMessage())
        );

    // Fetch User
    getUserById(1L)
        .subscribe(
            user -> System.out.println("Fetched user: " + user),
            error -> System.err.println("Error during fetch: " + error.getMessage())
        );

    // Update User
    updateUser(1L, new User("Jane", "Doe Updated"))
        .subscribe(
            user -> System.out.println("Updated user: " + user),
            error -> System.err.println("Error during update: " + error.getMessage())
        );

    // Delete User
    deleteUser(1L)
        .subscribe(
            message -> System.out.println("User deleted."),
            error -> System.err.println("Error during deletion: " + error.getMessage())
        );
}

Here, subscribe is called with two lambda expressions: one for the success case and one for the error case. This allows for a simple way to handle the asynchronous results of these operations.

WebClient with Basic Authentication

Basic authentication with Spring Boot's WebClient is straightforward. It can be set globally on the WebClient.Builder when creating the WebClient bean or on a per-request basis. Here's how to do it both ways:

Global Basic Authentication Configuration 

If all requests from the WebClient need to be authenticated using the same credentials, you can configure the basic authentication globally:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;

@Configuration
public class WebClientConfig {

    private static String encodeCredentials(String username, String password) {
        String credentials = username + ":" + password;
        return "Basic " + Base64Utils.encodeToString(credentials.getBytes());
    }

    @Bean
    public WebClient webClient() {
        String encodedCredentials = encodeCredentials("user", "password");

        return WebClient.builder()
                .baseUrl("http://localhost:8080/api/users")
                .defaultHeader(HttpHeaders.AUTHORIZATION, encodedCredentials)
                .build();
    }
}
This will add the Authorization header with basic authentication to every request made by this WebClient. 

Per-Request Basic Authentication Configuration 

For situations where you don't want to set the credentials globally, you can set them per request:
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;

public class UserServiceClient {

    private final WebClient webClient;

    public UserServiceClient(WebClient webClient) {
        this.webClient = webClient;
    }

    private String basicAuthHeader(String username, String password) {
        String auth = username + ":" + password;
        return "Basic " + Base64Utils.encodeToString(auth.getBytes());
    }

    public Mono<User> getUserById(Long userId, String username, String password) {
        return webClient.get()
                        .uri("/{id}", userId)
                        .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(username, password))
                        .retrieve()
                        .bodyToMono(User.class);
    }

    // Other methods...
}

Here, the Authorization header is added only to the getUserById request. Similarly, you can add to other methods

Remember, for any production-grade application, it is recommended to use a more secure form of authentication/authorization like OAuth or JWT tokens. Basic Authentication can be suitable for internal services, development, or when used with additional security layers.

Conclusion 

In this tutorial, we learned how to use WebClient to consume the REST APIs, how to handle errors using WebClient, how to call REST APIs reactively using WebClient, and how to use basic authentication with WebClient.

Comments