32

I'm using the following exception handler in Spring 4.0.3 to intercept exceptions and display a custom error page to the user:

@ControllerAdvice
public class ExceptionHandlerController
{
    @ExceptionHandler(value = Exception.class)
    public ModelAndView handleError(HttpServletRequest request, Exception e)
    {
        ModelAndView mav = new ModelAndView("/errors/500"));
        mav.addObject("exception", e);
        return mav;
    }
}

But now I want a different handling for JSON requests so I get JSON error responses for this kind of requests when an exception occurred. Currently the above code is also triggered by JSON requests (Using an Accept: application/json header) and the JavaScript client doesn't like the HTML response.

How can I handle exceptions differently for HTML and JSON requests?

kayahr
  • 20,913
  • 29
  • 99
  • 147

7 Answers7

14

The ControllerAdvice annotation has an element/attribute called basePackage which can be set to determine which packages it should scan for Controllers and apply the advices. So, what you can do is to separate those Controllers handling normal requests and those handling AJAX requests into different packages then write 2 Exception Handling Controllers with appropriate ControllerAdvice annotations. For example:

@ControllerAdvice("com.acme.webapp.ajaxcontrollers")
public class AjaxExceptionHandlingController {
...
@ControllerAdvice("com.acme.webapp.controllers")
public class ExceptionHandlingController {
dnang
  • 939
  • 2
  • 12
  • 17
8

The best way to do this (especially in servlet 3) is to register an error page with the container, and use that to call a Spring @Controller. That way you get to handle different response types in a standard Spring MVC way (e.g. using @RequestMapping with produces=... for your machine clients).

I see from your other question that you are using Spring Boot. If you upgrade to a snapshot (1.1 or better in other words) you get this behaviour out of the box (see BasicErrorController). If you want to override it you just need to map the /error path to your own @Controller.

kryger
  • 12,906
  • 8
  • 44
  • 65
Dave Syer
  • 56,583
  • 10
  • 155
  • 143
  • 1
    I don't understand your solution quite clearly. What do you mean by `register an error page with container ` and use that to call controller ? Could you elaborate please with some example or a link that gives some more explanation ? – Utkarsh Jul 25 '15 at 18:15
  • 1
    This has more details. http://stackoverflow.com/questions/25356781/spring-boot-remove-whitelabel-error-page – ravindrab Sep 02 '15 at 19:39
  • @Dave Syer When you say "register an error page with the container" I suppose that you think about an and possibly in web.xml. As far as I know, when using this technique, you lose some context (for example the Controller handling such errors won't have access to the Principal or to the original request made by the user). Any idea how to circumvent this issue ? How can you get a grip of the original request, not the new one made by the container ? (or to prevent the swallowing.. Not exactly sure if a new request is really made) – niilzon Apr 27 '16 at 07:16
  • Indeed, container error pages are rather limited (but the request attributes are available so the principal and error are usually available). The Spring Boot solution is still valid and gives you a higher level API to work with. – Dave Syer Apr 27 '16 at 08:39
  • @Dave Syer In case of a 403:forbidden due to an invalid CSRF check triggered by Spring-Security, "everything" (principal, request params etc) are lost in the controller. Do you believe that this is possibly due to an error in my config or that, in that case, this behaviour is normal ? I'll create a full post on stack if necessary, depending on your answer :) – niilzon Apr 27 '16 at 08:56
  • It's normal but AFAIK everything is not lost. The request is preserved (and all its attributes). The principal is lost (see discussion here https://github.com/spring-projects/spring-boot/issues/1048 for workarounds if you need them). – Dave Syer Apr 28 '16 at 10:12
2

As you have the HttpServletRequest, you should be able to get the request "Accept" header. Then you could process the exception based on it.

Something like:

String header = request.getHeader("Accept");
if(header != null && header.equals("application/json")) {
    // Process JSON exception
} else {
    ModelAndView mav = new ModelAndView("/errors/500"));
    mav.addObject("exception", e);
    return mav;
}
Daniel
  • 209
  • 1
  • 3
  • 12
2

Since i didn't find any solution for this, i wrote some code that manually checks the accept header of the request to determine the format. I then check if the user is logged in and either send the complete stacktrace if he is or a short error message.

I use ResponseEntity to be able to return both JSON or HTML like here.
Code:

@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleExceptions(Exception ex, HttpServletRequest request) throws Exception {

    final HttpHeaders headers = new HttpHeaders();
    Object answer; // String if HTML, any object if JSON
    if(jsonHasPriority(request.getHeader("accept"))) {
        logger.info("Returning exception to client as json object");
        headers.setContentType(MediaType.APPLICATION_JSON);
        answer = errorJson(ex, isUserLoggedIn());
    } else {
        logger.info("Returning exception to client as html page");
        headers.setContentType(MediaType.TEXT_HTML);
        answer = errorHtml(ex, isUserLoggedIn());
    }
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    return new ResponseEntity<>(answer, headers, status);
}

private String errorHtml(Exception e, boolean isUserLoggedIn) {
    String error = // html code with exception information here
    return error;
}

private Object errorJson(Exception e, boolean isUserLoggedIn) {
    // return error wrapper object which will be converted to json
    return null;
}

/**
 * @param acceptString - HTTP accept header field, format according to HTTP spec:
 *      "mime1;quality1,mime2;quality2,mime3,mime4,..." (quality is optional)
 * @return true only if json is the MIME type with highest quality of all specified MIME types.
 */
private boolean jsonHasPriority(String acceptString) {
    if (acceptString != null) {
        final String[] mimes = acceptString.split(",");
        Arrays.sort(mimes, new MimeQualityComparator());
        final String firstMime = mimes[0].split(";")[0];
        return firstMime.equals("application/json");
    }
    return false;
}

private static class MimeQualityComparator implements Comparator<String> {
    @Override
    public int compare(String mime1, String mime2) {
        final double m1Quality = getQualityofMime(mime1);
        final double m2Quality = getQualityofMime(mime2);
        return Double.compare(m1Quality, m2Quality) * -1;
    }
}

/**
 * @param mimeAndQuality - "mime;quality" pair from the accept header of a HTTP request,
 *      according to HTTP spec (missing mimeQuality means quality = 1).
 * @return quality of this pair according to HTTP spec.
 */
private static Double getQualityofMime(String mimeAndQuality) {
    //split off quality factor
    final String[] mime = mimeAndQuality.split(";");
    if (mime.length <= 1) {
        return 1.0;
    } else {
        final String quality = mime[1].split("=")[1];
        return Double.parseDouble(quality);
    }
}
Community
  • 1
  • 1
Katharsas
  • 540
  • 5
  • 16
2

The trick is to have a REST controller with two mappings, one of which specifies "text/html" and returns a valid HTML source. The example below, which was tested in Spring Boot 2.0, assumes the existence of a separate template named "error.html".

@RestController
public class CustomErrorController implements ErrorController {

