Spring WebClient (with Hands-On Examples)

Spring WebClient is a non-blocking and reactive web client for performing HTTP requests. The WebClient has been added in Spring 5 (spring-webflux module) and provides the fluent functional-style API for sending HTTP requests and handling the responses.

Before Spring 5, RestTemplate has been the primary technique for client-side HTTP accesses, which is part of the Spring MVC project.

Since Spring 5 (and Spring 6), the WebClient is the recommended approach for sending HTTP requests.

1. Maven

To use WebClient api, we must have the spring-boot-starter-webflux module imported into our Spring Boot project.

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

In gradle, add the following dependency:

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

2. Creating a Spring WebClient Instance

To create WebClient bean, we can follow any one of the given approaches.

2.1. Using WebClient.create()

The create() method is an overloaded method and can optionally accept a base URL for requests.

WebClient webClient = WebClient.create();  // With empty URI

WebClient webClient = WebClient.create("https://client-domain.com");  // With specified root URI

2.2. Using WebClient.Builder API

We can also build the client using the DefaultWebClientBuilder class, which uses builder pattern style fluent-API.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebConfig {

  @Bean
  public WebClient webClient() {

    WebClient webClient = WebClient.builder()
      .baseUrl("http://localhost:3000")
      .defaultCookie("cookie-name", "cookie-value")
      .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .build();
  }
}

3. Using WebClient for Sending Requests and Handling Responses

To send a request, we can use its fluent API and execute the necessary steps as per requirements. For example, sending an HTTP POST request involves the following steps.

  • Create WebClient.UriSpec reference using prebuilt methods such as get(), put(), post() or delete().
  • Set the request URI if not set already.
  • Set the request headers and authentication details, if any.
  • Set the request body, if any.
  • Call the retrieve() or exchange() method. The retrieve() method directly performs the HTTP request and retrieves the response body. The exchange() method returns ClientResponse having the response status and headers. We can get the response body from ClientResponse instance.
  • Handle the response returned from the server.

In the following example, we send an HTTP POST request to URI http://localhost:3000/employees that returns an Employee object after the successful call.

@Service
public class MyService {

  private final WebClient webClient;

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

  public Mono<Employee> createEmployee(Employee employee) {

    Mono<Employee> employeeMono = webClient.post()
      .uri("/employees")
      .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
      .body(Mono.just(employee), Employee.class)
      .retrieve()
      .bodyToMono(Employee.class);

    return employeeMono;
  }
}

Here’s an example of how we can use the service method and handle success and error scenarios:

public void someMethod() {

  Employee employee = new Employee(...); // Create an Employee instance

  myService.createEmployee(employee)
    .subscribe(
      createdEmployee -> {
        // Handle the successful response
        System.out.println("Employee created: " + createdEmployee);
      },
      error -> {
        // Handle errors
        System.err.println("Error creating employee: " + error.getMessage());
      }
    );
}

4. WebClient retrieve() vs. exchange() APIs

In Spring WebClient, both the retrieve() and exchange() methods are used for making HTTP requests, but they offer different levels of control and flexibility over the request and response handling. Let us compare both approaches.

Since Spring 5.3, the exchange() method is deprecated due to potential memory and connection leaks. Prefer exchangeToMono(), exchangeToFlux(), or retrieve() instead.

4.1. Using retrieve() API

The retrieve() is a simplified API for common use cases where we want to send an HTTP request, receive the response, and handle it in a reactive way.

When we call retrieve(), the request is sent, and the response is automatically processed and deserialized into a reactive type (e.g., Mono or Flux). We don’t need to explicitly subscribe to the response.

The response type is inferred from the final call to bodyToMono() or bodyToFlux(). For example, if we use bodyToMono(Employee.class), we’ll get a Mono<Employee> as the result.

Mono<Employee> employeeMono = webClient.get()
    .uri("/employees/{id}", 123)
    .retrieve()
    .bodyToMono(Employee.class);

Please note that bodyToMono() and bodyToFlux() methods always expect a response body of a given class type. If the response status code is 4xx (client error) or 5xx (Server error) i.e. there is no response body then these methods throw WebClientException.

Use bodyToMono(Void.class) if no response body is expected. This is helpful in DELETE operations.

webClient.delete()
  .uri("/employees/" + id)
  .retrieve()
  .bodyToMono(Void.class);

4.2. Using exchange() API

