Spring WebFlux Retries
If you use Spring WebFlux, you probably want your requests to be more resilient. Here, learn to use the retries that come packaged with the WebFlux library.
Join the DZone community and get the full member experience.
Join For FreeIf you use Spring WebFlux, you probably want your requests to be more resilient. In this case, we can just use the retries that come packaged with the WebFlux library.
There are various cases that we can take into account:
- Too many requests to the server
- An internal server error
- Unexpected format
- Server timeout
We would make a test case for those using MockWebServer.
We will add the WebFlux and the MockWebServer to a project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.7.15</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
<version>3.5.9</version>
</dependency>
Let’s check the scenario of too many requests on the server. In this scenario, our request fails because the server will not fulfill it. The server is still functional however and on another request, chances are we shall receive a proper response.
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.SocketPolicy;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
class WebFluxRetry {
@Test
void testTooManyRequests() throws IOException {
MockWebServer server = new MockWebServer();
MockResponse tooManyRequests = new MockResponse()
.setBody("Too Many Requests")
.setResponseCode(429);
MockResponse successfulRequests = new MockResponse()
.setBody("successful");
server.enqueue(tooManyRequests);
server.enqueue(tooManyRequests);
server.enqueue(successfulRequests);
server.start();
WebClient webClient = WebClient.builder()
.baseUrl("http://" + server.getHostName() + ":" + server.getPort())
.build();
Mono<String> result = webClient.get()
.retrieve()
.bodyToMono(String.class)
.retry(2);
StepVerifier.create(result)
.expectNextMatches(s -> s.equals("successful"))
.verifyComplete();
server.shutdown();
}
}
We used the mock server in order to enqueue requests. Essentially the requests we placed on the mock server will be enqueued and consumed every time we do a request. The first two responses would be failed 429
responses from the server.
Let’s check the case of 5xx
responses. A 5xx
can be caused by various reasons. Usually, if we face a 5xx
, there is probably a problem in the server codebase. However, in some cases, 5xx
might come as a result of an unstable service that regularly restarts. Also, a server might be deployed in an availability zone that faces network issues; it can even be a failed rollout that is not fully in effect. In this case, a retry makes sense. By retrying, the request will be routed to the next server behind the load balancer.
We will try a request that has a bad status:
@Test
void test5xxResponse() throws IOException {
MockWebServer server = new MockWebServer();
MockResponse tooManyRequests = new MockResponse()
.setBody("Server Error")
.setResponseCode(500);
MockResponse successfulRequests = new MockResponse()
.setBody("successful");
server.enqueue(tooManyRequests);
server.enqueue(tooManyRequests);
server.enqueue(successfulRequests);
server.start();
WebClient webClient = WebClient.builder()
.baseUrl("http://" + server.getHostName() + ":" + server.getPort())
.build();
Mono<String> result = webClient.get()
.retrieve()
.bodyToMono(String.class)
.retry(2);
StepVerifier.create(result)
.expectNextMatches(s -> s.equals("successful"))
.verifyComplete();
server.shutdown();
}
Also, a response with the wrong format is possible to happen if an application goes haywire:
@Data
@AllArgsConstructor
@NoArgsConstructor
private static class UsernameResponse {
private String username;
}
@Test
void badFormat() throws IOException {
MockWebServer server = new MockWebServer();
MockResponse tooManyRequests = new MockResponse()
.setBody("Plain text");
MockResponse successfulRequests = new MockResponse()
.setBody("{\"username\":\"test\"}")
.setHeader("Content-Type","application/json");
server.enqueue(tooManyRequests);
server.enqueue(tooManyRequests);
server.enqueue(successfulRequests);
server.start();
WebClient webClient = WebClient.builder()
.baseUrl("http://" + server.getHostName() + ":" + server.getPort())
.build();
Mono<UsernameResponse> result = webClient.get()
.retrieve()
.bodyToMono(UsernameResponse.class)
.retry(2);
StepVerifier.create(result)
.expectNextMatches(s -> s.getUsername().equals("test"))
.verifyComplete();
server.shutdown();
}
If we break it down, we created two responses in plain text format. Those responses would be rejected since they cannot be mapped to the UsernameResponse
object. Thanks to the retries we managed to get a successful response.
Our last request would tackle the case of a timeout:
@Test
void badTimeout() throws IOException {
MockWebServer server = new MockWebServer();
MockResponse dealayedResponse= new MockResponse()
.setBody("Plain text")
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)
.setBodyDelay(10000, TimeUnit.MILLISECONDS);
MockResponse successfulRequests = new MockResponse()
.setBody("successful");
server.enqueue(dealayedResponse);
server.enqueue(successfulRequests);
server.start();
WebClient webClient = WebClient.builder()
.baseUrl("http://" + server.getHostName() + ":" + server.getPort())
.build();
Mono<String> result = webClient.get()
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofMillis(5_000))
.retry(1);
StepVerifier.create(result)
.expectNextMatches(s -> s.equals("successful"))
.verifyComplete();
server.shutdown();
}
That’s it. Thanks to retries, our codebase was able to recover from failures and become more resilient. Also, we used MockWebServer, which can be very handy for simulating these scenarios.
Published at DZone with permission of Emmanouil Gkatziouras, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments