11

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?

Raedwald
  • 46,613
  • 43
  • 151
  • 237
WayneC
  • 2,530
  • 3
  • 31
  • 44

3 Answers3

4

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;
    }

}
John Strickler
  • 25,151
  • 4
  • 52
  • 68
  • Is it better to return ResponseEntity instead of String. And should I have 404 error-page in web.xml with this approach? Please check my question: http://stackoverflow.com/questions/17322855/how-to-handle-404-exception-invoked-by-ajax-spring-mvc-3-2-controlleradvice – Alex Jun 27 '13 at 01:48
3

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);
    }
}
WayneC
  • 2,530
  • 3
  • 31
  • 44
2

Yes indeed, the application Exceptions and HTTP Response error codes are two different things.

You can adapt the code like below, so that you have access to the requestUri. I guess you can find the content type based on it. I know its crude but I don't think we have alternate solution:

@RequestMapping(value = "/404", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<ErrorResponse> error404(HttpServletRequest request) {

    ErrorResponse errorBody = new ErrorResponse(404, "Resource Not Found!");

    String requestUri = request.getRequestURI();

    return new ResponseEntity<ErrorResponse>(errorBody, HttpStatus.NOT_FOUND);
}

From the example I presume your application is REST service, then probably you could refer to this link on how 404 is handled in a REST full service.

n00begon
  • 3,503
  • 3
  • 29
  • 42
Sudhakar
  • 4,823
  • 2
  • 35
  • 42
  • Thanks for the input. I've seen that article, but unless I'm missing something, it only handles exceptions from the controllers and not general servlet errors. Also, in this context `request.getRequestURI()` gives you the URI of the error page. Looks like you have to use `req.getAttribute("javax.servlet.error.request_uri")` to get URI of original request. I'm wondering if there is some way to override the information that the ContentNegotiationManager uses and set it to the info from the original request instead of the error page? – WayneC Feb 06 '13 at 12:24