Guide to Spring Boot REST API Error Handling
Handling errors correctly in APIs while providing meaningful error messages is a very desirable feature, as it can help the API client properly respond to issues. The default behavior tends to be returning stack traces that are hard to understand and ultimately useless for the API client. Partitioning the error information into fields also enables the API client to parse it and provide better error messages to the user. In this article, we will cover how to do proper error handling when building a REST API with Spring Boot.
Building REST APIs with Spring became the standard approach for Java developers during the last couple of years. Using Spring Boot helps substantially, as it removes a lot of boilerplate code and enables auto-configuration of various components. We will assume that you’re familiar with the basics of API development with those technologies before applying the knowledge described here. If you are still unsure about how to develop a basic REST API, then you should start with this article about Spring MVC or another one about building a Spring REST Service.
Making Error Responses Clearer
Throughout this article, we’ll be using the source code hosted on GitHub of an application that implements a REST API for retrieving objects that represent birds. It has the features described in this article and a few more examples of error handling scenarios. Here’s a summary of endpoints implemented in that application:
GET /birds/{birdId} | Gets information about a bird and throws an exception if not found. |
GET /birds/noexception/{birdId} | This call also gets information about a bird, except it doesn’t throw an exception in case that the bird is not found. |
POST /birds | Creates a bird. |
The Spring framework MVC module comes with some great features to help with error handling. But it is left to the developer to use those features to treat the exceptions and return meaningful responses to the API client.
Let’s look at an example of the default Spring Boot answer when we issue an HTTP POST to the /birds
endpoint with the following JSON object, that has the string aaa on the field mass, which should be expecting an integer:
{ "scientificName": "Common blackbird", "specie": "Turdus merula", "mass": "aaa", "length": 4 }
The Spring Boot default answer, without proper error handling:
{ "timestamp": 1500597044204, "status": 400, "error": "Bad Request", "exception": "org.springframework.http.converter.HttpMessageNotReadableException", "message": "JSON parse error: Unrecognized token 'three': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@cba7ebc; line: 4, column: 17]", "path": "/birds" }
Well… the response message has some good fields, but it is focused too much on what the exception was. By the way, this is the DefaultErrorAttributes
timestamp
exception
As we’ll be using Java 8 date and time classes, we first need to add a Maven dependency for the Jackson JSR310 converters. They take care of converting Java 8 date and time classes to JSON representation using the @JsonFormat
annotation:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
Ok, so let’s define a class for representing API errors. We’ll be creating a class ApiError
class ApiError { private HttpStatus status; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss") private LocalDateTime timestamp; private String message; private String debugMessage; private List<ApiSubError> subErrors; private ApiError() { timestamp = LocalDateTime.now(); } ApiError(HttpStatus status) { this(); this.status = status; } ApiError(HttpStatus status, Throwable ex) { this(); this.status = status; this.message = "Unexpected error"; this.debugMessage = ex.getLocalizedMessage(); } ApiError(HttpStatus status, String message, Throwable ex) { this(); this.status = status; this.message = message; this.debugMessage = ex.getLocalizedMessage(); } }
The status
property holds the operation call status. It will be anything from 4xx to signalize client errors or 5xx to mean server errors. A common scenario is ahttp code 400 that means a BAD_REQUEST, when the client, for example, sends an improperly formatted field, like an invalid email address.The timestamp
property holds the date-time instance of when the error happened.The message
property holds a user-friendly message about the error.The debugMessage
property holds a system message describing the error in more detail.The subErrors
property holds an array of sub-errors that happened. This is used for representing multiple errors in a single call. An example would be validation errors in which multiple fields have failed the validation.The ApiSubError
class is used to encapsulate those.
abstract class ApiSubError { } @Data @EqualsAndHashCode(callSuper = false) @AllArgsConstructor class ApiValidationError extends ApiSubError { private String object; private String field; private Object rejectedValue; private String message; ApiValidationError(String object, String message) { this.object = object; this.message = message; } }
So then ApiValidationError
ApiSubError
Below, you’ll see some examples of JSON responses that are being generated after we have implemented the improvements described here, just to get an idea of what we’ll have by the end of this article.
Here is an example of JSON returned when an entity is not found while calling endpoint GET /birds/2
:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "18-07-2017 06:20:19", "message": "Bird was not found for parameters {id=2}" } }
Here is another example of JSON returned when issuing a POST /birds
call with an invalid value for the bird’s mass:
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "18-07-2017 06:49:25", "message": "Validation errors", "subErrors": [ { "object": "bird", "field": "mass", "rejectedValue": 999999, "message": "must be less or equal to 104000" } ] } }
Spring Boot Error Handling
Let’s explore some of the Spring annotations that will be used to handle exceptions.
RestController
is the base annotation for classes that handle REST operations.
ExceptionHandler
is a Spring annotation that provides a mechanism to treat exceptions that are thrown during execution of handlers (Controller operations). This annotation, if used on methods of controller classes, will serve as the entry point for handling exceptions thrown within this controller only. Altogether, the most common way is to @ExceptionHandler
@ControllerAdvice
ControllerAdvice
is an annotation introduced in Spring 3.2, and as the name suggests, is Advice for multiple controllers. It is used to enable a ExceptionHandler
ControllerAdvice
@ControllerAdvice
annotations()
basePackageClasses()
basePackages()
ControllerAdvice
So by @ExceptionHandler
@ControllerAdvice
ApiError
Handling Exceptions
The next step is to create the class that will handle the exceptions. For simplicity, we are calling RestExceptionHandler
ResponseEntityExceptionHandler
ResponseEntityExceptionHandler
Overriding Exceptions Handled In ResponseEntityExceptionHandler
If you take a look ResponseEntityExceptionHandler
handle******()
handleHttpMessageNotReadable()
handleHttpMessageNotWritable()
handleHttpMessageNotReadable()
HttpMessageNotReadableException
handleHttpMessageNotReadable()
in our RestExceptionHandler
class:
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request"; return buildResponseEntity(new ApiError(HttpStatus.BAD_REQUEST, error, ex)); } private ResponseEntity<Object> buildResponseEntity(ApiError apiError) { return new ResponseEntity<>(apiError, apiError.getStatus()); } //other exception handlers below }
We have declared that in case of a HttpMessageNotReadableException
being thrown, the error message will be Malformed JSON request and the error will be encapsulated inside ApiError
{ "apierror": { "status": "BAD_REQUEST", "timestamp": "21-07-2017 03:53:39", "message": "Malformed JSON request", "debugMessage": "JSON parse error: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'aaa': was expecting ('true', 'false' or 'null')\n at [Source: java.io.PushbackInputStream@7b5e8d8a; line: 4, column: 17]" } }
Handling Custom Exceptions
Now we’ll see how to create a method that handles an exception that is not yet declared inside Spring ResponseEntityExceptionHandler
.
A common scenario for a Spring application that handles database calls is to have a call to find a record by its ID using a repository class. But if we look into CrudRepository.findOne()
null
To handle this case, we’ll be creating a custom exception EntityNotFoundException
javax.persistence.EntityNotFoundException
javax.persistence
That said, let’s create ExceptionHandler
EntityNotFoundException
RestExceptionHandler
handleEntityNotFound()
@ExceptionHandler
EntityNotFoundException.class
EntityNotFoundException
@ExceptionHandler
WebRequest
Locale
and others as described here. We’ll just provide the EntityNotFoundException
handleEntityNotFound
@Order(Ordered.HIGHEST_PRECEDENCE) @ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { //other exception handlers @ExceptionHandler(EntityNotFoundException.class) protected ResponseEntity<Object> handleEntityNotFound( EntityNotFoundException ex) { ApiError apiError = new ApiError(NOT_FOUND); apiError.setMessage(ex.getMessage()); return buildResponseEntity(apiError); } }
Great! In handleEntityNotFound()
NOT_FOUND
GET /birds/2
endpoint looks like now:
{ "apierror": { "status": "NOT_FOUND", "timestamp": "21-07-2017 04:02:22", "message": "Bird was not found for parameters {id=2}" } }
Concluion
It is important to get in control of the exception handling so we can properly map those exceptions to ApiError
MethodArgumentTypeMismatchException
ConstraintViolationException
and others in the GitHub code.
Here are some additional resources that helped in the composition of this article:
Baeldung - Error handling for REST with Spring
Spring Blog - Exception handling in Spring MVC