Custom Error Handling in REST Controllers with Spring Boot

Custom Error Handling in REST Controllers with Spring Boot

This guide shows you how to implement custom error handling in Spring Boot. We use not only the well-known ControllerAdvice and ExceptionHandler annotations but also DefaultErrorAttributes and ErrorController to make your custom error attributes uniform and consistent.

Table of Contents

Introduction

First, this guide covers the basic topics: how to capture an exception at controller level in Spring Boot and map it to a given HTTP status code. We’ll do that with the @ExceptionHandler and @ControllerAdvice annotations.

Then, we’ll see why we shouldn’t stop there and go one step further to have advanced, production-ready error handling in Spring Boot. In the sample code, we’ll show how to achieve this by customizing the error responses with our own Error Attributes and overriding the default ErrorController.

All the code in this post is available on GitHub: Spring Boot REST Exceptions. If you find it useful, please give it a star!
Custom error handling in Spring Boot (REST controllers)
Custom error handling in Spring Boot (REST controllers)

The sample application

I’ll use as a base for this post part of the Spring Boot app I created for the Guide to Testing Controllers in Spring Boot: SuperHeroes. It has a 2-layer composition with a Controller and a Repository that retrieves SuperHero entities from a hardcoded map in code. More than enough for what we want to demonstrate.

To be able to “switch on and off” the different configurations, I included three properties in the Spring Boot’s application.properties file: controlleradvice, attributes and controller. We’ll see how to use them later in this post.

The SuperHero class:

SuperHero Domain class
package io.tpd.superheroes.domain;

import javax.validation.constraints.NotBlank;

public final class SuperHero {

    @NotBlank
    private String firstName;
    private String lastName;
    @NotBlank
    private String heroName;

    // Empty constructor is needed for Jackson to recreate the object from JSON
    public SuperHero() {
    }

    public SuperHero(String firstName, String lastName, String heroName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.heroName = heroName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public String getHeroName() {
        return heroName;
    }

    // equals and hashcode
}

These are the repository interface and the in-memory implementation:

SuperHero Repository
package io.tpd.superheroes.repository;

import io.tpd.superheroes.domain.SuperHero;

import java.util.Optional;

/**
 * Provides access to io.tpd.superheroes' data
 * @author moises.macero
 */
public interface SuperHeroRepository {

    /**
     * Retrieves a super hero by the id.
     * If the id does not exist, a {@link io.tpd.superheroes.exceptions.NonExistingHeroException} will be thrown.
     *
     * @param id the unique id of the super hero
     * @return the SuperHero details
     */
    SuperHero getSuperHero(int id);

    /**
     * Retrieves a super hero given his super hero alias.
     *
     * @param heroName the super hero name
     * @return the SuperHero details
     */
    Optional<SuperHero> getSuperHero(String heroName);

    /**
     * Saves the super hero.
     *
     * @param superHero the details of the super hero
     */
    void saveSuperHero(SuperHero superHero);

}

SuperHero Repository Impl
package io.tpd.superheroes.repository;

import io.tpd.superheroes.domain.SuperHero;
import io.tpd.superheroes.exceptions.NonExistingHeroException;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Simple, In-memory implementation of SuperHero Repository. It comes with some predefined data.
 *
 * @author moises.macero
 */
@Component
class SuperHeroRepositoryImpl implements SuperHeroRepository {

    private List<SuperHero> superHeroList;

    public SuperHeroRepositoryImpl() {
        superHeroList = new ArrayList<>();
        superHeroList.add(new SuperHero("Jean", "Grey", "Phoenix"));
        superHeroList.add(new SuperHero("Bruce", "Wayne", "Batman"));
        superHeroList.add(new SuperHero("Susan", "Storm", "Invisible woman"));
        superHeroList.add(new SuperHero("Peter", "Parker", "Spiderman"));
    }

    @Override
    public SuperHero getSuperHero(int id) {
        if (id > superHeroList.size()) throw new NonExistingHeroException("Sorry, there are only 4 superheroes...");
        return superHeroList.get(id - 1);
    }

