Skip to content
SimplyMe
Go back

Robust Error Handling with Quarkus REST Client and SmallRye Fault Tolerance: A Deep Dive

Edit page

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:

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:

  1. 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:


Edit page
Share this post on:

Previous Post
Decoding the "@" in TM Forum Data Models
Next Post
The Dunning-Kruger Effect: Why We Often Don't Know What We Don't Know