Spring error message

declaration: package: org.springframework.messaging.support, class: ErrorMessage

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 (never null)
    • 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 (never null)
      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 (never null)
      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 (never null)
      headers — message headers to use for initialization
      originalMessage — 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 class GenericMessage<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.

@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) { } }

Code language: Java (java)

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.

server: error: include-message: always

Code language: YAML (yaml)

Next is an example of a response object the REST API returns. Note that the response object has the specified error message.

{ "timestamp": "", "status": 404, "error": "Not Found", "message": "Requested Student Not Found", "path": "/students/Jack" }

Code language: JSON / JSON with Comments (json)

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

@Data @RequiredArgsConstructor public class Error { private final HttpStatus httpStatus; private final String message; }

Code language: Java (java)

Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.

@ExceptionHandler(StudentNotFoundException.class) public ResponseEntity handleException( StudentNotFoundException e) { Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); }

Code language: Java (java)

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.

throw new StudentNotFoundException ("Student service failed, studentId : " + studentId);

Code language: Java (java)

When the REST API cannot find the requested resource, we get a detailed error as a response.

{ "httpStatus": "NOT_FOUND", "message": "Student service failed, studentId : Jack" }

Code language: JSON / JSON with Comments (json)

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.

@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); }

Code language: Java (java)

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.

@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); }

Code language: Java (java)

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.

{ "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"" }

Code language: JSON / JSON with Comments (json)

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.

@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); }

Code language: Java (java)

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.

@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); }

Code language: Java (java)

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.

{ "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" }

Code language: JSON / JSON with Comments (json)

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.

@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); }

Code language: Java (java)

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.

@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); }

Code language: Java (java)

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.

{ "error": "GET is not one of the supported Http Methods (POST)", "message": "Request method 'GET' not supported", "status": "405 METHOD_NOT_ALLOWED" }

Code language: JSON / JSON with Comments (json)

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.

@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); }

Code language: Java (java)

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 likes E001.
  • 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’s value
    or basePackages 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 the ResponseEntityExceptionHandler use this function to build the
    ResponseEntity similar to our buildErrorResponse(). 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 the basePackages or annotations 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 specified basePackages or annotations 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:

Spring Exception Handling Flow

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. The ApiSubError 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:

  1. 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).
  2. Spring Boot automatically detects all classes annotated with @ControllerAdvice during startup as part of the classpath scanning.
  3. We can narrow down the selected controllers by using basePackageClasses(), and basePackages() 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.

Error Handling for REST with Spring

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.

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.

Понравилась статья? Поделить с друзьями:
  • Spring boot error controller
  • Spring 406 error
  • Spring 302 error
  • Spotify как изменить адрес электронной почты
  • Spotify installer error code 24 как исправить