    @Override
    public Optional<SuperHero> getSuperHero(String heroName) {
        return superHeroList.stream().filter(h -> h.getHeroName().equals(heroName)).findAny();
    }

    @Override
    public void saveSuperHero(SuperHero superHero) {
        superHeroList.add(superHero);
    }
}

As you see in code, the method to get a superhero by id may throw a custom exception. The NonExistingHeroException extends in our case from RuntimeException, what makes it an unchecked exception.

Get the book Practical Software Architecture

Why do we need common error handling at all?

In Java, exceptions will just happen. If you’re building the common 3-tier application, they might be coming from your repository layer, business logic and/or controllers. You can keep the most relevant ones to you under control if they are somehow expected during your flow and you can recover from them. Many other exceptions will be just propagated to the controller level and it’s up to you to decide what to do with them.

Handling exceptions in code

If you decide to go the low-level way, you can find yourself surrounding all the methods that implement endpoints with try-catch blocks and deciding which error response and message should be returned by inspecting at the error. Maybe you can even extract some common logic, but still, you’ll have a lot of boilerplate try-catch blocks.

The Spring Web module comes with some tools that you can use to avoid all that unnecessary code. Let’s review in the coming sections the basics and also some advanced tips to make sure you build something you can be confident to deliver to production.

Why not letting exceptions to be managed by the server?

In case you don’t do anything with exceptions, they’ll be managed by the framework or the web server you use, normally following some default behavior to convert them to HTTP responses. These responses are just ugly, may expose your internal logic and your API consumers won’t know what to do after they get them. Not a good idea.

In practice: Spring Boot app with default error handling

Remember: all the code in this post is available on GitHub: Spring Boot REST Exceptions. If you find it useful, please give it a star!

To follow this guide, you can either clone the repository from Github or build your own from scratch. Just bear in mind that I won’t include all the code in this post to keep it focused on the error handling, although I’ll link the most relevant classes.

If you cloned the repository, make sure all configuration properties are set to false:

application.properties
superheroes.errors.controlleradvice = false
superheroes.errors.attributes = false
superheroes.errors.controller = false

Then our application context won’t include any customizations or specific configuration. It’s just an app with a controller and a repo that throws an exception. Now, you can use your preferred IDE to run the app or, from the command line:

Running the app
./mvnw spring-boot:run

Let’s make some HTTP calls and see what happens. I use HTTPie since it’s available for multiple platforms, and I’m also trimming part of the responses that are not relevant for the explanation. You can also use other tools like curl or the UI-powered Postman.

Case 1: Get a Superhero

Call the GET endpoint with a proper ID and you’ll get a superhero in JSON format.

Getting a superhero
$ http localhost:8080/superheroes/2
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
{
    "firstName": "Bruce",
    "heroName": "Batman",
    "lastName": "Wayne"
}

Case 2: Force the exception

If the ID is bigger than the size of the list, the application throws a NonExistingHeroException. Without a proper ExceptionHandler, this ends up with the stacktrace in the application logs and a 500 Internal Server Error status. Not nice at all, since it gives the impression that something failed on the server’s side but actually this should be treated as a Bad Request (let’s assume we told our clients in docs that only indexes 1-4 are available).

Exception without ExceptionHandler
$ http localhost:8080/superheroes/10
HTTP/1.1 500
Content-Type: application/json;charset=UTF-8
{
    "error": "Internal Server Error",
    "message": "Sorry, there are only 4 superheroes...",
    "path": "/superheroes/10",
    "status": 500,
    "timestamp": "2019-09-01T08:45:07.099+0000"
}

Pay also attention to the format of the error. It’s a JSON structure with a few generic fields. This is actually coming from Spring Boot, which injects a default error controller BasicErrorController [javadoc] (injected by default in the web context) that uses a set of DefaultErrorAttributes [javadoc] (injected as a bean in the context). Spoiler: we’ll override both these beans later in this guide, to perform a fully customized error handling with Spring Boot.

Case 3: Try getting a superhero in HTML

Rest Controllers in Spring Boot will return application/json by default but, what if we try to get a superhero asking for HTML content?

Get a hero in HTML
$ http localhost:8080/superheroes/1 Accept:'text/html'
HTTP/1.1 406
Content-Type: text/html;charset=UTF-8

<html><body><h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, 
so you are seeing this as a fallback.</p>
<div id='created'>Sun Sep 01 10:58:53 CEST 2019</div>
<div>There was an unexpected error 
(type=Not Acceptable, status=406).</div>
<div>Could not find acceptable representation</div>
</body></html>

This is weird. We get something called Whitelabel Error Page, in HTML, even though we’re trying to build a web service that only spits JSON. It seems, at least, that the HTTP status code is consistent: 406 Not Acceptable.

The problem with this response is that, even though you could say it’s the client’s fault (asked for HTML), the error format is not consistent with the previous error format (JSON). Not a big deal but, if somebody is testing this service from a web browser, we can do better and don’t provide HTML at all if we are implementing a REST API that is supposed to deal only with JSON content.

Case 4: Try getting an invalid id using HTML

Let’s do the same again but now with an invalid id:

Get invalid superhero in HTML
$ http localhost:8080/superheroes/8 Accept:'text/html'
HTTP/1.1 500
Content-Type: text/html;charset=UTF-8

<html><body><h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error,
 so you are seeing this as a fallback.</p>
<div id='created'>Sun Sep 01 11:12:20 CEST 2019</div>
<div>There was an unexpected error 
(type=Internal Server Error, status=500).</div>
<div>Sorry, there are only 4 superheroes...</div>
</body></html>

This is even weirder. If we don’t accept HTML, why would we let an internal error to be propagated? Now they get a 500 status code and this error because the code finds the exception first (before Spring tries to convert the response).

Case 5: The classic 404

Let’s ask for an invalid resource and see what happens. First, accepting JSON (HTTPie requests default to JSON):

Resource not found in JSON
$ http localhost:8080/supers
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
{
    "error": "Not Found",
    "message": "No message available",
    "path": "/supers",
    "status": 404,
    "timestamp": "2019-09-01T09:18:08.725+0000"
}

Now, asking for HTML response:

Resource not found in HTML
$ http localhost:8080/supers Accept:'text/html'
HTTP/1.1 404
Content-Type: text/html;charset=UTF-8

<html><body><h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping 
for /error, so you are seeing this as a fallback.</p>
<div id='created'>Sun Sep 01 11:19:14 CEST 2019</div>
<div>There was an unexpected error 
(type=Not Found, status=404).</div>
<div>No message available</div></body></html>

Again, it’s at least debatable whether we should respond with a Not Found 404 page or just tell the client we don’t accept HTML (406), given that our intention is to build a REST API using JSON.

Conclusion: why do you need proper error handling in Spring Boot?

If you want your service to look robust and production-ready, it’s better than you use some of the available tools in Spring Boot to customize your error handling behavior instead of using the basic defaults.

In this post, we’ll focus on some core requirements:

