Skip to content

SSE - ClientAbortException doesn't reach @ExceptionHandler #35141

@icguy

Description

@icguy

I have a proof of concept Spring Boot application with the following controller returning server sent events:

@RestController
class FooController {
    private val logger = LoggerFactory.getLogger(this::class.java)

    @GetMapping("/")
    fun foo(request: HttpServletRequest): SseEmitter {
        val sse = SseEmitter(-1)
        thread {
            var i = 0
            while (true) {
                Thread.sleep(1000)
                try {
                    sse.send(SseEmitter.event().data(++i).build())
                } catch (ex: Exception) {
                    logger.info("Exception caught", ex)
                    break
                }
            }
        }
        return sse
    }

    @ExceptionHandler(Exception::class)
    fun handleException(ex: Exception): ResponseEntity<*> {
        logger.error("Error in exception handler", ex)
        return ResponseEntity.status(500).body(null)
    }
}

If I run it from the command line using gradlew.bat bootRun these exceptions are thrown when a client disconnects :

2025-07-01T09:26:03.527+02:00  INFO 24452 --- [demo] [       Thread-1] com.example.demo.FooController           : Exception caught

org.springframework.web.context.request.async.AsyncRequestNotUsableException: ServletOutputStream failed to flush: java.io.IOException: An established connection was aborted by the software in your host machine
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleHttpServletResponse.handleIOException(StandardServletAsyncWebRequest.java:346) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:418) ~[spring-web-6.2.8.jar:6.2.8]
        at java.base/java.io.FilterOutputStream.flush(FilterOutputStream.java:153) ~[na:na]
        at com.fasterxml.jackson.core.json.UTF8JsonGenerator.flush(UTF8JsonGenerator.java:1205) ~[jackson-core-2.18.4.1.jar:2.18.4.1]
        at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1063) ~[jackson-databind-2.18.4.jar:2.18.4]
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:485) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.http.converter.AbstractGenericHttpMessageConverter.writeInternal(AbstractGenericHttpMessageConverter.java:135) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:235) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.sendInternal(ResponseBodyEmitterReturnValueHandler.java:310) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.send(ResponseBodyEmitterReturnValueHandler.java:297) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.sendInternal(ResponseBodyEmitter.java:226) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send(ResponseBodyEmitter.java:217) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at com.example.demo.FooController$foo$1.invoke(DemoApplication.kt:28) ~[main/:na]
        at com.example.demo.FooController$foo$1.invoke(DemoApplication.kt:23) ~[main/:na]
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) ~[kotlin-stdlib-1.9.25.jar:1.9.25-release-852]
Caused by: 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.doFlush(OutputBuffer.java:304) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:266) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:133) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:415) ~[spring-web-6.2.8.jar:6.2.8]
        ... 13 common frames omitted
Caused by: java.io.IOException: An established connection was aborted by the software in your host machine
        at java.base/sun.nio.ch.SocketDispatcher.write0(Native Method) ~[na:na]
        at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:54) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:132) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:97) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:53) ~[na:na]
        at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:532) ~[na:na]
        at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:125) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1411) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:732) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:698) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:683) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.flush(Http11OutputBuffer.java:574) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.filters.ChunkedOutputFilter.flush(ChunkedOutputFilter.java:156) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11OutputBuffer.flush(Http11OutputBuffer.java:216) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11Processor.flush(Http11Processor.java:1271) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:408) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.Response.action(Response.java:208) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:300) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        ... 16 common frames omitted

2025-07-01T09:26:03.553+02:00 ERROR 24452 --- [demo] [nio-8080-exec-2] com.example.demo.FooController           : Error in exception handler

java.io.IOException: An established connection was aborted by the software in your host machine
        at java.base/sun.nio.ch.SocketDispatcher.write0(Native Method) ~[na:na]
        at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:54) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:132) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:97) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:53) ~[na:na]
        at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:532) ~[na:na]
        at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:125) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1411) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:732) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:698) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:683) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.flush(Http11OutputBuffer.java:574) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.filters.ChunkedOutputFilter.flush(ChunkedOutputFilter.java:156) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11OutputBuffer.flush(Http11OutputBuffer.java:216) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.http11.Http11Processor.flush(Http11Processor.java:1271) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:408) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.coyote.Response.action(Response.java:208) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:300) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:266) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:133) ~[tomcat-embed-core-10.1.42.jar:10.1.42]
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleServletOutputStream.flush(StandardServletAsyncWebRequest.java:415) ~[spring-web-6.2.8.jar:6.2.8]
        at java.base/java.io.FilterOutputStream.flush(FilterOutputStream.java:153) ~[na:na]
        at com.fasterxml.jackson.core.json.UTF8JsonGenerator.flush(UTF8JsonGenerator.java:1205) ~[jackson-core-2.18.4.1.jar:2.18.4.1]
        at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1063) ~[jackson-databind-2.18.4.jar:2.18.4]
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:485) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.http.converter.AbstractGenericHttpMessageConverter.writeInternal(AbstractGenericHttpMessageConverter.java:135) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:235) ~[spring-web-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.sendInternal(ResponseBodyEmitterReturnValueHandler.java:310) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$DefaultSseEmitterHandler.send(ResponseBodyEmitterReturnValueHandler.java:297) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.sendInternal(ResponseBodyEmitter.java:226) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.send(ResponseBodyEmitter.java:217) ~[spring-webmvc-6.2.8.jar:6.2.8]
        at com.example.demo.FooController$foo$1.invoke(DemoApplication.kt:28) ~[main/:na]
        at com.example.demo.FooController$foo$1.invoke(DemoApplication.kt:23) ~[main/:na]
        at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) ~[kotlin-stdlib-1.9.25.jar:1.9.25-release-852]

My problem is that in the exception handler method only the original IOException shows up which:

  • makes it cumbersome to detect cases when the user disconnected because the ClientAbortException disappears
  • might even be considered incorrect behavior given that the exception was handled by the caller

My question is: Is this the intended behavior? If so how can I gracefully handle the exceptions resulting from the client disconnecting?

This issue might be related: #32509

I've tried to investigate why the ClientAbortException doesn't show up in the exception handler. What I found was that in this class the original exception is stored in the request attributes and then it gets rethrown here thereby ignoring the fact that it has been handled on the caller side.

Here's the repo with my code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions