In modern microservices architectures, applications frequently interact with external services. Ensuring resilience and graceful degradation in the face of failures is paramount. Quarkus, with its lightweight nature and developer-friendly features, coupled with SmallRye Fault Tolerance, provides powerful tools to achieve this.
One common requirement is to treat 5xx (Server Error) HTTP responses from external services as failures, triggering a circuit breaker to prevent cascading failures and provide a fallback mechanism. This post explores the best practices for implementing such error handling using Quarkus REST Client and SmallRye Fault Tolerance, emphasizing specific exception handling for improved robustness.
The Challenge: Handling External Service Errors
When a Quarkus application communicates with an external service using the REST Client, various issues can arise:
- 5xx Server Errors: The external service might encounter internal problems and return a 5xx status code.
- Network Issues: Connectivity problems, such as the service being down or network interruptions, can occur.
- Timeouts: The external service might take too long to respond, leading to timeout errors.
We need a strategy to detect these failures and react appropriately. SmallRye Fault Tolerance’s @CircuitBreaker annotation is ideal for this, but we need to tell it when to consider a call as a failure.
The Solution: Specific Exception Handling
Instead of broadly catching Throwable, a more robust approach involves catching specific exceptions that indicate a failure scenario. This provides better control and clarity in our error handling logic.
1. Global Exception Mapper for Consistent Error Responses
A global exception mapper in Quarkus allows us to centralize the handling of exceptions and return consistent error responses to the client. We can create a mapper that specifically handles exceptions related to REST Client interactions and network issues:
```java
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeoutException;
import jakarta.ws.rs.WebApplicationException;
import org.jboss.resteasy.reactive.client.impl.ClientWebApplicationException;
@Provider
public class SpecificExceptionHandler implements ExceptionMapper { @Override public Response toResponse(Exception exception) { if (exception instanceof ClientWebApplicationException) { ClientWebApplicationException cwae = (ClientWebApplicationException) exception; int status = cwae.getResponse().getStatus(); if (status >= 500 && status < 600) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(new ErrorResponse("External Service Error", "The external service responded with a server error (" + status + ").")) .type("application/json") .build(); } return cwae.getResponse(); } else if (exception instanceof ConnectException) { return Response.status(Response.Status.SERVICE_UNAVAILABLE) .entity(new ErrorResponse("Connection Error", "Failed to connect to the external service.")) .type("application/json") .build(); } else if (exception instanceof SocketTimeoutException || exception instanceof TimeoutException) { return Response.status(Response.Status.GATEWAY_TIMEOUT) .entity(new ErrorResponse("Timeout Error", "Timeout occurred while communicating with the external service.")) .type("application/json") .build(); } else { exception.printStackTrace(); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(new ErrorResponse("Internal Error", "An unexpected internal error occurred.")) .type("application/json") .build(); } } public static class ErrorResponse { public String message; public String details; public ErrorResponse(String message, String details) { this.message = message; this.details = details; } }
}
This SpecificExceptionHandler catches:
- ClientWebApplicationException: The specific exception thrown by RESTEasy Reactive for non-2xx HTTP responses, allowing us to check the status code.
- ConnectException: Indicates failure to establish a connection.
- SocketTimeoutException and TimeoutException: Represent timeout scenarios.
For each of these, we return a relevant HTTP status code and a structured error response.
- Integrating with @CircuitBreaker
To make the @CircuitBreaker react to these specific failure conditions, we need to catch the relevant exceptions in our service method and throw custom exceptions that the circuit breaker is configured to monitor:
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeoutException;
import jakarta.ws.rs.WebApplicationException;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import org.jboss.resteasy.reactive.client.impl.ClientWebApplicationException;
@Path(“/api”)
public class MyService { @Inject @RestClient ExternalServiceClient externalServiceClient; @GET @Path("/data") @Produces(MediaType.TEXT_PLAIN) @CircuitBreaker( requestVolumeThreshold = 20, failureRatio = 0.5, delay = "5s", successThreshold = 3, failOn = {ExternalServiceFailureException.class, ExternalServiceUnavailableException.class, ExternalServiceTimeoutException.class} ) @Fallback(fallbackMethod = "getDataFallback") public String getData() { try { return externalServiceClient.getData(); } catch (ClientWebApplicationException e) { if (e.getResponse().getStatus() >= 500 && e.getResponse().getStatus() < 600) { throw new ExternalServiceFailureException("External service returned a 5xx error"); } else { throw e; // Re-throw other client-related web application exceptions } } catch (ConnectException e) { throw new ExternalServiceUnavailableException("Could not connect to external service"); } catch (SocketTimeoutException | TimeoutException e) { throw new ExternalServiceTimeoutException("Timeout communicating with external service"); } } public String getDataFallback() { return "Data unavailable due to external service issues."; } public static class ExternalServiceFailureException extends RuntimeException { public ExternalServiceFailureException(String message) { super(message); } } public static class ExternalServiceUnavailableException extends RuntimeException { public ExternalServiceUnavailableException(String message) { super(message); } } public static class ExternalServiceTimeoutException extends RuntimeException { public ExternalServiceTimeoutException(String message) { super(message); } }
}
In this service method:
- We catch ClientWebApplicationException, ConnectException, and TimeoutException.
- For a ClientWebApplicationException with a 5xx status code, we throw an ExternalServiceFailureException.
- For connection issues, we throw an ExternalServiceUnavailableException.
- For timeouts, we throw an ExternalServiceTimeoutException.
- The @CircuitBreaker is configured with failOn to specifically track these custom exceptions as failures.
The global SpecificExceptionHandler will then handle the HTTP response for these custom exceptions if the circuit breaker is open and the fallback method is executed, or if the exception propagates beyond the circuit breaker.
Benefits of Specific Exception Handling - Improved Clarity: The code clearly indicates which failure scenarios are being handled.
- Enhanced Maintainability: Changes to error handling for specific cases are isolated.
- Precise Circuit Breaker Behavior: The circuit breaker reacts only to the intended failure conditions.
- Better Error Reporting: Clients receive more informative error messages tailored to the specific problem.
Conclusion
By combining the power of Quarkus REST Client and SmallRye Fault Tolerance with a strategy of catching and handling specific exception types, you can build more resilient and robust applications. Using a global exception mapper ensures consistent error responses, while targeted exception handling in your service methods allows for precise control over circuit breaker behavior, ultimately leading to a better user experience even when external services encounter issues. Remember to always strive for specific exception handling over broad catch-all approaches for more maintainable and predictable applications.