Controller Advice - Exception Handlers

Clean and reusable error handling through Spring @ControllerAdvice

Posted on 24 December 2016

Spring is a framework that gives you great flexibility and extensibility. Sometimes the flexibility is even a bit overwhelming; there are often multiple roads leading to Rome. In this blog post I am going to show how we approach error handling in our Spring Boot based micro service project.

The code can be found at this repository.

Introduction

When you are building a REST service you will always have to handle exception flows. A client might misbehave and send the wrong headers or request parameters for example, or a certain resource does not exist while the client thinks it does. Fortunately Spring makes handling these exception flows quite easy and convenient by handling most of the boilerplate for us.

However; many of the examples found online point to annotating your exceptions as the way to handle exception flows. I will present an example of this practice even though I am not a fan of handling errors this way; it leads to your domain classes (what exceptions often are) 'knowing' about your REST layer.

I will then show a better approach by using Controller Advice handlers, show some ways to reuse code and also show how easily these can be tested using Spring MockMvc.

Lombok is used to handle most boilerplate so if you aren’t familiar with it I would suggest you check it out first.

Exceptions with Annotations

The 'easiest' way to handle exceptions in Spring is to simply annotate them with the @ResponseStatus exception:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User not found")
public class UserNotFoundException extends RuntimeException {
}

This example will result in Spring returning a 404 with a "User not found" message when this exception gets thrown. While this is an easy way to attach HTTP status codes to thrown exceptions (and requires a lot less code than handling this in every controller) it’s not very flexible and leads to a rather sloppy architecture where your model contains exceptions that 'know' about the view layer. If you would ever want to separate these classes into a separate library you would need to have that library depend on the imported Spring classes shown above.

Fortunately Spring supports a better approach.

Controller Advice

Spring’s @ControllerAdvice annotation is a specialized form of @Component that exists to handle cross cutting concerns for controllers. The most common use case is to provide exception handlers, but it can do a few other neat things as well. It can for example intercept the responses from controllers. In this post I will focus on Exception Handling.

So let’s start with a very basic controller advice to handle a few exceptions:

@ControllerAdvice
@Slf4j
public class ExceptionHandlers {
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public ErrorResponse handleUserNotFoundException(final UserNotFoundException ex) {
        log.error("User not found thrown", ex);
        return new ErrorResponse("USER_NOT_FOUND", "The user was not found");
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ErrorResponse handleThrowable(final Throwable ex) {
      log.error("Unexpected error", ex);
      return new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected internal server error occured");
    }

    @Data
    public static class ErrorResponse {
        private final String code;
        private final String message;
    }
}

In the example above I have created two exception handlers. One for our UserNotFoundException and one 'catch all' exception for Throwable (which will be the superclass for almost anything that can go wrong). The first will return a 404 "Not Found" response while the latter will return a 500 "Internal Server Error" code. The @Slf4j annotation is, in case you haven’t seen it before, a Lombok annotation that provides the Slf4j logger 'log' for me. The @ControllerAdvice annotation makes sure that Spring component-scans and loads the controller advice.

If we take a look at the UserNotFound handler the @ExceptionHandler annotation tells Spring that this is an exception handler for the UserNotFoundException class. Within we create a nice simple ErrorResponse DTO that returns an error code and description. The @ResponseStatus annotation tells Spring that we want a 404 "Not Found" HTTP response code on this response.

As you can see this approach solves two problems already. First of all our UserNotFoundException does not need to know about HTTP response codes anymore; we can leave that exception as simple as we want. Secondly we have a lot more flexibility in how we handle our error states; we can return any response and execute any logic inside our handler.

Code reuse

In the team I’m currently working in we use a micro service architecture. Since I am, like most developers, quite lazy I would prefer to reuse as much code as possible over our 12 or so services. Fortunately this works perfectly fine with Spring ControllerAdvice; ErrorHandlers defined in a base class get picked up in a class extending it. So I can split it into two separate classes. First we define our BaseExceptionHandler:

public abstract class BaseExceptionHandler {
    private final Logger log;

    protected BaseExceptionHandler(final Logger log) {
        this.log = log;
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ExceptionHandlers.ErrorResponse handleThrowable(final Throwable ex) {
        log.error("Unexpected error", ex);
        return new ErrorResponse("INTERNAL_SERVER_ERROR", "An unexpected internal server error occured");
    }

    @Data
    public static class ErrorResponse {
        private final String code;
        private final String message;
    }
}

This abstract base class contains the shared logic and shared handlers for all our services (and would be inside a reusable library). In this example it only has the 'catch-all' handler but there are actually quite a few common exception scenario’s that you need to handle in a Spring Service. A few to name are:

