We will examine here how to use Spring WebClient to make calls to a service by sending request.
What is Spring WebClient?
WebClient provides a common interface for making web requests in a non-blocking way. It is part of the Spring Web Reactive module and will replace the well-known RestTemplate. We will see below the dependencies we need, how to create a web client, and some more configurations that we can use with Spring WebClient.
What dependencies do you need to use Spring WebClient?
The first thing we need is to define the dependencies.
- spring-boot-starter-webflux is the necessary dependency for the webclient
- mockserver-netty we will use it to mock the web server in the unit tests for this example
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty</artifactId>
<version>5.11.1</version>
</dependency>
</dependencies>
The structure of this example looks like
How to create a WebClient?
To use WebClient we will use the builder provided by WebClient WebClient.builder()
This way we will create a WebClient instance with a base url.
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8899")
.build();
How to add default headers to the WebClient?
Commonly, when it comes to calls with a body Json, the header is added indicating that it is an application/json call.
We can indicate headers by default.
WebClient webClient = WebClient.builder()
.baseUrl("https://run.mocky.io")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
How to send a request and receive a response with WebClient
Once we have our instance of WebClient we can send a request.
In this example, we have our request that we have defined as ClientRequest and our response in ClientResponse.
We will be sending a POST to an uri through webClient.post().uri(…)
The retrieve method executes the HTTP request and returns the response.
The bodyToMono method takes the body from the retrieve and converts it to our response class that we specify.
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
.bodyToMono(ClientResponse.class)
.block();
How to configure a timeout in the WebClient?
We can configure the timeout of our WebClient by indicating a duration in seconds or any other duration provided by the Duration class.
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
.bodyToMono(ClientResponse.class)
.timeout(Duration.ofSeconds(3)) // timeout
.block();
Another way to specify a timeout is through the configuration of a TcpClient where we can be more specific.
Next we see the specification of a timeout for the connection and read/write of the WebClient.
// tcp client timeout
TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.doOnConnected(connection ->
connection.addHandlerLast(new ReadTimeoutHandler(3))
.addHandlerLast(new WriteTimeoutHandler(3)));
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8899")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) // timeout
.build();
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
.bodyToMono(ClientResponse.class)
.block();
return response;
Manage http status in WebClient
We can know and manage any http status in our WebClient using the onStatus method.
For example, here in case of an error 500 we log in and throw an error using onStatus(…) throwing an exception our ApiWebClientException.
Let’s see what we do next, there is a lot here:
- We create our webclient.
- We indicate that it will do a POST.
- It will receive a request from our ClientRequest class.
- It will do a retrieve executing the HTTP request
- If an HttpStatus 500 error occurs, it will evaluate it in onStatus and decide what to do with that error.
- Convert the response to our ClientResponse class.
- Wait 3 seconds maximum for the connection.
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
// handle status
.onStatus(HttpStatus::is5xxServerError, clientResponse -> {
logger.error("Error endpoint with status code {}", clientResponse.statusCode());
throw new ApiWebClientException("HTTP Status 500 error"); // throw custom exception
})
.bodyToMono(ClientResponse.class)
.timeout(Duration.ofSeconds(3)) // timeout
.block();
What does our finished web client look like?
Adding up everything we have seen before, our web client looks like this.
@Component
public class ApiWebClient {
private static final Logger logger = LoggerFactory.getLogger(ApiWebClient.class);
public ClientResponse createClient(ClientRequest request) {
// tcp client timeout
TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.doOnConnected(connection ->
connection.addHandlerLast(new ReadTimeoutHandler(3))
.addHandlerLast(new WriteTimeoutHandler(3)));
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8899")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient))) // timeout
.build();
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
// handle status
.onStatus(HttpStatus::is5xxServerError, clientResponse -> {
logger.error("Error endpoint with status code {}", clientResponse.statusCode());
throw new ApiWebClientException("HTTP Status 500 error"); // throw custom exception
})
.bodyToMono(ClientResponse.class)
.block();
return response;
}
public ClientResponse createClientWithDuration(ClientRequest request) {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8899")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
ClientResponse response = webClient.post()
.uri("/accounts")
.body(Mono.just(request), ClientRequest.class)
.retrieve()
// handle status
.onStatus(HttpStatus::is5xxServerError, clientResponse -> {
logger.error("Error endpoint with status code {}", clientResponse.statusCode());
throw new ApiWebClientException("HTTP Status 500 error"); // throw custom exception
})
.bodyToMono(ClientResponse.class)
.timeout(Duration.ofSeconds(3)) // timeout
.block();
return response;
}
}
How to create a test to test Spring WebClient
First, we define a Spring test in which we will use MockServer .
For each test, we raise a server on port 8899 and at the end of each test, we stop it.
We are going to create several tests to try different cases. We will try:
- A correct connection.
- A connection with an expired timeout.
- A connection that throws an error 500 and returns an error ApiWebClientException.
Notice below that we lift the mock from the server and then stop it.
ClientAndServer mockServer = ClientAndServer.startClientAndServer(8899);
mockServer.stop();
Then we set the expectations we want for the server.
In this example, we indicate that when a POST occurs for the path /account, it should respond with the body that we indicated, the header and with a delay time.
mockServer.when(HttpRequest.request().withMethod("POST")
.withPath("/accounts")).
respond(HttpResponse.response()
.withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withDelay(TimeUnit.MILLISECONDS, 5000));
Let’s see how the unit tests look for different cases.
@SpringBootTest
public class ClientServiceTest {
private static final Logger logger = LoggerFactory.getLogger(ClientServiceTest.class);
@Autowired
private ApiWebClient webClient;
private ClientAndServer mockServer;
@BeforeEach
public void startServer() {
mockServer = ClientAndServer.startClientAndServer(8899);
}
@AfterEach
public void stopServer() {
mockServer.stop();
}
@Test
void createShouldReturnTheResponse() {
// set up mock server with a delay of 1 seconds
mockServer.when(HttpRequest.request().withMethod("POST")
.withPath("/accounts")).
respond(HttpResponse.response()
.withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withDelay(TimeUnit.MILLISECONDS, 1000));
ClientRequest request = new ClientRequest();
request.setName("Frank");
request.setName("frank@mail.com");
ClientResponse response = webClient.createClient(request);
assertThat(response).isNotNull();
assertThat(response.getName()).isEqualTo("Frank");
assertThat(response.getEmail()).isEqualTo("frank@mail.com");
}
@Test
void createWithTimeoutShouldThrowReadTimeOut() {
// set up mock server with a delay of 5 seconds
mockServer.when(HttpRequest.request().withMethod("POST")
.withPath("/accounts")).
respond(HttpResponse.response()
.withBody("{ \"name\": \"Frank\", \"email\": \"frank@mail.com\"}")
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withDelay(TimeUnit.MILLISECONDS, 5000));
ClientRequest request = new ClientRequest();
request.setName("Frank");
request.setName("frank@mail.com");
assertThrows(ReadTimeoutException.class, () -> webClient.createClient(request));
}
@Test
void createWithStatusShouldThrowACustomException() {
// set up mock server with a http status 500
mockServer.when(HttpRequest.request().withMethod("POST")
.withPath("/accounts"))
.respond(HttpResponse.response().withStatusCode(500)
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withDelay(TimeUnit.MILLISECONDS, 1000));
ClientRequest request = new ClientRequest();
request.setName("Frank");
request.setName("frank@mail.com");
ApiWebClientException apiWebClientException = assertThrows(ApiWebClientException.class, () -> webClient.createClient(request));
assertTrue(apiWebClientException.getMessage().contains("HTTP Status 500 error"));
}
}
Conclusion
We saw how to simply create a web client using Spring WebClient. We examined how to configure the client, send a request and, receive the response.
You can see this code at:
https://github.com/gustavopeiretti/spring-boot-examples