ajaxspring-bootexceptionhandlercontroller-advice

spring boot dynamic response type


I am using @ControllerAdvice to do exception handling globally. but I want to return the exception in Json format if the request is ajax(xhr) and if it's not I want to show my exception as html.

so the response type is dynamic. it might be html or json. And I am using thymleaf to generate my html pages.

how can I do that in spring boot?

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public ModelAndView handle(Exception exception, WebRequest request) {
        this.logException(exception, request);
        var model = getErrorPageModel(exception);
        return new ModelAndView("exception", model, HttpStatus.OK);
    }

Solution

  • Step 1: Determine which type of view you want

    From the WebRequest, you have to retrieve the necessary information to either return JSON or HTML. One possible way of doing so is by parsing the Accept header:

    List<MediaType> mediaTypes = MediaType.parseMediaTypes(request.getHeader("Accept"));
    if (mediaTypes.contains(MediaType.TEXT_HTML)) {
       // TODO: Implement HTML view
    } else {
        // TODO: Implement JSON view
    }
    

    A better alternative is to use Spring's ContentNegotiationManager by autowiring it into your controller advice:

    // TODO: Autowire a bean of `ContentNegotiationManager` into your ControllerAdvice class
    List<MediaType> mediaTypes = contentNegotiationManager.resolveMediaTypes(request);
    if (mediaTypes.contains(MediaType.TEXT_HTML)) {
       // TODO: Implement HTML view
    } else {
        // TODO: Implement JSON view
    }
    

    By default, Spring Boot's ContentNegotiationManager will do exactly the same as the original code, but this can be extended with other strategies.

    Be aware that the resolveMediaTypes() method requires a NativeWebRequest. So you have to change the type of the request parameter.

    Step 2: Implement the model + view

    Now what you have to do is implement the model and view for both the HTML and JSON view.

    For the HTML view, you can use a Thymeleaf template, for example error.html:

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <body>
    <p><strong>Error:</strong> <span th:text="${message}"></span></p>
    </body>
    

    After that, you can return the following ModelAndView from your exception handler:

    return new ModelAndView("error", Map.of("message", ex.getMessage()));
    

    For the JSON part, you can use the MappingJackson2JsonView view class:

    // TODO: Autowire a bean of `ObjectMapper` into your ControllerAdvice class
    return new ModelAndView(new MappingJackson2JsonView(objectMapper), Map.of("message", ex.getMessage()));
    

    This requires an ObjectMapper, which you can autowire into your controller advice.

    Everything combined you get:

    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception ex, NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        List<MediaType> mediaTypes = contentNegotiationManager.resolveMediaTypes(request);
        Map<String, String> model = Map.of("message", ex.getMessage());
        if (mediaTypes.contains(MediaType.TEXT_HTML)) {
            return new ModelAndView("error", model);
        } else {
            return new ModelAndView(new MappingJackson2JsonView(objectMapper), model);
        }
    }
    

    Note: In this example I'm assuming that if the Accept header contains text/html you want to return the HTML view and in every other case you want to return the JSON view. You can change this logic to whatever you want.