Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MVC 3.2 - Content Negotiation for Error Pages?

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?

like image 240
WayneC Avatar asked Feb 05 '13 19:02

WayneC


2 Answers

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

}
like image 168
John Strickler Avatar answered Nov 03 '22 00:11

John Strickler


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);
    }
}
like image 43
WayneC Avatar answered Nov 03 '22 01:11

WayneC