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