Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
rod2j opened this issue Dec 24, 2020 · 10 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@rod2j
Copy link

rod2j commented Dec 24, 2020

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

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 24, 2020
@rod2j
Copy link
Author

rod2j commented Dec 24, 2020

Just added a demo project to illustrate the problem:

spring-exception-handler-issue-26317

@jhoeller jhoeller added in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 27, 2020
@jhoeller jhoeller added this to the 5.3.3 milestone Dec 27, 2020
@jhoeller jhoeller self-assigned this Dec 27, 2020
@dongbaibai
Copy link

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

@rod2j
Copy link
Author

rod2j commented Jan 4, 2021

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

@rstoyanchev
Copy link
Contributor

@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?

@rod2j
Copy link
Author

rod2j commented Jan 7, 2021

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.

@rstoyanchev
Copy link
Contributor

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

@rod2j
Copy link
Author

rod2j commented Jan 11, 2021

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.

@jhoeller
Copy link
Contributor

jhoeller commented Jan 11, 2021

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).

@rod2j
Copy link
Author

rod2j commented Jan 11, 2021

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 ?

@jhoeller
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants