To globally handle errors (such as HTTP 404's) which can occur outside of a Controller, I have entries similar to the following in my web.xml:
<error-page>
<error-code>404</error-code>
<location>/errors/404</location>
</error-page>
In my ErrorController I have corresponding methods similar to the following:
@Controller
@RequestMapping("/errors")
public class ErrorController {
@RequestMapping(value = "/404", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<ErrorResponse> error404() {
ErrorResponse errorBody = new ErrorResponse(404, "Resource Not Found!");
return new ResponseEntity<ErrorResponse>(errorBody, HttpStatus.NOT_FOUND);
}
}
The issue I'm facing is that the ContentNegotiationManager
and message converters I have configured are not being used in this case. I suspect that since the request is being redirected to the error page, the original request's attributes used in content negotiation are lost and this is treated as a completely separate request. (i.e. original request for /mycontroller/badresource.json --> /errors/404 (w/no file extension))
Is there any way in an error handler like this determine and/or respond with the appropriate content type as requested in the original request?
Spring MVC 3.2 now includes a useful annotation called @ControllerAdvice
. You can add an ExceptionHandler
method that will globally handle any exception that you defined.
For me, I only care about two possible content-types to return to the client - application/json
or text/html
.
Here is how I would set it up -
@ControllerAdvice
public class ExceptionControllerAdvice {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private static final MediaType JSON_MEDIA_TYPE = new MediaType("application", "json", DEFAULT_CHARSET);
//I decided to handle all exceptions in this one method
@ExceptionHandler(Throwable.class)
public @ResponseBody String handleThrowable(HttpServletRequest request, HttpServletResponse response, Throwable ex) throws IOException {
...
if(supportsJsonResponse(request.getHeader("Accept"))) {
//return response as JSON
response.setStatus(statusCode);
response.setContentType(JSON_MEDIA_TYPE.toString());
//TODO serialize your error in a JSON format
//return ...
} else {
//return as HTML
response.setContentType("text/html");
response.sendError(statusCode, exceptionMessage);
return null;
}
}
private boolean supportsJsonResponse(String acceptHeader) {
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
for(MediaType mediaType : mediaTypes) {
if(JSON_MEDIA_TYPE.includes(mediaType)) {
return true;
}
}
return false;
}
}
I came up with a bit of a hack for this but it seems to work. It basically involves an extra forward in the error handling to determine the file extension of the original request.
In my web.xml I have the error forwarded to an intermediate action:
<error-page>
<error-code>404</error-code>
<location>/errors/redirect</location>
</error-page>
Then, before forwarding to the action that will generate the error response, a check is done to see if there was a file extension on the original request. If there was, it ensures it is appended to the forward URI. The HTTP headers are automatically forwarded, so if the content negotiation you have setup only involves file extensions or HTTP header, this will effectively allow the "error page" to return the error in the appropriate content type.
@Controller
@RequestMapping("/errors")
public class ErrorController {
@RequestMapping(value = "/redirect", method = RequestMethod.GET)
public void errorRedirect(HttpServletRequest request, HttpServletResponse response) {
// Get original request URI
String uri = (String)request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE);
// Try to determine file extension
String filename = WebUtils.extractFullFilenameFromUrlPath(uri);
String extension = StringUtils.getFilenameExtension(filename);
extension = StringUtils.hasText(extension) ? "." + extension : "";
// Forward request to appropriate handler with original request's file extension (i.e. /errors/404.json)
String forwardUri = "/errors/404" + extension);
request.getRequestDispatcher(forwardUri).forward(request, response);
}
@RequestMapping(value = "/404", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<ErrorResponse> error404() {
ErrorResponse errorBody = new ErrorResponse(404, "Resource Not Found!");
return new ResponseEntity<ErrorResponse>(errorBody, HttpStatus.NOT_FOUND);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With