Skip to main content
Redhat Developers  Logo
  • Products

    Featured

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat OpenShift AI
      Red Hat OpenShift AI
    • Red Hat Enterprise Linux AI
      Linux icon inside of a brain
    • Image mode for Red Hat Enterprise Linux
      RHEL image mode
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • Red Hat Developer Hub
      Developer Hub
    • View All Red Hat Products
    • Linux

      • Red Hat Enterprise Linux
      • Image mode for Red Hat Enterprise Linux
      • Red Hat Universal Base Images (UBI)
    • Java runtimes & frameworks

      • JBoss Enterprise Application Platform
      • Red Hat build of OpenJDK
    • Kubernetes

      • Red Hat OpenShift
      • Microsoft Azure Red Hat OpenShift
      • Red Hat OpenShift Virtualization
      • Red Hat OpenShift Lightspeed
    • Integration & App Connectivity

      • Red Hat Build of Apache Camel
      • Red Hat Service Interconnect
      • Red Hat Connectivity Link
    • AI/ML

      • Red Hat OpenShift AI
      • Red Hat Enterprise Linux AI
    • Automation

      • Red Hat Ansible Automation Platform
      • Red Hat Ansible Lightspeed
    • Developer tools

      • Red Hat Trusted Software Supply Chain
      • Podman Desktop
      • Red Hat OpenShift Dev Spaces
    • Developer Sandbox

      Developer Sandbox
      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Secure Development & Architectures

      • Security
      • Secure coding
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
      • View All Technologies
    • Start exploring in the Developer Sandbox for free

      sandbox graphic
      Try Red Hat's products and technologies without setup or configuration.
    • Try at no cost
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • Java
      Java icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • API Catalog
    • Product Documentation
    • Legacy Documentation
    • Red Hat Learning

      Learning image
      Boost your technical skills to expert-level with the help of interactive lessons offered by various Red Hat Learning programs.
    • Explore Red Hat Learning
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

REST API error modeling with Quarkus 2.0

March 3, 2022
Stephen Nimmo
Related topics:
APIsJavaKubernetesQuarkus
Related products:
Red Hat OpenShift