  • Manage exceptions and translate them to proper HTTP Status Codes.
  • Avoid returning HTML responses in Spring Boot if we are only accepting JSON.
  • Unify Error formats so their structure always looks the same.

Basics: ControllerAdvice and ExceptionHandler in Spring

Controller Advice

A Controller Advice is just a kind of interceptor that surrounds the logic in your Controllers and allows you to apply some common logic to them. If you know Aspect-Oriented Programming, the word Advice will be familiar to you anyways.

You can simply annotate a class with @ControllerAdvice to make it the default one for all your controllers. If you prefer to have more than one, the annotation also allows you to specify to which packages, classes or types it applies.

Since Spring 4.3, there is a new @RestControllerAdvice annotation that is quite handy when you plan to write exception handlers. It’s a combination of @ControllerAdvice with @ResponseBody automatically added to all your methods annotated with @ExceptionHandler. The @ResponseBody annotation indicates that you’re sending a response body as a result of handling an error, which is what you typically want to do in a REST API if you want to keep your API consumers happy.

Exception Handlers

The Controller Advice may be used for a few different tasks but the most popular one is to capture exceptions from your application stack and translating them to HTTP responses in an organized manner. To achieve this, you use the @ExceptionHandler annotation in a method and indicate which type of Exception you want to handle. The request and the exception instance will be injected via method arguments if you specify them.

You can use this annotation together with @ResponseBody as indicated before, if you want to return contents in the response body. We won’t use it in the example since we use @RestControllerAdvice and therefore those come implicitly.

Also, you may want to use @ResponseStatus to set the HTTP status code that you want to return for that exception. However, given that we’re planning to create our own ResponseEntity with custom contents, we’ll include the response status when constructing the response.

In practice: Controller Advice and Exception Handler

The first step to improve our Spring Boot application to deal with exceptions is adding an Exception Handler for, at least, our custom exception.

We can start with something like this:

A basic ControllerAdvice
@RestControllerAdvice
@ConditionalOnProperty(name = "superheroes.errors.controlleradvice", 
                       havingValue = "true")
public class SuperHeroControllerAdvice {

