Enable async-supported for filters in spring boot

Hi,

I am getting the following error when calling /external-task/fetchAndLock

Jul 06, 2020 9:57:33 AM org.glassfish.jersey.servlet.internal.ResponseWriter suspend
WARNING: Attempt to put servlet request into asynchronous mode has failed. Please check your servlet configuration - all Servlet instances and Servlet filters involved in the request processing must explicitly declare support for asynchronous request processing.
java.lang.IllegalStateException: !asyncSupported: EmptyBodyFilter@27ae0829==org.camunda.bpm.engine.rest.filter.EmptyBodyFilter,inst=true,async=false
        at org.eclipse.jetty.server.Request.startAsync(Request.java:2241)
        at javax.servlet.ServletRequestWrapper.startAsync(ServletRequestWrapper.java:389)
        at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.getAsyncContext(AsyncContextDelegateProviderImpl.java:89)
        at org.glassfish.jersey.servlet.async.AsyncContextDelegateProviderImpl$ExtensionImpl.suspend(AsyncContextDelegateProviderImpl.java:73)
        at org.glassfish.jersey.servlet.internal.ResponseWriter.suspend(ResponseWriter.java:101)
        at org.glassfish.jersey.server.ServerRuntime$AsyncResponder.suspend(ServerRuntime.java:836)
        at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:374)
        at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:80)
        at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:253)
        at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
        at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
        at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
        at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
        at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:232)
        at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:680)
        at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:392)
        at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:346)
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:365)
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:318)
        at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
        at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:760)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1617)
        at org.camunda.bpm.engine.rest.filter.CacheControlFilter.doFilter(CacheControlFilter.java:45)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.camunda.bpm.engine.rest.filter.EmptyBodyFilter.doFilter(EmptyBodyFilter.java:99)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)
        at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:226)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1604)
        at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)
        at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
        at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:536)
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
        at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
        at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1592)
        at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

Looking in the non spring boot versions that contain a web.xml the affected filters have

<async-supported>true</async-supported>

But looking in CamundaBpmRestInitializer it doesn’t set async supported to true when registering the filters.
Would you know how best to enable this for spring boot?

Thanks,

Matt

Hi,

We have investigated this further with the following results

The cause seems to be coming from the fact that fetchAndLock is annotated as @Suspended – invoking servlet asynchronous response.

package org.camunda.bpm.engine.rest;


import org.camunda.bpm.engine.rest.dto.externaltask.FetchExternalTasksExtendedDto;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.MediaType;

/**
* @author Tassilo Weidner
*/
public interface FetchAndLockRestService {

  String PATH = "/external-task/fetchAndLock";

  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  void fetchAndLock(FetchExternalTasksExtendedDto dto, @Suspended final AsyncResponse asyncResponse);

}

This requires that all Servlet instances and Servlet filters involved in the request processing must explicitly declare support for asynchronous request processing. But in CamundaBpmRestInitializer it registers two filters without defining asyncSupported

registerFilter("EmptyBodyFilter", EmptyBodyFilter.class, restApiPathPattern);
registerFilter("CacheControlFilter", CacheControlFilter.class, restApiPathPattern);

This leaves async support value to the discretion of the container.
Using just the default container in spring boot, which is tomcat, this is fine as it defaults async supported to true. However if jetty is used, as is in my case, it defaults to false in the latest version.

See https://stackoverflow.com/questions/20643970/asyncsupported-exception-when-switch-from-jetty-7-to-jetty-9

Ah, the evolution of the spec ...

Jetty 7 was Servlet 2.5 (no async there)
Jetty 8 was Servlet 3.0 (async introduced) - spec was vague on what was default, so Jetty defaulted to async-supported == true
Jetty 9 is Servlet 3.1 (even more async) - the spec was clarified, and jetty chose its default poorly. The default according to the spec is async-supported == false

That's why you didn't have to specify async-supported in the past, but now you do.

Bug about this bugs.eclipse.org/410893

Commit: 9bf7870c7c8a209f2660f63c14dd4acb62b07533

As a work around we we extended the standard CamundaBpmRestInitializer as follows

@Component
public class PatchRestInitializer extends CamundaBpmRestInitializer implements ServletContextInitializer {

  public PatchRestInitializer(JerseyApplicationPath applicationPath) {
    super(applicationPath);
  }


  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);

    Arrays.asList("CacheControlFilter", "EmptyBodyFilter").forEach(fn -> {
      Registration.Dynamic filterRegistration = (Dynamic) servletContext.getFilterRegistration(fn);
      filterRegistration.setAsyncSupported(true);
    });

  }
}

Is this a bug?

Thanks,

Matt

Hi. Is there any way to switch between tomcat and jetty?