The exchange() API allows us to handle the request and response explicitly. It returns the ClientResponse which has all the response elements such as status, headers and response body as well.

With exchange(), we are responsible for subscribing to the response explicitly using subscribe(), block() or similar methods. This gives us more control over when and how the request is executed.

When using exchange(), we must always use any of the bodyToMono(), bodyToFlux() or toEntity() methods of ClientResponse which provides more flexibility in choosing the reactive type for the response.

Mono<ClientResponse> responseMono = webClient.get()
    .uri("/employees/{id}", 123)
    .exchange();

responseMono.subscribe(clientResponse -> {

  HttpStatus statusCode = clientResponse.statusCode();  // HTTP Status
  HttpHeaders headers = clientResponse.headers();  // HTTP Headers
  Mono<Employee> employeeMono = clientResponse.bodyToMono(Employee.class);  // Response Body
  // Handle the response, including error handling based on status code
});

Here’s an example of how to use exchangeToMono() to make a GET request with Spring WebClient and handle the response:

@Service
public class MyService {

  private final WebClient webClient;

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

  public Mono<Employee> fetchEmployeeById(int id) {
    return webClient.get()
      .uri("/employees/{id}", id)
      .exchangeToMono(this::handleResponse);
  }

  private Mono<Employee> handleResponse(ClientResponse response) {

    if (response.statusCode().is2xxSuccessful()) {
      return response.bodyToMono(Employee.class);
    } 
    else if (response.statusCode().is4xxClientError()) {
      // Handle client errors (e.g., 404 Not Found)
      return Mono.error(new EmployeeNotFoundException("Employee not found"));
    } 
    else if (response.statusCode().is5xxServerError()) {
      // Handle server errors (e.g., 500 Internal Server Error)
      return Mono.error(new RuntimeException("Server error"));
    } 
    else {
      // Handle other status codes as needed
      return Mono.error(new RuntimeException("Unexpected error"));
    }
  }
}

5. Spring WebClient Examples

5.1. GET API Example

Generally, we will use GET API to fetch either a collection of resources or a singular resource. Let’s see the example of both use cases using get() method calls.

  • HTTP GET /employees : collection of employees as Flux
  • HTTP GET /employees/{id} : single employee by id as Mono
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class EmployeeService {

  private final WebClient webClient;

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

  public Flux<Employee> getAllEmployees() {

    return webClient.get()
      .uri("/employees")
      .retrieve()
      .onStatus(httpStatus -> !httpStatus.is2xxSuccessful(),
              clientResponse -> handleErrorResponse(clientResponse.statusCode()))
      .bodyToFlux(Employee.class)
      .onErrorResume(Exception.class, e -> Flux.empty()); // Return an empty collection on error
  }

  public Mono<Employee> getEmployeeById(int id) {
    return webClient.get()
            .uri("/employees/{id}", id)
            .retrieve()
            .onStatus(httpStatus -> !httpStatus.is2xxSuccessful(),
                    clientResponse -> handleErrorResponse(clientResponse.statusCode()))
            .bodyToMono(Employee.class);
  }

  private Mono<? extends Throwable> handleErrorResponse(HttpStatus statusCode) {

    // Handle non-success status codes here (e.g., logging or custom error handling)
    return Mono.error(new EmployeeServiceException("Failed to fetch employee. Status code: " + statusCode));
  }
}

Now, in the application code, we can inject the EmployeeService and use it as follows:

// Example of using getAllEmployees()
Flux<Employee> allEmployees = employeeService.getAllEmployees();

allEmployees.subscribe(employee -> {
    // Process each employee in the Flux
    System.out.println("Employee: " + employee);
});

// Example of using getEmployeeById(int id)
int employeeId = 123; // Replace with the desired employee ID
Mono<Employee> employeeById = employeeService.getEmployeeById(employeeId);

employeeById.subscribe(employee -> {
    // Process the employee retrieved by ID
    System.out.println("Employee by ID: " + employee);
});

5.2. POST API Example

POST API is commonly used for creating a resource. Let’s see an example of post() method to create an employee.

  • HTTP POST /employees : creates a new employee from the request body and returns the created employee in response.
@Service
public class EmployeeService {

  private final WebClient webClient;

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

  public Mono<ResponseEntity<Employee>> createEmployee(Employee newEmployee) {

    return webClient.post()
      .uri("/employees")
      .body(Mono.just(newEmployee), Employee.class)
      .retrieve()
      .onStatus(HttpStatus::is4xxClientError, response -> {
        //logError("Client error occurred");
        return Mono.error(new WebClientResponseException
          ("Bad Request", response.statusCode().value(), null, null, null));
      })
      .onStatus(HttpStatus::is5xxServerError, response -> {
        //logError("Server error occurred");
        return Mono.error(new WebClientResponseException
          ("Server Error", response.statusCode().value(), null, null, null));
      })
      .toEntity(Employee.class);
  }
}

Now, in the application code, we can inject the EmployeeService and use it to create an employee:

Employee newEmployee = new Employee(180, "Lokesh", "Active");

employeeService.createEmployee(newEmployee)
    .subscribe(responseEntity -> {

        System.out.println("Status: " + responseEntity.getStatusCodeValue());
        System.out.println("Location URI: " + responseEntity.getHeaders().getLocation());
        System.out.println("Created New Employee: " + responseEntity.getBody());
    });

5.3. PUT API Example

PUT API is commonly used for updating a resource. Let’s see an example of put() method to update an employee.

  • HTTP PUT /employees/{id} : updates an existing employee data from the request body and returns the updated employee in response.
public Mono<Employee> updateEmployee(Employee updatedEmployee) {

  return webClient.put()
    .uri("/employees/{id}", updatedEmployee.getId())
    .body(Mono.just(updatedEmployee), Employee.class)
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, clientResponse -> handleClientError(clientResponse))
    .onStatus(HttpStatus::is5xxServerError, clientResponse -> handleServerError(clientResponse))
    .bodyToMono(Employee.class);
}

private Mono<? extends Throwable> handleClientError(ClientResponse clientResponse) {
  // Handle client errors (e.g., 404 Not Found) here
  return Mono.error(new EmployeeNotFoundException("Employee not found"));
}

private Mono<? extends Throwable> handleServerError(ClientResponse clientResponse) {
  // Handle server errors (e.g., 500 Internal Server Error) here
  return Mono.error(new RuntimeException("Server error"));
}

5.4. DELETE API Example

DELETE API is commonly used for deleting a resource. Let’s see an example of delete() method to delete an employee from records.

  • HTTP DELETE /employees/{id} : deletes an existing employee by its ID. It does not accept any request body as well as does not return any response body as well.
public Mono<Void> deleteEmployee(Integer id) {
  return webClient.delete()
    .uri("/employees/" + id)
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, clientResponse -> handleClientError(clientResponse))
    .onStatus(HttpStatus::is5xxServerError, clientResponse -> handleServerError(clientResponse))
    .bodyToMono(Void.class);
}

private Mono<? extends Throwable> handleClientError(ClientResponse clientResponse) {
    // Handle client errors (e.g., 404 Not Found) here
    return Mono.error(new EmployeeNotFoundException("Employee not found"));
}

private Mono<? extends Throwable> handleServerError(ClientResponse clientResponse) {
    // Handle server errors (e.g., 500 Internal Server Error) here
    return Mono.error(new RuntimeException("Server error"));
}

6. Advance Configurations for Spring WebClient

6.1. Configuring Memory Limit

Spring WebFlux configures the default memory limit for buffering data in-memory to 256KB. If this limit is exceeded in any case then we will encounter DataBufferLimitException error.

To reset the memory limit, configure the below property in application.properties file.

spring.codec.max-in-memory-size=1MB

6.2. Configuring Connection Timeouts

We can use Apache HttpClient class to set timeout periods for connection timeout, read timeout and write timeouts.

@Bean
public WebClient getWebClient() {

  HttpClient httpClient = HttpClient.create()
    .tcpConfiguration(client ->
        client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
        .doOnConnected(conn -> conn
            .addHandlerLast(new ReadTimeoutHandler(10))
            .addHandlerLast(new WriteTimeoutHandler(10))));

  ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);     

  return WebClient.builder()
    .baseUrl("http://localhost:3000")
    .clientConnector(connector)
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .build();
}

7. Conclusion

In this Spring tutorial, we explored the powerful capabilities of Spring WebClient for making HTTP requests in a reactive and non-blocking manner. We discussed various aspects of WebClient, from its setup and configuration to making GET, POST, PUT, and DELETE requests. We also covered how to handle responses, error scenarios, and handle empty responses.

Happy Learning !!

Sourcecode Download

Comments

Subscribe
Notify of
guest
8 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode