SpringTechnology

Custom Error Messages in Spring REST API

Create global or application level Exception handlers and return Custom Error Messages in Spring REST APIs.

Overview

To achieve a healthy and efficient relationships, effective communication is the key. Interestingly, the same applies to any Client and Server relationships. The Client’s request may succeed or fail at the server. However, in either of the outcomes, server should provide the most appropriate status code.

Although, sending a correct status codes is enough of a client to take correct action based on the outcome of a request, incase of failures client may need more details about what went wrong. For example, failure details like type of the exception and an error message can help client to log the error or provide appropriate failure message to their clients.

In this article we will learn How to handle a different failures and return a Custom Error Messages from a Spring REST API. If you are not aware of how to handle exceptions in Spring REST API, please read Spring Rest Service Exception Handling.

Return a Generic Error Message using @ResponseStatus

The most basic way of returning an error message from a REST API is to use @ResponseStatus annotation. Using that, we can add the error message in the annotation’s reason field. Although we can only return a generic error message, we we can specify different error message for different exceptions.

Next, is an example of a @ControllerAdvice using @ResponseStatus annotations to return exception specific error messages.

@ControllerAdvice
public class ApplicationExceptionHandler {
    @ResponseStatus(
            value = HttpStatus.NOT_FOUND,
            reason = "Requested Student Not Found")
    @ExceptionHandler(StudentNotFoundException.class)
    public void handleException(StudentNotFoundException e) {
    }

    @ResponseStatus(
            value = HttpStatus.BAD_REQUEST,
            reason = "Received Invalid Input Parameters")
    @ExceptionHandler(InputValidationException.class)
    public void handleException(InputValidationException e) {
    }

    @ResponseStatus(
            value = HttpStatus.GATEWAY_TIMEOUT,
            reason = "Upstream Service Not Responding, Try Again")
    @ExceptionHandler(ServiceUnavailableException.class)
    public void handleException(ServiceUnavailableException e) {
    }
}

The Exception handler class has 3 exception handlers and each of them is returning a specific HTTP Response Status. Each of the response statuses specify a reason field with specific error message.

In order to view the error message in the response, make sure you have turned on include-messages in server configuration. To learn more on Spring Boot server configurations please visit Spring Boot Embedded Tomcat Configuration.

server:
  error:
    include-message: always

Next, is an example of a response object the REST API returns. Note that the response object has the specified error message.

{
    "timestamp": "<TIMESTAMP>",
    "status": 404,
    "error": "Not Found",
    "message": "Requested Student Not Found",
    "path": "/students/Jack"
}

As mentioned earlier, although we can specify exception specific error message it is still not informative. Therefore in the next sections, we will learn how to return a more specific error message from Spring REST API.

Return Error Message Using Custom Error Object

To begin with, create a class to represent the error message and the status code. In case of errors, the Error class will be returned from the controllers or from the exception handlers.

Next, is the Error class that represents status code and a String message. Note that we are using couple of Lombok annotations, that introduces normal getter and setter methods, and a constructor using the final fields.

Custom Response Error Class

@Data
@RequiredArgsConstructor
public class Error {
    private final HttpStatus httpStatus;
    private final String message;
}

Now that we have an error model created, we will use it to return a detailed error message from Controller Advice.

@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity<Error> handleException(StudentNotFoundException e) {
    Error error = new Error(HttpStatus.NOT_FOUND, e.getLocalizedMessage());
    return new ResponseEntity<>(error, error.getHttpStatus());
}

The exception handler returns an instance of Error class that is populated with the exception message and HTTP Status Code.

Now, we can throw our Not Found Exception with a custom error message.

throw new StudentNotFoundException("Student service failed, studentId : " + studentId);

When, the REST API is unable to find the requested resource, we get the detailed error as response.

{
    "httpStatus": "NOT_FOUND",
    "message": "Student service failed, studentId : Jack"
}

Return Error Message Using HashMap

Also instead of creating a dedicated error class we can return detailed error message using a simple HashMap. To demonstrate, next is an example of returning Custom Error Message using Java HashMap.

@ExceptionHandler(StudentNotFoundException.class)
public ResponseEntity<Map<String, String>> handleException(StudentNotFoundException e) {
    Map<String, String> errorResponse = new HashMap<>();

    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.NOT_FOUND.toString());
    return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

Handle Bad Request Exceptions

The Bad Request errors are also called as Client error. In other words, the client is responsible for these errors. Typically, this occurs when the request doesn’t meet the requirements of the targeted resource. In this section we will see how to handle Bad Request exceptions and provide a custom or detailed error response.

Type Mismatch Exceptions

The Type Mismatch Exceptions occur when Spring Controller is unable to map the request parameters, path variables or header values into controller method arguments. This section covers handling of MethodArgumentTypeMismatchException and TypeMismatchException.

Spring throws MethodArgumentTypeMismatchException when the controller argument doesn’t have a required type. On the other hand, Spring throws TypeMismatchException when the there is type mismatch while setting Bean properties. Also, both of these exception instances provide a detailed error message that we can use to prepare Error object.

To demonstrate that, next is an example of Handling MethodArgumentTypeMismatchException and TypeMismatchException and returning a detailed error message in Controller Advice.

@ExceptionHandler({
        MethodArgumentTypeMismatchException.class,
        TypeMismatchException.class
})
public ResponseEntity<Map<String, String>> handleException(TypeMismatchException e) {
    Map<String, String> errorResponse = new HashMap<>();

    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.BAD_REQUEST.toString());
    return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

Note that, the controller advice catches both exceptions however the method arguments accepts an exception of type TypeMismatchException, because it is parent of the other exception.

