-
Notifications
You must be signed in to change notification settings - Fork 38.8k
Description
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.