In our previous tutorials, we discussed how build a CRUD REST API in a Spring Boot environment. However, we have not yet discussed how to handle common errors that can occur, such as when the user tries to fetch a non-existing item.
In this tutorial, we will discuss some solutions and strategies for handling errors that might appear in your application.
This tutorial is based on other REST API tutorials. I recommend that you read them first before moving on to this one. For your convenience, I have listed them below:
- How to build a CRUD REST API in Spring boot.
- How to validate REST API input.
- HTTP POST vs HTTP PATCH in REST (Optional)
Now, let us get started!.
The goal of error handling
Before we start, we thought it might be important to discuss the overall goals of why we do error handling (the “big picture”). This part is purely theoretical. So, if you are not interested, you can skip ahead to the next section.
Error handling is a key part of designing any application. Depending on your application and your needs, you might have to perform certain actions when an exception occurs.
When an error occurs, you need to ask yourself a few questions:
- Why did the error occur?
- How to recover from the error?
- How to avoid the error in the future?
The goal of error handling is to be able to answer these three questions when an error is encountered.
The first question can be answered by providing the user with enough information when the error occurs in order to identify the cause. For example, when the user tries to access a resource which does not exist, it would make sense to reply with an HTTP error code 404 (not found) instead of just 400 (bad request) or worse, an HTTP code 500 (server error)!.
There are many things that you can do to recover from an error. The strategy that you implement will depend on the error, from which perspective are you handling the error (are you the developer, are you a user?) and your application. Sometimes, you cannot recover instantly from an error and you might need to retry your request later. Sometimes you might have to make changes in your application. We will not discuss recovery scenarios in this tutorial. However, we thought it is an important point to keep in mind.
Finally, in order to avoid the error in the future, you might need to make some changes in your configuration or implementation. For example, if you see that users are complaining that they are always getting X error when calling Y method in your production environment, you might have to change method Y to satisfy the customer’s needs. To enable you, other developers and users of the system to do this, you will need to gather enough information about the error in order to troubleshoot it and fix it. This can be done by logging the error and saving the logs for future analysis.
In the next few sections, we will discuss different ways of handling errors in a REST endpoint in Spring Boot.
Customizing the HTTP status code inside the REST Controller
Thanks to the ResponseEntity class from Spring, you can customize the response you send to the caller of your REST API. This also includes the HTTP status code sent in the response.
Let us take the example of accessing a non-existing resource. Until now, we have not tried to access our REST endpoints with a non-existing id. If we try to access a non-existing item using the GET method of our “/api/tickets” endpoint from our previous tutorials, we would get an HTTP 500 error.
HTTP/1.1 500
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 13 Apr 2020 12:06:54 GMT
Connection: close
{"timestamp":"2020-04-13T12:06:54.091+0000","status":500,"error":"Internal Server Error","message":"No value present","path":"/api/tickets/111"}
Obviously, this is not a good design as it indicates to the caller that a server side error has occurred. We need to tell the user that the resource they are trying to access does not exist. This can be done by replying to the user with an HTTP status code 404. Let us try to do that using the ResponseEntity class.
@GetMapping("/{id}")
public ResponseEntity getTicketById(@PathVariable(name = "id") long ticketId){
Optional<Ticket> optionalTicket = ticketRepository.findById(ticketId);
if(!optionalTicket.isPresent())
{
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Entity not found");
}
Ticket ticket = optionalTicket.get();
TicketRestModel ticketRestModel = new TicketRestModel();
ticketRestModel.setIdentifier(ticket.getId());
ticketRestModel.setTitle(ticket.getTitle());
ticketRestModel.setDescription(ticket.getDescription());
ticketRestModel.setStatus(ticket.getStatus());
return ResponseEntity.ok(ticketRestModel);
}
In the example above, we first check if the item exists. If it does, we proceed with the rest of the method. Otherwise, we build a “not found” response by using the ResponseEntity.status(..) method and setting the status to HttpStatus.NOT_FOUND.
If we try now the GET method with an invalid ID, we get the following response.
HTTP/1.1 404
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 16
Date: Mon, 13 Apr 2020 12:12:13 GMT
Keep-Alive: timeout=60
Connection: keep-alive
Entity not found
Please note that while this approach works for this error, it is not recommended for handling “not found” errors. The reason is that you will need to repeat the same code in every controller, and every controller method. We will discuss a better approach for implementing HTTP 404 errors (among others) in the next section using @ControllerAdvice.
What is the @ControllerAdvice annotation?
The @ControllerAdvice is an annotation from Spring-Web that can be used to annotate classes which will be used for exception handling inside your application. The annotated classes can be used to handle errors which occur in a subset or in all of the controllers that exist in a Spring application.
@ControllerAdvice
public class DefaultControllerAdvice{
You can then define which exceptions your controller advice class should handle. This can be done by annotating error handling methods with the @ExceptionHandler annotation, and specifying the type of exception to be handled.
@ExceptionHandler(MyException.class)
public final ResponseEntity<Object> handleMyException(Exception ex, HttpServletRequest request) {
......
......
When an exception of the given class or a subclass of the exception class specified is thrown during a controller method call, the corresponding handler method will be triggered.
Spring will pass to your handler method the exception instance, and the request which triggered the exception. Using this information, you can formulate an appropriate response to the user.
Inside the handler method, you can also perform other useful functions, such as logging the error for future investigation and debugging, triggering recovery scenarios, among others.
Please keep in mind the following:
- You can narrow down the scope of your controller advice class by specifying which controllers does it “advise”. This can be done by configuring the @ControllerAdvice annotation with the package where your target controllers reside.
- There are other ways to configure the @ControllerAdvice, such as specifying the actual controllers to be enhanced, specifying controllers annotated with specific annotations (such as @RestControllers or any custom annotation), etc.
- You can use the @ExceptionHandler annotation inside the controller itself. This allows you to build handlers only for that controller.
There are numerous configuration options for the @ControllerAdvice and the @ExceptionHandler annotations. We will discuss these in future posts. In order to better understand how to use the @ControllerAdvice annotation, let us implement a handling mechanism for not-found items.
Handling 404 “Not Found” errors using @ControllerAdvice
Previously, we mentioned that we would like to build a mechanism which will allow us to handle errors where items were not found in our system, but in a more generic and maintainable way.
If you are using Spring-Data and JPA, then an attempt to access a non-existing item will cause the framework to throw a “NoSuchElementException”. We can either handle this exception, or throw our own.
To make things more clear, let us create our own business exception, to be thrown when a requested item does not exist.
package com.nullbeans.customerservice.incidentmanagement.rest;
/**
* By: Nullbeans.com
* Exception thrown when the requested item is not found.
*/
public class ItemNotFoundException extends RuntimeException {
public ItemNotFoundException(){
super();
}
public ItemNotFoundException(String message){
super(message);
}
}
Next, let us configure our controller’s GET method to throw this exception when an item is not found.
@GetMapping("/{id}")
public ResponseEntity getTicketById(@PathVariable(name = "id") long ticketId){
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new ItemNotFoundException("Ticket with ID:["+ticketId+"] was not found"));
TicketRestModel ticketRestModel = new TicketRestModel();
ticketRestModel.setIdentifier(ticket.getId());
ticketRestModel.setTitle(ticket.getTitle());
ticketRestModel.setDescription(ticket.getDescription());
ticketRestModel.setStatus(ticket.getStatus());
return ResponseEntity.ok(ticketRestModel);
}
The next step is to define our own error class. This class will be serialized into JSON which will be sent as the response body of our response message to the user.
In this class, we can add details to the user which will help them to identify the error.
mport lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import org.springframework.http.HttpStatus;
@Data
@AllArgsConstructor
@ToString
public class RestApiError {
HttpStatus httpStatus;
String errorMessage;
String errorDetails;
}
The final step is to configure our controller to handle the exception when it gets thrown. Our controller advice class will now look as follows.
mport com.nullbeans.customerservice.incidentmanagement.rest.ItemNotFoundException;
import com.nullbeans.customerservice.incidentmanagement.rest.errorhandling.errors.RestApiError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.http.HttpServletRequest;
@ControllerAdvice
public class DefaultControllerAdvice{
private static final Logger log = LoggerFactory.getLogger(DefaultControllerAdvice.class);
@ExceptionHandler(ItemNotFoundException.class)
protected ResponseEntity<Object> handleEntityNotFound(
ItemNotFoundException ex, HttpServletRequest request) {
log.debug("A REST API error occurred during web call [{}:{}].", request.getMethod(), request.getRequestURI() ,ex);
RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND,
"The requested resource was not found.",
ex.getMessage());
return new ResponseEntity<>(apiError, apiError.getHttpStatus());
}
}
Let us describe the steps that we did in the handleEntityNotFound method:
- First, we log the error. These logs can then be used to debug issues such as configuration issues between systems or user input errors.
- We create our own “Error” entity. This entity contains useful details about the issue and will be sent to the requester in the response entity’s body.
- We set the error code of the response entity as HttpStatus.NOT_FOUND. This allows the API caller to classify the error and to better troubleshoot the issue.
If we now call the GET method of the TicketController using a non-existing ticket ID, we will get the following response:
HTTP/1.1 404
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 13 Apr 2020 13:07:04 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"httpStatus": "NOT_FOUND",
"errorMessage": "The requested resource was not found.",
"errorDetails": "Ticket with ID:[111] was not found"
}
As you can see, this is a much nicer and more detailed error response than an HTTP 500 error.
Handling business exceptions
The strategy described in the previous sections is not limited to just “not found” errors. We can also extend this mechanism to cover business exceptions which can occur in our applications.
A business exception is an error which occurs due to the user violating the rules of the system in use. For example, an unauthorized user tries to read data from the system, or a user tries to reserve an already reserved room in a hotel website. These errors are usually indicated with HTTP 4XX status codes.
This is different from system exceptions, where an error occurs due to a failure of the system. A system exception can occur due to a broken database connection, running out of memory, or some other internal error which occurs due to a failure in one or more system components. These errors are usually indicated with HTTP 5XX status codes.
It is recommended to handle business exceptions that are thrown during REST calls in order to guide the user and help them recover from the error.
One way to do this is to create a “BusinessException” class.
/**
* A generic business exception which can occur due to violation of business logic in the application.
*/
public class BusinessException extends RuntimeException{
public BusinessException(String msg){
super(msg);
}
public BusinessException(Exception ex){
super(ex);
}
public BusinessException(String msg, Exception ex){
super(msg, ex);
}
}
Now that we have defined our base business exception, we can then handle this exception and subclasses of this exception in our exception handler method.
@ExceptionHandler(BusinessException.class)
public final ResponseEntity<Object> handleBusinessException(Exception ex, HttpServletRequest request) {
log.info("A REST API error occurred during web call [{}:{}].", request.getMethod(), request.getRequestURI() ,ex);
RestApiError apiError = new RestApiError(HttpStatus.BAD_REQUEST,
"An error occurred while processing your request.",
ex.getMessage());
return new ResponseEntity<>(apiError, apiError.getHttpStatus());
}
This will cause Spring to trigger this handler when an exception of type BusinessException or a subclass of it is thrown during a call to a Controller.
You can also also write a handler for all other exceptions which were not handled somewhere else inside your controller advice class by using the Exception class.
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleUnexpectedExceptions(Exception ex, HttpServletRequest request) {
log.error("A REST API error occurred during web call [{}:{}].", request.getMethod(), request.getRequestURI() ,ex);
RestApiError apiError = new RestApiError(HttpStatus.INTERNAL_SERVER_ERROR,
"An error occurred while processing your request. We are currently working on resolving the problem as soon as possible.",
"Internal Server Error");
return new ResponseEntity<>(apiError, apiError.getHttpStatus());
}
This has the advantage of
- Hiding the complete stack trace from the caller. This can be important for security reasons.
- You can log such errors in your handler method for troubleshooting and analysis.
- You can also trigger a warning or alarm systems to indicate to the system owners that there is an issue in the application.
Note that Spring will try to trigger the handler that best matches the exception. For example, if a BusinessException is thrown, then only the handler configured with the BusinessException will be triggered. The handler configured with the parent class Exception will not be triggered.
Final thoughts
In this tutorial, we discussed how to handle errors such as “not found” errors and how to return a specific HTTP error code to the REST API caller.
Please note that error handling in Spring Boot is a very large topic. We will go into more details regarding @ControllerAdvice and @ExceptionHandler in future posts, and we will update this post with new resources, as soon as they are ready.
However, please keep in mind the goals of error handling discussed at the beginning of this post. It does not matter which strategy you choose to handle errors. Your goal should be to have errors in your system identifiable, debuggable, and hopefully fixable 🙂
Leave a Reply
You must be logged in to post a comment.