- All Implemented Interfaces:
Serializable
,Message<Throwable>
A GenericMessage
with a Throwable
payload.
The payload is typically a MessagingException
with the message at the point of failure in its failedMessage
property.
An optional originalMessage
may be provided, which represents the message
that existed at the point in the stack where the error message is created.
Consider some code that starts with a message, invokes some process that performs
transformation on that message and then fails for some reason, throwing the exception.
The exception is caught and an error message produced that contains both the original
message, and the transformed message that failed.
- Since:
- 4.0
- Author:
- Mark Fisher, Oleg Zhurakousky, Gary Russell
- See Also:
-
MessageBuilder
- Serialized Form
-
Constructor Summary
Constructors
Create a new message with the given payload.
Create a new message with the given payload and headers.
Create a new message with the given payload, headers and original message.
Create a new message with the given payload and original message.
Create a new message with the payload,
MessageHeaders
and original message. -
Method Summary
Return the original message (if available) at the point in the stack
where the ErrorMessage was created.toString()
-
Constructor Details
-
ErrorMessage
public ErrorMessage(Throwable payload)
Create a new message with the given payload.
- Parameters:
payload
— the message payload (nevernull
)
-
ErrorMessage
Create a new message with the given payload and headers.
The content of the given header map is copied.- Parameters:
payload
— the message payload (nevernull
)headers
— message headers to use for initialization
-
ErrorMessage
public ErrorMessage(Throwable payload,
Message<?> originalMessage)Create a new message with the given payload and original message.
- Parameters:
payload
— the message payload (nevernull
)originalMessage
— the original message (if present) at the point
in the stack where the ErrorMessage was created- Since:
- 5.0
-
ErrorMessage
Create a new message with the given payload, headers and original message.
The content of the given header map is copied.- Parameters:
payload
— the message payload (nevernull
)headers
— message headers to use for initializationoriginalMessage
— the original message (if present) at the point
in the stack where the ErrorMessage was created- Since:
- 5.0
-
-
Method Details
-
getOriginalMessage
Return the original message (if available) at the point in the stack
where the ErrorMessage was created.- Since:
- 5.0
-
toString
- Overrides:
toString
in classGenericMessage<Throwable>
-
Create global or application-level Exception handlers and return Custom Error Messages in Spring REST APIs.
Overview
Effective communication is the key to healthy and efficient relationships. Interestingly, the same applies to any Client and Server relationships. The client’s request may succeed or fail on the server. However, the server should provide the most appropriate status code in either of the outcomes.
Although sending a correct status code is enough for a client to take real action based on the outcome of a request, in case of failures, the client may need more details about what went wrong. For example, failure details like the exception type and an error message can help clients log the error or provide appropriate failure messages to their clients.
This article will teach How to handle different failures and return Custom Error Messages from a Spring REST API. If you don’t know how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.
Return a Generic Error Message using @ResponseStatus
The most basic way of returning an error message from a REST API is to use the @ResponseStatus annotation. We can add the error message in the annotation’s reason field. Although we can only return a generic error message, we can specify exception-specific error messages.
Next is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception-specific error messages.
Code language: Java (java)
@ControllerAdvice public class ApplicationExceptionHandler { @ResponseStatus( value = HttpStatus.NOT_FOUND, reason = "Requested Student Not Found") @ExceptionHandler(StudentNotFoundException.class) public void handleException(StudentNotFoundException e) { } @ResponseStatus( value = HttpStatus.BAD_REQUEST, reason = "Received Invalid Input Parameters") @ExceptionHandler(InputValidationException.class) public void handleException(InputValidationException e) { } @ResponseStatus( value = HttpStatus.GATEWAY_TIMEOUT, reason = "Upstream Service Not Responding, Try Again") @ExceptionHandler(ServiceUnavailableException.class) public void handleException(ServiceUnavailableException e) { } }
The Exception handler class has three exception handlers, each of which returns a specific HTTP Response Status. Each response status specifies a reason field with a particular error message.
To view the error message in the response, ensure you have turned on include-messages in the server configuration. To learn more about Spring Boot server configurations, please visit Spring Boot Embedded Tomcat Configuration.
Code language: YAML (yaml)
server: error: include-message: always
Next is an example of a response object the REST API returns. Note that the response object has the specified error message.
Code language: JSON / JSON with Comments (json)
{ "timestamp": "", "status": 404, "error": "Not Found", "message": "Requested Student Not Found", "path": "/students/Jack" }
Although we can specify exception-specific error messages, it is still not informative. Therefore in the following sections, we will learn how to return a more specific error message from Spring REST API.
Return Error Message Using Custom Error Object
Let’s create a class representing the error message and the status code. We will return an instance of that in case of errors.
Next is the Error class representing the status code and a String message. We use a few Lombok annotations that introduce regular getter and setter methods and a constructor using the final fields.
Custom Response Error Class
Code language: Java (java)
@Data @RequiredArgsConstructor public class Error { private final HttpStatus httpStatus; private final String message; }
Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.
Code language: Java (java)
@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity handleException( StudentNotFoundException e) { Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); }
The exception handler returns an instance of the Error class populated with the exception message and HTTP Status Code.
Now, we can throw our Not Found Exception with a custom error message.
Code language: Java (java)
throw new StudentNotFoundException ("Student service failed, studentId : " + studentId);
When the REST API cannot find the requested resource, we get a detailed error as a response.
Code language: JSON / JSON with Comments (json)
{ "httpStatus": "NOT_FOUND", "message": "Student service failed, studentId : Jack" }
Return Error Message Using HashMap
Also, instead of creating a dedicated error class, we can return a detailed error message using a simple HashMap. Next is an example of producing and returning a Custom Error Message using Java HashMap.
Code language: Java (java)
@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity<Map<String, String>> handleException(StudentNotFoundException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.NOT_FOUND.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); }
Handle Bad Request Exceptions
The Bad Request errors are the Client errors where the client’s request doesn’t meet the requirements of the target server. This section will see how to handle Bad Request exceptions and provide a custom or detailed error response.
Type Mismatch Exceptions
The Type Mismatch Exceptions occur when Spring Controller cannot map the request parameters, path variables, or header values into controller method arguments. This section covers the handling of MethodArgumentTypeMismatchException and TypeMismatchException.
Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when there is a type mismatch while setting Bean properties. Also, both exceptions provide a detailed error message that we can use to prepare the Error object.
To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.
Code language: Java (java)
@ExceptionHandler({ MethodArgumentTypeMismatchException.class, TypeMismatchException.class }) public ResponseEntity<Map<String, String>> handleException(TypeMismatchException e) { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Note that the controller advice catches both exceptions; however, the method arguments accept an exception of type TypeMismatchException because it is the parent of the other exception.
Next, the snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.
Code language: JSON / JSON with Comments (json)
{ "httpStatus": "BAD_REQUEST", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Long'; nested exception is java.lang.NumberFormatException: For input String: "Jack"" }
Bean Validation Exceptions
The Bean Validation exceptions occur when the request contents do not pass the provided validations.
The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API’s exception handler.
Code language: Java (java)
@ExceptionHandler({ BindException.class, MethodArgumentNotValidException.class }) public ResponseEntity<Map<String, Object>> handleException(BindException e) { List<String> errors = new ArrayList<>(); e.getFieldErrors() .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage())); e.getGlobalErrors() .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage())); Map<String, Object> errorResponse = Map.of( "error", errors, "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead, we can add a List<String>
field to the Error class we created in the previous section and populate the list with individual errors.
Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or PATCH endpoint on the server cannot handle the content type sent by the client. The REST Controllers on the server specify the content type they can support. When the media type that a client sends doesn’t match, the client gets this exception back.
To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.
Code language: Java (java)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpMediaTypeNotSupportedException e) { String provided = e.getContentType().toString(); List<String> supported = e.getSupportedMediaTypes().stream() .map(MimeType::toString) .collect(Collectors.toList()); String error = provided + " is not one of the supported media types (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE); }
As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type we provided and a list of actually supported media types. Thus, we create a custom error message based on the available information.
Code language: JSON / JSON with Comments (json)
{ "error":"text/plain;charset=UTF-8 is not one of the supported media types ( application/octet-stream, text/plain, application/xml, text/xml, application/x-www-form-urlencoded, application/*+xml, multipart/form-data, multipart/mixed, application/json, application/*+json, */*)", "message":"Content type 'text/plain;charset=UTF-8' not supported", "status":"415 UNSUPPORTED_MEDIA_TYPE" }
The above snippet shows a client’s sample error response when it sends a request with an invalid media type.
Handle Request Body Not Readable Exception
Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when the request body is missing or unreadable.
Code language: Java (java)
@ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Map<String, String>> handleException( HttpMessageNotReadableException e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.BAD_REQUEST.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); }
Handle HTTP Request Method Not Supported Exception
The HttpMethodNotSupportedException occurs when the HTTP endpoint on the REST API does not support the HTTP request method. Let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.
Code language: Java (java)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity<Map<String, String>> handleException( HttpRequestMethodNotSupportedException e) throws IOException { String provided = e.getMethod(); List<String> supported = List.of(e.getSupportedMethods()); String error = provided + " is not one of the supported Http Methods (" + String.join(", ", supported) + ")"; Map<String, String> errorResponse = Map.of( "error", error, "message", e.getLocalizedMessage(), "status", HttpStatus.METHOD_NOT_ALLOWED.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED); }
As seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods. We use it to form a clear error message.
Code language: JSON / JSON with Comments (json)
{ "error": "GET is not one of the supported Http Methods (POST)", "message": "Request method 'GET' not supported", "status": "405 METHOD_NOT_ALLOWED" }
The snippet showed an example response when the client attempted to execute a GET endpoint, while the REST API supports only POST.
Default Exception Handler
Similarly, we can create a default exception handler advice that handles all Exception types. Spring attempts to find the most specific handler when we have multiple exception handlers and falls back to the default handler if there is no suitable handler.
Code language: Java (java)
@ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleException( Exception e) throws IOException { Map<String, String> errorResponse = Map.of( "message", e.getLocalizedMessage(), "status", HttpStatus.INTERNAL_SERVER_ERROR.toString() ); return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); }
Above is an example of writing a default exception handler that returns an error message by the exception instance and an HTTP Status of 500.
Summary
This detailed tutorial taught us how to Return Custom Error Messages in Spring REST API. Firstly, we understood that Spring returns a generic error message and the most suitable HTTP Status Code by default. However, we can write our exception handlers for specific exceptions using @ControllerAdvice and produce a custom and detailed error response.
For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.
Часто при работе с микросервисами, построенными с помощью технологии Spring Boot, можно видеть стандартный вывод ошибок подобный этому:
{
"timestamp": 1510417124782,
"status": 500,
"error": "Internal Server Error",
"exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
"message": "ApplicationRepository#save(Application) failed and no fallback available.",
"path": "/application"
}
Такой вывод может быть излишним и ненужным клиентам вашего сервиса. Если вы хотите упростить жизнь сторонним сервисам в случае ошибки, то как раз об этом и пойдет речь в данном посте.
Начнем мы с построения небольшого сервиса с одним контроллером. Наш сервис будет принимать запрос на получение пользователя и в случае успеха отдавать данные по пользователю. В случае провала к нам возвращается ошибка. Начнем с простого и далее в статье будем усовершенствовать проект.
Итак, первое, что нам понадобится, это пользователь:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String firstName;
private String lastName;
}
Здесь я использовал библиотеку lombok. Аннотация Data подставляет геттеры и сеттеры в класс. Остальные аннотации добавляют пустой конструктор и конструктор с параметрами. Если вы хотите повторить данный пример у себя в IntelliJ Idea, то вам необходимо поставить галочку в пункте enable annotation processing, либо написать все руками.
Далее нам понадобится сервис (для краткости репозиторий создавать не будем):
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
private static final Map<Integer, User> userStorage
= new HashMap<>();
static {
userStorage.put(1, new User(1, "Petr", "Petrov"));
userStorage.put(2, new User(2, "Ivan", "Ivanov"));
userStorage.put(3, new User(3, "Sergei", "Sidorov"));
}
public User get(int id) {
return userStorage.get(id);
}
}
Ну и, конечно, сам контроллер:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("{id}")
public User get(@PathVariable(name = "id") int id) {
return userService.get(id);
}
}
Итак, у нас есть почти полноценный сервис с пользователями. Запускаем его и смотрим.
При запросе на URL localhost:8080/user/1 нам возвращается json в таком формате:
{
"id": 1,
"firstName": "Petr",
"lastName": "Petrov"
}
Все отлично. Но что будет, если сделать запрос на URL localhost:8080/user/4 (у нас всего 3 пользователя)? Правильный ответ: мы получим статус 200 и ничего в ответе. Ситуация не особо приятная. Ошибки нет, но и запрашиваемого объекта тоже нет.
Давайте улучшим наш сервис и добавим в него выбрасывание ошибки в случае неудачи. Для начала создадим исключение:
public class ThereIsNoSuchUserException extends RuntimeException { }
Теперь добавим пробрасывание ошибки в сервис:
public User get(int id) {
User user = userStorage.get(id);
if (user == null) {
throw new ThereIsNoSuchUserException();
}
return user;
}
Сделаем перезапуск сервиса и снова посмотрим, что будет при запросе несуществующего пользователя:
{
"timestamp": 1510479979781,
"status": 500,
"error": "Internal Server Error",
"exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
"message": "No message available",
"path": "/user/4"
}
Это уже лучше. Гораздо более информативно и код статуса не 200. Такую ситуацию клиент на своей стороне уже сможет успешно и легко обработать. Но, как говорится, есть один нюанс. Ошибки могут быть совершенно разными, и клиенту нашего сервиса придется ставить кучу условных операторов и исследовать, что у нас пошло не так и как это можно поправить. Получается немного грубо с нашей стороны.
Как раз для таких случаев и была придумана аннотация ResponseStatus. Подставим ее на место нашего исключения и на практике посмотрим, как это работает:
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "There is no such user")
public class ThereIsNoSuchUserException extends RuntimeException {
}
Повторим запрос и посмотрим результат:
{
"timestamp": 1510480307384,
"status": 404,
"error": "Not Found",
"exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
"message": "There is no such user",
"path": "/user/4"
}
Отлично! Код статуса и сообщение поменялись. Теперь клиент сможет определись по коду ответа причину ошибки и даже уточнить ее по полю message. Но все же есть проблема. Большинство полей клиенту могут быть просто не нужны. Например, код ответа как отдельное поле может быть излишним, поскольку мы его и так получаем с кодом ответа. С этим нужно что-то делать.
К счастью, со spring boot сделать последний шаг к нашему успешному оповещению об ошибке не так сложно.
Все, что для этого требуется, разобрать пару аннотаций и один класс:
- Аннотация ExceptionHandler. Используется для обработки собственных и каких-то специфичных исключений. Далее в примере будет понятно, что это значит. На всякий случай ссылка на документацию.
- Аннотация ControllerAdvice. Данная аннотация дает «совет» группе констроллеров по определенным событиям. В нашем случае — это обработка ошибок. По умолчанию применяется ко всем контроллерам, но в параметрах можно указать отпределенную группу. Подбронее тут.
- Класс ResponseEntityExceptionHandler. Данный класс занимается обработкой ошибок. У него куча методов, название которых построенно по принципу handle + название исключения. Если мы хотим обработать какое-то базовое исключение, то наследуемся от этого класса и переопределяем нужный метод.
Давайте теперь посмотрим, как все это обЪединить и построить наше уникальное и неповторимое сообщение об ошибке:
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(ThereIsNoSuchUserException.class)
protected ResponseEntity<AwesomeException> handleThereIsNoSuchUserException() {
return new ResponseEntity<>(new AwesomeException("There is no such user"), HttpStatus.NOT_FOUND);
}
@Data
@AllArgsConstructor
private static class AwesomeException {
private String message;
}
}
Сделаем все тот же запрос и увидим статус ответа 404 и наше сообщение с единственным полем:
{
"message": "There is no such user"
}
Аннотацию ResponseStatus над нашим исключением можно смело убирать.
В итоге у нас получилось приложение, в котором обработка ошибок настраивается максимально гибко и просто. Полный проект можно найти в репозитории github. Надеюсь, что все было просто и понятно. Спасибо за внимание и пишите комментарии! Буду рад вашим замечаниям и уточнениям!
Handling exceptions is an important part of building a robust application. Spring Boot offers more than one way of doing it.
This article will explore these ways and will also provide some pointers on when a given way might be preferable over another.
Example Code
This article is accompanied by a working code example on GitHub.
Introduction
Spring Boot provides us tools to handle exceptions beyond simple ‘try-catch’ blocks. To use these tools, we apply a couple of annotations
that allow us to treat exception handling as a cross-cutting concern:
@ResponseStatus
@ExceptionHandler
@ControllerAdvice
Before jumping into these annotations we will first look at how Spring handles exceptions thrown by our web controllers — our last line of defense for catching an exception.
We will also look at some configurations provided by Spring Boot to modify the default behavior.
We’ll identify the challenges we face while doing that, and then we will try to overcome those using these annotations.
Spring Boot’s Default Exception Handling Mechanism
Let’s say we have a controller named ProductController
whose getProduct(...)
method is throwing a NoSuchElementFoundException
runtime exception when a Product
with a given id is not found:
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
//constructor omitted for brevity...
@GetMapping("/{id}")
public Response getProduct(@PathVariable String id){
// this method throws a "NoSuchElementFoundException" exception
return productService.getProduct(id);
}
}
If we call the /product
API with an invalid id
the service will throw a NoSuchElementFoundException
runtime exception and we’ll get the
following response:
{
"timestamp": "2020-11-28T13:24:02.239+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/product/1"
}
We can see that besides a well-formed error response, the payload is not giving us any useful information. Even the message
field is empty, which we might want to contain something like “Item with id 1 not found”.
Let’s start by fixing the error message issue.
Spring Boot provides some properties with which we can add the exception message, exception class, or even a stack trace
as part of the response payload:
server:
error:
include-message: always
include-binding-errors: always
include-stacktrace: on_trace_param
include-exception: false
Using these Spring Boot server properties in our application.yml
we can alter the error response to some extent.
Now if we call the /product
API again with an invalid id
we’ll get the following response:
{
"timestamp": "2020-11-29T09:42:12.287+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Item with id 1 not found",
"path": "/product/1"
}
Note that we’ve set the property include-stacktrace
to on_trace_param
which means that only if we include the trace
param in the URL (?trace=true
), we’ll get a stack trace in the response payload:
{
"timestamp": "2020-11-29T09:42:12.287+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Item with id 1 not found",
"trace": "io.reflectoring.exception.exception.NoSuchElementFoundException: Item with id 1 not found...",
"path": "/product/1"
}
We might want to keep the value of include-stacktrace
flag to never
, at least in production, as it might reveal the internal
workings of our application.
Moving on! The status and error message — 500
— indicates that something is wrong with our server code but actually it’s a client error because the client provided an invalid id.
Our current status code doesn’t correctly reflect that. Unfortunately, this is as far as we can go with the server.error
configuration properties, so we’ll have to look at the annotations that Spring Boot offers.
@ResponseStatus
As the name suggests, @ResponseStatus
allows us to modify the HTTP status of our response. It can be applied in the following
places:
- On the exception class itself
- Along with the
@ExceptionHandler
annotation on methods - Along with the
@ControllerAdvice
annotation on classes
In this section, we’ll be looking at the first case only.
Let’s come back to the problem at hand which is that our error responses are always giving us the HTTP status 500 instead of a more descriptive status code.
To address this we can we annotate our Exception class with @ResponseStatus
and pass in the desired HTTP response status
in its value
property:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}
This change will result in a much better response if we call our controller with an invalid ID:
{
"timestamp": "2020-11-29T09:42:12.287+00:00",
"status": 404,
"error": "Not Found",
"message": "Item with id 1 not found",
"path": "/product/1"
}
Another way to achieve the same is by extending the ResponseStatusException
class:
public class NoSuchElementFoundException extends ResponseStatusException {
public NoSuchElementFoundException(String message){
super(HttpStatus.NOT_FOUND, message);
}
@Override
public HttpHeaders getResponseHeaders() {
// return response headers
}
}
This approach comes in handy when we want to manipulate the response headers, too, because we can override the getResponseHeaders()
method.
@ResponseStatus
, in combination with the server.error
configuration properties, allows us to manipulate almost all the fields
in our Spring-defined error response payload.
But what if want to manipulate the structure of the response payload as well?
Let’s see how
we can achieve that in the next section.
@ExceptionHandler
The @ExceptionHandler
annotation gives us a lot of flexibility in terms of handling exceptions. For starters, to use it, we
simply need to create a method either in the controller itself or in a @ControllerAdvice
class and
annotate it with @ExceptionHandler
:
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
//constructor omitted for brevity...
@GetMapping("/{id}")
public Response getProduct(@PathVariable String id) {
return productService.getProduct(id);
}
@ExceptionHandler(NoSuchElementFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<String> handleNoSuchElementFoundException(
NoSuchElementFoundException exception
) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(exception.getMessage());
}
}
The exception handler method takes in an exception or a list of exceptions as an argument that we want to handle in the defined
method. We annotate the method with @ExceptionHandler
and @ResponseStatus
to define the exception we want to handle and the status code we want to return.
If we don’t wish to use these annotations, then simply defining the exception as a parameter of the method will also do:
@ExceptionHandler
public ResponseEntity<String> handleNoSuchElementFoundException(
NoSuchElementFoundException exception)
Although it’s a good idea to mention the exception class in the annotation even though we have mentioned it in the method signature already. It gives better readability.
Also, the annotation @ResponseStatus(HttpStatus.NOT_FOUND)
on the handler method is not required as the HTTP status passed into the ResponseEnity
will take precedence, but we have kept it anyway for the same readability reasons.
Apart from the exception parameter, we can also have HttpServletRequest
, WebRequest
, or HttpSession
types as parameters.
Similarly, the handler
methods support a variety of return types such as ResponseEntity
, String
, or even void
.
Find more input and return types in @ExceptionHandler
java documentation.
With many different options available to us in form of both input parameters and return types in our exception handling function,
we are in complete control of the error response.
Now, let’s finalize an error response payload for our APIs. In case of any error, clients usually expect two things:
- An error code that tells the client what kind of error it is. Error codes can be used by clients in their code to drive
some business logic based on it. Usually, error codes are standard HTTP status codes, but I have also seen APIs returning
custom errors code likesE001
. - An additional human-readable message which gives more information on the error and even some hints
on how to fix them or a link to API docs.
We will also add an optional stackTrace
field which will help us with debugging in the development environment.
Lastly, we also want to handle validation errors in the response. You can find out more about bean
validations in this article on Handling Validations with Spring Boot.
Keeping these points in mind we will go with the following payload for the error response:
@Getter
@Setter
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {
private final int status;
private final String message;
private String stackTrace;
private List<ValidationError> errors;
@Getter
@Setter
@RequiredArgsConstructor
private static class ValidationError {
private final String field;
private final String message;
}
public void addValidationError(String field, String message){
if(Objects.isNull(errors)){
errors = new ArrayList<>();
}
errors.add(new ValidationError(field, message));
}
}
Now, let’s apply all these to our NoSuchElementFoundException
handler method.
@RestController
@RequestMapping("/product")
@AllArgsConstructor
public class ProductController {
public static final String TRACE = "trace";
@Value("${reflectoring.trace:false}")
private boolean printStackTrace;
private final ProductService productService;
@GetMapping("/{id}")
public Product getProduct(@PathVariable String id){
return productService.getProduct(id);
}
@PostMapping
public Product addProduct(@RequestBody @Valid ProductInput input){
return productService.addProduct(input);
}
@ExceptionHandler(NoSuchElementFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ErrorResponse> handleItemNotFoundException(
NoSuchElementFoundException exception,
WebRequest request
){
log.error("Failed to find the requested element", exception);
return buildErrorResponse(exception, HttpStatus.NOT_FOUND, request);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
WebRequest request
) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.UNPROCESSABLE_ENTITY.value(),
"Validation error. Check 'errors' field for details."
);
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
errorResponse.addValidationError(fieldError.getField(),
fieldError.getDefaultMessage());
}
return ResponseEntity.unprocessableEntity().body(errorResponse);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(
Exception exception,
WebRequest request){
log.error("Unknown error occurred", exception);
return buildErrorResponse(
exception,
"Unknown error occurred",
HttpStatus.INTERNAL_SERVER_ERROR,
request
);
}
private ResponseEntity<ErrorResponse> buildErrorResponse(
Exception exception,
HttpStatus httpStatus,
WebRequest request
) {
return buildErrorResponse(
exception,
exception.getMessage(),
httpStatus,
request);
}
private ResponseEntity<ErrorResponse> buildErrorResponse(
Exception exception,
String message,
HttpStatus httpStatus,
WebRequest request
) {
ErrorResponse errorResponse = new ErrorResponse(
httpStatus.value(),
exception.getMessage()
);
if(printStackTrace && isTraceOn(request)){
errorResponse.setStackTrace(ExceptionUtils.getStackTrace(exception));
}
return ResponseEntity.status(httpStatus).body(errorResponse);
}
private boolean isTraceOn(WebRequest request) {
String [] value = request.getParameterValues(TRACE);
return Objects.nonNull(value)
&& value.length > 0
&& value[0].contentEquals("true");
}
}
Couple of things to note here:
Providing a Stack Trace
Providing stack trace in the error response can save our developers and QA engineers the trouble of crawling through the log files.
As we saw in Spring Boot’s Default Exception Handling Mechanism, Spring already provides us
with this functionality. But now, as we are handling error responses ourselves, this also needs to be handled by us.
To achieve this, we have first introduced a server-side configuration property named reflectoring.trace
which, if set to true
,
To achieve this, we have first introduced a server-side configuration property named reflectoring.trace
which, if set to true
,
will enable the stackTrace
field in the response. To actually get a stackTrace
in an API response, our clients must additionally pass the
trace
parameter with the value true
:
curl --location --request GET 'http://localhost:8080/product/1?trace=true'
Now, as the behavior of stackTrace
is controlled by our feature flag in our properties file, we can remove it or set it
to false
when we deploy in production environments.
Catch-All Exception Handler
Gotta catch em all:
try{
performSomeOperation();
} catch(OperationSpecificException ex){
//...
} catch(Exception catchAllExcetion){
//...
}
As a cautionary measure, we often surround our top-level method’s body with a catch-all try-catch exception handler block, to avoid any unwanted side effects or behavior. The handleAllUncaughtException()
method in our controller behaves
similarly. It will catch all the exceptions for which we don’t have a specific handler.
One thing I would like to note here is that even if we don’t have this catch-all exception handler, Spring will handle it
anyway. But we want the response to be in our format rather than Spring’s, so we have to handle the exception ourselves.
A catch-all handler method is also be a good place to log exceptions as
they might give insight into a possible bug. We can skip logging on field validation exceptions such as MethodArgumentNotValidException
as they are raised because of syntactically invalid input, but we should always log unknown exceptions in the catch-all handler.
Order of Exception Handlers
The order in which you mention the handler methods doesn’t matter. Spring will first look for the most specific exception handler method.
If it fails to find it then it will look for a handler of the parent exception, which in our case is RuntimeException
, and if none is found, the
handleAllUncaughtException()
method will finally handle the exception.
This should help us handle the exceptions in this particular controller, but what if these same exceptions are being thrown
by other controllers too? How do we handle those? Do we create the same handlers in all controllers or create a base class with
common handlers and extend it in all controllers?
Luckily, we don’t have to do any of that. Spring provides a very elegant solution to this problem in form of “controller advice”.
Let’s study them.
@ControllerAdvice
Why is it called «Controller Advice»?
The term ‘Advice’ comes from Aspect-Oriented Programming (AOP) which allows us to inject cross-cutting code (called «advice») around existing methods. A controller advice allows us to intercept and modify the return values of controller methods, in our case to handle exceptions.
Controller advice classes allow us to apply exception handlers to more than one or all controllers in our application:
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
public static final String TRACE = "trace";
@Value("${reflectoring.trace:false}")
private boolean printStackTrace;
@Override
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request
) {
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
@ExceptionHandler(ItemNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<Object> handleItemNotFoundException(
ItemNotFoundException itemNotFoundException,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<Object> handleAllUncaughtException(
RuntimeException exception,
WebRequest request
){
//Body omitted as it's similar to the method of same name
// in ProductController example...
//.....
}
//....
@Override
public ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return buildErrorResponse(ex,status,request);
}
}
The bodies of the handler functions and the other support code are omitted as they’re almost
identical to the code we saw in the @ExceptionHandler section. Please find the full code in the Github Repo’s
GlobalExceptionHandler
class.
A couple of things are new which we will talk about in a while. One major difference here is that these handlers will handle exceptions thrown by all the controllers
in the application and not just ProductController
.
If we want to selectively apply or limit the scope of the controller advice to a particular controller, or a package, we can use the properties provided by the annotation:
@ControllerAdvice("com.reflectoring.controller")
: we can pass a package name or list of package names in the annotation’svalue
orbasePackages
parameter. With this, the controller advice will only handle exceptions of this package’s controllers.@ControllerAdvice(annotations = Advised.class)
: only controllers marked with the@Advised
annotation will be handled
by the controller advice.
Find other parameters in the @ControllerAdvice
annotation docs.
ResponseEntityExceptionHandler
ResponseEntityExceptionHandler
is a convenient base class for controller advice classes. It provides
exception handlers for internal Spring exceptions. If we don’t extend it, then all the exceptions will be redirected to DefaultHandlerExceptionResolver
which returns a ModelAndView
object. Since we are on the mission to shape our own error response, we don’t want that.
As you can see we have overridden two of the ResponseEntityExceptionHandler
methods:
handleMethodArgumentNotValid()
: in the @ExceptionHandler section we have implemented a handler for it ourselves. In here we have only
overridden its behavior.handleExceptionInternal()
: all the handlers in theResponseEntityExceptionHandler
use this function to build the
ResponseEntity
similar to ourbuildErrorResponse()
. If we don’t override this then the clients will receive only the HTTP status
in the response header but since we want to include the HTTP status in our response bodies as well, we have overridden the method.
Handling NoHandlerFoundException
Requires a Few Extra Steps
This exception occurs when you try to call an API that doesn’t exist in the system. Despite us implementing its handler
via ResponseEntityExceptionHandler
class the exception is redirected to DefaultHandlerExceptionResolver
.
To redirect the exception to our advice we need to set a couple of properties in the the properties file: spring.mvc.throw-exception-if-no-handler-found=true
and spring.web.resources.add-mappings=false
Credit: Stackoverflow user mengchengfeng.
Some Points to Keep in Mind when Using @ControllerAdvice
- To keep things simple always have only one controller advice class in the project. It’s good to have a single repository of
all the exceptions in the application. In case you create multiple controller advice, try to utilize thebasePackages
orannotations
properties
to make it clear what controllers it’s going to advise. - Spring can process controller advice classes in any order unless we have annotated it with the
@Order
annotation. So, be mindful when you write a catch-all handler if you have more than one controller advice. Especially
when you have not specifiedbasePackages
orannotations
in the annotation.
How Does Spring Process The Exceptions?
Now that we have introduced the mechanisms available to us for handling exceptions in Spring, let’s
understand in brief how Spring handles it and when one mechanism gets prioritized over the other.
Have a look through the following flow chart that traces the process of the exception handling by Spring if we have not built our own exception handler:
Conclusion
When an exception crosses the boundary of the controller, it’s destined to reach the client, either in form of a JSON response
or an HTML web page.
In this article, we saw how Spring Boot translates those exceptions into a user-friendly output for our
clients and also configurations and annotations that allow us to further mold them into the shape we desire.
Thank you for reading! You can find the working code at GitHub.
Editor’s note: This article was updated on September 5, 2022, by our editorial team. It has been modified to include recent sources and to align with our current editorial standards.
The ability to handle errors correctly in APIs while providing meaningful error messages is a desirable feature, as it can help the API client respond to issues. The default behavior returns stack traces that are hard to understand and ultimately useless for the API client. Partitioning the error information into fields enables the API client to parse it and provide better error messages to the user. In this article, we cover how to implement proper Spring Boot exception handling when building a REST API .
Building REST APIs with Spring became the standard approach for Java developers. Using Spring Boot helps substantially, as it removes a lot of boilerplate code and enables auto-configuration of various components. We assume that you’re familiar with the basics of API development with those technologies. If you are unsure about how to develop a basic REST API, you should start with this article about Spring MVC or this article about building a Spring REST Service.
Making Error Responses Clearer
We’ll use the source code hosted on GitHub as an example 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 when a bird doesn’t exist with that ID. |
POST /birds |
Creates a bird. |
The Spring framework MVC module has excellent features for 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, looks like this:
{
"timestamp": 1658551020,
"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 Spring Boot DefaultErrorAttributes
-generated response has some good fields, but it is too focused on the exception. The timestamp
field is an integer that doesn’t carry information about its measurement unit. The exception
field is only valuable to Java developers, and the message leaves the API consumer lost in implementation details that are irrelevant to them. What if there were more details we could extract from the exception? Let’s learn how to handle exceptions in Spring Boot properly and wrap them into a better JSON representation to make life easier for our API clients.
As we’ll be using Java date and time classes, we first need to add a Maven dependency for the Jackson JSR310 converters. They convert Java date and time classes to JSON representation using the @JsonFormat
annotation:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
Next, let’s define a class for representing API errors. We’ll create a class called ApiError
with enough fields to hold relevant information about errors during REST calls:
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, which will be anything from 4xx to signal client errors or 5xx to signal server errors. A typical scenario is an HTTP code 400: 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 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 detail. -
The
subErrors
property holds an array of suberrors when there are multiple errors in a single call. An example would be numerous validation errors in which multiple fields have failed. TheApiSubError
class encapsulates this information:
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;
}
}
The ApiValidationError
is a class that extends ApiSubError
and expresses validation problems encountered during the REST call.
Below, you’ll see examples of JSON responses generated after implementing these improvements.
Here is a JSON example returned for a missing entity while calling endpoint GET /birds/2
:
{
"apierror": {
"status": "NOT_FOUND",
"timestamp": "22-07-2022 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": "22-07-2022 06:49:25",
"message": "Validation errors",
"subErrors": [
{
"object": "bird",
"field": "mass",
"rejectedValue": 999999,
"message": "must be less or equal to 104000"
}
]
}
}
Spring Boot Error Handler
Let’s explore some Spring annotations 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 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 implementation is to use @ExceptionHandler
on methods of @ControllerAdvice
classes so that the Spring Boot exception handling will be applied globally or to a subset of controllers.
ControllerAdvice
is an annotation in Spring and, as the name suggests, is “advice” for multiple controllers. It enables the application of a single ExceptionHandler
to multiple controllers. With this annotation, we can define how to treat such an exception in a single place, and the system will call this handler for thrown exceptions on classes covered by this ControllerAdvice
.
The subset of controllers affected can be defined by using the following selectors on @ControllerAdvice
: annotations()
, basePackageClasses()
, and basePackages()
. ControllerAdvice
is applied globally to all controllers if no selectors are provided
By using @ExceptionHandler
and @ControllerAdvice
, we’ll be able to define a central point for treating exceptions and wrapping them in an ApiError
object with better organization than is possible with the default Spring Boot error-handling mechanism.
Handling Exceptions
Next, we’ll create the class that will handle the exceptions. For simplicity, we call it RestExceptionHandler
, which must extend from Spring Boot’s ResponseEntityExceptionHandler
. We’ll be extending ResponseEntityExceptionHandler
, as it already provides some basic handling of Spring MVC exceptions. We’ll add handlers for new exceptions while improving the existing ones.
Overriding Exceptions Handled in ResponseEntityExceptionHandler
If you take a look at the source code of ResponseEntityExceptionHandler
, you’ll see a lot of methods called handle******()
, like handleHttpMessageNotReadable()
or handleHttpMessageNotWritable()
. Let’s see how can we extend handleHttpMessageNotReadable()
to handle HttpMessageNotReadableException
exceptions. We just have to override the method 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 thrownHttpMessageNotReadableException
, the error message will be “Malformed JSON request” and the error will be encapsulated in the ApiError
object. Below, we can see the answer of a REST call with this new method overridden:
{
"apierror": {
"status": "BAD_REQUEST",
"timestamp": "22-07-2022 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]"
}
}
Implementing Custom Exceptions
Next, we’ll create a method that handles an exception not yet declared inside Spring Boot’s ResponseEntityExceptionHandler
.
A common scenario for a Spring application that handles database calls is to provide a method that returns a record by its ID using a repository class. But if we look into the CrudRepository.findOne()
method, we’ll see that it returns null
for an unknown object. If our service calls this method and returns directly to the controller, we’ll get an HTTP code 200 (OK) even if the resource isn’t found. In fact, the proper approach is to return a HTTP code 404 (NOT FOUND) as specified in the HTTP/1.1 spec.
We’ll create a custom exception called EntityNotFoundException
to handle this case. This one is different from javax.persistence.EntityNotFoundException
, as it provides some constructors that ease the object creation, and one may choose to handle the javax.persistence
exception differently.
That said, let’s create an ExceptionHandler
for this newly created EntityNotFoundException
in our RestExceptionHandler
class. Create a method called handleEntityNotFound()
and annotate it with @ExceptionHandler
, passing the class object EntityNotFoundException.class
to it. This declaration signalizes Spring that every time EntityNotFoundException
is thrown, Spring should call this method to handle it.
When annotating a method with @ExceptionHandler
, a wide range of auto-injected parameters like WebRequest
, Locale
, and others may be specified as described here. We’ll provide the exception EntityNotFoundException
as a parameter for this handleEntityNotFound
method:
@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 the handleEntityNotFound()
method, we set the HTTP status code to NOT_FOUND
and usethe new exception message. Here is what the response for the GET /birds/2
endpoint looks like now:
{
"apierror": {
"status": "NOT_FOUND",
"timestamp": "22-07-2022 04:02:22",
"message": "Bird was not found for parameters {id=2}"
}
}
The Importance of Spring Boot Exception Handling
It is important to control exception handling so we can properly map exceptions to the ApiError
object and inform API clients appropriately. Additionally, we would need to create more handler methods (the ones with @ExceptionHandler) for thrown exceptions within the application code. The GitHub code provides more more examples for other common exceptions like MethodArgumentTypeMismatchException
, ConstraintViolationException
.
Here are some additional resources that helped in the composition of this article:
-
Error Handling for REST With Spring
-
Exception Handling in Spring MVC
Further Reading on the Toptal Engineering Blog:
- Top 10 Most Common Spring Framework Mistakes
- Spring Security with JWT for REST API
- Using Spring Boot for OAuth2 and JWT REST Protection
- Building an MVC Application With Spring Framework: A Beginner’s Tutorial
- Spring Batch Tutorial: Batch Processing Made Easy with Spring
Understanding the basics
-
Why should the API have a uniform error format?
A uniform error format allows an API client to parse error objects. A more complex error could implement the ApiSubError class and provide more details about the problem so the client can know which actions to take.
-
How does Spring know which ExceptionHandler to use?
The Spring MVC class, ExceptionHandlerExceptionResolver, performs most of the work in its doResolveHandlerMethodException() method.
-
What information is important to provide to API consumers?
Usually, it is helpful to include the error origination, the input parameters, and some guidance on how to fix the failing call.
In the earlier post of REST with Spring series, we discussed Spring MVC Content Negotiation.In this article, we will discuss how to implement Spring REST Exception Handling.
Introduction
Exception handling in a RESTful API with a meaningful error message and the status code is a desired and must have feature. A good error message helps API client to take corrective actions. Some application sends the exception stack trace which can be a good option for a typical web application however this is not a very good solution for a REST API. In this post, we will discuss and implement Error Handling with Spring for a REST API.
In this article, we are using Spring Boot to develop and work on our REST API. Spring Boot provides several features which makes it easy and flexible to create REST API’s with minimum configurations. We will look at few methods outlining how to do a better error handling for REST API using Spring Boot.
1. Restful API Error / Exception Design
While designing exception handling in the RESTful API, it’s a good practice to set HTTP status code in the response to communicate why the request failed or showing a success. We should send more information along with HTTP status code. It will help the client understand the error and take any corrective action.
HTTP/1.1 404
Content-Type: application/json
{
"status": 404,
"error_code": 123,
"message": "Oops! It looks like that file does not exist.",
"details": "File resource does not exist.We are working on fixing this issue.",
"information_link": "http://www.javadevjournal.com/errors/123"
}
Let’s discuss this response to understand important points while designing response for your REST API.
- The status represents HTTP status code.
- error_code represents REST API specific error code.This field is helpful to pass on API / domain specific information.
- The message field represents human-readable error message.
- The details section represents error information with complete detail.
- The information_link field specifies a link for detail information about the error or exception.
I am dividing this article in to 2 section. The first half talks about the importance of a better error handling for the REST API and the second half focused on the Spring Boot built-in feature for REST Exception Handling. We are taking a simple example of an online ecommerce application where our client can create a customer or get customer information by passing the customer id in the request.
1.1 Default Exception Message
Let’s see how the response look like in case REST API do not have a clear response.
{
"timestamp":"2020-01-23T06:08:43.062+0000",
"status":400,
"error":"Bad Request",
"message":"Failed to convert value of type 'java.lang.String' to required type 'long'; nested exception is java.lang.NumberFormatException: For input string: "abc"",
"path":"/customers/customer/abc"
}
This is the default output when there is no custom error response for our REST API. Though it provides a lot of information but it’s difficult for the client API to parse every exception and display the error message to the customer. There should be a better way to communicate these exceptions to the client to show a better error message.
Spring and Spring Boot provides several options for error/exception handling. Let’s see what are the different options for Error Handling in REST API with Spring.Before we move in to the details, let’s create an Error response class for our Rest exception handling example.
2.1. Rest API Error Handling Class.
public class ApiErrorResponse {
//http status code
private HttpStatus status;
// in case we want to provide API based custom error code
private String error_code;
// customer error message to the client API
private String message;
// Any furthur details which can help client API
private String detail;
// Time of the error.make sure to define a standard time zone to avoid any confusion.
private LocalDateTime timeStamp;
// getter and setters
//Builder
public static final class ApiErrorResponseBuilder {
private HttpStatus status;
private String error_code;
private String message;
private String detail;
private LocalDateTime timeStamp;
public ApiErrorResponseBuilder() {}
public static ApiErrorResponseBuilder anApiErrorResponse() {
return new ApiErrorResponseBuilder();
}
public ApiErrorResponseBuilder withStatus(HttpStatus status) {
this.status = status;
return this;
}
public ApiErrorResponseBuilder withError_code(String error_code) {
this.error_code = error_code;
return this;
}
public ApiErrorResponseBuilder withMessage(String message) {
this.message = message;
return this;
}
public ApiErrorResponseBuilder withDetail(String detail) {
this.detail = detail;
return this;
}
public ApiErrorResponseBuilder atTime(LocalDateTime timeStamp) {
this.timeStamp = timeStamp;
return this;
}
public ApiErrorResponse build() {
ApiErrorResponse apiErrorResponse = new ApiErrorResponse();
apiErrorResponse.status = this.status;
apiErrorResponse.error_code = this.error_code;
apiErrorResponse.detail = this.detail;
apiErrorResponse.message = this.message;
apiErrorResponse.timeStamp = this.timeStamp;
return apiErrorResponse;
}
}
}
We will use this class to rest customer error message from our REST Api.
2.2. ExceptionHandler Annotation
The first approach is to use the ExceptionHandler annotation at the controller level. This annotation specifically handle exceptions thrown by request handling (@RequestMapping
) methods in the same controller. Let’s take an example where service can throw CustomerNotFound Exception but we like to send a different / customize message to the client API.
@RestController
@RequestMapping("/customers")
public class CustomerController {
@GetMapping("/customer/{id}")
public Customer getCustomer(@PathVariable long id) throws CustomerNotFoundException {
if (id == 1) {
throw new CustomerNotFoundException();
}
return new Customer();
}
@ExceptionHandler({CustomerNotFoundException.class})
public String handleException() {
return "bad_request";
}
}
We can define as many @RequestMapping
in our controller (Having a different mapping for a different exception type).There are multiple problems or drawbacks with the approach.
- This annotation is only active for the controller.
- This annotation is not global and we need to add to every controller (not very intuitive).
- The return type for this is void or String which add a lot of constraints.
Most of the enterprise application work by extending a basic controller (having common controller functionalities). We can overcome @ExceptionHandler
limitation by adding it to the base controller. This also has multiple limitations.
- The base controller is not suitable for all type of controller. We will end up by duplicating out code.
- Our Controllers may have to extend a third party class not under our control.
[pullquote align=”normal”]With all these limitations, I do not recommend it to use this approach while building your RESTful API [/pullquote]
3. The @ControllerAdvice Annotation
Spring 3.2 introduced @ControllerAdvice
annotation which supports global Exception handler mechanism. A controller advice allows you to use exactly the same exception handling techniques but applies them across the application, not just to an individual controller.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@ControllerAdvice
public class GlobalRestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({CustomerNotFoundException.class})
public ResponseEntity <ApiErrorResponse> customerNotFound(CustomerNotFoundException ex, WebRequest request) {
ApiErrorResponse apiResponse = new ApiErrorResponse
.ApiErrorResponseBuilder()
.withDetail("Not able to find customer record")
.withMessage("Not a valid user id.Please provide a valid user id or contact system admin.")
.withError_code("404")
.withStatus(HttpStatus.NOT_FOUND)
.atTime(LocalDateTime.now(ZoneOffset.UTC))
.build();
return new ResponseEntity <ApiErrorResponse> (apiResponse, HttpStatus.NOT_FOUND);
//We can define other handlers based on Exception types
}
}
If we call our customer API endpoint again with an invalid user id, we will get the following response from the API
{
"status": "NOT_FOUND",
"error_code": "404",
"message": "Not a valid user id.Please provide a valid user id or contact system admin.",
"detail": "Not able to find customer record",
"timeStamp": "2020-01-24T05:56:13.192"
}
Let’s discuss some important points for the @ControllerAdvice
annotation:
- The
@ControllerAdvice
annotation is a specialized@Component
annotation. We have the flexibility to use this annotation for multiple controller classes (This works based on the Exception and not bind to the Controller). - Spring Boot automatically detects all classes annotated with
@ControllerAdvice
during startup as part of the classpath scanning. - We can narrow down the selected controllers by using
basePackageClasses()
, andbasePackages()
parameters. For more details refer to the ControllerAdvice.
3.1. ResponseEntityExceptionHandler
In the above example we extended ResponseEntityExceptionHandler class. This is a convenient base class for @ControllerAdvice
classes that wish to provide centralized exception handling across all @RequestMapping
methods through @ExceptionHandler
methods. This is a convenient way while working on Spring based REST API since it allows the developer to specify ResponseEntity as return values. Let’s work on some most common client errors. We will look into few scenarios of a client sending an invalid request.
3.2. MethodArgumentTypeMismatchException
It throws this exception when method arguments are not the expected type
@ExceptionHandler({MethodArgumentTypeMismatchException.class})
protected ResponseEntity <Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ApiErrorResponse response = new ApiErrorResponse.ApiErrorResponseBuilder()
.withStatus(status)
.withDetail("not valid arguments")
.withMessage(ex.getLocalizedMessage())
.withError_code("406")
.withError_code(status.NOT_ACCEPTABLE.name())
.atTime(LocalDateTime.now(ZoneOffset.UTC))
.build();
return new ResponseEntity < > (response, response.getStatus());
}
3.3. HttpMessageNotReadable
It throws this exception when API cannot read the HTTP message
@Override
@ExceptionHandler({HttpMessageNotReadableException.class})
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = "Malformed JSON request "; ApiErrorResponse response =new ApiErrorResponse.ApiErrorResponseBuilder() .withStatus(status) .withError_code("BAD_DATA") .withMessage(ex.getLocalizedMessage()) .withDetail(error+ex.getMessage()) .build(); return new ResponseEntity<>(response, response.getStatus()); }
Below we can see the answer to a REST call
{
"status": "BAD_REQUEST",
"error_code": "BAD_DATA",
"message": "JSON parse error: Unexpected character
"detail": "Malformed JSON request JSON parse error: Unexpected character ('<' (code 60)): expected a valid value (number, String, array, object, 'true', 'false' or 'null');
}
4. Handling Custom Exceptions
While working on REST API, we may come across multiple use cases when a request is not fulfilled and we want to return a custom exception back to the client API. Let’s take a simple use case when client API call to find a customer by its unique id. Our service call might return a null or empty object if we do not find the object. Here, if not handled correctly, the API will return 200 (OK)
response to the client even if no record found. Let’s create a simple example for better clarity:
import com.javadevjournal.DefaultCustomerService;
import com.javadevjournal.data.Customer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
DefaultCustomerService customerService;
@GetMapping("/customer/{id}")
public Customer getCustomer(@PathVariable long id) {
return customerService.getCustomerById(id);
}
}
Here is the sample service file.
import com.javadevjournal.data.Customer;
import org.springframework.stereotype.Service;
@Service("customerService")
public class DefaultCustomerService {
public Customer getCustomerById(final long customerId) {
if (customerId == 1) {
return null;
}
return new Customer(customerId, "Test", "Customer", "[email protected]");
}
}
In above example, if we send a request with the user id as 1, our REST API send 200 (OK)
response.
To handle all similar use cases, we create a custom exception and handle this exception in our GlobalRestExceptionHandler
package com.javadevjournal.exception;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@ControllerAdvice
public class GlobalRestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(CustomRestServiceException.class)
protected ResponseEntity < Object > handleCustomAPIException(CustomRestServiceException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ApiErrorResponse response = new ApiErrorResponse.ApiErrorResponseBuilder()
.withStatus(status)
.withDetail("custom exception")
.withMessage(ex.getLocalizedMessage())
.withError_code("503")
.withError_code(status.SERVICE_UNAVAILABLE.name())
.atTime(LocalDateTime.now(ZoneOffset.UTC))
.build();
return new ResponseEntity < > (response, response.getStatus());
}
}
I will not go into details about handling different Exceptions in the REST API since we can handle all Exceptions in a similar way as explained above. Here is the list of some common exceptions in a REST API.
- HttpMediaTypeNotSupportedException
- HttpRequestMethodNotSupportedException
- TypeMismatchException
5. Default Exception Handler
We can not handle each exception within the system. Let’s create a fallback handler which will handle all exceptions that don’t have specific exception handler.
package com.javadevjournal.exception;
@ControllerAdvice
public class GlobalRestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity <Object> handleCustomAPIException(Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ApiErrorResponse response = new ApiErrorResponse.ApiErrorResponseBuilder()
.withStatus(status)
.withDetail("Something went wrong")
.withMessage(ex.getLocalizedMessage())
.withError_code("502")
.withError_code(status.BAD_GATEWAY.name())
.atTime(LocalDateTime.now(ZoneOffset.UTC))
.build();
return new ResponseEntity <> (response, response.getStatus());
}
}
6. Spring Boot REST Error Handling
Spring Boot provides several features to build RESTful API’s. Spring Boot 1.4 introduced the @RestControllerAdvice
annotation for easier exception handling. It is a convenience annotation that is itself annotated with @ControllerAdvice
and @ResponseBody.
Here is an example.
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(CustomNotFoundException.class)
public ApiErrorResponse handleNotFoundException(CustomNotFoundException ex) {
ApiErrorResponse response =new ApiErrorResponse.ApiErrorResponseBuilder()
.withStatus(HttpStatus.NOT_FOUND)
.withError_code("NOT_FOUND")
.withMessage(ex.getLocalizedMessage()).build();
return responseMsg;
}
}
While using above approach, set following property to true in Spring Boot application.properties
file
spring.mvc.throw-exception-if-no-handler-found=true # Whether a "NoHandlerFoundException" thrown if no Handler was found to process a request.
7. JSR 303 Validation Error (REST API)
The JSR 303 or also known as bean validation is a standard way to validate your incoming data. The @valid
annotation throws handleMethodArgumentNotValid
error if the incoming data is not valid. In case we like to provide a custom error message, we have the same option to add a separate handler in our GlobalRestExceptionHandler
class. This is our simple CustomerController
:
import com.javadevjournal.DefaultCustomerService;
import com.javadevjournal.data.Customer;
import com.javadevjournal.exception.CustomRestServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
DefaultCustomerService customerService;
@PostMapping("/customer/register")
public Customer createCustomer(@Valid @RequestBody Customer customer) throws CustomRestServiceException {
return customerService.registerCustomer(customer);
}
}
We have added bean validation constraints to our Customer class:
@NotNull(message = "Id can not be null")
private long id;
@NotNull(message = "Please provide first Name")
private String firstName;
@NotNull(message = "Please provide last Name")
private String lastName;
@Email(message = "please provide a valid email id")
private String email;
Here is the custom spring rest exception handling method in our GlobalRestExceptionHandler
class.
@ExceptionHandler({ MethodArgumentTypeMismatchException.class})
protected ResponseEntity < Object > handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
// let's get all validation error and send it across
List < String > errorMsg = ex.getBindingResult().getFieldErrors().stream().map(e - > e.getDefaultMessage()).collect(Collectors.toList());
ApiErrorResponse response = new ApiErrorResponse.ApiErrorResponseBuilder()
.withStatus(status)
.withDetail("not valid arguments")
.withMessage(errorMsg.toString())
.withError_code("406")
.withError_code(status.NOT_ACCEPTABLE.name())
.atTime(LocalDateTime.now(ZoneOffset.UTC))
.build();
return new ResponseEntity < > (response, response.getStatus());
}
When we call our REST controller, we will have a custom error response based on the JSR 303 bean validation.
Summary
It is important to handle and process exceptions properly in the Spring bases REST API. In this post, we covered different options to implement Spring REST Exception Handling. Building a good exception handling workflow for REST API is an iterative and complex process. A good exception handling mechanism allows API client to know what went wrong with the request. The source code is available on the GitHub.