Exception handling in Spring WebFlux
WebClient requires a reactive approach distinct from blocking REST templates. Developers must utilize operators like
onErrorResume,
onErrorReturn, and
exchangeFilterFunction to manage failures gracefully without blocking threads. This guide details how to intercept HTTP errors, parse response bodies for detailed error messages, and implement global error filters for consistent application-wide behavior.
The Reactive Error Handling Model
In the traditional synchronous world, exceptions are caught using try-catch blocks. However, WebFlux operates on a non-blocking, asynchronous model where code execution does not stop when an error occurs. Instead, errors are propagated down the reactive chain as signals. The
WebClient API exposes methods specifically designed to handle these signals, allowing you to transform or suppress errors at various stages of the request lifecycle. Understanding this flow is crucial because standard Java exception handling mechanisms will not work as expected within the reactive pipeline.
Handling HTTP Status Codes
The most common requirement is to distinguish between client errors (4xx) and server errors (5xx). The
WebClient provides the
status() method within its response body extractors, but often you need to handle errors before reaching the body parsing stage. You can use the
onErrorStatus operator to target specific HTTP status codes. For example, if you want to treat all 4xx errors differently than 5xx errors, you can chain multiple error handlers.
webClient.get()
.uri("/api/resource")
.retrieve()
.bodyToMono(String.class)
.onErrorStatus(HttpStatus.NOT_FOUND, ex -> {
return Mono.just("Resource not found");
})
.onErrorStatus(HttpStatus.INTERNAL_SERVER_ERROR, ex -> {
return Mono.error(new RuntimeException("Server failed"));
});
This approach allows you to provide default values or custom logic for specific failure modes. The
onErrorStatus operator accepts a predicate or a specific status code and a function that returns a new
Mono. This ensures that the stream continues with a valid value rather than terminating abruptly with an unhandled exception.
Accessing Error Response Bodies
Often, servers return detailed error information in the response body, such as JSON structures containing error codes and messages. By default,
WebClient may discard this body if an error status is encountered. To access this data, you should use the
onErrorResume operator combined with a custom error handler. When an error occurs, you can subscribe to the error signal and attempt to read the body again.
webClient.get()
.uri("/api/data")
.retrieve()
.bodyToMono(MyResponse.class)
.onErrorResume(WebClientResponseException.class, ex -> {
// Access the error body if available
String errorMessage = ex.getResponseBodyAsString();
return Mono.just(new MyResponse("Fallback", errorMessage));
});
Here,
WebClientResponseException is the specific exception type thrown by
WebClient when an HTTP error occurs. It contains methods like
getStatusCode() and
getResponseBodyAsString() which allow you to inspect the raw response content. This technique is essential for building robust APIs that provide meaningful feedback to clients even during failures.
Global Error Filters
For larger applications, handling errors individually in every service call leads to code duplication. A better practice is to implement a global error filter using
ExchangeFilterFunction. This function intercepts every request and response, allowing you to apply consistent logging, retry policies, or error transformations across the entire application.
public class LoggingFilter implements ExchangeFilterFunction {
@Override
public Mono filter(ClientRequest request, ExchangeFunction next) {
return next.exchange(request)
.doOnNext(response -> {
if (!response.statusCode().is2xxSuccessful()) {
log.warn("Failed request: {} {}", response.statusCode(), response.headers().header("X-Request-Id"));
}
});
}
}
You can add this filter to your
WebClient builder configuration. This ensures that any request made through this client instance is monitored uniformly. It is particularly useful for debugging production issues where tracking failed requests is critical for maintaining system health.
Retry Strategies
Transient network failures are common in distributed systems. Rather than failing immediately, it is often beneficial to retry failed requests. The
retryWhen operator provides fine-grained control over retry logic. You can specify conditions under which retries should occur, such as only retrying on connection timeouts or specific HTTP status codes.
webClient.get()
.uri("/api/unstable")
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(ex -> ex instanceof WebClientResponseException));
This configuration attempts the request up to three times with an exponential backoff delay. The
filter method ensures that retries only happen for specific types of exceptions, preventing infinite loops on fatal errors. Combining retry logic with proper error handling creates resilient microservices that can withstand temporary infrastructure instability.
Frequently Asked Questions
What happens if I don't handle errors in WebClient?
If no error handler is specified, the reactive stream terminates with the original exception. In a web application context, this usually results in a generic 500 Internal Server Error response being sent to the client, hiding the actual cause of the failure.
Can I use try-catch blocks with WebClient?
No, you cannot use traditional try-catch blocks directly around
WebClient calls because they are asynchronous. You must use reactive operators like
onErrorResume or
doOnError to handle exceptions within the reactive chain.
How do I catch all exceptions regardless of status code?
Use the
onErrorResume operator without specifying a status code. Pass a predicate that matches
Throwable or specific exception classes to catch any unexpected errors that slip past status-specific handlers.
Is it safe to retry all HTTP errors?
No, you should only retry idempotent requests like GET. Retrying POST or PUT requests can lead to duplicate operations, such as double-charging a user. Always check the HTTP method before applying retry logic.
How do I access headers in an error response?
You can access headers inside an
onErrorResume block by casting the exception to
WebClientResponseException. Although the body might be consumed, some headers remain accessible depending on the underlying HTTP client implementation used.