    @ExceptionHandler(NonExistingHeroException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public void handleNonExistingHero() {
    }

}

If you’re running the repo example, you have to set superheroes.errors.controlleradvice=true and modify the final version of the code to look like the version above. Note that we’re adding the @ConditionalOnProperty annotation to be able to switch on and off capabilities for the sake of this tutorial. You can omit that part if you want.

Now, let’s check again the Case 2: Force the exception and see what the response is.

Empty response with basic exception handler
$ http localhost:8080/superheroes/10
HTTP/1.1 404
Content-Length: 0
Date: Sun, 01 Sep 2019 10:38:13 GMT


Cool? Now the response status code is 404 Not Found, that’s exactly what we wanted. We also got rid of the errors in the logs (stacktrace) since this exception is now captured. However, what about the response body? It seems that we have overridden the default behavior and, since our method doesn’t return a response, this is translated to a response without body. Not cool, because the case 5 (asking for a non existing resouce e.g. /supers/3) still returns a body using Spring Boot’s DefaultErrorAttributes. So, we have a new inconsistency.

We can fix that, by adding a ResponseEntity return result to our exception handler method. You may think that it’s even possible to instantiate a DefaultErrorAttributes object and just mimic what Spring Boot is doing by default. But that is not the intention of that class, you can’t simply create a new instance and pass the attribute values you want.

A workaround would be creating a bean that has exactly the same fields as the DefaultErrorAttributes class. Or you could simply return a Map from the exception handler. However, this feels a bit hacky because you would be adapting your logic to the default one embedded in Spring Boot. You would also need to know part of the internals to return consistent responses.

So, how do we proceed? Simple: we can just create our own ErrorAttributes bean and inject it in the context. We’ll be then overriding the DefaultErrorAttributes with our custom implementation and we can use that error structure for all our errors, instead of the default one.

Creating a Custom Error Schema for Exception Handlers

Custom Error Example

The idea is simple, we want to use our own error format and replace these default fields:

Error fields returned in Spring Boot
{
    "error": "Not Found",
    "message": "No message available",
    "path": "/supers/3",
    "status": 404,
    "timestamp": "2019-09-01T09:18:08.725+0000"
}

For this example, we will try to convert all our API errors in Spring Boot using the Google JSON Style Guide. By doing that, I’m not recommending you to do this in your projects; it’s just an example. You may have your own error style guide (simpler than this) or use any other convention available on the Internet.

In the Google JSON Style Guide, this is the format of the API errors:

Error example in Google JSON style guide
{
  "apiVersion": "2.0",
  "error": {
    "code": 404,
    "message": "File Not Found",
    "errors": [{
      "domain": "Calendar",
      "reason": "ResourceNotFoundException",
      "message": "File Not Found
    }]
  }
}

The specification is not very strict so you may have your own interpretation for fields like code or domain.

Get the book Practical Software Architecture

Custom error attributes using Google JSON style guide with Spring Boot

First, we model this error structure in Java. Note that, for the sake of the example, I simplified the logic in this class so it only contains one error in the array of errors proposed by the Google specs. I also added the field from specs sendReport [link] because it’s a useful example of how our API clients could report incidents using a unique identifier that we can correlate in our backend service.

Model Google JSON style guide in Java
package io.tpd.superheroes.controller.errors;

import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.List;
import java.util.Map;
import java.util.UUID;

public class SuperHeroAppError {

