docs
reference
tessl install tessl/maven-io-quarkus--quarkus-resteasy-reactive@3.15.0A Jakarta REST implementation utilizing build time processing and Vert.x for high-performance REST endpoints with reactive capabilities in cloud-native environments.
Quarkus REST provides declarative exception-to-response mapping through @ServerExceptionMapper, allowing centralized error handling with support for global and local mappers.
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.*;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
}Exception mapper methods can inject the following parameters (first parameter must be the exception type):
ContainerRequestContext - Request contextUriInfo - URI informationHttpHeaders - HTTP headersRequest - HTTP requestResourceInfo - Resource method info (full)SimpleResourceInfo - Resource method info (lightweight)Response - JAX-RS responseRestResponse<T> - Type-safe responseUni<Response> - Async JAX-RS responseUni<RestResponse<T>> - Async type-safe responseMarks 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)
}@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();
}@ServerExceptionMapper({IllegalArgumentException.class, IllegalStateException.class})
public Response mapIllegalExceptions(RuntimeException ex) {
return Response.status(400)
.entity("Invalid request: " + ex.getMessage())
.build();
}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());
}@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();
}@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();
});
}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();
}
}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));
}
}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)Priorities.USER + 100Use @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();
}Unwrap only to specific cause types:
@UnwrapException({BusinessLogicException.class, ValidationException.class})
public class ServiceException extends RuntimeException {
public ServiceException(Throwable cause) {
super(cause);
}
}@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();
}@ServerExceptionMapper
public RestResponse<String> mapNotFound(NotFoundException ex) {
return RestResponse.status(404, "Resource not found");
}@ServerExceptionMapper
public Response mapAuth(AuthenticationException ex) {
return Response.status(401)
.header("WWW-Authenticate", "Bearer realm=\"API\"")
.entity("Authentication required")
.build();
}@ServerExceptionMapper
public RestResponse<String> mapAuthz(AuthorizationException ex) {
return RestResponse.status(403, "Access denied");
}@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();
}@ServerExceptionMapper
public RestResponse<String> mapTimeout(TimeoutException ex) {
return RestResponse.status(504, "Request timeout");
}@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();
}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();
}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();
}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();
}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.