  • MissingServletRequestParameterException: when you’re missing a parameter

  • MethodArgumentTypeMismatchException: when your parameter has the wrong type

  • HttpRequestMethodNotSupportedException: when you do for example a GET on a POST route

  • ServletRequestBindingException: when you’re missing a required header

These can all be implemented in the reusable base handler.

This makes our service specific handler quite a bit simpler:

@ControllerAdvice
@Slf4j
public class ExceptionHandlers extends BaseExceptionHandler {
    public ExceptionHandlers() {
        super(log);
    }

    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public ErrorResponse handleUserNotFoundException(final UserNotFoundException ex) {
        log.error("User not found thrown", ex);
        return new ErrorResponse("USER_NOT_FOUND", "The user was not found");
    }
}

But still; if all you do for all exceptions is to log the exception and return an error specific JSON with a code, a message and a HTTP status code, do we really need 7 lines of code for every one of them? Fortunately we don’t!

Smarter base handler

Since these exception handlers basically do look-ups based on the exception class we can make the service handlers even simpler if we add some more logic to our base handler:

public abstract class BaseExceptionHandler {
    private static final ExceptionMapping DEFAULT_ERROR = new ExceptionMapping(
            "SERVER_ERROR",
            "Internal server error",
            INTERNAL_SERVER_ERROR);

    private final Logger log;
    private final Map<Class, ExceptionMapping> exceptionMappings = new HashMap<>();

    public BaseExceptionHandler(final Logger log) {
        this.log = log;
    }

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public ErrorResponse handleThrowable(final Throwable ex, final HttpServletResponse response) {
        ExceptionMapping mapping = exceptionMappings.getOrDefault(ex.getClass(), DEFAULT_ERROR);

        response.setStatus(mapping.status.value());

        log.error("{} ({}): {}", mapping.message, mapping.code, ex.getMessage(), ex);

        return new ErrorResponse(mapping.code, mapping.message);
    }

    protected void registerMapping(
            final Class<?> clazz,
            final String code,
            final String message,
            final HttpStatus status) {
        exceptionMappings.put(clazz, new ExceptionMapping(code, message, status));
    }

    @Data
    public static class ErrorResponse {
        private final String code;
        private final String message;
    }

    @AllArgsConstructor
    private static class ExceptionMapping {
        private final String message;
        private final String code;
        private final HttpStatus status;
    }
}

So now all our exception handling goes through the catch-all handler. It looks up the predefined code, message and status code in the exceptionMappings map. Now we can simplify our exception handler in our service to this:

@ControllerAdvice
@Slf4j
public class ExceptionHandlers extends BaseExceptionHandler {
    public ExceptionHandlers() {
        super(log);
        registerMapping(UserNotFoundException.class, "USER_NOT_FOUND", "User not found", HttpStatus.NOT_FOUND);
    }
}

We can register the service specific exceptions via the registerMapping method inside our ExceptionHandlers class. So all that is left is basically a bit of configuration; one line per exception. If we would ever need a more customized handler for a specific exception we can still add them in the separately.

The example project contains a more complete version of the base handler and error handler so please check these out to see how it would work in a 'real' service.

Now that we have created these handlers, let’s also add some code to test them!

Spring Controller tests

Spring Boot provides spring-boot-starter-test for you as a convenient way to test your Spring Boot service. So lets have a look at a really simple controller test:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class UserControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        mockMvc = webAppContextSetup(this.wac).build();
    }

    @Test
    public void testGetUserById_InvalidId() throws Exception {
        mockMvc.perform(get("/user/foo"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code", is("ARGUMENT_TYPE_MISMATCH")))
                .andExpect(jsonPath("$.message", is("Argument type mismatch")));
    }
}

The SpringJUnit4ClassRunner runner and @SpringBootTest annotation make sure you have enough of a Spring Web Application Context (injected via @Autowired annotation) to be able to perform your tests. Spring MockMvc lets you call your MVC endpoints and perform assertions on the result. jsonPath lets us easily reach into the JSON result to grab parts of it to test.

The full source of the test can be found here.

Conclusion

Spring’s Controller Advice based exception handlers are both powerful and flexible. They allow us to solve the cross cutting concern of exception handling in a single place without forcing us to change our architecture. They are powerful and extensible enough to let us employ basic OO reusability patterns. As such I consider them the go to approach when it comes to handling exceptions.