Share:

    In the previous installment of the Quarkus from the ground up series, you saw the beginnings of a fully functional, OpenAPI-compliant REST API built using Quarkus. That article covered all of the architectural layers, from managing database schemas with Flyway to building the API itself with RESTEasy Reactive. You saw happy-path use cases, but didn't get into the concepts around error handling. In this article, you'll dive into error handling, build a solid error response model, and see how you can help API consumers reduce toil in their work.

    Note: Once you're ready to dive in, you can download the complete source code for this article.

    Why error handling matters

    Error handling is one of those aspects of a system's architecture that deserves more attention. When you're designing a new feature, most of the discussion centers on what happens when things go right—but when that feature goes into production, most of the attention is focused on what is going wrong. As is true with observability and other architectural concerns, if the development team spent a bit more time upfront in the design of error handling within an application, they could reap considerable rewards in production in terms of application availability and stability.

    Out-of-the-box functionality

    Before modeling and implementing error handling into the sample application, it would be good to explore what error responses look like without any additional code. Begin by cloning the repository for the application you developed in the last article. Next, you'll need to create a new test that uses Mockito to create a response with an unexpected runtime exception. To use Mockito, you need to add the Quarkus extension:

    
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5-mockito</artifactId>
        <scope>test</scope>
    </dependency>
    

    The test creates a mocked CustomerService instance configured to throw a RuntimeException. When the response is returned, extract it as a Response object and set a breakpoint to better understand what the returned JSON structure looks like:

    
    package com.redhat.exception;
    
    import com.redhat.customer.CustomerService;
    import io.quarkus.test.junit.QuarkusTest;
    import io.quarkus.test.junit.mockito.InjectMock;
    import io.restassured.response.Response;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mockito;
    
    import static io.restassured.RestAssured.given;
    
    @QuarkusTest
    public class ThrowableMapperTest {
    
        @InjectMock
        CustomerService customerService;
    
        @Test
        public void throwUnexpectedRuntimeExceptionInCustomerService() {
            Mockito.when(customerService.findAll()).thenThrow(new RuntimeException("Completely Unexpected"));
            Response errorResponse = given()
                    .when()
                    .get("/customers")
                    .then()
                    .statusCode(500)
                    .extract().response();
            errorResponse.prettyPrint();
        }
    
    }

    Here's the response JSON structure:

    
    {
        "details": "Error id 327095e1-b8fa-491e-b3f1-e8944bd8023c-1, java.lang.RuntimeException: Completely Unexpected",
        "stack": "java.lang.RuntimeException: Completely Unexpected\n\tat com.redhat.customer.CustomerService_ClientProxy.findAll(Unknown Source)\n\tat com.redhat.customer.CustomerResource.get(CustomerResource.java:43)\n\tat com.redhat.customer.CustomerResource$quarkusrestinvoker$get_458c9ef52b4c83180ab57cf43ffebc046fc42b84.invoke(Unknown Source)\n\tat org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)\n\tat org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:7)\n\tat org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:141)\n\tat io.quarkus.vertx.core.runtime.VertxCoreRecorder$13.runWith(VertxCoreRecorder.java:543)\n\tat org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)\n\tat org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)\n\tat org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)\n\tat org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)\n\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\n\tat java.base/java.lang.Thread.run(Thread.java:833)"
    }
    
    

    This response is pretty gnarly, and the API consumer would have to perform a ton of parsing to get useful information out of it. The stack field also leaks information about our implementation, which is a security problem. While the framework has transformed a RuntimeException into a well-formed HTTP response with the proper HTTP status code, the result is unusable.

    Out of the box: ConstraintViolationException

    Now that you've seen the results of an unexpected exception, let's take a look at the constraint violation use case. This application uses the Hibernate Validator framework to validate data objects as they are passed between layers. The Customer object has @NotEmpty validations on both the firstName and lastName fields, along with an @Email validator on the email field that will validate the value of the String as a well-formed email address if present. When the method parameter for the Customer object has a @Valid annotation, the validator framework will perform the validations. If any constraint violations are present, the framework will throw the ConstraintViolationException.

    To see what happens out of the box, you can temporarily create a new test based on one of the existing tests. This temporary test will send a Customer object with a null firstName field:

    
        @Test
        public void postFailNoFirstNameResponse() {
            Customer customer = createCustomer();
            customer.setFirstName(null);
            Response errorResponse = given()
                    .contentType(ContentType.JSON)
                    .body(customer)
                    .post()
                    .then()
                    .statusCode(400)
                    .extract().response();
            errorResponse.prettyPrint();
        }
    

    Here's the JSON response:

    
    {
        "title": "Constraint Violation",
        "status": 400,
        "violations": [
            {
                "field": "post.customer.firstName",
                "message": "must not be empty"
            }
        ]
    }
    

    While it's a vast improvement over the unexpected runtime exception case, the structure and content are still insufficient. The response structure is too specific to the underlying implementation. You want a more generic design that can handle all error responses.

    Update the validation messages

    Building a better error response model begins with the error messages. How you implement those messages will depend on your need for internationalization. If your API is scoped for use in a single language, the annotations themselves can use default message interpolation by passing a message parameter to the annotation. This method supports building messages using message expressions:

    
    @NotEmpty(message = "Customer's first name is required.")
    private String firstName;
    

    If your application needs to support internationalization, you should use message properties files. Name these files using the pattern ValidationMessages_loc.properties, substituting the appropriate locale abbreviation (en, es, etc.) for loc, so developers can build message bundles for every language. Here's an example of the ValidationMessages.properties file, the default, non-locale-specific message bundle placed in the src/main/resources folder:

    
    System.error=An unexpected error has occurred. Please contact support.
    Customer.firstName.required=Customer's first name is required
    Customer.lastName.required=Customer's last name is required
    Customer.email.invalid=Customer's email address is invalid
    

    With this method, you can use the keys of the message bundle to express the correct validation message as part of the annotation:

    
    @NotEmpty(message = "{Customer.firstName.required}")
    private String firstName;
    

    Once you've implemented this response model, your response JSON content will contain much more helpful error messages:

    
    {
        "title": "Constraint Violation",
        "status": 400,
        "violations": [
            {
                "field": "post.customer.firstName",
                "message": "Customer's first name is required"
            }
        ]
    }
    

    Now that you have taken care of the error message aspect of the application, you can restructure the error response.

    Model the error response

    Modeling the error response can be a point of contention in application architecture because no full-featured standards govern the response structure. There have been some attempts at standardization, such as a Request for Comments (RFC) published in 2016 called Problem details for HTTP APIs. This RFC is a good start for standardizing things like the field names of the response structure. However, the design doesn't easily support common use cases like the constraint violation, so the result is a collection of errors for a single response.

    When it comes to modeling errors, developers must choose between multiple structures or a single unified structure. Using multiple structures requires the consumer to perform introspection on the error structure to determine where the data resides. In contrast, a single suitable structure requires the consumer to create an implied model where the cardinality of the messages is the focal point. Here's are a couple of examples of how an error structure response might be modeled:

    
    // System Error Response - Single
    {
        "errorId": "971e0747-f7ec-4d21-915b-c66257db05c3",
        "message": "An unexpected error has occurred. Please contact support"
    }
    
    // System Error Response - Multiple
    {
        "errorId": "971e0747-f7ec-4d21-915b-c66257db05c3",
        "errors": [
            {
                "message": "An unexpected error has occurred. Please contact support"
            }
        ]
    }
    

    For this article, we will focus on creating a single unified error response structure. This structure should handle both application and system errors, whether they are expected or not:

    
    package com.redhat.exception;
    
    import com.fasterxml.jackson.annotation.JsonInclude;
    import lombok.EqualsAndHashCode;
    import lombok.Getter;
    
    import java.util.List;
    
    @Getter
    @EqualsAndHashCode
    public class ErrorResponse {
    
        @JsonInclude(JsonInclude.Include.NON_NULL)
        private String errorId;
        private List<ErrorMessage> errors;
    
        public ErrorResponse(String errorId, ErrorMessage errorMessage) {
            this.errorId = errorId;
            this.errors = List.of(errorMessage);
        }
    
        public ErrorResponse(ErrorMessage errorMessage) {
            this(null, errorMessage);
        }
    
        public ErrorResponse(List<ErrorMessage> errors) {
            this.errorId = null;
            this.errors = errors;
        }
    
        public ErrorResponse() {
        }
    
        @Getter
        @EqualsAndHashCode
        public static class ErrorMessage {
    
            @JsonInclude(JsonInclude.Include.NON_NULL)
            private String path;
            private String message;
    
            public ErrorMessage(String path, String message) {
                this.path = path;
                this.message = message;
            }
    
            public ErrorMessage(String message) {
                this.path = null;
                this.message = message;
            }
    
            public ErrorMessage() {
            }
        }
    
    }

    Some implementation notes:

    • The application uses Lombok to manage the boilerplate code, as in the previous article in this series.
      • The @Getter annotation creates getters for the class variables and the @EqualsAndHashCode annotation autogenerates the equals and hashCode methods using a default methodology in which all class variables are used for both implementations.
      • As mentioned in the last article, you should use libraries such as Lombok with extreme care. These annotations can sometimes create generated implementations with easily hidden performance or runtime execution impacts.
    • The @JsonInclude(JsonInclude.Include.NON_NULL) annotation provides instructions to not serialize the item if it is null.

    Catching Throwable: Introduction to the ExceptionMapper

    Now that you have the data structure for the error response, you can begin to model how to handle exceptions in the application. In Quarkus, you can use the JAX-RS ExceptionMapper class to perform the necessary transformations between exceptions and your error response.

    The exception mapper infrastructure starts by catching Throwable, which is the ultimate superclass of all errors and exceptions, checked and unchecked. By catching Throwable in an ExceptionMapper, you can ensure that all unexpected exceptions can be caught, processed, logged, and transformed to the ErrorResponse. This will eliminate the unwanted behavior in which an unexpected exception might leak stack traces and other internals back to the consumer.

    
    package com.redhat.exception;
    
    import lombok.extern.slf4j.Slf4j;
    
    import javax.ws.rs.core.Response;
    import javax.ws.rs.ext.ExceptionMapper;
    import javax.ws.rs.ext.Provider;
    import java.util.ResourceBundle;
    import java.util.UUID;
    
    @Provider
    @Slf4j
    public class ThrowableMapper implements ExceptionMapper<Throwable> {
    
        @Override
        public Response toResponse(Throwable e) {
            String errorId = UUID.randomUUID().toString();
            log.error("errorId[{}]", errorId, e);
            String defaultErrorMessage = ResourceBundle.getBundle("ValidationMessages").getString("System.error");
            ErrorResponse.ErrorMessage errorMessage = new ErrorResponse.ErrorMessage(defaultErrorMessage);
            ErrorResponse errorResponse = new ErrorResponse(errorId, errorMessage);
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errorResponse).build();
        }
    
    }
    

    Some implementation notes:

    • The implementation uses the Lombok @Slf4j annotation to create a private, final, and static reference to a log variable that uses the Simple Logging Facade for Java (SLF4J).
    • ResourceBundle.getBundle("ValidationMessages") allows you to get a specific error message from the configured validation messages. You'll learn more about the details of this when you dive into the constraint violation exception scenario in the next section.
    • In the toResponse method, a unique identifier for the error is created using UUID. This identifier is used when generating the log statements to facilitate debugging in a production environment. If you pass the unique identifier back to the consumer, the consumer can then reference the identifier in a support ticket, which the support team can use to quickly search the logs and identify what the cause of the error might be.
    • Once the error is logged, it is then transformed into the ErrorResponse object and returned to the consumer with an HTTP status code of 500, denoting a generic internal server error.

    ConstraintViolationExceptionMapper

    Now, consider the constraint violation use case once again. What you would like to see is a response message body produced with an array of error messages. To model this, you can use the existing ErrorResponse and write a new ConstraintViolationExceptionMapper to handle the specific exception:

    
    package com.redhat.exception;
    
    import javax.validation.ConstraintViolationException;
    import javax.ws.rs.core.Response;
    import javax.ws.rs.ext.ExceptionMapper;
    import javax.ws.rs.ext.Provider;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Provider
    public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
    
        @Override
        public Response toResponse(ConstraintViolationException e) {
            List<ErrorResponse.ErrorMessage> errorMessages = e.getConstraintViolations().stream()
                    .map(constraintViolation -> new ErrorResponse.ErrorMessage(constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage()))
                    .collect(Collectors.toList());
            return Response.status(Response.Status.BAD_REQUEST).entity(new ErrorResponse(errorMessages)).build();
        }
    
    }

    With both of these mechanisms in place, the error response body is exactly what you're looking for:

    {
        "errors": [
            {
                "path": "post.customer.lastName",
                "message": "Customer's last name is required"
            },
            {
                "path": "post.customer.email",
                "message": "Customer's email address is invalid"
            },
            {
                "path": "post.customer.firstName",
                "message": "Customer's first name is required"
            }
        ]
    }

    Why your enterprise should standardize its error model

    Using the common ErrorResponse object and creating ExceptionMapper implementations for all of your use cases gives you a unified and easily consumable API error model. This approach works great for an individual team; however, another problem arises when all of the development teams in an enterprise design their error models independently and end up with different naming conventions and structures, especially in a polyglot world. The API consumers, whether they are other microservices or front-end developers building UIs, end up toiling over the myriad of different error responses.

    This is a great opportunity for enterprise standardization. Bringing together the API teams in a common guild allows everyone in the company to come together in building and publishing a common standard for error responses. This eliminates toil on the API consumer end and reduces API development work by utilizing existing, documented response structures and even building shared libraries for use across teams.

    Last updated: September 20, 2023

    Related Posts

    • Build a REST API from the ground up with Quarkus 2.0

    • Why should I choose Quarkus over Spring for my microservices?

    • Authentication and authorization using the Keycloak REST API

    Recent Posts

    • More Essential AI tutorials for Node.js Developers

    • How to run a fraud detection AI model on RHEL CVMs

    • How we use software provenance at Red Hat

    • Alternatives to creating bootc images from scratch

    • How to update OpenStack Services on OpenShift

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue