Spring Boot Error Responses

I have written about Spring’s support for web response error handling a few times before (e.g. about custom error responses and how they can be generalized). This time, we will take a look at what Spring Boot has to offer.

Start by creating a simple controller:

First, we verify that the controller is working (the -v flag is curl’s verbose flag):

$ curl -v localhost:8080/greet?name=Mattias
[...]
< HTTP/1.1 200 OK
[...]
Hello Mattias!

Good, it works as expected. Now, let us introduce an error. Since the @RequestParam by default require the presence of the specified parameter, you will get an error if a request is made without it (json_pp is the convenient JSON Pure Perl tool for pretty printing JSON):

curl -v localhost:8080/greet | json_pp
[...]
< HTTP/1.1 400 Bad Request
[...]
{
   "timestamp" : 1413313361387,
   "exception" : "org.springframework.web.bind.MissingServletRequestParameterException",
   "status" : 400,
   "error" : "Bad Request",
   "path" : "/greet",
   "message" : "Required String parameter 'name' is not present"
}

Nice! Not only did we get a 400 Bad Request HTTP status code as one would expect. More importantly, Spring Boot returned an informative response body with data that may be very useful for client developers:

  • A time stamp when the error occured, i.e. System.currentTimeMillis().
  • The kind of exception that occurred.
  • The HTTP status code (same as the response status code).
  • The HTTP status code description (derived from the response status code).
  • The path that was requested.
  • And a message which elaborates the problem further.

What happens if we make a request that has the required name parameter, but no value?

$ curl -v localhost:8080/greet?name | json_pp
[...]
< HTTP/1.1 500 Internal Server Error
[...]
{
   "timestamp" : 1413314685675,
   "exception" : "java.lang.IllegalArgumentException",
   "status" : 500,
   "error" : "Internal Server Error",
   "path" : "/greet",
   "message" : "The 'name' parameter must not be null or empty"
}

The value of the exception field has been replaced with IllegalArgumentException and the message field corresponds exactly to the exception message stated by the controller above. Similarily, the HTTP status code in the response header and body is updated to 500 Internal Server Error, which in some way is correct (in the sense that the server throws an exception it did not handle). However, I argue that 400 Bad Request is more suitable, since the error occurs because the client did not provide all the required information. So how can we change this?

It turns out that the solution is pretty simple once you know it. The following lines added to your controller will do the trick:

@ExceptionHandler
void handleIllegalArgumentException(IllegalArgumentException e, HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value());
}

Repeat the request:

$ curl -v localhost:8080/greet?name | json_pp
[...]
< HTTP/1.1 400 Bad Request
[...]
{
   "timestamp" : 1413315159949,
   "exception" : "java.lang.IllegalArgumentException",
   "status" : 400,
   "error" : "Bad Request",
   "path" : "/greet",
   "message" : "The 'name' parameter must not be null or empty"
}

Voila! The HTTP status code is now 400 Bad Request, both in the response header as well as in the response body.

If you would like to return the same HTTP status code for multiple exception, you can declare the exceptions in the @ExceptionHandler annotation instead of passing them as a method parameters:

@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
void handleBadRequests(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value());
}

Lastly, if you add the @ExceptionHandler snippets in a controller, it will only work for that particular controller. But if you add them to a separate class annotated with the @ControllerAdvice annotation it will work for all controllers (or a subset of them).

Update

As of Spring Boot version 1.2.0.RC1 issues 1731 and 1762 have been resolved which enables the possibility to set a custom error message in the response:

@ExceptionHandler(IllegalArgumentException.class)
void handleBadRequests(HttpServletResponse response) throws IOException {
    response.sendError(HttpStatus.BAD_REQUEST.value(), "Please try again and with a non empty string as 'name'");
}

Whatever message is passed to the sendError() method will be copied to the response body:

$ curl -v localhost:8080/greet?name | json_pp
[...]
< HTTP/1.1 400 Bad Request
[...]
{
   "timestamp" : 1413315159949,
   "exception" : "java.lang.IllegalArgumentException",
   "status" : 400,
   "error" : "Bad Request",
   "path" : "/greet",
   "message" : "Please try again and with a non empty string as 'name'"
}

Considerations

If you would like to validate more complex requests, you can benefit from using the standard JSR-303 Bean Validation API together with Spring Validation. Please see the Validating Form Input Getting Started Guide for a working example.

Acknowledgements

All credits go to Andy Wilkinsson and Dave Syer for answering the issue I filed against Spring Boot regarding this matter.

References

Dependency

Spring Boot version 1.1+.

