Hello everyone, in this article we are going to take a look at how to handle REST API exceptions in Spring Boot. It is crucial to handle errors correctly in APIs by displaying meaningful messages as it helps API clients to address problems easily. What happens if we don’t handle the errors manually? By default, the spring application throws a stack trace, which is difficult to understand. Stack traces are mainly for developers hence it is useless for API clients. That's why its very important to use proper HTTP code and error messages to convey errors and exception to client and also logging so that support team can better handle them. Ideally you should tell what went wrong and how to fix it? For example, if the error is due to duplicate data then clearly say, already existed, try with a new one. Similarly, if authentication fail then clearly say authentication failed instead of throwing some other exception.
Handling REST exception in Spring Boot Application
An API error comprises multiple parts such as status code, stack trace, error message, etc. A better approach to solve this problem is to split the error message into various error fields, later the message will be parsed by the API client and give the user a better error message. Here we are going to learn how to handle errors properly in Spring Boot Application.Understand the problem
Below are the endpoints in my spring application
GET /api/v1/fruits/ Get all fruits data
GET /api/v1/fruits/{id} Get a single fruit with provided id
POST /api/v1/fruits/ Register a new Fruit
Let's see what happens when we hit the above URLs
GET /api/v1/fruits/
{
"id": 0,
"name": "Orange",
"color": "orange",
"taste": "bitter",
"avgWeight": 0.25
}
What happens, if we try to access a Fruit with an ID that is not present in the database such as 10? The following response will return
GET /api/v1/fruits/10 (with non-existing ID)
{
"timestamp": "2023-01-12T06:29:47.878+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.http.converter.HttpMessageNotWritableException:
Could not write JSON: Unable to find com.javarevisited.ipldashboard.model.Fruit
with id ... 54 more\r\n",
"message": "Could not write JSON: Unable to find
com.javarevisited.ipldashboard.model.Fruit with id 1",
"path": "/api/v1/fruits/1"
}
As you can see, it comes up with a huge error trace and that too is completely useless for API clients. Similarly, if we try to post a Fruit with a valid and then an invalid request body, Spring will return the following response.
POST /api/v1/fruits/
{
"name": "Orange",
"color": "orange",
"taste": "bitter",
"avgWeight": 0.25
}
POST /api/v1/fruits/ (with invalid request body)
{
"name": "Orange",
"color": "orange",
"taste": "bitter",
"avgWeight": "Half quarter"
}
{
"timestamp": "2023-01-12T05:30:06.948+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `double` from String \"Half quarter\": not a valid `double` value (as String to convert)\r\n\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406)... and many more",
"message": "JSON parse error: Cannot deserialize value of type `double` from String \"Half quarter\": not a valid `double` value (as String to convert),
"path": "/api/v1/fruits/"
}
As you can see, Spring Boot's default response returns some useful information such as the status code, but it is overly exception-focused. The dark side of it is, by showing this the API consumer will lost in useless internal details. I’m sure API clients won’t love to see this. Let’s make things easier for our API consumer by learning how to handle REST API exceptions properly by wrapping them in a pretty JSON object.
Create a class to hold API errors. Let’s call it AppError with a related field about errors in REST API.
class AppError {
private HttpStatus status;
private LocalDateTime timestamp;
private String message;
private String debugMessage;
private AppError() {
timestamp = LocalDateTime.now();
}
AppError(HttpStatus status) {
this.status = status;
}
AppError(HttpStatus status, Throwable ex) {
this.status = status;
this.message = "Unexpected error";
this.debugMessage = ex.getLocalizedMessage();
}
AppError(HttpStatus status, String message, Throwable ex) {
this.status = status;
this.message = message;
this.debugMessage = ex.getLocalizedMessage();
}
}
AppError class has the following four properties:
status: Hold HttpStatus info such as 404 (NOT_FOUND), 400 (BAD_REQUEST), etc.
message: It holds a user-friendly message related to an error.
timestamp: Describes when the error took place.
debugMessage: Holds a system message which describe an error in more detail.
Here is the updated JSON response for the invalid argument
GET /api/v1/fruits/10 (with non-existing ID)
{
"status": "BAD_REQUEST",
"timestamp": null,
"message": "Unexpected error",
"debugMessage": "Could not write JSON: Unable to find
com.javarevisited.ipldashboard.model.Fruit with id 10"
}
POST /api/v1/fruits/ (with invalid request body)
{
"status": "BAD_REQUEST",
"timestamp": "12-01-2023 02:51:41",
"message": "Malformed JSON request",
"debugMessage": "JSON parse error: Cannot deserialize value of
type `double` from String \"Half quarter\": not a valid `double`
value (as String to convert)"
}
Exception Handler and ControllerAdvice
Every spring REST API is annotated with @RestController. One can handle errors by using ExceptionHandler annotation, this annotation can be written down on top of controller class methods. By doing this, it will act as the starting point for processing any exceptions that are thrown just within that controller.
@RestController
public class FruitController {
@RequestMapping("/api/v1/fruits")
@ExceptionHandler
public ResponseEntity<List<Fruit>> getAllFruits(){
return new ResponseEntity<>(fruitRepo.findAll(), HttpStatus.OK);
}
}
The issue with the above approach is, it can’t be applied globally. To cope with this, the most common implementation is to use @ControllerAdvice class and annotate its method with ExceptionHandler The system will call this handler for thrown exceptions on classes covered by this ControllerAdvice.
The significant advantage is, now we have a central point for processing exceptions by wrapping them in an AppError object with a greater organization by utilizing @ExceptionHandler and @ControllerAdvice than we can with the built-in Spring Boot error-handling technique.
Handling Exception
The class we created for handling exceptions is ApplicationExceptionHandler which must extend ResponseEntityExceptionHandler. The whole reason to extend ResponseEntityExceptionHandler is to have access to pre-define exception handlers. As you can see below.
handleMethodArgumentNotValid
handleTypeMismatch
handleHttpMessageNotWritable
handleHttpMessageNotReadable
handleMissingPathVariable
package com.javarevisited.ipldashboard.exception;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMissingPathVariable(MissingPathVariableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return super.handleMissingPathVariable(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return super.handleMethodArgumentNotValid(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return super.handleTypeMismatch(ex, headers, status, request);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return buildResponseEntity(new AppError(HttpStatus.BAD_REQUEST, ex));
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
String error = "Malformed JSON request";
return buildResponseEntity(new AppError(HttpStatus.BAD_REQUEST, error, ex));
}
private ResponseEntity<Object> buildResponseEntity(AppError appError) {
return new ResponseEntity<>(appError, appError.getStatus());
}
}
Handling custom exception
We may need to handle other types of exceptions based on our business needs, therefore these exceptions have to handle manually for this we may need to add an extra handler method annotated with @ExceptionHandler.
An example can be seen below.
//other exception handlers
@ExceptionHandler
protected ResponseEntity<Object> exception {
AppError appError = new AppError(NOT_FOUND);
appError.setMessage(ex.getMessage());
return buildResponseEntity(appError);
}
That's all about how to handle Error and Exception in RESTful Spring Boot Application. This article explains how to handle REST API errors in spring-based apps in a recommended manner. To begin, we look at the default error answer and identify the issue it has. Later, we developed a Java class that stores pertinent error information (meaningful to the API client).
In addition to this, we set up an exception handler class at the application root level to handle errors regardless of controllers. I hope that this little post will assist you in managing REST exceptions and providing the API user with a more insightful answer.
No comments:
Post a Comment
Feel free to comment, ask questions if you have any doubt.