CODE WITH SIBIN

Solving Real Problems with Real Code


Master Global Exception Handling in Quarkus REST APIs – Full Tutorial

Exception handling is a critical aspect of building robust REST APIs. In Quarkus, you can implement global exception handling to consistently manage errors across your application. This guide covers various approaches with complete examples.

1. Using @Provider with ExceptionMapper

The most common approach is to implement JAX-RS ExceptionMapper interfaces.

Example: Basic Exception Mapper

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
    
    @Override
    public Response toResponse(Exception exception) {
        ErrorResponse errorResponse = new ErrorResponse(
            "SERVER_ERROR", 
            exception.getMessage(), 
            Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
        );
        
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                       .entity(errorResponse)
                       .build();
    }
}

// Error response DTO
public class ErrorResponse {
    private String code;
    private String message;
    private int status;
    
    // Constructors, getters, setters
    public ErrorResponse(String code, String message, int status) {
        this.code = code;
        this.message = message;
        this.status = status;
    }
    
    // Getters and setters...
}

2. Handling Specific Exceptions

You can create mappers for specific exception types.

Example: Custom Business Exception

public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
    
    @Override
    public Response toResponse(BusinessException exception) {
        ErrorResponse errorResponse = new ErrorResponse(
            "BUSINESS_ERROR", 
            exception.getMessage(), 
            Response.Status.BAD_REQUEST.getStatusCode()
        );
        
        return Response.status(Response.Status.BAD_REQUEST)
                       .entity(errorResponse)
                       .build();
    }
}

3. Validation Exception Handling

For handling bean validation errors (Hibernate Validator):

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Response;

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        List<Violation> violations = exception.getConstraintViolations()
            .stream()
            .map(this::toViolation)
            .collect(Collectors.toList());
            
        ErrorResponse errorResponse = new ErrorResponse(
            "VALIDATION_ERROR", 
            "Validation failed", 
            Response.Status.BAD_REQUEST.getStatusCode(),
            violations
        );
        
        return Response.status(Response.Status.BAD_REQUEST)
                     .entity(errorResponse)
                     .build();
    }
    
    private Violation toViolation(ConstraintViolation<?> violation) {
        return new Violation(
            violation.getPropertyPath().toString(),
            violation.getMessage()
        );
    }
}

// Extended ErrorResponse with violations
public class ErrorResponse {
    private String code;
    private String message;
    private int status;
    private List<Violation> violations;
    
    // Constructors, getters, setters...
}

public class Violation {
    private String field;
    private String message;
    
    // Constructors, getters, setters...
}

4. Using Quarkus Reactive Routes Exception Handling

For reactive routes, you can use:

import io.quarkus.vertx.web.Route;
import io.vertx.ext.web.RoutingContext;

public class ExceptionHandlingRoutes {

    @Route(path = "/api/items", methods = HttpMethod.GET)
    void getItems(RoutingContext rc) {
        try {
            // Your business logic
            rc.response().end("Success");
        } catch (Exception e) {
            handleException(rc, e);
        }
    }
    
    private void handleException(RoutingContext rc, Throwable throwable) {
        if (throwable instanceof BusinessException) {
            rc.response()
              .setStatusCode(400)
              .end(new ErrorResponse("BUSINESS_ERROR", throwable.getMessage(), 400).toString());
        } else {
            rc.response()
              .setStatusCode(500)
              .end(new ErrorResponse("SERVER_ERROR", "Internal server error", 500).toString());
        }
    }
}

5. Using CDI Interceptors

You can create a CDI interceptor for exception handling:

import javax.annotation.Priority;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@ErrorHandled
public class ErrorHandlingInterceptor {

    @AroundInvoke
    public Object handleErrors(InvocationContext context) throws Exception {
        try {
            return context.proceed();
        } catch (BusinessException e) {
            throw new WebApplicationException(
                Response.status(Response.Status.BAD_REQUEST)
                        .entity(new ErrorResponse("BUSINESS_ERROR", e.getMessage(), 400))
                        .build()
            );
        } catch (Exception e) {
            throw new WebApplicationException(
                Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                        .entity(new ErrorResponse("SERVER_ERROR", e.getMessage(), 500))
                        .build()
            );
        }
    }
}

// Annotation to mark methods for interception
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ErrorHandled {
}

// Usage in your resource
@Path("/api")
public class MyResource {

    @GET
    @Path("/data")
    @ErrorHandled
    public Response getData() {
        // Your code that might throw exceptions
    }
}

6. Handling Asynchronous Exceptions

For async endpoints, you need to handle exceptions differently:

import java.util.concurrent.CompletionException;

@Provider
public class AsyncExceptionMapper implements ExceptionMapper<CompletionException> {

