or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.quarkus/quarkus-resteasy-reactive@3.15.x

docs

index.md
tile.json

tessl/maven-io-quarkus--quarkus-resteasy-reactive

tessl install tessl/maven-io-quarkus--quarkus-resteasy-reactive@3.15.0

A Jakarta REST implementation utilizing build time processing and Vert.x for high-performance REST endpoints with reactive capabilities in cloud-native environments.

exception-mapping.mddocs/reference/

Exception Mapping

Quarkus REST provides declarative exception-to-response mapping through @ServerExceptionMapper, allowing centralized error handling with support for global and local mappers.

Import

import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
import org.jboss.resteasy.reactive.server.UnwrapException;
import org.jboss.resteasy.reactive.RestResponse;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.*;

@ServerExceptionMapper

Declarative exception mapper that converts exceptions to HTTP responses.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerExceptionMapper {
    Class<? extends Throwable>[] value() default {};  // Exception types to map
    int priority() default Priorities.USER;            // Priority for mapper selection
}

Supported Parameter Types

Exception mapper methods can inject the following parameters (first parameter must be the exception type):

  • Exception type (required) - The exception to map
  • ContainerRequestContext - Request context
  • UriInfo - URI information
  • HttpHeaders - HTTP headers
  • Request - HTTP request
  • ResourceInfo - Resource method info (full)
  • SimpleResourceInfo - Resource method info (lightweight)

Supported Return Types

  • Response - JAX-RS response
  • RestResponse<T> - Type-safe response
  • Uni<Response> - Async JAX-RS response
  • Uni<RestResponse<T>> - Async type-safe response

@UnwrapException

Marks an exception type to automatically unwrap to its cause for mapper matching.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnwrapException {
    Class<? extends Throwable>[] value() default {};  // Specific cause types (empty = any)
}

Exception Mapping Patterns

Basic Exception Mapper

@ServerExceptionMapper
public RestResponse<ErrorResponse> mapBusinessException(BusinessException ex) {
    ErrorResponse error = new ErrorResponse();
    error.setMessage(ex.getMessage());
    error.setCode(ex.getErrorCode());

    return RestResponse.status(400).entity(error).build();
}

Multiple Exception Types

@ServerExceptionMapper({IllegalArgumentException.class, IllegalStateException.class})
public Response mapIllegalExceptions(RuntimeException ex) {
    return Response.status(400)
        .entity("Invalid request: " + ex.getMessage())
        .build();
}

Exception Type from Parameter

If value() is not specified, the exception type is inferred from the first parameter:

@ServerExceptionMapper
public RestResponse<String> mapNotFound(NotFoundException ex) {
    // Automatically maps NotFoundException
    return RestResponse.status(404, "Resource not found: " + ex.getMessage());
}

With Request Context

@ServerExceptionMapper
public Response mapWithContext(
    ValidationException ex,
    UriInfo uriInfo,
    HttpHeaders headers
) {
    ErrorResponse error = new ErrorResponse();
    error.setMessage(ex.getMessage());
    error.setPath(uriInfo.getPath());
    error.setUserAgent(headers.getHeaderString("User-Agent"));

    return Response.status(400).entity(error).build();
}

Async Exception Mapper

@ServerExceptionMapper
public Uni<RestResponse<ErrorResponse>> mapAsync(DatabaseException ex) {
    return errorService.createErrorReportAsync(ex)
        .map(report -> {
            ErrorResponse error = new ErrorResponse();
            error.setMessage(ex.getMessage());
            error.setReportId(report.getId());
            return RestResponse.status(500).entity(error).build();
        });
}

Global vs Local Mappers

Global Mappers

Defined at the class level (outside resource classes) and apply to all resources:

@ApplicationScoped
public class GlobalExceptionMappers {

    @ServerExceptionMapper
    public RestResponse<ErrorResponse> mapGlobalException(Exception ex) {
        // Applies to all resources
        return RestResponse.status(500)
            .entity(new ErrorResponse("Internal error: " + ex.getMessage()))
            .build();
    }
}

Local Mappers

Defined within resource classes and only apply to that resource:

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

    @ServerExceptionMapper
    public RestResponse<String> mapLocalException(ItemNotFoundException ex) {
        // Only applies to ItemResource methods
        return RestResponse.status(404, "Item not found: " + ex.getItemId());
    }

    @GET
    @Path("/{id}")
    public Item get(@RestPath String id) {
        return itemService.findById(id)
            .orElseThrow(() -> new ItemNotFoundException(id));
    }
}

Mapper Priority

When multiple mappers match an exception, the most specific mapper with the highest priority wins:

// Lower priority (executes if no higher priority mapper matches)
@ServerExceptionMapper(priority = Priorities.USER + 100)
public Response mapGeneric(RuntimeException ex) {
    return Response.status(500).entity("Generic error").build();
}

// Higher priority (executes first)
@ServerExceptionMapper(priority = Priorities.USER)
public Response mapSpecific(IllegalArgumentException ex) {
    return Response.status(400).entity("Invalid argument").build();
}

Priority values (lower number = higher priority):

  • Priorities.USER = 5000 (default)
  • Custom priorities: use offsets like Priorities.USER + 100

Exception Unwrapping

Automatic Unwrapping

Use @UnwrapException to automatically unwrap wrapper exceptions to their causes:

@UnwrapException
public class ServiceException extends RuntimeException {
    public ServiceException(Throwable cause) {
        super(cause);
    }
}

