Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring DeferredResult causes IOException: An established connection was aborted by the software in your host machine

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:

  • What is causing the original ClientAbortException? Should I be doing something different?
  • 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?

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!

like image 758
Kevin Workman Avatar asked Aug 20 '15 23:08

Kevin Workman


2 Answers

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.

About your other questions

  • What are the best practices for doing this kind of thing, preferably in a way that doesn't generate exceptions?

    • You should consider using WebSockets for this. Spring 4.2 comes with some nice features for it, and also check Atmosphere
    • You can also give a chance to server-sent events
  • Is there a better way to keep track of DeferredResults for long polling?

    • I guess your way is ok if everyone is able to click the button and notify everyone listening. If you want some more targeted behaviour (like specifically notifying a user of his individual events) then you should keep a Map of users and pending long polling DeferredResults.
  • 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?

    • That error happens for the same reason than the first one. As an error has occurred, the servlet container is trying to print the error JSP to the closed stream causing an error. The way to solve this is just making no error to happen ever when a request is closed before getting answered :-).
like image 83
Alfonso Presa Avatar answered Nov 17 '22 00:11

Alfonso Presa


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.

like image 42
MS Ibrahim Avatar answered Nov 17 '22 00:11

MS Ibrahim