    @Override
    public Response toResponse(CompletionException exception) {
        Throwable cause = exception.getCause();
        
        if (cause instanceof BusinessException) {
            return Response.status(Response.Status.BAD_REQUEST)
                         .entity(new ErrorResponse("BUSINESS_ERROR", cause.getMessage(), 400))
                         .build();
        }
        
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                     .entity(new ErrorResponse("SERVER_ERROR", "Internal server error", 500))
                     .build();
    }
}

7. Logging Exceptions

It's important to log exceptions while handling them:

import org.jboss.logging.Logger;

@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
    
    private static final Logger LOG = Logger.getLogger(GlobalExceptionHandler.class);
    
    @Override
    public Response toResponse(Exception exception) {
        LOG.error("Unhandled exception occurred", exception);
        
        ErrorResponse errorResponse = new ErrorResponse(
            "SERVER_ERROR", 
            "Internal server error", // Don't expose internal details to clients
            Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
        );
        
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                     .entity(errorResponse)
                     .build();
    }
}

8. Complete Example Application

Here's a complete example with all components:

1. Exception Classes

public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

public class NotFoundException extends RuntimeException {
    public NotFoundException(String message) {
        super(message);
    }
}

2. DTOs

public class ErrorResponse {
    private String code;
    private String message;
    private int status;
    private List<Violation> violations;
    
    // Constructors, getters, setters
}

public class Violation {
    private String field;
    private String message;
    
    // Constructors, getters, setters
}

3. Exception Mappers

@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
    
    @Override
    public Response toResponse(BusinessException exception) {
        ErrorResponse errorResponse = new ErrorResponse(
            "BUSINESS_ERROR", 
            exception.getMessage(), 
            Response.Status.BAD_REQUEST.getStatusCode()
        );
        
        return Response.status(Response.Status.BAD_REQUEST)
                     .entity(errorResponse)
                     .build();
    }
}

@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
    
    @Override
    public Response toResponse(NotFoundException exception) {
        ErrorResponse errorResponse = new ErrorResponse(
            "NOT_FOUND", 
            exception.getMessage(), 
            Response.Status.NOT_FOUND.getStatusCode()
        );
        
        return Response.status(Response.Status.NOT_FOUND)
                     .entity(errorResponse)
                     .build();
    }
}

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        List<Violation> violations = exception.getConstraintViolations()
            .stream()
            .map(this::toViolation)
            .collect(Collectors.toList());
            
        ErrorResponse errorResponse = new ErrorResponse(
            "VALIDATION_ERROR", 
            "Validation failed", 
            Response.Status.BAD_REQUEST.getStatusCode(),
            violations
        );
        
        return Response.status(Response.Status.BAD_REQUEST)
                     .entity(errorResponse)
                     .build();
    }
    
    private Violation toViolation(ConstraintViolation<?> violation) {
        return new Violation(
            violation.getPropertyPath().toString(),
            violation.getMessage()
        );
    }
}

@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Exception> {
    
    private static final Logger LOG = Logger.getLogger(GlobalExceptionHandler.class);
    
    @Override
    public Response toResponse(Exception exception) {
        LOG.error("Unhandled exception occurred", exception);
        
        ErrorResponse errorResponse = new ErrorResponse(
            "SERVER_ERROR", 
            "Internal server error",
            Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()
        );
        
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                     .entity(errorResponse)
                     .build();
    }
}

4. Resource Example

@Path("/api/items")
public class ItemResource {

    @GET
    @Path("/{id}")
    public Response getItem(@PathParam("id") Long id) {
        if (id == 0) {
            throw new NotFoundException("Item not found");
        }
        if (id < 0) {
            throw new BusinessException("Invalid item ID");
        }
        return Response.ok().entity(new Item(id, "Sample Item")).build();
    }
    
    @POST
    @ValidateRequest
    public Response createItem(@Valid Item item) {
        // Your business logic
        return Response.status(Response.Status.CREATED).entity(item).build();
    }
}

// Item class with validation
public class Item {
    @NotNull
    private Long id;
    
    @NotBlank
    @Size(min = 3, max = 50)
    private String name;
    
    // Constructors, getters, setters
}

Best Practices

  1. Don't expose stack traces: In production, avoid exposing detailed error messages or stack traces to clients.
  2. Use appropriate HTTP status codes: Match the status code to the error type.
  3. Log exceptions: Always log exceptions for debugging purposes.
  4. Standardize error responses: Use a consistent error response format across your API.
  5. Handle validation separately: Provide detailed validation error messages.
  6. Consider security: Ensure error messages don't reveal sensitive information.

This comprehensive approach to exception handling will make your Quarkus REST API more robust and maintainable while providing consistent error responses to clients.

Leave a Reply

Your email address will not be published. Required fields are marked *