@ServerExceptionMapper
public Response mapBusinessLogic(BusinessLogicException ex) {
    // Will match even if thrown as ServiceException(BusinessLogicException)
    return Response.status(400).entity(ex.getMessage()).build();
}

Specific Cause Types

Unwrap only to specific cause types:

@UnwrapException({BusinessLogicException.class, ValidationException.class})
public class ServiceException extends RuntimeException {
    public ServiceException(Throwable cause) {
        super(cause);
    }
}

Common Exception Mapping Patterns

Validation Errors

@ServerExceptionMapper
public RestResponse<ValidationErrorResponse> mapValidation(ValidationException ex) {
    ValidationErrorResponse response = new ValidationErrorResponse();
    response.setMessage("Validation failed");
    response.setErrors(ex.getErrors());

    return RestResponse.status(400).entity(response).build();
}

Not Found

@ServerExceptionMapper
public RestResponse<String> mapNotFound(NotFoundException ex) {
    return RestResponse.status(404, "Resource not found");
}

Authentication Errors

@ServerExceptionMapper
public Response mapAuth(AuthenticationException ex) {
    return Response.status(401)
        .header("WWW-Authenticate", "Bearer realm=\"API\"")
        .entity("Authentication required")
        .build();
}

Authorization Errors

@ServerExceptionMapper
public RestResponse<String> mapAuthz(AuthorizationException ex) {
    return RestResponse.status(403, "Access denied");
}

Database Errors

@ServerExceptionMapper
public Response mapDatabase(SQLException ex, UriInfo uriInfo) {
    logger.error("Database error at {}: {}", uriInfo.getPath(), ex.getMessage());

    ErrorResponse error = new ErrorResponse();
    error.setMessage("Database error occurred");
    error.setPath(uriInfo.getPath());

    return Response.status(500).entity(error).build();
}

Timeout Errors

@ServerExceptionMapper
public RestResponse<String> mapTimeout(TimeoutException ex) {
    return RestResponse.status(504, "Request timeout");
}

Generic Fallback

@ServerExceptionMapper(priority = Priorities.USER + 1000)  // Low priority fallback
public Response mapGenericException(Exception ex, UriInfo uriInfo) {
    logger.error("Unhandled exception at {}", uriInfo.getPath(), ex);

    ErrorResponse error = new ErrorResponse();
    error.setMessage("An unexpected error occurred");
    error.setPath(uriInfo.getPath());
    error.setTimestamp(Instant.now());

    return Response.status(500).entity(error).build();
}

Error Response Structures

Simple Error Response

public class ErrorResponse {
    private String message;
    private Instant timestamp;
    private String path;

    // Getters and setters
}

@ServerExceptionMapper
public RestResponse<ErrorResponse> map(BusinessException ex, UriInfo uriInfo) {
    ErrorResponse error = new ErrorResponse();
    error.setMessage(ex.getMessage());
    error.setTimestamp(Instant.now());
    error.setPath(uriInfo.getPath());

    return RestResponse.status(400).entity(error).build();
}

Detailed Error Response

public class DetailedErrorResponse {
    private String error;
    private String message;
    private int status;
    private String path;
    private Instant timestamp;
    private String requestId;
    private Map<String, Object> details;

    // Getters and setters
}

@ServerExceptionMapper
public RestResponse<DetailedErrorResponse> map(
    ApplicationException ex,
    UriInfo uriInfo,
    HttpHeaders headers
) {
    DetailedErrorResponse error = new DetailedErrorResponse();
    error.setError(ex.getClass().getSimpleName());
    error.setMessage(ex.getMessage());
    error.setStatus(400);
    error.setPath(uriInfo.getPath());
    error.setTimestamp(Instant.now());
    error.setRequestId(headers.getHeaderString("X-Request-ID"));
    error.setDetails(ex.getDetails());

    return RestResponse.status(400).entity(error).build();
}

Validation Error Response

public class ValidationErrorResponse {
    private String message;
    private List<FieldError> errors;

    public static class FieldError {
        private String field;
        private String message;
        private Object rejectedValue;

        // Getters and setters
    }

    // Getters and setters
}

@ServerExceptionMapper
public RestResponse<ValidationErrorResponse> map(ValidationException ex) {
    ValidationErrorResponse response = new ValidationErrorResponse();
    response.setMessage("Validation failed");
    response.setErrors(ex.getFieldErrors().stream()
        .map(e -> {
            FieldError error = new FieldError();
            error.setField(e.getField());
            error.setMessage(e.getMessage());
            error.setRejectedValue(e.getRejectedValue());
            return error;
        })
        .collect(Collectors.toList())
    );

    return RestResponse.status(400).entity(response).build();
}

Integration with Jakarta REST

Exception mappers work alongside standard JAX-RS ExceptionMapper<T> implementations. Both can coexist:

// JAX-RS style
@Provider
public class LegacyExceptionMapper implements ExceptionMapper<LegacyException> {
    @Override
    public Response toResponse(LegacyException ex) {
        return Response.status(500).entity(ex.getMessage()).build();
    }
}

// RESTEasy Reactive style (preferred)
@ServerExceptionMapper
public Response mapModern(ModernException ex) {
    return Response.status(500).entity(ex.getMessage()).build();
}

The @ServerExceptionMapper approach is preferred for its simplicity and declarative style.