    private final String apiVersion;
    private final ErrorBlock error;

    public SuperHeroAppError(final String apiVersion, final String code, final String message, final String domain,
                             final String reason, final String errorMessage, final String errorReportUri) {
        this.apiVersion = apiVersion;
        this.error = new ErrorBlock(code, message, domain, reason, errorMessage, errorReportUri);
    }

    public static SuperHeroAppError fromDefaultAttributeMap(final String apiVersion,
                                                            final Map<String, Object> defaultErrorAttributes,
                                                            final String sendReportBaseUri) {
        // original attribute values are documented in org.springframework.boot.web.servlet.error.DefaultErrorAttributes
        return new SuperHeroAppError(
                apiVersion,
                ((Integer) defaultErrorAttributes.get("status")).toString(),
                (String) defaultErrorAttributes.getOrDefault("message", "no message available"),
                (String) defaultErrorAttributes.getOrDefault("path", "no domain available"),
                (String) defaultErrorAttributes.getOrDefault("error", "no reason available"),
                (String) defaultErrorAttributes.get("message"),
                sendReportBaseUri
        );
    }

    // utility method to return a map of serialized root attributes,
    // see the last part of the guide for more details
    public Map<String, Object> toAttributeMap() {
        return Map.of(
          "apiVersion", apiVersion,
          "error", error
        );
    }

    public String getApiVersion() {
        return apiVersion;
    }

    public ErrorBlock getError() {
        return error;
    }

    private static final class ErrorBlock {

        @JsonIgnore
        private final UUID uniqueId;
        private final String code;
        private final String message;
        private final List<Error> errors;

        public ErrorBlock(final String code, final String message, final String domain,
                          final String reason, final String errorMessage, final String errorReportUri) {
            this.code = code;
            this.message = message;
            this.uniqueId = UUID.randomUUID();
            this.errors = List.of(
                    new Error(domain, reason, errorMessage, errorReportUri + "?id=" + uniqueId)
            );
        }

        private ErrorBlock(final UUID uniqueId, final String code, final String message, final List<Error> errors) {
            this.uniqueId = uniqueId;
            this.code = code;
            this.message = message;
            this.errors = errors;
        }

        public static ErrorBlock copyWithMessage(final ErrorBlock s, final String message) {
            return new ErrorBlock(s.uniqueId, s.code, message, s.errors);
        }

        public UUID getUniqueId() {
            return uniqueId;
        }

        public String getCode() {
            return code;
        }

        public String getMessage() {
            return message;
        }

        public List<Error> getErrors() {
            return errors;
        }

    }

    private static final class Error {
        private final String domain;
        private final String reason;
        private final String message;
        private final String sendReport;

        public Error(final String domain, final String reason, final String message, final String sendReport) {
            this.domain = domain;
            this.reason = reason;
            this.message = message;
            this.sendReport = sendReport;
        }

        public String getDomain() {
            return domain;
        }

        public String getReason() {
            return reason;
        }

        public String getMessage() {
            return message;
        }

        public String getSendReport() {
            return sendReport;
        }
    }
}

The chosen field names and class structure follows the resulting JSON we want to achieve so we can let the JSON serializer do its job without including more conversion logic. Note that there is a handy method fromAttributeMap() to make the conversion from the default fields used by Spring Boot. You could also build them from scratch if you don’t want to depend on them, for that you could have a look at some of the logic included in DefaultErrorAttributes.

Returning the custom response from the Exception Handler

We have already defined our error format so we can start using it in the no-response-content ExceptionHandler we implemented in the first place. To do that, we just create a new error object and return it with a ResponseEntity wrapper.

Return custom error in ExceptionHandler
@ConditionalOnProperty(name = "superheroes.errors.controlleradvice", havingValue = "true")
@RestControllerAdvice
public class SuperHeroControllerAdvice {

    @Value("${superheroes.sendreport.uri}")
    private String sendReportUri;
    @Value("${superheroes.api.version}")
    private String currentApiVersion;

    @ExceptionHandler(NonExistingHeroException.class)
    public ResponseEntity<SuperHeroAppError> handleNonExistingHero(NonExistingHeroException ex) {
        final SuperHeroAppError error = new SuperHeroAppError(
                currentApiVersion,
                Integer.toString(HttpStatus.NOT_FOUND.value()),
                "This superhero is hiding in the cave",
                "superhero-exceptions",
                "You can't find this superhero right now. Try later.",
                "Saving someone",
                sendReportUri
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

}

With our new code, we run again the Case 2: Force the exception and see what the response is.

Custom error response in ExceptionHandler
$ http localhost:8080/superheroes/10
HTTP/1.1 404 
Content-Type: application/json;charset=UTF-8

{
    "apiVersion": "1.0",
    "error": {
        "code": "404",
        "errors": [
            {
                "domain": "superhero-exceptions",
                "message": "Saving someone",
                "reason": "You can't find this superhero right now. Try later.",
                "sendReport": "https://dummypage.sendreport.com?id=8c384f5c-8511-4cfd-95f6-71161958abfb"
            }
        ],
        "message": "This superhero is hiding in the cave"
    }
}

Looks better. What we got now is not only a 404 Not Found response but also an error message that follows the Google JSON style guide. In real life, we’d implement the web page that supports the report sending functionality.

We just started making errors homogeneous. Now, we only get this error if that particular exception is thrown since it’s in an exception handler. We want to avoid creating exception handlers for each exception in Java so let’s see how we can override the defaults in Spring Boot to customize the REST API error format returned.

Customize REST error responses in Spring Boot

Overriding DefaultErrorAttributes returned by controllers

Once we have defined our error structure, we have to replace the default configuration.

Remember: all the code in this post is available on GitHub: Spring Boot REST Exceptions. If you find it useful, please give it a star!

First, we create a class that extends DefaultErrorAttributes. As indicated before, you can also go from scratch and create your own implementation implementing the interface ErrorAttributes.

Extending DefaultErrorAttributes
package io.tpd.superheroes.controller.errors;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

class SuperHeroAppErrorAttributes extends DefaultErrorAttributes {
    private final String currentApiVersion;
    private final String sendReportUri;

    public SuperHeroAppErrorAttributes(final String currentApiVersion, final String sendReportUri) {
        this.currentApiVersion = currentApiVersion;
        this.sendReportUri = sendReportUri;
    }

    @Override
    public Map<String, Object> getErrorAttributes(final WebRequest webRequest, final boolean includeStackTrace) {
        final Map<String, Object> defaultErrorAttributes = super.getErrorAttributes(webRequest, false);
        final SuperHeroAppError superHeroAppError = SuperHeroAppError.fromDefaultAttributeMap(
                currentApiVersion, defaultErrorAttributes, sendReportUri
        );
        return superHeroAppError.toAttributeMap();
    }
}

What we’re doing here is a kind of wrapper that overrides the default implementation of getErrorAttributes and replaces the default values by our own ones, using the mapper method we created in SuperHeroAppError. Note that the method signature in the interface ErrorAttributes requires us to return a Map; that’s why we created that method toAttributeMap() in SuperHeroAppError instead of serializing the complete object in JSON.

Now, we can add a Configuration class to override the default bean.

Override DefaultErrorAttributes
@ConditionalOnProperty(name = "superheroes.errors.attributes", havingValue = "true")
@Configuration
public class WebErrorConfiguration {

    @Value("${superheroes.api.version}")
    private String currentApiVersion;
    @Value("${superheroes.sendreport.uri}")
    private String sendReportUri;

    /**
     * We override the default {@link DefaultErrorAttributes}
     *
     * @return A custom implementation of ErrorAttributes
     */
    @Bean
    public ErrorAttributes errorAttributes() {
        return new SuperHeroAppErrorAttributes(currentApiVersion, sendReportUri);
    }

}

Again, we use here @ConditionalOnProperty just to be able to turn it on and off; you don’t need it in your code. As you see, the only thing we need is to inject an ErrorAttributes bean in the context to make Spring Boot not to inject the default implementation. The API version and base report URI come from the application.properties file.

In practice: Example of overriding ErrorAttributes in Spring Boot

Remember: all the code in this post is available on GitHub: Spring Boot REST Exceptions. If you find it useful, please give it a star!

Now, your configuration should look as follows:

Enabling Custom Error Attributes in Spring Boot
superheroes.api.version = 1.0
superheroes.sendreport.uri = https://dummypage.sendreport.com
superheroes.errors.controlleradvice = true
superheroes.errors.attributes = true
superheroes.errors.controller = false

After running the application, we can test what happens when any generic error occurs, for example the Case 5: a classic 404 Not Found.

Custom message format for REST errors in Spring Boot
http localhost:8080/supers
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8

{
    "apiVersion": "1.0",
    "error": {
        "code": "404",
        "errors": [
            {
                "domain": "/supers",
                "message": "No message available",
                "reason": "Not Found",
                "sendReport": "https://dummypage.sendreport.com?id=45d3c3d3-32d8-44ed-9a86-3856a8c28141"
            }
        ],
        "message": "No message available"
    }
}

What happens now is that our own implementation of ErrorAttributes is being injected in the context and used to build the error response. This logic lives in a BasicErrorController implementation [link] that Spring Boot injects by default in the web context when you use the web dependencies. It’s configured by default with the route /error, which is itself the one used by the web server to render the error when something wrong occurs.

The reason why I’m explaining the BasicErrorController logic is that it’s the part we’re going to customize next. Even though we already have a good base implementation to handle all kind of errors, it’s still possible to get a Whitelabel error page if you try the cases 4 or 5 described above (passing a header Accept: text/html).

Customize ErrorController to avoid HTML Whitelabel error pages

Basic error controller in Spring Boot

As introduced before, Spring Boot loads a default BasicErrorController [link] into the web application context when we use the web dependencies. This basic controller handles errors for both JSON and HTML response types. It’s also, as its name suggests, a basic implementation of AbstractErrorController, and it’s in this parent class where we find the ErrorAttributes field and logic that we’ve been taking advantage of.

Given that we only want to get rid of the HTML error response handling but we want to keep the ErrorAttributes, I’ll create for this application a new SuperHeroErrorController that extends AbstractErrorController. There is also the option to implement the interface ErrorController [link] from scratch.

In practice

We’ll use an even more simplified version of the Basic Controller:

Custom error controller in Spring Boot
@ConditionalOnProperty(name = "superheroes.errors.controller", havingValue = "true")
@RestController
@RequestMapping({SuperHeroErrorController.ERROR_PATH})
public class SuperHeroErrorController extends AbstractErrorController {

    static final String ERROR_PATH = "/error";

    public SuperHeroErrorController(final ErrorAttributes errorAttributes) {
        super(errorAttributes, Collections.emptyList());
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = this.getErrorAttributes(request, false);
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity<>(body, status);
    }

    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }
}

Again, the @ConditionalOnProperty annotation is there just to be able to switch this on and off. You don’t need it.

When we enable this controller, Spring Boot will detect that there is already one in the web context and won’t use the basic implementation. Our error controller doesn’t use any view resolvers (we pass an empty list) since we’re not using views from MVC, just building a REST API. We simply take any error and map it to our customized response, given that this controller will get our SuperHeroErrorAttributes via constructor injection.

If you cloned the repository, it’s time to set all properties as enabled:

Enable all properties
superheroes.api.version = 1.0
superheroes.sendreport.uri = https://dummypage.sendreport.com
superheroes.errors.controlleradvice = true
superheroes.errors.attributes = true
superheroes.errors.controller = true

Then, run the application and you can try some requests for HTML. You’ll get an empty-content response this time, with the corresponding HTTP status code 406 Not Acceptable.

No HTML at all
$ http localhost:8080/superheroes/3 Accept:'text/html'
HTTP/1.1 406
Content-Length: 0

The HttpMediaTypeNotAcceptableException also gets handled by our custom implementation of the controller and the response attributes are set, but we didn’t configure any HTML conversion for a SuperHeroAppError so the response body is empty. This is descriptive enough to flag any distracted HTML API clients with the status code, but you could also consider returning something else in response if you need it, via configuring a dedicated @ExceptionHandler for that exception.

Add custom error codes to REST error responses

To finish this guide, let’s cover a minor change in the sample code that can make your error responses even better for your API clients: the introduction of specific error codes.

In our example, we already have the ability to generate a unique identifier for each error that we could use to correlate support requests with internal logs (we don’t have any, but you can get the idea).

However, what we can also do is to generate custom application error codes to cover the most common error types. For instance, we could have a code like NE001 that goes together with our 404 responses but it’s more specific: it will be returned only if clients try to get IDs from the superheroes resource that don’t exist. This helps API clients because some HTTP status codes are too generic to determine exactly what went wrong, and you can use application codes instead of the description messages -which may vary- in your documentation.

First, we create an interface that any class may implement to provide a generic Error Code. We’ll use it for Exceptions.

public interface ErrorCode {

