Tsury
Tsury

Reputation: 739

@ExceptionHandler for Error gets called only if there's no mapping for Exception

Using spring-web-4.2.6, I have the following Controller and ExceptionHandler:

@ControllerAdvice
public class ExceptionsHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorDTO> HandleDefaultException(Exception ex) {
    ...
    }

    @ExceptionHandler(InternalError.class)
    public ResponseEntity<ErrorDTO> HandleInternalError(InternalError ex) {
    ...
    }
}

@RestController
@RequestMapping("/myController")
public class MyController {
    @RequestMapping(value = "/myAction", method = RequestMethod.POST)
    public boolean myAction() {
        throw new InternalError("");
    }
}

For some reason, the ExceptionsHandler's HandleDefaultException (for Exception.class) method is invoked, with an exception of type NestedServletException, instead of the HandleInternalError call.

When removing the default call, the IntenalError call is called with the proper InternalError exception.

I do not want to remove the default call as it is important to me to have a default handler to allow for a better experience for my users.

What am I missing here?

EDIT:

Apparently I'm using spring-web-4.3.3, without asking for it. I don't understand why exactly, here's my Gradle dependencies tree: http://pastebin.com/h6KXSyp2

Upvotes: 3

Views: 4752

Answers (1)

Sotirios Delimanolis
Sotirios Delimanolis

Reputation: 279970

Spring MVC should only exhibit the behavior you describe with version 4.3 and above. See this JIRA issue. Previously, Spring MVC would not expose any Throwable values to @ExceptionHandler methods. See


Since 4.3, Spring MVC will catch any Throwable thrown from your handler methods and wrap it in a NestedServletException, which it will then expose to the normal ExceptionHandlerExceptionResolver process.

Here's a short description of how it works:

  1. Checks if the handler method's @Controller class contains any @ExceptionHandler methods.
  2. If it does, tries to resolve one that can handle the Exception type (including NestedServletException). If it can, it uses that (there's some sorting if multiple matches are found). If it can't, and the Exception has a cause, it unwraps and tries again to find a handler for that. That cause might now be a Throwable (or any of its subtypes).
  3. If it doesn't. It gets all the @ControllerAdvice classes and tries to find a handler for the Exception type (including NestedServletException) in those. If it can, it uses that. If it can't, and the Exception has a cause, it unwraps it and tries again with that Throwable type.

In your example, your MyController throws an InternalError. Since this is not a subclass of Exception, Spring MVC wraps it in an NestedServletException.

MyController doesn't have any @ExceptionHandler methods, so Spring MVC skips it. You have a @ControllerAdvice annotated class, ExceptionsHandler, so Spring MVC checks that. The @ExceptionHandler annotated HandleDefaultException method can handle Exception, so Spring MVC chooses it to handle the NestedServletException.

If you remove that HandleDefaultException, Spring MVC won't find something that can handle Exception. It will then attempt to unwrap the NestedServletException and check for its cause. It'll then find the HandleInternalError which can handle that InternalError.

This is not an easy issue to deal with. Here are some options:

Create an @ExceptionHandler that handles NestedServletException and do the check for InternalError yourself.

@ExceptionHandler(NestedServletException.class)
public ResponseEntity<String> HandleNested(NestedServletException ex) {
    Throwable cause = ex.getCause();
    if (cause instanceof InternalError) {
        // deal with it
    } else if (cause instanceof OtherError) {
        // deal in some other way
    }
}

This is fine unless there's a bunch of different Error or Throwable types you want to handle. (Note that you can rethrow these if you can't or don't know how to handle them. Spring MVC will default to some other behavior, likely returning a 500 error code.)

Alternatively, you can take advantage of the fact that Spring MVC first checks the @Controller (or @RestController) class for @ExceptionHandler methods first. Just move the @ExceptionHandler method for InternalError into the controller.

@RestController
@RequestMapping("/myController")
public class MyController {
    @RequestMapping(value = "/myAction", method = RequestMethod.POST)
    public boolean myAction() {
        throw new InternalError("");
    }

    @ExceptionHandler(value = InternalError.class)
    public ResponseEntity<String> HandleInternalError(InternalError ex) {
         ...
    }
}

Now Spring will first attempt to find a handler for NestedServletException in MyController. It won't find any so it will unwrap NestedServletException and get an InternalError. It will try to find a handler for InternalError and find HandleInternalError.

This has the disadvantage that if multiple controllers' handler methods throw InternalError, you have to add an @ExceptionHandler to each. This might also be an advantage. Your handling logic will be closer to the thing that throws the error.

Upvotes: 7

Related Questions