Wednesday, May 24, 2023

How to Handle REST exception in Spring Boot Application? Example Tutorial

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.