    /**
     * Provides an app-specific error code to help find out exactly what happened.
     * It's a human-friendly identifier for a given exception.
     *
     * @return a short text code identifying the error
     */
    String getErrorCode();
}

Next step is to implement it in the NonExistingHeroException class.

Exception returning an Error Code
public class NonExistingHeroException extends RuntimeException implements ErrorCode {

    public NonExistingHeroException(final String message) {
        super(message);
    }

    @Override
    public String getErrorCode() {
        return "NE-001";
    }
}

Now, we can use the error code in our exception handlers in the controller advice. The style guide we chose is not very strict about the code field so we’re not doing anything crazy here. Remember that we’re using the standard 404 code as the HTTP response status anyway.

Returning custom error codes from Exception Handler
    @ExceptionHandler(NonExistingHeroException.class)
    public ResponseEntity<SuperHeroAppError> handleNonExistingHero(HttpServletRequest request,
                                                                   NonExistingHeroException ex) {
        final SuperHeroAppError error = new SuperHeroAppError(
                currentApiVersion,
                ex.getErrorCode(),
                "This superhero is hiding in the cave",
                "superhero-exceptions",
                "You can't find this superhero right now. Try later.",
                "Saving someone",
                sendReportUri
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

If you now try to access a non-existing id, you’ll get this error in the customized response. As mentioned before, these application-specific error codes are sometimes very useful to give more information to your API clients, and they’re easier to find in docs compared to language-specific error messages.

Conclusions

In this guide, you learned how to create custom error responses in Spring Boot’s REST Controllers. We covered not only the classic @ControllerAdvice and @ExceptionHandler examples but also more advanced topics like changing error fields in the response using ErrorAttributes, and overriding the default BasicErrorController to be more in control about the content we show. In this case, we used this knowledge to avoid displaying the white label error page.

Having a consistent error format and proper error description is a big advantage when developing REST APIs. It helps your API consumers understand what exactly went wrong and improve user experience by displaying descriptive errors.

If you’re building a Microservices Architecture, this is normally one of the concerns that you want to extract as a common artifact. You could build your own small library with these classes and use it in your Microservices. You’ll get a consistent error handling mechanism even though you may have multiple Spring Boot applications.

Get the book Practical Software Architecture

I hope you find this guide useful. As always, if you want to learn more about other topics you can have a look at:

And don’t forget to comment and give a star to the GitHub repository!

Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Check my workshops

Amsterdam, The Netherlands https://thepracticaldeveloper.com

Comments