Configuring OSGI Enroute REST application to allow async requests

368 Views Asked by At

I am currently trying to configure a REST application to enable Server-side events to be sent to a client using EventSource. Currently I am running into an IllegalStateException when attempting to open an asynchronous connection to a REST request which appears to be the result of the server not being configured to allow asynchronous requests.

Some code

@RequireBootstrapWebResource(resource="css/bootstrap.css")
@RequireWebServerExtender
@RequireHttpImplementation
@RequireConfigurerExtender
@Component(name="my.web", property={"debug=true"})
public final class MyApplication implements REST
{
   public void getBatching(RESTRequest request)
   {
      User user = authenticate(request);

      HttpServletResponse response = request._response();
      response.setStatus(HttpServletResponse.SC_OK);
      response.setContentType("text/event-stream");
      response.setCharacterEncoding(StandardCharsets.UTF_8.name());
      try
      {
         AsyncContext async = (AsyncContext)request._request().startAsync();
         async.addListener(new InternalAsyncListener());
         sseConnectionMap.put(UUID.randomUUID(), async);

         response.flushBuffer();
      }
      catch (Exception e)
      {
         System.out.println("Message=" + e.getMessage());
         e.printStackTrace();
      }
   }

   @SuppressWarnings("unused")
   @Activate
   void activate(ComponentContext cc, BundleContext bc, Map<String, Object> config)
   {
      System.out.println(this.getClass() + " started");

      if ("true".equals(config.get("debug")))
      {
         debug = true;
      }

      System.out.println("Link Start");
   }
}

Currently that prints the following:

Message=null
java.lang.IllegalStateException
at org.apache.felix.http.base.internal.dispatch.ServletRequestWrapper.startAsync(ServletRequestWrapper.java:314)
at my.application.MyApplication.getBatching(WebApplication.java:399)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at osgi.enroute.rest.simple.provider.Function.invoke(Function.java:203)
at osgi.enroute.rest.simple.provider.RestMapper.execute(RestMapper.java:333)
at osgi.enroute.rest.simple.provider.RestServlet.service(RestServlet.java:66)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.felix.http.base.internal.handler.ServletHandler.handle(ServletHandler.java:85)
at org.apache.felix.http.base.internal.dispatch.InvocationChain.doFilter(InvocationChain.java:79)
at my.application.CORSFilter.doFilter(CORSFilter.java:75)
at org.apache.felix.http.base.internal.handler.FilterHandler.handle(FilterHandler.java:135)
at org.apache.felix.http.base.internal.dispatch.InvocationChain.doFilter(InvocationChain.java:74)
at org.apache.felix.http.base.internal.dispatch.Dispatcher.dispatch(Dispatcher.java:124)
at org.apache.felix.http.base.internal.DispatcherServlet.service(DispatcherServlet.java:61)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:845)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:583)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:224)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1160)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:511)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1092)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:213)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
at org.eclipse.jetty.server.Server.handle(Server.java:518)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:308)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:244)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:273)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:95)
at org.eclipse.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.produceAndRun(ExecuteProduceConsume.java:246)
at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:156)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:654)
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:572)
at java.lang.Thread.run(Unknown Source)

Where authenticate() currently modifies headers when in debug mode to allow for CORS while developing the application and attempts to obtain a User from a token passed in the Authorization header... however even if I put the startAsync() line above that I still get the same IllegalStateException so I don't think it's an issue of header modification.

Some googling has brought up that the ISE on that particular line ServletRequestWrapper.java:314 indicates that the underlying Felix/Jetty server is not set up to allow asynchronous requests.

I also tried to simply set the header to text/event-stream and continue to write to the response but of course it was closed upon exiting the REST method.

Further investigation has provided other solutions, including running a second servlet which is configured for handling asynchronous requests and binding that to a separate URL pattern. That is, of course, it's own effort, and I'm unsure of how well multiple servlets will play together. Preferably I would like to be able to just open asynchronous responses from the one Enroute application.

If it is possible to configure the enroute application to allow asynchronous responses, what is the setting I am messing?

1

There are 1 best solutions below

0
On BEST ANSWER

Sigh... found the answer while having difficulties getting an @WebServlet to start up and kept coming back to Whiteboard. A search for 'whiteboard async Java' brought up the HttpWhiteboardConstants setting: HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED:

@Component(..., property={...,
     HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED+"=true", ...}...)

This, however, did not work on the @Component implementing REST , instead I was only able to get it to work in a separate component as follows:

@RequireHttpImplementation
@Component(name="sse", property={
    HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN+"=/sse/*",
    HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_ASYNC_SUPPORTED+"=true",
    HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED+"=true"},
    service=Servlet.class)
public final class ServerSideEventServlet extends HttpServlet
{
   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse response) 
       throws ServletException, IOException
   {
   }
}

and this Servlet is started parallel to the one in my question. When I tried to pull this configuration over to the REST server, I still got the same exception. It could be because the request is touched outside of the REST method, which does make sense.