arrow-rightgithublinkedinscroll-to-topzig-zag

Enhance exception handling when building RESTful API with Spring Boot

Last Updated On

Table of Contents:

Introduction

We all know, that it's important to communicate the right information about what went wrong when building a REST API. Messages should be descriptive, otherwise, it's hard to tell where the actual problem is. Misleading or very tech riddled notice without proper context results into the bad user experience, wasted debug time... you name it.

Out of the box, Spring Boot comes with "Whitelabel Error Page", which aims more towards MVC pattern and returns an HTML page instead of desired JSON. In this article, we will take a look how to alter default Spring Boot configuration to return a JSON response with a proper error message, write some code to handle each exception individually and have a fallback mechanism with a default exception handler in a case where none of the implemented handlers will correspond to a thrown exception.

Building a small service as an example

In order to demonstrate a good error case, we need to build a RESTful web service first. Saying that we will create an API that accepts the following request:

http://localhost:8080/greeting?name=John

and produces the following response:

{
    "content": "Hello, John!"
}

Project configuration

I chose the most basic setup for the project using Spring Boot Initializr. It uses the latest version of Spring Boot (2.1.1) which under the hood contains (or speaking more professionally has a transient dependency of) Spring Core (5.1.3). I also utilized Maven, but you can choose Gradle or any other build tool of your preference. It really doesn't matter since our focus is on exception handling.

Project source code

It is a very minimalistic Spring Application that doesn't require a lot of code. Mentioned behavior can be achieved by adding two elements:

  • Controller that handles a request
  • Response object which will be represented as a simple POJO.

Controller's code:

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";

    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam String name) {
        return new Greeting(String.format(template, name));
    }
}

Greeting object:

public class Greeting {

    private final String content;

