I have a project (Spring 5.3.7) that contains a controller and exception handlers using the @ControllerAdvice and @ExceptionHandler annotations.

I'm also using Spring Integration DSL to create a HTTP inbound gateway. No error channel is set on the gateway.

My exception handlers are not being called when an exception is thrown in the integration flow (in the same thread).

I actually did not expect it to work automatically, but while debugging the code I noticed that it actually almost all works except for one line.

Line 51-53 of AbstractHandlerMethodExceptionResolver contains:

else if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {
            return super.shouldApplyTo(request, handler);
        }

Because there are no handler mappings, my exceptions handlers are never applied to exceptions from the integration flow.

I'm surprised about the check of hasHandlerMappings() in the if statement. Might this be a bug?

I ask this because the logic in the block above it, and in shouldApplyTo, suggests that a resolver should always be applied when no handler mappings are available.

Line 183 of AbstractHandlerExceptionResolver:

return !hasHandlerMappings();

If the code actually is intended to work like this, then I'd like to know how I can set the mappings so that the exception handlers work both for my controllers and spring integration.

I'm currently doing it in a very hacky way by iterating over the ExceptionHandlerExceptionResolver beans, and doing some casts, and calling setMappedHandlerClasses.

How are the handler mappings supposed to be set?

Comment From: poutsma

This is not a bug. The @ControllerAdvice and @ExceptionHandler annotations support error within the context of Spring Framework. In this context we cannot handle errors from Spring Integration, because adding a dependency on Spring Integration would introduce a cyclic dependency.

That said, it might be possible for Spring Integration to add their own support for the @ControllerAdvice and @ExceptionHandler annotations, so that you could also use them in that context. Pinging @artembilan because I am not a Spring Integration expert.

Comment From: robertvanloenhout

Hi @poutsma No dependency on Spring Integration is needed. DispatcherServlet is still used even though I use Spring Integration. The DispatcherServlet does the exception resolving as usual. All my exception resolvers are there. It just thinks it should not apply my global exception handlers, because there are no handler mappings. There are no handler mappings if use a Controller too, but in that case the additional check to see if there are any handler mappings isn't done, because the handler is a HandlerMethod.

I think everything will just work fine if the hasHandlerMappings() is removed from the if statement.

Comment From: poutsma

I think everything will just work fine if the hasHandlerMappings() is removed from the if statement.

I am sure that everything will be fine for your particular use case. I am not sure if that is also true for the other Spring users.

We can take a further look at this, if you could please take the time to provide a complete minimal sample (something that we can unzip or git clone, build, and deploy) that reproduces the problem.

Comment From: robertvanloenhout

I understand. I just didn't want to give the impression I'm asking for a big overhaul so it works nicely with spring integration. I have made a minimal project here: https://github.com/robertvanloenhout/spring-framework-27054 You can use requests.http to do a request handled by the controller, and a request handled by an integration flow. I'd like the call to http://localhost:8080/flow to use MyExceptionHandler and return my custom message.

Comment From: artembilan

I concur with @poutsma : while it is your use-case it doesn't mean that such a behavior must be by default. See setMappedHandlerClasses JavaDocs:

    /**
     * Specify the set of classes that this exception resolver should apply to.
     * <p>The exception mappings and the default error view will only apply to handlers of the
     * specified types; the specified types may be interfaces or superclasses of handlers as well.
     * <p>If no handlers or handler classes are set, the exception mappings and the default error
     * view will apply to all handlers. This means that a specified default error view will be used
     * as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
     * ignored in this case.
     */
    public void setMappedHandlerClasses(Class<?>... mappedHandlerClasses) {

So, we may consider to customize it from the target project perspective. See Spring Boot's WebMvcRegistrations. Perhaps its getExceptionHandlerExceptionResolver() with supplied HttpRequestHandlingEndpointSupport.class should help you to solve your requirements:

 @Bean
  WebMvcRegistrations integrationMvcRegistration() {
    return new WebMvcRegistrations() {

      @Override
      public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
        ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver();
        exceptionHandlerExceptionResolver.setMappedHandlerClasses(HttpRequestHandlingEndpointSupport.class);
        return exceptionHandlerExceptionResolver;
      }
    };
  }

