Reputation: 739
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
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:
@Controller
class contains any @ExceptionHandler
methods. 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). @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