I'm trying to use Spring's DeferredResult
to perform long polling. In this example, one user visits a page that uses long polling to wait for another user to click a link. A second user (you in another browser) then clicks that link, and the long polling returns to the first user, notifying her of the second user's click.
The jsp looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Spring Example</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script>
function pollContent() {
$.ajax({url: "waitForClick", success: function(result){
console.log("Polled result: " + result);
$("#polledContent").html(result);
pollContent();
}});
}
$(pollContent);
</script>
</head>
<body>
<p><a href="clickTheThing">Click this thing.</a></p>
<p id="polledContent">Waiting for somebody to click the thing...</p>
</body>
</html>
And the controller looks like this:
package com.example.controller;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import com.example.controller.interfaces.ExampleControllerInterface;
@Component
public class ExampleController implements ExampleControllerInterface{
private int clickCount = 0;
private List<DeferredResult<String>> waiting = new ArrayList<>();
@Override
public String viewHomePage(HttpServletRequest request, ModelMap model, HttpSession session){
return "index";
}
@RequestMapping(value = "/clickTheThing", method = RequestMethod.GET)
public String clickTheThing(HttpServletRequest request, ModelMap model, HttpSession session){
new Thread(){
public void run(){
clickCount++;
System.out.println("Somebody clicked the thing! Click count: " + clickCount);
Iterator<DeferredResult<String>> iterator = waiting.iterator();
while(iterator.hasNext()){
DeferredResult<String> result = iterator.next();
System.out.println("Setting result.");
result.setResult("Somebody clicked the thing! Click count: " + clickCount);
iterator.remove();
}
}
}.start();
return "clicked";
}
@ResponseBody
@RequestMapping(value = "/waitForClick", method = RequestMethod.GET)
public DeferredResult<String> waitForClick(HttpServletRequest request, ModelMap model, HttpSession session){
final DeferredResult<String> result = new DeferredResult<>();
waiting.add(result);
return result;
}
@ResponseBody
@RequestMapping(value = "/getClickCount", method = RequestMethod.GET)
public String getClickCount(HttpServletRequest request, ModelMap model, HttpSession session){
return String.valueOf(clickCount);
}
}
And for completeness, here is my ErrorConfig
class:
package com.example.config;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class ErrorConfig{
@ExceptionHandler(Exception.class)
public String handleException (HttpServletRequest request, HttpServletResponse response, HttpSession session, Exception e) {
e.printStackTrace();
return "index";
}
}
This seems to work okay. The first user is indeed notified whenever another user clicks the link.
However, if that first user refreshes the page before the second user clicks the link, I also get a stack trace for every "old" DeferredResult:
org.apache.catalina.connector.ClientAbortException: java.io.IOException: An established connection was aborted by the software in your host machine
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:396)
at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:426)
at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:342)
at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:316)
at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:110)
at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:297)
at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
at org.springframework.util.StreamUtils.copy(StreamUtils.java:106)
at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:106)
at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:40)
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:208)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:143)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:89)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:193)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:71)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:938)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:639)
at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:605)
at org.apache.catalina.core.AsyncContextImpl$1.run(AsyncContextImpl.java:208)
at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:363)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:78)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:405)
at org.apache.coyote.http11.AbstractHttp11Processor.asyncDispatch(AbstractHttp11Processor.java:1636)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:646)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.IOException: An established connection was aborted by the software in your host machine
at sun.nio.ch.SocketDispatcher.write0(Native Method)
at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:51)
at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
at sun.nio.ch.IOUtil.write(IOUtil.java:65)
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:470)
at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:122)
at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101)
at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:173)
at org.apache.coyote.http11.InternalNioOutputBuffer.writeToSocket(InternalNioOutputBuffer.java:139)
at org.apache.coyote.http11.InternalNioOutputBuffer.addToBB(InternalNioOutputBuffer.java:197)
at org.apache.coyote.http11.InternalNioOutputBuffer.access$000(InternalNioOutputBuffer.java:41)
at org.apache.coyote.http11.InternalNioOutputBuffer$SocketOutputBuffer.doWrite(InternalNioOutputBuffer.java:320)
at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:84)
at org.apache.coyote.http11.AbstractOutputBuffer.doWrite(AbstractOutputBuffer.java:257)
at org.apache.coyote.Response.doWrite(Response.java:523)
at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:391)
... 50 more
Aug 20, 2015 7:19:24 PM org.apache.catalina.core.ApplicationDispatcher invoke
SEVERE: Servlet.service() for servlet jsp threw exception
java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:535)
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:212)
at javax.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:103)
at org.apache.jasper.runtime.JspWriterImpl.initOut(JspWriterImpl.java:115)
at org.apache.jasper.runtime.JspWriterImpl.flushBuffer(JspWriterImpl.java:108)
at org.apache.jasper.runtime.PageContextImpl.release(PageContextImpl.java:173)
at org.apache.jasper.runtime.JspFactoryImpl.internalReleasePageContext(JspFactoryImpl.java:120)
at org.apache.jasper.runtime.JspFactoryImpl.releasePageContext(JspFactoryImpl.java:75)
at org.apache.jsp.WEB_002dINF.jsp.index_jsp._jspService(index_jsp.java:93)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:432)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:403)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:347)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:584)
at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:523)
at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:201)
at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:267)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1221)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1005)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:952)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:639)
at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:605)
at org.apache.catalina.core.AsyncContextImpl$1.run(AsyncContextImpl.java:208)
at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:363)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:78)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:405)
at org.apache.coyote.http11.AbstractHttp11Processor.asyncDispatch(AbstractHttp11Processor.java:1636)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:646)
at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
I can simply ignore these exceptions, but that feels wrong.
So, my questions are:
getOutputStream() has already been called for this response
exceptions, which I gather are caused by the error page of the exception handler?I have a mavenized version of this project available on GitHub here if you want to try it yourself.
In the end I'm trying to add a notifications system to my Spring website, similar to StackOverflow's notification system. If there's a better way to do that with Spring and long polling, I'm all ears.
Edit: I haven't received any answers (or even comments), so I've added a bounty. I'd definitely appreciate any feedback!
The reason why it's failing is because when the first user closes the browser the Stream gets closed, and when you try to set the DeferredResult's result spring tries to send it to the client, causing the error.
You should try checking if the DeferredResult is in a usable status by calling it's isSetOrExpired()
method before writting the result:
while(iterator.hasNext()){
DeferredResult<String> result = iterator.next();
System.out.println("Setting result.");
if(!result.isSetOrExpired()){
result.setResult("Somebody clicked the thing! Click count: " + clickCount);
}
iterator.remove();
}
If Spring still doesn't get the Deferred canceled when the client closes the connection, then there's not too much to do to prevent the exception from actually happening: Detecting client disconnect in tomcat servlet?.
Note that long-poling and comet in general is a hard thing to build from scratch, you should consider using something like Atmosphere for this. It will give you both websockets and comet for old browser compatibility.
What are the best practices for doing this kind of thing, preferably in a way that doesn't generate exceptions?
Is there a better way to keep track of DeferredResults for long polling?
Is there a way around the subsequent getOutputStream() has already been called for this response exceptions, which I gather are caused by the error page of the exception handler?
What is causing the original ClientAbortException? Should I be doing something different?
When we are using DeferredResult or Callable:
ClientAbortException:Client made some other request/clicking some other link. which means current action suspended and it throws ClientAbortException on the current running thread.
An established connection was aborted by the software in your host machine
Most Probably,Windows Firewall can also do this kind of abort functionality check for it once.
What are the best practices for doing this kind of thing, preferably in a way that doesn't generate exceptions?
WebSockets
Is there a better way to keep track of DeferredResults for long polling?
Define a collection for different User along with long polling.
Is there a way around the subsequent getOutputStream() has already been called for this response exceptions, which I gather are caused by the error page of the exception handler?
This may also be a reason for this Current Exception being thrown.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With