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<>();
.put("message", e.getMessage());
responsereturn 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<>();
.put("message", e.getMessage());
responsereturn response;
}
@ExceptionHandler(MissingPathVariableException.class)
public HashMap<String, String> handleMissingPathVariableException(HttpServletRequest request, MissingPathVariableException e) {
HashMap<String, String> response = new HashMap<>();
.put("message", "Required path variable is missing in this request. Please add it to your request.");
responsereturn response;
}
@ExceptionHandler(NoHandlerFoundException.class)
public HashMap<String, String> handleNotFoundResourceException(HttpServletRequest request, NoHandlerFoundException e) {
HashMap<String, String> response = new HashMap<>();
.put("message", "Requested resource wasn't found on the server");
responsereturn 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:
GlobalExceptionResolver:
@RestControllerAdvice public class GlobalExceptionResolver { @ExceptionHandler(Exception.class) public HashMap<String, String> handleException(HttpServletRequest request, Exception e) { HashMap<String, String> response = new HashMap<>(); .put("message", e.getMessage()); responsereturn response; } }
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<>(); .put("message", "Required path variable is missing in this request. Please add it to your request."); responsereturn response; } }
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<>(); .put("message", "Requested resource wasn't found on the server"); responsereturn 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 :-)