Mattias Severson

Mattias is a senior software engineer specialized in backend architecture and development with experience of cloud based applications and scalable solutions. He is a clean code proponent who appreciates Agile methodologies and pragmatic Test Driven Development. Mattias has experience from many different environments, including everything between big international projects that last for years and solo, single day jobs. He is open-minded and curious about new technologies. Mattias believes in continuous improvement on a personal level as well as in the projects that he is working on. Additionally, Mattias is a frequent speaker at user groups, companies and conferences.

This Post Has 15 Comments

  1. When using @RepositoryRestResource an exception in a REST client returns this hot mess:

    {
    “cause”: {
    “cause”: null,
    “message”: “{ \”serverUsed\” : \”localhost:27017\” , \”err\” : \”E11000 duplicate key error index: test.cats.$name dup key: { : \\\”Killer\\\” }\” , \”code\” : 11000 , \”n\” : 0 , \”connectionId\” : 197 , \”ok\” : 1.0}”
    },
    “message”: “{ \”serverUsed\” : \”localhost:27017\” , \”err\” : \”E11000 duplicate key error index: test.cats.$name dup key: { : \\\”Killer\\\” }\” , \”code\” : 11000 , \”n\” : 0 , \”connectionId\” : 197 , \”ok\” : 1.0}; nested exception is com.mongodb.MongoException$DuplicateKey: { \”serverUsed\” : \”localhost:27017\” , \”err\” : \”E11000 duplicate key error index: test.cats.$name dup key: { : \\\”Killer\\\” }\” , \”code\” : 11000 , \”n\” : 0 , \”connectionId\” : 197 , \”ok\” : 1.0}”
    }

    Any thoughts on getting a nice clean error response like you’ve demonstrated in the post rather than this garbled mess?

    1. @Matt: I am sorry, I have not used Spring Data’s @RepositoryRestResource so I cannot tell how you create custom error responses in that environment. You could try using the HttpServletResponse.sendError(int, String) that I described in my blog about Spring Boot Error Responses, however I do not know whether or not that will actually work. Alternatively, post a question at Stack Overflow, or file a new issue to the Spring Data Rest Jira.

  2. Hey question – how can I get this to work when I have servlet filters in place?

    This captures all of my exceptions fine when I disable the filters but when I have them registered the exceptions just get dumped in the log and the rest service returns 200 with a null body.

    Have you seen this before? Thanks

    1. @mick: I am sorry, I have not experienced this problem. My guess is that the exception is thrown somewhere in the filter chain before the request hits Spring’s DispatcherServlet and your controller. Consequently, any @ExceptionHandler code will never be invoked because the request never reaches that far. The 200 response is presumably because the exception is caught (and subsequently logged). If you re-throw the exception you will likely see a 500 status response and the web server’s default error page instead.

  3. with spring boot 1.3, 1.4, is it the good way to transfert error to the client or there are some new better way?

    1. @Robert: Have you checked the Spring Boot reference docs? In version 1.3, the 27.1.7 Error Handling paragraph was updated. It shares many of the same ideas that I presented in my blog posts Improve Your Spring REST API, Part I, Improve Your Spring REST API, Part II and Improve Your Spring REST API, Part III (all of which can be used in Spring Boot as well as in a “plain” Spring MVC). Spring Boot 1.4 offers the possibility to create Custom error pages if you would like to return HTML pages. Moreover, you can take a closer look at ErrorAttributes (available as of Spring Boot 1.1), e.g.

      @Autowired
      ErrorAttributes errorAttributes;
      
      // ...
      
      @ExceptionHandler
      @ResponseStatus(HttpStatus.BAD_REQUEST)
      @ResponseBody
      Map handleIllegalArgumentException(IllegalArgumentException e, WebRequest request) {
          return errorAttributes.getErrorAttributes(request, false);
      }
      
  4. Exactly what I needed. Thanks :)

  5. Nice share.
    But why you did not put this on github.
    I would be nice.

    Thanks.

    1. @Hendi: Thank you. I guess the reason for not putting the code on GitHub is that the “business logic” in this blog post consists just of one-liner that has been annotated with the @ExceptionHandler annotation.

  6. Really helpful as this lets you re-use the response body building functionality from BasicErrorController.
    Thanks!

  7. In your exception field you are revealing implementation details about your service, if someone hits your service they now know what language it is written in, what framework you are using etc. This could have security implications.

    1. @Mike: Certainly, you always have to be very careful about what you reveal. Any details that help your client developers could potentially also be used by a malicious attacker.

Leave a Reply

Close Menu