Error Handling for REST with Spring
Before Spring 3.2 the two main approaches to handle exceptions where HandleExceptionResolver or @ExceptionHandler annotation. The it was added @ControllerAdvice and ResponseStatusException.
Controller level (MVC)
This strategy only works for active controllers is not global to the entire application.
@ExceptionHandler({Exception.class,Exception1.class})
public void handleException(){
//handle
}
HandleExceptionResolver
This is enabled by default in DispatchServlet.
DefaultHandlerExceptionResolver
Used to resolve Spring exceptions to their corresponding HTTP Status codes
Handling Standard Spring MVC Exceptions
ResponseStatusExceptionResolver
Uses @ResponseStatus annotation on custom exceptions to map them into HTTP Status codes.
However this does not allow to set body content.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
public MyResourceNotFoundException() {
super();
}
public MyResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public MyResourceNotFoundException(String message) {
super(message);
}
public MyResourceNotFoundException(Throwable cause) {
super(cause);
}
}
Custom HadlerExceptionResolver
Creates an extension of AbstractHandlerExceptionResolver that allows to set the body content.
@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
return handleIllegalArgument(
(IllegalArgumentException) ex, response, handler);
}
...
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "]
resulted in Exception", handlerException);
}
return null;
}
private ModelAndView
handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response)
throws IOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
...
return new ModelAndView();
}
}
ControllerAdvice and ExceptionHandler
More suitable for Rest, this solution breaks from the MVC model allows to return an ResponseEntity object using @ExceptionHandler annotation. @ControllerAdvice allows to set the handler as common to all controllers and consolidate our multiple, scattered @ExceptionHandlers from before into a single, global error handling component.
@ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler(value
= { IllegalArgumentException.class, IllegalStateException.class })
protected ResponseEntity<Object> handleConflict(
RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse,
new HttpHeaders(), HttpStatus.CONFLICT, request);
}
}
ResponseStatusException
Allow to throw an exception of type ResponseStatusException with an Http status and optional reason and clause, a more flexible but less methodical approach.
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
try {
Foo resourceById = RestPreconditions.checkFound(service.findOne(id));
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
return resourceById;
}
catch (MyResourceNotFoundException exc) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", exc);
}
}
More details
Using HTTP Status Codes
Normally any unhandled exception thrown when processing a web-request causes the server to return an HTTP 500 response. However, any exception that you write yourself can be annotated with the @ResponseStatus
annotation (which supports all the HTTP status codes defined by the HTTP specification). When an annotated exception is thrown from a controller method, and not handled elsewhere, it will automatically cause the appropriate HTTP response to be returned with the specified status-code.
For example, here is an exception for a missing order.
COPY @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404
public class OrderNotFoundException extends RuntimeException {
// ...
}
And here is a controller method using it:
COPY @RequestMapping(value="/orders/{id}", method=GET)
public String showOrder(@PathVariable("id") long id, Model model) {
Order order = orderRepository.findOrderById(id);
if (order == null) throw new OrderNotFoundException(id);
model.addAttribute(order);
return "orderDetail";
}
A familiar HTTP 404 response will be returned if the URL handled by this method includes an unknown order id.
Controller Based Exception Handling
Using @ExceptionHandler
You can add extra (@ExceptionHandler
) methods to any controller to specifically handle exceptions thrown by request handling (@RequestMapping
) methods in the same controller. Such methods can:
- Handle exceptions without the
@ResponseStatus
annotation (typically predefined exceptions that you didn’t write) - Redirect the user to a dedicated error view
- Build a totally custom error response
The following controller demonstrates these three options:
COPY@Controller
public class ExceptionHandlingController {
// @RequestHandler methods
...
// Exception handling methods
// Convert a predefined exception to an HTTP Status code
@ResponseStatus(value=HttpStatus.CONFLICT,
reason="Data integrity violation") // 409
@ExceptionHandler(DataIntegrityViolationException.class)
public void conflict() {
// Nothing to do
}
// Specify name of a specific view that will be used to display the error:
@ExceptionHandler({SQLException.class,DataAccessException.class})
public String databaseError() {
// Nothing to do. Returns the logical view name of an error page, passed
// to the view-resolver(s) in usual way.
// Note that the exception is NOT available to this view (it is not added
// to the model) but see "Extending ExceptionHandlerExceptionResolver"
// below.
return "databaseError";
}
// Total control - setup a model and return the view name yourself. Or
// consider subclassing ExceptionHandlerExceptionResolver (see below).
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
logger.error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
In any of these methods you might choose to do additional processing – the most common example is to log the exception.
Handler methods have flexible signatures so you can pass in obvious servlet-related objects such as HttpServletRequest
, HttpServletResponse
, HttpSession
and/or Principle
.
Important Note: The Model
may not be a parameter of any @ExceptionHandler
method. Instead, setup a model inside the method using a ModelAndView
as shown by handleError()
above.