Error Handling for REST with Spring

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": "https://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.

 

2. Spring REST Error Handling

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.

13 thoughts on “Error Handling for REST with Spring”

  1. This error handling does not support cross domain. So I have to convert this to forward a general successful rest controller, then transfer the error message to that.

Comments are closed.