In Spring Boot, the WebClient is a non-blocking and reactive HTTP client that replaced the legacy RestTemplate. It is part of the Spring WebFlux module and supports synchronous and asynchronous communications with external services.
This tutorial discusses the basics of using WebClient in Spring Boot to make GET requests, as well as handling query parameters, headers, cookies, and errors.
public Mono<User> getUserById(int id, String includeFields) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/users/{id}")
.queryParam("includeFields", includeFields)
.build(id))
.header(HttpHeaders.AUTHORIZATION, "Bearer your-token") // Add headers
.cookie("sessionId", "your-session-id") // Add cookies
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Client Error: " + body))))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Server Error: " + body))))
.bodyToMono(User.class) // Handle response and map to User class
.doOnError(WebClientResponseException.class, e -> {
// Handle error and log it
System.err.println("Error occurred: " + e.getStatusCode() + " - " + e.getResponseBodyAsString());
});
}
1. Getting Started with Spring WebClient
We must add the spring-boot-starter-webflux dependency to the project to use WebClient.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
In a Gradle project, include:
implementation 'org.springframework.boot:spring-boot-starter-webflux'
We assume that you have already created an instance of WebClient in a @Configuration class. We have configured the base URI using the WebClient builder, and in the examples in the next sections, we will only use relative paths.
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() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.build();
}
}
2. Making Synchronous GET Requests
Synchronous requests block the calling thread until a response is received.
To make a simple synchronous GET request, we can use the block() method, which blocks the request until the response is received. In this example, the block() method waits for the response, making the call synchronous.
private WebClient webClient; // Inject using constructor
User user = webClient
.get()
.uri("/api/users/{id}", 123)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(User.class)
.block(); // Blocks until the response is available
System.out.println("Response: " + user);
We can use the toEntity() method to access the API response as ResponseEntity. This allows us to access the response code, headers, and other helpful information.
private WebClient webClient;
ResponseEntity<User> userEntity = webClient
.get()
.uri("/api/users/{id}", 123)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(User.class) // Fetch response entity
.block(); // Blocks until the response is available
To get the list of users, we have two options:
- If the remote API returns is synchronous, we can use ParameterizedTypeReference with
List<User>
type.
private WebClient webClient;
List<User> users = webClient
.get()
.uri("/api/users")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<User>>() {})
.block();
If the remote API returns is asynchronous, we can use ‘.bodyToFlux().collectList()…’ method calls.
As a best practice, please avoid using bodyToFlux() and block() methods together as it defeats the whole purpose of asynchronous processing.
private WebClient webClient;
List<User> users = webClient
.get()
.uri("/api/users/{id}", 123)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(User.class)
.collectList()
.block();
3. Making Asynchronous GET Requests
Asynchronous requests do not block the calling thread and allow other tasks to proceed. This is the recommended approach in most of the cases.
We can make a call asynchronous simply by not blocking it using the block() method. Here, we have two options to handle the response:
- Return the Mono or the Flux response to the asynchronous client and let it handle the response status and body.
public Mono<User> getUserById(int id) {
return webClient
.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.onStatus(HttpStatus::is4xxClientError, response -> {
return response.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Client Error: " + body)));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
return response.bodyToMono(String.class)
.flatMap(body -> Mono.error(new RuntimeException("Server Error: " + body)));
});
}
- Subscribe to the response, and handle it when it arrives.
public void handleUserRequest() {
return webClient
.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.subscribe(
user -> System.out.println("User: " + user), // Handle success response
error -> System.err.println("Error: " + error.getMessage()) //handle error
);
}
4. Advanced Use Cases
Let us discuss a few advanced uses of the WebClient API when sending a GET request and processing the API response.
4.1. Adding Query Parameters
Use queryParam() method to add query parameters:
String fields = "id,name";
Mono<List<User>> userList = webClient
.get()
.uri(uriBuilder -> uriBuilder
.uri("/api/users/{id}", id)
.queryParam("fields", fields)
.build())
.retrieve()
.bodyToFlux(User.class)
.collectList();
4.2. Adding Headers and Cookies
To add headers and cookies, use the methods headers() and cookies() and pass the names and values. For example, in the following code, we are passing the OAuth2 token in the AUTHORIZATION header and a cookie sessionId
with its value.
public Mono<User> getUserById(int id) {
return webClient
.get()
.uri("/api/users/{id}", id)
.headers(headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer token"))
.cookie("sessionId", "123456789")
.retrieve()
.bodyToMono(User.class);
}
4.3. Handling Pagination and Streaming Data
To enable pagination and handle the streaming data from an async data source, we can pass the paging parameters in the request and receive the response as Flux.
public Flux<User> getPaginatedUsers(int page, int size) {
return webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/api/users")
.queryParam("page", page)
.queryParam("size", size)
.build())
.retrieve()
.bodyToFlux(User.class);
}
4.4. Timeout and Retry Strategies
To handle the timeout, pass the timeout duration in timeout() method. Not that we can also set the timeout, globally, by configuring in WebClient bean configuration as well.
Also, we can use the retryWhen() method to set the number of retries before concluding the request failure.
public Mono<User> getUserById(int id) {
return webClient
.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(3, Duration.ofSeconds(10)));
}
5. Error Handling
We can use the following methods for adding the error handling code in WebClient calls:
Method | Description |
---|---|
onStatus() | Handle specific HTTP status codes and define custom behavior based on the status code of the HTTP response. |
onRawStatus() | Provides a more granular approach to handle HTTP status codes, allowing for custom behavior based on exact status codes. |
doOnError() | Perform an action when an error is detected, such as logging or performing side effects. |
onErrorResume() | Provides a fallback mechanism by allowing a default value or alternative handling when an error occurs. |
User user = webClient
.get()
.uri("/api/users/{id}", 123)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> Mono.error(new RuntimeException("Client Error: " + errorBody))))
.onStatus(HttpStatusCode::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> Mono.error(new RuntimeException("Server Error: " + errorBody))))
.bodyToMono(User.class) // Fetch response entity
.doOnError(throwable -> log.warn("Error when issuing request :: ", throwable))
.block();
6. Testing WebClient GET Requests
Testing WebClient GET requests involves using the class WebClientTest for unit tests or mocking libraries like Mockito. The @WebFluxTest(UserController.class) sets up a WebTestClient for testing the UserController without starting a full HTTP server.
In the following test, we perform a GET request to ‘/api/users/123
‘, check that the status is OK, and assert that the response body matches the expected User details.
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.beans.factory.annotation.Autowired;
@WebFluxTest(UserController.class)
public class UserControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void testGetUserById() {
webTestClient.get()
.uri("/api/users/123")
.exchange()
.expectStatus().isOk()
.expectBody(User.class)
.isEqualTo(new User(123, "John Doe"));
}
}
Here, the UserController is a simple class for testing purposes:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
public class UserController {
@GetMapping("/api/users/{id}")
public Mono<User> getUserById(@PathVariable int id) {
// Fetch the user from a database
User user = new User(id, "John Doe");
return Mono.just(user);
}
}
7. Conclusion
In this Spring Boot tutorial, we learned how to make GET requests using Spring WebClient. We discuss various approaches, including both synchronous and asynchronous operations. We also discussed handling responses, advanced use cases, and implementing robust error-handling strategies.
Happy Learning !!
Reference: Spring Boot Docs
Comments