    @Autowired
    private ErrorAttributes errorAttributes;

    private Map<String,Object> getErrorAttributes( HttpServletRequest request ) {
        WebRequest webRequest = new ServletWebRequest(request);
        boolean includeStacktrace = false;
        return errorAttributes.getErrorAttributes(webRequest,includeStacktrace);
    }

    @GetMapping(value="/error", produces="text/html")
    ModelAndView errorHtml(HttpServletRequest request) {
        return new ModelAndView("error.html",getErrorAttributes(request));
    }

    @GetMapping(value="/error")
    Map<String,Object> error(HttpServletRequest request) {
        return getErrorAttributes(request);
    }

    @Override public String getErrorPath() { return "/error"; }

}

References

Brent Bradburn
  • 51,587
  • 17
  • 154
  • 173
  • You may or may not choose to further annotate the error handlers with [@ExceptionHandler](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html). – Brent Bradburn Apr 21 '18 at 02:27
0

The controlleradvice annotation has several properties that can be set, since spring 4. You can define multiple controller advices applying different rules.

One property is "annotations. Probably you can use a specific annotation on the json request mapping or you might find another property more usefull?

Martin Frey
  • 10,025
  • 4
  • 25
  • 30
  • I don't believe that functionality is what the creation authors had in mind but it is definitely an interesting way to accomplish his goal... if it works _(I don't have a need to test this myself)_. Just for clarification, you are suggesting marking up each controller with a different custom annotation based on how you what to respond when an error occurs. ie. `@HtmlErrorResponse` or `@JsonErrorResponse`, and then having your separate advice controllers to set the annotation property, ie. `@ControllerAdvice(annotations = HtmlErrorResponse.class)` for an html error response. – Prancer May 01 '15 at 13:18
0

Use @ControllerAdvice Let the exception handler send a DTO containing the field errors.

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) {
    BindingResult result = ex.getBindingResult();
    List<FieldError> fieldErrors = result.getFieldErrors();

    return processFieldErrors(fieldErrors);
}

This code is of this website:http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ Look there for more info.

Michiel
  • 1,061
  • 10
  • 17