spring-projects / spring-framework

Spring Framework

Home Page:https://spring.io/projects/spring-framework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

@ExceptionHandler methods not invokable if matched on exception's cause level > 1

rod2j opened this issue · comments

Affects: 5.3.2


It looks like PR #23380 introduced a major regression in the way exceptions are handled by @ControllerAdvice components.

Before that PR was merged, Spring resolved @ExceptionHandler methods only on the main exception or its 1st level cause.
ExceptionHandlerExceptionResolver was aligned with that, and after resolution it would invoke the resolved method by using the exception and its cause as 'candidate' arguments for the target method:

protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
	HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {

    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    if (exceptionHandlerMethod == null) {
	return null;
    }

    if (this.argumentResolvers != null) {
	exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    }
    if (this.returnValueHandlers != null) {
	exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
    }

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    ModelAndViewContainer mavContainer = new ModelAndViewContainer();

    try {
	if (logger.isDebugEnabled()) {
		logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
	}
	Throwable cause = exception.getCause();
	if (cause != null) {
		// Expose cause as provided argument as well
		exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
	}
	else {
		// Otherwise, just the given exception as-is
		exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
	}
    }
    catch (Throwable invocationEx) {
	// Any other than the original exception (or its cause) is unintended here,
	// probably an accident (e.g. failed assertion or the like).
	if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
		logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
	}
	// Continue with default processing of the original exception...
	return null;
}

But PR #23380 didn't come with any change in the way arguments of the target method are resolved... and that's the problem.
Whenever the @ExceptionHandler method is resolved due to a match on a cause above the 1st level, and if a throwable of this type is used as an argument of the method, ExceptionHandlerExceptionResolver has no way to resolve this argument as it still only tries the main exception and its 1st cause.

So if we do this in a controller...

@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {

	if ("err3".equals(name)) {
		throw new RuntimeException(
			new Exception(
				new IllegalArgumentException("ExceptionHandler will be resolved but cannot be invoked")));
	}
	...
}

... and have this kind of @ControllerAdvice :

@ControllerAdvice
public class MyControllerAdvice {

	@ExceptionHandler(IllegalArgumentException.class)
	public ResponseEntity<?> handleIllegalArgumentException(IllegalArgumentException ex) {
		return ResponseEntity.badRequest().body(ex.toString());
	}
}

... then the handleIllegalArgumentException is resolved but actually not invokable when exception thrown is like in the code above, and we get this in the log:

2020-12-22 19:52:45.387  WARN 29244 --- [           main] .m.m.a.ExceptionHandlerExceptionResolver : Failure in @ExceptionHandler com.example.restservice.MyControllerAdvice#handleIllegalArgumentException(IllegalArgumentException)

java.lang.IllegalStateException: Could not resolve parameter [0] in public org.springframework.http.ResponseEntity<?> com.example.restservice.MyControllerAdvice.handleIllegalArgumentException(java.lang.IllegalArgumentException): No suitable resolver
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ~[spring-web-5.3.2.jar:5.3.2]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) ~[spring-web-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(ExceptionHandlerExceptionResolver.java:420) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver.doResolveException(AbstractHandlerMethodExceptionResolver.java:75) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:141) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1321) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.test.web.servlet.TestDispatcherServlet.processHandlerException(TestDispatcherServlet.java:149) [spring-test-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1132) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1078) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) [spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) [spring-webmvc-5.3.2.jar:5.3.2]

So if the changes made by PR #23380 are to be kept, i.e. @ExceptionHandler methods have to be able to handle any cause in the causality chain of an exception, a change of design is necessary in arguments resolution.

Regards

Just added a demo project to illustrate the problem:

spring-exception-handler-issue-26317

the cause is:
ExceptionHandlerMethodResolver#resolveMethodByThrowable Find a Method to handle the given Throwable
only find Exception.cause twice

i think it should be a nested finding

@dongbaibai,
I just added comments on your PR #26343 about how IMHO it cannot fix the actual issue.

@rod2j as far as I can tell, such a exception handler for a more deeply nested cause would not have matched at all previously. Do you really mean regression or a bug?

Hi @rstoyanchev ,
I do actually mean regression:
Even if you're right by saying "such a exception handler for a more deeply nested cause would not have matched at all previously", and because this new feature does not appear in any documentation or Javadoc that's not a big deal, it actually is a regression because:

  • as this exceptionhandler is resolved now (but invocation will fail), no other exceptionhandler method will be matched, and the handling of the original exception will fall back to a default processing by Spring (with a default error response format)
  • in the previous release, this exceptionhandler would not have matched, but another might have in another @ControllerAdvice with a lowest priority => For example I could have (and I always do have) an ExceptionHandler like the following to be make sure every single exception leads to a normalized error response format:
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class FallBackExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<ProblemResponse> handleAnyException(Exception exception) {

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ProblemResponse.of(exception));
    }
}

So after migrating to 5.3.2 this handleAnyException will never be called in cases where another @ExceptionHandler has been
matched first because of a deeply nested cause that cannot actually be invoked.

Okay so an existing exception handler may get matched where it didn't before which prevents further handling that might have happened before. Thanks.

This PR #26366 is one of the 2 solutions I thought about to fix this issue and the one requiring the least code / tests change:

  • it doesn't change anything in ExceptionHandlerMethodResolver
  • it modifies the way provided arguments to HandlerMethod are resolved to treat Throwable parameter / argument as a special case, allowing to match them on their cause and not only on the main exception type.

A second solution, more elegant IMO, but requiring more code change, and even more tests change is the following:

  • As ExceptionHandlerMethodResolver is able to find the most appropiate Method by inspecting the given throwable and its causes, it actually knows which Throwable was matched : the main exception or one of its causes
  • It could simply return it along with the Method object in a "Pair" instead of only returning the Method. ExceptionHandlerExceptionResolver would then know which Throwable to use without looking up the cause chain again.
    I already implemented this solution on my current project to implement an ExceptionHandlerMethodResolver adapted to SOAP services (that's how i detected the issue by the way), so would you prefer that solution I can submit a new PR very quickly.

Last question: Reference Documentation and Javadoc for ExceptionHandler will need some updates about this, which is not included yet.

There seems to be an easier - and pretty local - solution: simply building a list of all causes for the exceptionHandlerMethod.invokeAndHandle call. The provided ExceptionHandlerExceptionResolverTests seem to pass. Unless I'm missing something here, I intend to commit this approach ASAP (along with the tests from the PR).

Hi @jhoeller,
Didn't have time to reply to your comment before you committed, but your solution was indeed even simpler.
What about the Javadoc about @ExceptionHandler which has lot of information about how Exception are resolved
and should be updated ? (because the PR #23380 which introduced this feature did not document it).
Same question for the reference documentation ?

Good point, I'll revise those documentation bits before the 5.3.3 release still. Thanks for the reminder!