Introduction
The purpose of this article is to show different ways of handling exceptions in a REST application built with Spring/Spring boot.
In the coming sections, exception handling capability will be added to our customer restapi application. Source code is available in the following link
Currently, the customer rest application returns a ResponseEntity (a Http Response wrapper consisting of status, headers and body) whenever a special case occurs. For instance, a customer was not found when details were requested.
@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers(
@PathVariable("customerId") Long id) {
return customerRepo
.findById(id)
.map(ResponseEntity::ok)
.orElse(new ResponseEntity("Customer "+id+" not found"
,HttpStatus.NOT_FOUND));
}
There is no exception handling strategy in place. Implementing one will make the code easier to read and separate "regular code" from what to do when something out of the ordinary happens.
The above code will return 404 error and a meesage as shown in the below picture.
Now lets look at the first mechanism to manage exception in our applications.
Custom exceptions with @ResponseStatus
It marks a method or exception class with the status code() and reason() that should be returned. For example, a custom exception can be declared as follows:
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class CustomerNotFoundException extends RuntimeException {
public CustomerNotFoundException(Long id) {
super("Customer was "+id+" not found");
}
}
Now in the controller, the custom exception would be thrown
@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers(
@PathVariable("customerId") Long id) {
return customerRepo
.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
As per Spring documentation this annotation is not suitable for REST APIs because the HttpServletResponse.sendError method will be used and the Servlet container will typically write an HTML error page. This means we have no control on the body.
Another drawback is that it highly couples the exception with Spring framework. We may want to avoid intrusion in the exception class (as it would part of the core architecture of the app) and prevent it from depending directly on Spring.
ResponseStatusException
Spring 5 introduced a new Exception class that accepts a status code and optionally a reason and a cause. This provides a good solution to manage the same situation/case in many different ways.
But we still have no common place where global rules are applied for the whole application, plus it can lead to code duplication.
Our code would look like
@GetMapping("{customerId}")
public ResponseEntity<Customer> findCustomers(
@PathVariable("customerId") Long id) {
return customerRepo
.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND,"Customer "+id+" not found." ));
}
Output when a non-existing customer is fetched.
{
"timestamp": "2023-04-16T14:10:49.752+00:00",
"status": 404,
"error": "Not Found",
"path": "/api/v1/customers/100"
}
By default Spring does not reveal the error message in the response as a security measure. This is to prevent detailed messages leaking from the server. By adding the below property to application.properties file Spring Boot will display the message.
server.error.include-message=always
Now the response comes with the message in it.
{
"timestamp": "2023-04-16T17:09:36.281+00:00",
"status": 404,
"error": "Not Found",
"message": "Customer 1001 not found.",
"path": "/api/v1/customers/1001"
}
The above JSON may not suit our requirements. We will see how to use a custom JSON error response for any exceptions in the next section.
Exception handling with @ExceptionHandler
It allows to manage exceptions in a method. Handler methods annotated with it are allowed to have very flexible signatures. In our case, the method takes the exception type as parameter and returns a ResponseEntity.
The way it works is when the exception is thrown, the handler method will intercept it and return the particular response if any. More info can be found here
First, we will create a record representing the response we want to send back to the client. It is a very simple inmutable class containing three properties the status, message and timestamp.
public record RestErrorResponse(int status, String message,
LocalDateTime timestamp) {}
Next, a new method will be added to the controller to handle the exception.
@ExceptionHandler
public ResponseEntity<RestErrorResponse> handleException(
CustomerNotFoundException ex) {
var response = new RestErrorResponse(
HttpStatus.NOT_FOUND.value(), ex.getMessage(),
LocalDateTime.now();
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
And the output would be
{
"status": 404,
"message": "Customer 1001 not found!!",
"timestamp": "2023-04-16T12:25:10.3432534"
}
This works fine at controller level but that is a limitation if we need to set up a global configuration for our application. Also, we may not want the controllers to be responsible for handling exceptions and separate that concern from them.
Global Configuration with @ControllerAdvice
The @ControllerAdvice is part of Spring AOP and it is wired up to Spring MVC project. It operates similar to a filter/interceptor providing pre-process request and post-process response features. It allows to keep the exception handling centralized and promotes code reusability.
First, the exceptionhandler method from the previous section must be deleted or commented. Second, we create a new class and move the code to it as displayed in the following snippet:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomerNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
RestErrorResponse handleCustomerNotFoundException(
CustomerNotFoundException ex) {
return new RestErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now());
}
// Handle any other exception too.
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
RestErrorResponse handleException(Exception ex) {
return new RestErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
LocalDateTime.now());
}
}
The @RestControllerAdvice annotation is a combination of @ControllerAdvice and @ResponseBody which is very convinient for REST apps. Note that the @ResponseStatus is needed to return the httd code and the body will be our RestErrorResponse record.
Again, the output when hitting the endpoint http://localhost:8080/api/v1/customers/1001 is the expected.
{
"status": 404,
"message": "Customer 1001 not found!",
"timestamp": "2023-04-16T13:39:26.1711689"
}
For real time projects and large scale developments this is best practice as it can be applied to many REST services.
Summary
We have covered some of the options Spring offers when it comes to managing exceptions in the Controller layer. Below is a quick recap of what was done:
- @ResponseStatus: Not appropiate for rest app because the server will present an HTML error page and it causes highly-coupling.
- ResponseStatusException: it is a fast solution and versatile. However, it can lead to code duplication and has no full control on the body.
- @ExceptionHandler: Only works for the controller where the method is declared.
- @ControllerAdvice: Provides global configuration in a centralized fashion. Best practice for production ready apps.
In the second part of this mini series, we will look at Spring 6 new support for Problem Details for HTTP APIs (RFC-7807).
That is all for today!
Top comments (0)