NOTE: Spring Boot 2.5 comes with Spring Integration 5.5: no need to override it to something else manually.

Comment From: robertvanloenhout

Thanks @artembilan for giving a way for me to set the mapped handler classes.

Sorry that I still don't really understand. The javadoc you pasted states: "If no handlers or handler classes are set, the exception mappings and the default error view will apply to all handlers." So, why is this true for HandlerMethod handlers, but not for HttpRequestHandlingEndpointSupport or other handlers? I don't see an explanation for that in the javadocs.

I have added the WebMvcRegistrations bean. Setting the mappedHandlerClasses to HttpRequestHandlingEndpointSupport.class lets my exception handler be applied in the Spring Integration context. However then it no longer works in the MyController context. The only way I can make it work in both context is by doing resolver.setMappedHandlerClasses(Object.class); At least this solves it for my use case.

You have noted that Spring Boot 2.5 comes with Spring Integration 5.5. Unfortunately spring-boot-starter-integration does not come with spring-integration-http Therefore I have added this extra dependency. I forgot to update the spring-integration-http version, when updating the spring boot version in my real project. It's something I find easy to forget when dealing with many pom files. Thanks for noticing this.

Comment From: poutsma

The underlying cause is that even though both Spring MVC and Spring Integration have web support, the way they work internally is quite different. For one thing, they use different kinds of handlers. In annotation-based MVC the handler is a HandlerMethod. In Spring Integration, the handler is the HttpRequestHandlingMessagingGateway which extends from HttpRequestHandlingEndpointSupport.

As you can see in AbstractHandlerMethodExceptionResolver:: shouldApplyTo, the path for HandlerMethod instances is different than other types, which need to be mapped explicitly through the mappedClasses property. The underlying assumption here is that annotation-based handler methods do not require explicit configuration, but other handlers do. As to why we can't make an exception for HttpRequestHandlingEndpointSupport as well, we are running into my original explanation: we cannot introduce circular dependencies.

As an alternative to @artembilan's suggestion to register a custom resolver, you can also choose to customize the default resolvers by implementing a WebMvcConfigurer. And instead of setting mappedHandlerClasses an Object class, you can also set it to an array of classes. Adding this configuration class to your project did the job for me:

@Configuration
class WebConfig implements WebMvcConfigurer {

  @Override
  public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    for (HandlerExceptionResolver resolver : resolvers) {
      if (resolver instanceof ExceptionHandlerExceptionResolver exceptionResolver) {
        exceptionResolver.setMappedHandlerClasses(HttpRequestHandlingMessagingGateway.class, MyController.class);
      }
    }
  }
}

Comment From: artembilan

Unfortunately spring-boot-starter-integration does not come with spring-integration-http

It must not. We can't assume that target project is always going to use web. Well, from the big height Spring Integration is about Messaging not Web.

I forgot to update the spring-integration-http version

You must not do that. Spring Boot manages a BOM dependency for Spring Integration and all the modules you are going to declare in your project are going to inherit the version from dependency management controlled by Spring Boot. Typically we don't recommend to override version for deps which are controlled by Spring Boot: only when there is a critical bug to fix quickly on the end-user side.

Now re. error handling: I think @poutsma did the proper point. The default global @ExceptionHandler is really for method handlers like @RequestMapping which is a declarative annotation based configuration. Since there is no end-user method signature when we declare Spring Integration channel adapter, but rather some generic low-level API, it is better to control error handling also programmatic way. You may consider to use an Http.inboundControllerGateway() instead which is very close to the @Controller infrastructure and comes with viewName and errorCode to handle exceptions some desired way.