WebClient is a non-blocking, reactive HTTP client introduced in Spring 5 and is part of the Spring WebFlux module. Reactor is the foundation of WebClient’s functional and fluent API for making HTTP requests in Spring applications, especially in reactive, non-blocking scenarios. It's designed for modern, scalable applications that can handle high volumes of concurrent requests efficiently.
Some of the key points
Non-blocking and Reactive: WebClient operates in a non-blocking manner, allowing application to handle multiple concurrent requests efficiently without blocking threads.
Fluent API: It offers a fluent and functional API for building and executing HTTP requests, making it easy to compose complex requests and handle responses.
Supports Reactive Streams: WebClient integrates well with reactive programming concepts and supports reactive streams, allowing to work with Mono and Flux types for handling asynchronous data streams.
Customizable and Extensible: WebClient provides various configuration options and allows customization of request and response handling through filters, interceptors, and other mechanisms.
Supports WebClient.Builder: We can create a WebClient instance using WebClient.Builder, which allows for centralized configuration and reuse across multiple requests.
Codec Integration: WebClient integrates with Spring's HTTP codecs for automatic marshalling and unmarshalling of request and response data formats (e.g., JSON, XML).
The default HttpClient used by WebClient is the Netty implementation, so to see details like requests we need to change the reactor.netty.http.client** logging level to _DEBUG**._
Commonly used WebClient configuration
Base URL:
This specifies the default root URL for the requests.
Configure timeouts to prevent application from hanging indefinitely if a response takes too long. We can set timeouts for connection establishment, read operations, and write operations.
Interceptors allows us to intercept requests and responses before and after their execution. We can use them for tasks like logging, adding authentication headers dynamically, or error handling.
WebClient webClient = WebClient.builder()
.filter((req, next) -> {
// Add logging or custom logic before the request
return next.exchange(req);
})
.build();
Error Handling:
WebClient allows to chain error handling logic using operators like onErrorResume or onErrorReturn. This helps to gracefully handle unexpected errors and provide appropriate responses.
WebClient provides the onErrorRetry operator for retrying a failed request a certain number of times. This is useful for handling transient errors like network issues or server overload.
Mono<String> responseMono = webClient.get()
.uri("/users")
.retrieve()
.bodyToMono(String.class)
.onErrorRetry(3, // Retry up to 3 times
throwable -> throwable instanceof IOException); // Retry only for IOExceptions
Making HTTP Requests
WebClient offers a fluent API for building different types of HTTP requests:
.retrieve(): Use this for GET requests.
.post(): Use this for POST requests.
.put(): Use this for PUT requests.
.patch(): Use this for PATCH requests.
.delete(): Use this for DELETE requests
exchange() : This method allows explicit handling of the request and response. It returns a ClientResponse object, which contains details such as status, headers, and the response body. With exchange(), we need to explicitly subscribe to the response using methods like subscribe(), block(), or similar ones. This gives more control over the execution of the request and when we want to consume the response. exchange() is deprecated in the latest versions.
Additional Notes
When using WebClient in Spring WebFlux, the bodyToMono() and bodyToFlux() methods expect to deserialize the response body into a specified class type. If the response status code indicates a client error (4xx) or a server error (5xx), and there's no response body, these methods will throw a WebClientException
Under the hood, WebClient operates using an ExchangeFunction, which is responsible for executing requests and handling responses. We can customize this behavior by providing our own ExchangeFunction implementation.
We can use bodyToMono(Void.class) if no response body is expected. This is helpful in DELETE operations.
Examples
Service 1 API calling Service 2 GET API via WebClient.
package org.example.api;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.service.SampleService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@RestController
public class SampleController {
private final SampleService sampleService;
@GetMapping("/api/service-1/details")
public Mono<String> getInterServiceDetails() {
return sampleService.fetchService2Details();
}
}
WebClientConfig.java
package org.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8082") // Set the base URL for the requests
.defaultCookie("cookie-name", "cookie-value") // Set a default cookie for the requests
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // Set a default header for the requests
.build();
}
}
SampleService.java
package org.example.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Slf4j
@RequiredArgsConstructor
@Service
public class SampleService {
private static final String SERVICE_2_ENDPOINT_PATH = "/api/service-2/details";
private final WebClient webClient;
// Method to retrieve service 2 details using a GET request
public Mono<String> fetchService2Details() {
log.info("Calling service 2 API via WebClient");
return webClient.get()
.uri(SERVICE_2_ENDPOINT_PATH)
.retrieve()
.bodyToMono(String.class)
.doOnNext(data -> log.info("Received data from Service-2: {}", data)) // Do logging after receiving response
.onErrorReturn("Failed to retrieve data from Service-2"); // Default error message
}
}