Next, snippet shows a detailed error message when we call a rest endpoint with an incompatible path variable leading to MethodArgumentTypeMismatchException.

{
    "httpStatus": "BAD_REQUEST",
    "message": 
        "Failed to convert value of type 'java.lang.String' 
         to required type 'java.lang.Long'; nested exception 
         is java.lang.NumberFormatException: For input 
         string: \"Jack\""
}

Bean Validation Exceptions

The Bean Validation exceptions are thrown when the contents of the request do not pass the provided validations.

The BindException occurs when the binding errors are fatal. While the MethodArgumentNotValidException occurs when validations specified by @Valid fail. Note that the MethodArgumentNotValidException is a subclass of BindException. Thus, we can handle them using the same Spring REST API exception handler.

@ExceptionHandler({
        BindException.class,
        MethodArgumentNotValidException.class
})
public ResponseEntity<Map<String, Object>> handleException(BindException e) {
        
    List<String> errors = new ArrayList<>();
    e.getFieldErrors()
             .forEach(err -> errors.add(err.getField() + ": " + err.getDefaultMessage()));
    e.getGlobalErrors()
             .forEach(err -> errors.add(err.getObjectName() + ": " + err.getDefaultMessage()));

    Map<String, Object> errorResponse = new HashMap<>();
    errorResponse.put("error", errors);

    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.BAD_REQUEST.toString());
    return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

Here we have created a List<String> to represent individual binding errors and add that to the response Map. Instead we can add List<String> field to the Error class we created in previous sections and populate it with individual errors.

Handle Media Type Not Supported Exception

Spring throws HttpMediaTypeNotSupportedException, when a POST, PUT, or a PATCH endpoint on the server is unable to handle the content type sent by the client. The REST Controllers on the server specifies the content type they can support. When media type that a client sends doesn’t match the client gets this exception back.

To demonstrate, next is an example of handling HttpMediaTypeNotSupportedException and returning a custom error response.

@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<Map<String, String>> handleException(
        HttpMediaTypeNotSupportedException e) {

    String provided = e.getContentType().toString();
    List<String> supported = e.getSupportedMediaTypes().stream()
            .map(MimeType::toString)
            .collect(Collectors.toList());

    String error = provided + " is not one of the supported media types (" +
            String.join(", ", supported) + ")";

    Map<String, String> errorResponse = new HashMap<>();
    errorResponse.put("error", error);
    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.UNSUPPORTED_MEDIA_TYPE.toString());
    
    return new ResponseEntity<>(errorResponse, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}

As seen in the exception handler above, the instance of HttpMediaTypeNotSupportedException provides detailed information about the incorrect media type that we provided, as well as a list of actually supported media types. Thus, we are creating a custom error message based on the available information.

{
   "error":"text/plain;charset=UTF-8 is not one of the supported media types (
       application/octet-stream, 
       text/plain, application/xml, 
       text/xml, application/x-www-form-urlencoded, 
       application/*+xml, 
       multipart/form-data, 
       multipart/mixed, 
       application/json, 
       application/*+json, */*)",
   "message":"Content type 'text/plain;charset=UTF-8' not supported",
   "status":"415 UNSUPPORTED_MEDIA_TYPE"
}

The above snippet shows a sample error response, that is retuned when a client sends request with invalid media type.

Handle Request Body Not Readable Exception

Now we will see an example of handling HttpMessageNotReadableException and returning a custom error response. The HttpMessageNotReadableException occurs when request body is missing or it is unreadable.

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, String>> handleException(
        HttpMessageNotReadableException e) throws IOException {

    Map<String, String> errorResponse = new HashMap<>();
    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.BAD_REQUEST.toString());

    return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

Handle Http Request Method Not Supported Exception

The HttpMethodNotSupportedException occurs when HTTP endpoint on the REST API does not support the HTTP request method. Given that let’s write an exception handler for HttpMethodNotSupportedException and return a detailed error message.

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, String>> handleException(
        HttpRequestMethodNotSupportedException e) throws IOException {
    Map<String, String> errorResponse = new HashMap<>();
    String provided = e.getMethod();
    List<String> supported = Arrays.asList(e.getSupportedMethods());

    String error = provided + " is not one of the supported Http Methods (" +
            String.join(", ", supported) + ")";
    errorResponse.put("error", error);
    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.METHOD_NOT_ALLOWED.toString());

    return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED);
}

As can be seen in the exception handler above, the exception instance provides detailed information about the provided HTTP Method and an array of Supported HTTP Methods and we use it to form a detailed error message.

{
    "error": "GET is not one of the supported Http Methods (POST)",
    "message": "Request method 'GET' not supported",
    "status": "405 METHOD_NOT_ALLOWED"
}

The snippet shows an example response when client attempted to execute a GET endpoint, while only POST is supported by the REST API.

Default Exception Handler

Similarly, we can create a default exception handler advice that handles all Exception types. When we have multiple exception handlers, Spring attempts to find the most specific handler and falls back to the default handler if none found.

@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleException(
        Exception e) throws IOException {
    Map<String, String> errorResponse = new HashMap<>();
    errorResponse.put("message", e.getLocalizedMessage());
    errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.toString());

    return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

Above is an example of writing a default exception handler that returns error message returned by the exception instance along with HTTP Status of 500.

Summary

To conclude, in this detailed tutorial we learned How to Return Custom Error Messages in Spring REST API. Firstly, we understood that by default Spring returns a generic error message along with most suitable HTTP Status Code. However, we can write our own exception handlers for specific exceptions by using @ControllerAdvice and return a custom and detailed error response.

For more on Spring and Spring Boot Tutorials, please visit Spring Tutorials.


Leave a Reply

Your email address will not be published. Required fields are marked *