Exception 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.
Spring Boot's default error handling mechanism can be enhanced to achieve this objective. Let's look at an example:
As referenced in this article, if 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
}
By default, Spring Boot returns errors using the following format:
{
"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"
}
The above response message is focused too much on the exception and is from the class DefaultErrorAttributes of Spring Boot. The timestamp field is an integer number that doesn’t carry information of what measurement unit the timestamp is in. The exception field is only interesting to Java developers and the message leaves the API consumer lost in all the implementation details that are irrelevant to them.
To return better error messages to the API Client, we:
-
Centralize all the exception handling using Spring Boot’s @ControllerAdvice annotation on a ReSTExceptionHandler class that extends [ResponseEntityExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html) abstract class, and
-
Create new classes named ApiError and ApiSubError that will be used to encapsulate API client-friendly error messages.
Centralizing exception handling
@ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler
@ControllerAdvice annotation enables us to centralize the exception handlers for all the ReST controllers, instead of defining an exception handler for each ReST controller separately using @ExceptionHandler annotation.
The ResponseEntityExceptionHandler is a base class that can be used to provide centralized exception handling across all @RequestMapping methods through @ExceptionHandler methods. This base class provides an @ExceptionHandler method for handling internal Spring MVC exceptions.
In our RestExceptionHandler class, to provide more details to the API client, we can either @override one or more of the methods provided by the ResponseEntityExceptionHandler or implement our own @ExceptionHandler annotated methods for exceptions that are not handled by the ResponseEntityExceptionHandler.
This base class provides an @ExceptionHandler method for handling the following
internal Spring MVC exceptions:
HttpRequestMethodNotSupportedException.class
HttpMediaTypeNotSupportedException.class
HttpMediaTypeNotAcceptableException.class
MissingPathVariableException.class
MissingServletRequestParameterException.class
ServletRequestBindingException.class
ConversionNotSupportedException.class
TypeMismatchException.class
HttpMessageNotReadableException.class
HttpMessageNotWritableException.class
MethodArgumentNotValidException.class
MissingServletRequestPartException.class
BindException.class
NoHandlerFoundException.class
AsyncRequestTimeoutException.class
Create new ApiError and ApiSubError classes
To return better error messages, we have defined a class named ApiError that has enough fields to hold relevant information about errors that happen during REST calls.
@Getter
@Setter
public 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();
}
public 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();
}
private void addSubError(ApiSubError subError) {
if (subErrors == null) {
subErrors = new ArrayList<>();
}
subErrors.add(subError);
}
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 a http 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 the subErrors.
public abstract class ApiSubError {}
@Getter
@Setter
public class ApiValidationError extends ApiSubError {
private String object;
private String field;
private Object rejectedValue;
private String message;
@Override
public String toString() {
return ("ApiValidationError{" + "object='" + object + '\'' + ", field='" + field + '\'' +
", rejectedValue=" + rejectedValue + ", message='" + message + '\'' + '}'
);
}
public ApiValidationError(String object, String field, Object rejectedValue, String message) {
this.object = object;
this.field = field;
this.rejectedValue = rejectedValue;
this.message = message;
}
public ApiValidationError(String object, String message) {
this.object = object;
this.message = message;
}
}
In the generated code, exceptions are only handled at the ReST Controller layer through a single central class. Exceptions from the domain layer and the application service layer are not handled at these layers and instead bubble up to the ReST controller classes.
Updated over 2 years ago