This recipe presents a technique for handling exception globally in a web application.
There are different ways to handle exceptions in Spring MVC. We can choose to define controller-specific @ExceptionHandler
or we can choose to register @ExceptionHandler
globally in the @ControllerAdvice
classes.
We developed the second option in our REST API, even if our CloudstreetApiWCI
super-class could have shared @ExceptionHandler
among its controllers.
Now we will see how to automatically map custom and generic exception types to HTTP status codes, and how to wrap the right error messages in a generic response object that can be used by any client.
public class ErrorInfo { public final String error; public int status; public final String date; private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); public ErrorInfo(Throwable throwable, HttpStatus status){ this.error = ExceptionUtil.getRootMessage(throwable); this.date = dateFormat.format(new Date()); this.status = status.value(); } public ErrorInfo(String message, HttpStatus status) { this.error = message; this.date = dateFormat.format(new Date()); this.status = status.value(); } @Override public String toString() { return "ErrorInfo [status="+status+", error="+error+ ", date=" + date + "]"; } }
RestExceptionHandler
class annotated with @ControllerAdvice
. This RestExceptionHandler
class also inherits the ResponseEntityExceptionHandler
support class, which gives us access to a default mapping exception/response status ready to be overridden:@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if(body instanceof String){ return new ResponseEntity<Object>(new ErrorInfo((String) body, status), headers, status); } return new ResponseEntity<Object>(new ErrorInfo(ex, status), headers, status); } // 400 @Override protected ResponseEntity<Object> handleHttpMessageNotReadable(final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { return handleExceptionInternal(ex, "The provided request body is not readable!", headers, HttpStatus.BAD_REQUEST, request); } @Override protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleExceptionInternal(ex, "The request parameters were not valid!", headers, HttpStatus.BAD_REQUEST, request); } (...) @ExceptionHandler({ InvalidDataAccessApiUsageException.class, DataAccessException.class , IllegalArgumentException.class }) protected ResponseEntity<Object> handleConflict(final RuntimeException ex, final WebRequest request) { return handleExceptionInternal(ex, "The request parameters were not valid!", new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } (...) // 500 @ExceptionHandler({ NullPointerException.class, IllegalStateException.class }) public ResponseEntity<Object> handleInternal(final RuntimeException ex, final WebRequest request) { return handleExceptionInternal(ex, "An internal error happened during the request! Please try again or contact an administrator.", new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request); } }
Both the
ErrorInfo
wrapper and this RestExceptionHandler
will support internationalization. It will be demonstrated in Chapter 7, Developing CRUD Operations and Validations.
public class MarketCodeEditor extends PropertyEditorSupport{ public void setAsText(String text) { try{ setValue(MarketCode.valueOf(text)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("The provided value for the market code variable is invalid!"); } } } public class QuotesIntervalEditor extends PropertyEditorSupport { public void setAsText(String text) { try{ setValue(QuotesInterval.valueOf(text)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("The provided value for the quote-interval variable is invalid!"); } } }
getHistoIndex
method of the AngularJS factory (in the home_financial_graph.js
file). Change the call from $http.get("/api/indices/"+market+"wrong/"+index+"/histo.json")
to $http.get("/api/indices/"+market+"/"+index+"/histo.json")
.http://localhost:8080/portal/index
will induce the Ajax GET request for the index loading to result in a 400 status code:json
response:The received error message—The provided value for the market variable is invalid! is acceptable for now.
home_financial_graph.js
file after getting this result.Here, we are focusing on the way we handle exceptions in a REST environment. The expectations are slightly different than in a pure web app because the direct user may not necessarily be a human. For this reason, a REST API has to maintain standard, consistent, and self-explanatory communication even if a process has generated an error or has been unsuccessful.
This consistency is achieved by always returning an appropriate HTTP status code feedback to the client from the server about the request treatment, and by always returning a response body in a format that is expected by the client (a format that matches one of the mime types listed in the Accept header of the HTTP request).
Spring 3.2 has brought a solution that is much more suitable to REST environments than the previous exception handling mechanisms. With this solution, classes annotated with @ControllerAdvice
can be registered in a different locations of the API. These annotations are looked-up by classpath scanning and are auto-registered in a common repository to support all of the controllers (by default) or subsets of controllers (using the annotation options).
In our case, we defined one single @ControllerAdvice
to monitor this entire API. The idea is to define, in the @ControllerAdvice
annotated class(es)
, the relevant methods that can match match specific exception Type(s) to specific ResponseEntity(ies). A ReponseEntity carries a body and a response status code.
These methods to define are annotated with @ExceptionHandler
. The options of this annotation allow you to target specific exception Types. A common pattern when defining a @ControllerAdvice
is to make it extend the support class ResponseEntityExceptionHandler
.
The support ResponseEntityExceptionHandler
class provides a predefined mapping between native exceptions (such as NoSuchRequestHandlingMethodException
, ConversionNotSupportedException
, TypeMismatchException
, and so on) and HTTP status codes.
The ResponseEntityExceptionHandler
implements a common pattern for response rendering. It invokes case-specific rendering methods declared as protected, such the following handleNoSuchRequestHandlingMethod
.
protected ResponseEntity<Object> handleNoSuchRequestHandlingMethod(NoSuchRequestHandlingMethod Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request) { pageNotFoundLogger.warn(ex.getMessage()); return handleExceptionInternal(ex, null, headers, status, request); }
These methods are obviously fully overridable in the @ControllerAdvice
annotated class(es). The important thing is to return the handleExceptionInternal
method.
This handleExceptionInternal
method is also defined as protected and then overridable. This is what we have done—returned a uniform ErrorInfo
instance:
@Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { return new ResponseEntity<Object>(new ErrorInfo(ex, (body!=null)? body.toString() : null, status), headers, status); }
There are no specific standard practices about the fields that the uniform error response object should expose. We decided to offer the following structure for the ErrorInfo
object:
{ error: "Global categorization error message", message: "Specific and explicit error message", status: 400, date: "yyyy-MM-dd HH:mm:ss.SSS" }
Using two different levels of messages (the global error message coming from the exception Type and the case-specific message) allows the client side to choose the more appropriate one (or even both!) to be rendered in the application for each situation.
As we already said, this ErrorInfo
object doesn't support internationalization yet. We will improve it later in the Chapter 7, Developing CRUD Operations and Validations.
We provide here a collection of resources related to exception handling in a web environment:
The World Wide Web Consortium specifies explicit response status codes for HTTP/1.1. More important than the error messages themselves, it is critical for a REST API to implement them. You can read more about this at:
An article from in the spring.io blog is a very interesting resource. It is not limited to the REST use case. It can be accessed from this address: http://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc.