The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
In this tutorial, weβre going to examine WebClient, which is a reactive web client introduced in Spring 5.
Weβre also going to look at the WebTestClient, a WebClient designed to be used in tests.
Further reading:
Spring WebClient Requests with Parameters
2. What Is the WebClient?
Simply put, WebClient is an interface representing the main entry point for performing web requests.
It was created as part of the Spring Web Reactive module and will be replacing the classic RestTemplate in these scenarios. In addition, the new client is a reactive, non-blocking solution that works over the HTTP/1.1 protocol.
Itβs important to note that even though it is, in fact, a non-blocking client and it belongs to the spring-webflux library, the solution offers support for both synchronous and asynchronous operations, making it suitable also for applications running on a Servlet Stack.
This can be achieved by blocking the operation to obtain the result. Of course, this practice is not suggested if weβre working on a Reactive Stack.
Finally, the interface has a single implementation, the DefaultWebClient class, which weβll be working with.
3. Dependencies
Since we are using a Spring Boot application, all we need is the spring-boot-starter-webflux dependency to obtain Spring Frameworkβs Reactive Web support.
3.1. Building with Maven
Letβs add the following dependencies to the pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.5.7</vesion>
</dependency>
3.2. Building with Gradle
With Gradle, we need to add the following entries to the build.gradle file:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
4. Working with the WebClient
In order to work properly with the client, we need to know how to:
- create an instance
- make a request
- handle the response
4.1. Creating a WebClient Instance
There are three options to choose from. The first one is creating a WebClient object with default settings:
WebClient client = WebClient.create();
The second option is to initiate a WebClient instance with a given base URI:
WebClient client = WebClient.create("http://localhost:8080");
The third option (and the most advanced one) is building a client by using the DefaultWebClientBuilder class, which allows full customization:
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
4.2. Creating a WebClient Instance with Timeouts
Oftentimes, the default HTTP timeouts of 30 seconds are too slow for our needs, to customize this behavior, we can create an HttpClient instance and configure our WebClient to use it.
We can:
- set the connection timeout via the ChannelOption.CONNECT_TIMEOUT_MILLIS option
- set the read and write timeouts using a ReadTimeoutHandler and a WriteTimeoutHandler, respectively
- configure a response timeout using the responseTimeout directive
As we said, all these have to be specified in the HttpClient instance weβll configure:
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Note that while we can call timeout on our client request as well, this is a signal timeout, not an HTTP connection, a read/write, or a response timeout; itβs a timeout for the Mono/Flux publisher.
4.3. Preparing a Request β Define the Method
First, we need to specify an HTTP method of a request by invoking method(HttpMethod method):
UriSpec<RequestBodySpec> uriSpec = client.method(HttpMethod.POST);
Or calling its shortcut methods such as get, post, and delete:
UriSpec<RequestBodySpec> uriSpec = client.post();
Note: although it might seem we reuse the request spec variables (WebClient.UriSpec, WebClient.RequestBodySpec, WebClient.RequestHeadersSpec, WebClient.ResponseSpec), this is just for simplicity to present different approaches. These directives shouldnβt be reused for different requests, they retrieve references, and therefore the latter operations would modify the definitions we made in previous steps.
4.4. Preparing a Request β Define the URL
The next step is to provide a URL. Once again, we have different ways of doing this.
We can pass it to the uri API as a String:
RequestBodySpec bodySpec = uriSpec.uri("/resource");
Using a UriBuilder Function:
RequestBodySpec bodySpec = uriSpec.uri(
uriBuilder -> uriBuilder.pathSegment("/resource").build());
Or as a java.net.URL instance:
RequestBodySpec bodySpec = uriSpec.uri(URI.create("/resource"));
Keep in mind that if we defined a default base URL for the WebClient, this last method would override this value.
4.5. Preparing a Request β Define the Body
Then we can set a request body, content type, length, cookies, or headers if we need to.
For example, if we want to set a request body, there are a few available ways. Probably the most common and straightforward option is using the bodyValue method:
RequestHeadersSpec<?> headersSpec = bodySpec.bodyValue("data");
Or by presenting a Publisher (and the type of elements that will be published) to the body method:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
Mono.just(new Foo("name")), Foo.class);
Alternatively, we can make use of the BodyInserters utility class. For example, letβs see how we can fill in the request body using a simple object as we did with the bodyValue method:
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromValue("data"));
Similarly, we can use the BodyInserters#fromPublisher method if we are using a Reactor instance:
RequestHeadersSpec headersSpec = bodySpec.body(
BodyInserters.fromPublisher(Mono.just("data")),
String.class);
This class also offers other intuitive functions to cover more advanced scenarios. For instance, in case we have to send multipart requests:
LinkedMultiValueMap map = new LinkedMultiValueMap();
map.add("key1", "value1");
map.add("key2", "value2");
RequestHeadersSpec<?> headersSpec = bodySpec.body(
BodyInserters.fromMultipartData(map));
All these methods create a BodyInserter instance that we can then present as the body of the request.
The BodyInserter is an interface responsible for populating a ReactiveHttpOutputMessage body with a given output message and a context used during the insertion.
A Publisher is a reactive component in charge of providing a potentially unbounded number of sequenced elements. It is an interface too, and the most popular implementations are Mono and Flux.
4.6. Preparing a Request β Define the Headers
After we set the body, we can set headers, cookies, and acceptable media types. Values will be added to those that have already been set when instantiating the client.
Also, there is additional support for the most commonly used headers like βIf-None-Matchβ, βIf-Modified-Sinceβ, βAcceptβ, and βAccept-Charsetβ.
Hereβs an example of how these values can be used:
ResponseSpec responseSpec = headersSpec.header(
HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.acceptCharset(StandardCharsets.UTF_8)
.ifNoneMatch("*")
.ifModifiedSince(ZonedDateTime.now())
.retrieve();
4.7. Getting a Response
The final stage is sending the request and receiving a response. We can achieve this by using either the exchangeToMono/exchangeToFlux or the retrieve method.
The exchangeToMono and exchangeToFlux methods allow access to the ClientResponse along with its status and headers:
Mono<String> response = headersSpec.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(String.class);
} else if (response.statusCode().is4xxClientError()) {
return Mono.just("Error response");
} else {
return response.createException()
.flatMap(Mono::error);
}
});
While the retrieve method is the shortest path to fetching a body directly:
Mono<String> response = headersSpec.retrieve()
.bodyToMono(String.class);
Itβs important to pay attention to the ResponseSpec.bodyToMono method, which will throw a WebClientException if the status code is 4xx (client error) or 5xx (server error).
5. Working with the WebTestClient
The WebTestClient is the main entry point for testing WebFlux server endpoints. It has a very similar API to the WebClient, and it delegates most of the work to an internal WebClient instance focusing mainly on providing a test context. The DefaultWebTestClient class is a single interface implementation.
The client for testing can be bound to a real server or work with specific controllers or functions.
5.1. Binding to a Server
To complete end-to-end integration tests with actual requests to a running server, we can use the bindToServer method:
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
5.2. Binding to a Router
We can test a particular RouterFunction by passing it to the bindToRouterFunction method:
RouterFunction function = RouterFunctions.route(
RequestPredicates.GET("/resource"),
request -> ServerResponse.ok().build()
);
WebTestClient
.bindToRouterFunction(function)
.build().get().uri("/resource")
.exchange()
.expectStatus().isOk()
.expectBody().isEmpty();
5.3. Binding to a Web Handler
The same behavior can be achieved with the bindToWebHandler method, which takes a WebHandler instance:
WebHandler handler = exchange -> Mono.empty();
WebTestClient.bindToWebHandler(handler).build();
5.4. Binding to an Application Context
A more interesting situation occurs when weβre using the bindToApplicationContext method. It takes an ApplicationContext and analyses the context for controller beans and @EnableWebFlux configurations.
If we inject an instance of the ApplicationContext, a simple code snippet may look like this:
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
5.5. Binding to a Controller
A shorter approach would be providing an array of controllers we want to test by the bindToController method. Assuming weβve got a Controller class and we injected it into a needed class, we can write:
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();
5.6. Making a Request
After building a WebTestClient object, all following operations in the chain are going to be similar to the WebClient until the exchange method (one way to get a response), which provides the WebTestClient.ResponseSpec interface to work with useful methods like the expectStatus, expectBody, and expectHeader:
WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build()
.post()
.uri("/resource")
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", "application/json")
.expectBody().jsonPath("field").isEqualTo("value");
6. Conclusion
In this article, we explored WebClient, a new enhanced Spring mechanism for making requests on the client-side.
We also looked at the benefits it provides by going through configuring the client, preparing the request, and processing the response.