    public Greeting(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

The full project source code is available on on GitHub.

If this code or some of its parts are unfamiliar to you, please take a look at "Building a RESTful Web Service" guide page, where all of the Spring Boot basics are covered. Our REST API almost exactly replicates the sample provided on the official Spring site.

Handle exception in REST service

Our small service is capable of throwing an exception in at least in two cases. Particularly, when:

  • Request parameter name is missing (MissingServletRequestParameterException)
  • Resource path is not implemented (NoHandlerFoundException)

Both scenarios will produce unwanted "Whitelabel Error Page". The aim is to replace it with a proper JSON response. Let's start by removing whitelable page first.

Remove Whitelabel Error Page

This can be done by adding the following key-value pair to application.properties file:

server.error.whitelabel.enabled=false

As it's clear from the property and value itself, default whitelabel error page is disabled and will not be shown anymore.

Looking ahead, there are other two important pieces of configuration that we need to add:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

First one sets Spring to throw NoHandlerFoundException exception, which we can easily catch later on to generate a proper response. Second removes default mapping to static pages to allow us to return JSON (by default, Spring tries to find HTML page with the same name as a String returned from the Controller's method).

You can find the project at this stage on GitHub.

Now, since we have finished with properties and "Whitelabel Error Page" is gone, there should be an entry point where all exceptions will be handled and proper response will be returned. That's where ExceptionResolver will come into play.

Add ExceptionResolver

It is represented by a Controller or, to be precise, by a Controller Advice which will intercept exceptions across Spring Application. Java code for this functionality is pretty straightforward and looks in the following way:

@RestControllerAdvice
public class ExceptionResolver {

    @ExceptionHandler(Exception.class)
    public HashMap<String, String> handleException(HttpServletRequest request, Exception e) {
        HashMap<String, String> response = new HashMap<>();
        response.put("message", e.getMessage());
        return response;
    }
}

Few words about used annotations:

  • @RestControllerAdvice allows handling exceptions across the whole application. Basically, it acts as an Exception interceptor. If you used @ControllerAdvice annotation before, @RestControllerAdvice does the same, plus adds @ResponseBody annotation as a convenience (more info in official Javadoc).
  • Methods annotated with @ExceptionHandler are used to process an exception specified as an annotation parameter.

From now on, when an exception occurs, handleException method will be called and implemented JSON response will be generated.

However, it's still a good idea to handle exceptions individually and process them by type. For that purpose, let's setup specific handlers and tell a consumer about incorrect usage, when

  • Required name parameter for the greeting resource wasn't specified (MissingServletRequestParameterException)
  • Non-existing resource was requested (NoHandlerFoundException)

Add Specific Exception Handlers

After adding handlers for MissingPathVariableException and NoHandlerFoundException the result will be:

@RestControllerAdvice
public class ExceptionResolver {

    @ExceptionHandler(Exception.class)
    public HashMap<String, String> handleException(HttpServletRequest request, Exception e) {
        HashMap<String, String> response = new HashMap<>();
        response.put("message", e.getMessage());
        return response;
    }

    @ExceptionHandler(MissingPathVariableException.class)
    public HashMap<String, String> handleMissingPathVariableException(HttpServletRequest request, MissingPathVariableException e) {
        HashMap<String, String> response = new HashMap<>();
        response.put("message", "Required path variable is missing in this request. Please add it to your request.");
        return response;
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    public HashMap<String, String> handleNotFoundResourceException(HttpServletRequest request, NoHandlerFoundException e) {
        HashMap<String, String> response = new HashMap<>();
        response.put("message", "Requested resource wasn't found on the server");
        return response;
    }
}

As you can see from the code above, It's possible to dedicate individual method to each exception and process them separately. If there's a requirement to combine multiple errors together and process them in one method, @ExceptionHandler does support an array:

@ExceptionHandler(value = {Exception.class, Exception1.class, ...})

So far, we build a resolver that can process exceptions by type, as well as the ones, weren't caught by a dedicated method. Practically, you can add as many methods as needed to handle individual exceptions. However, when the application grows, having too much code in one class might become cumbersome to support. For this situation, it's possible to segment handlers into their own separate Controllers.

Move specific exception handlers into their own Controllers

As you might guess, everything that left is to create another two rest-controller-advice for "Missing Path Exception" and "No Handler Found" exception. However, there is one caveat. Since all exceptions are inherited from "Exception" class there is no guarantee that specific exceptions will be intercepted by dedicated Controller and not by "Global" Controller.

For that purpose, Spring Framework supports @Order annotation. Under the hood, Spring adds handlers to the List in the strict order and iterates through them trying to find one that matches an exception. Saying that we need to make sure that the Class with a method annotated by @ExceptionHandler(Exception.class) goes latest. It's also worth mentioning, that all methods have the lowest precedence by default and there is no guaranteed order of the execution.

Taking everything into the account, the solution is to mark every "specific" controller as a one with high precedence and leave the "Global" one as is. The final code will take the following shape:

  1. GlobalExceptionResolver:

    @RestControllerAdvice
    public class GlobalExceptionResolver {
    
        @ExceptionHandler(Exception.class)
        public HashMap<String, String> handleException(HttpServletRequest request, Exception e) {
            HashMap<String, String> response = new HashMap<>();
            response.put("message", e.getMessage());
            return response;
        }
    
    }
  2. MissingPathVariableExceptionResolver:

    @RestControllerAdvice
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class MissingPathVariableExceptionResolver {
    
        @ExceptionHandler(MissingPathVariableException.class)
        public HashMap<String, String> handleMissingPathVariableException(HttpServletRequest request, MissingPathVariableException e) {
            HashMap<String, String> response = new HashMap<>();
            response.put("message", "Required path variable is missing in this request. Please add it to your request.");
            return response;
        }
    }
  3. NotFoundResourceExceptionResolver:

    @RestControllerAdvice
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class NotFoundResourceExceptionResolver {
    
        @ExceptionHandler(NoHandlerFoundException.class)
        public HashMap<String, String> handleNotFoundResourceException(HttpServletRequest request, NoHandlerFoundException e) {
            HashMap<String, String> response = new HashMap<>();
            response.put("message", "Requested resource wasn't found on the server");
            return response;
        }
    }

The source code of this project is available on GitHub.

Conclusion

In this article, we took a look at how to configure Exceptions in a Spring Boot Application that exposes a web service. There're multiple things can be done to improve our project, e.g. add Response class that produces an error, extract additional information from HttpServletRequest, Exception and show it to a consumer, etc.

If you want to learn more, much more in-depth information is available about exception handling on the official Spring portal. Happy coding :-)