[PR #7732] Fix to make web sockets close gracefully on server shutdown #11508

Closed
opened 2026-02-07 06:39:44 +03:00 by OVERLORD · 0 comments
Owner

Original Pull Request: https://github.com/jellyfin/jellyfin/pull/7732

State: closed
Merged: Yes


Changes

  • Added IAsyncDisposable to multiple classes in order to call CloseOutputAsync on any open web sockets when server shutdown occurs
  • Logs provided below to show the before and after log messages

Issues
Fixes #7538

Remarks

  • Not a huge fan of the nest for loops I did, maybe there's a better way?
  • Should I change everything I did to async?
  • This is my first time messing with a lot of this stuff. I'm very much looking forward to feedback!
  • Whoops! I messed up the rebase, here's my previous PR, sorry for the mess...

Kotlin SDK logs before:

Starting subscription
[main] INFO org.jellyfin.sdk.api.client.KtorClient - POST http://localhost:8096/Users/AuthenticateByName
State DISCONNECTED
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Reconnecting
State CONNECTING
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has opened
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ForceKeepAlive","MessageId":"00000000000000000000000000000000","Data":60}
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for SessionsMessage
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.KeepAliveHelper - Using a KeepAlive message delay of 30 seconds
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"SessionsStart","Data":"0,1000"}
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}}
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ActivityLogEntryMessage
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"eda0a22c5f8045e2bef3382736abbb3e"}
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ScheduledTasksInfoMessage
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ActivityLogEntryStart","Data":"0,1000"}
State CONNECTED
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ScheduledTasksInfoStart","Data":"0,1000"}
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}}
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"791199526eb34384bd602bd91707b9a6"}
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ServerShuttingDown","MessageId":"87116115715c4b219793656a5dd505dd","Data":""}
Received ServerShuttingDownMessage(messageId=87116115-715c-4b21-9793-656a5dd505dd)
[OkHttp http://localhost:8096/...] WARN org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has failed
java.io.EOFException
        at okio.RealBufferedSource.require(RealBufferedSource.kt:199)
        at okio.RealBufferedSource.readByte(RealBufferedSource.kt:209)
        at okhttp3.internal.ws.WebSocketReader.readHeader(WebSocketReader.kt:119)
        at okhttp3.internal.ws.WebSocketReader.processNextFrame(WebSocketReader.kt:102)
        at okhttp3.internal.ws.RealWebSocket.loopReader(RealWebSocket.kt:293)
        at okhttp3.internal.ws.RealWebSocket$connect$1.onResponse(RealWebSocket.kt:195)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:829)
State DISCONNECTED

Kotlin SDK logs after:

Starting subscription
[main] INFO org.jellyfin.sdk.api.client.KtorClient - POST http://localhost:8096/Users/AuthenticateByName
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - ReconnectingState DISCONNECTED

State CONNECTING
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has opened
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ForceKeepAlive","MessageId":"00000000000000000000000000000000","Data":60}
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for SessionsMessage
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"SessionsStart","Data":"0,1000"}
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ActivityLogEntryMessage
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.KeepAliveHelper - Using a KeepAlive message delay of 30 seconds
[DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ScheduledTasksInfoMessage
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ActivityLogEntryStart","Data":"0,1000"}
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}}
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"3d76f92db07b4be9b631d710c33d9c00"}
State CONNECTED
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ScheduledTasksInfoStart","Data":"0,1000"}
[OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket is closing, code=1000, reason=System Shutdown
State DISCONNECTED
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.ReconnectHelper - Reconnect scheduled in 5s (error=false)
[DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Reconnecting
State CONNECTING
[OkHttp WebSocket http://localhost:8096/... writer] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has closed, code=1000, reason=System Shutdown
[OkHttp http://localhost:8096/...] WARN org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has failed
java.net.SocketTimeoutException: timeout
        at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:143)
        at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:162)
        at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:335)
        at okio.RealBufferedSource.indexOf(RealBufferedSource.kt:427)
        at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:320)
        at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29)
        at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178)
        at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106)
        at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
        at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
        at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
        at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
        at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
        at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:517)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.net.SocketException: Socket closed
        at java.base/java.net.SocketInputStream.read(SocketInputStream.java:183)
        at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140)
        at okio.InputStreamSource.read(JvmOkio.kt:90)
        at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:129)
        ... 20 more
**Original Pull Request:** https://github.com/jellyfin/jellyfin/pull/7732 **State:** closed **Merged:** Yes --- **Changes** - Added IAsyncDisposable to multiple classes in order to call CloseOutputAsync on any open web sockets when server shutdown occurs - Logs provided below to show the before and after log messages **Issues** Fixes #7538 **Remarks** - ~~Not a huge fan of the nest for loops I did, maybe there's a better way?~~ - ~~Should I change everything I did to `async`?~~ - This is my first time messing with a lot of this stuff. I'm very much looking forward to feedback! - **Whoops! I messed up the rebase, here's [my previous PR](https://github.com/jellyfin/jellyfin/pull/7731), sorry for the mess...** **Kotlin SDK logs before:** ``` Starting subscription [main] INFO org.jellyfin.sdk.api.client.KtorClient - POST http://localhost:8096/Users/AuthenticateByName State DISCONNECTED [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Reconnecting State CONNECTING [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has opened [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ForceKeepAlive","MessageId":"00000000000000000000000000000000","Data":60} [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for SessionsMessage [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.KeepAliveHelper - Using a KeepAlive message delay of 30 seconds [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"SessionsStart","Data":"0,1000"} [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}} [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ActivityLogEntryMessage [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"eda0a22c5f8045e2bef3382736abbb3e"} [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ScheduledTasksInfoMessage [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ActivityLogEntryStart","Data":"0,1000"} State CONNECTED [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ScheduledTasksInfoStart","Data":"0,1000"} [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}} [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"791199526eb34384bd602bd91707b9a6"} [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ServerShuttingDown","MessageId":"87116115715c4b219793656a5dd505dd","Data":""} Received ServerShuttingDownMessage(messageId=87116115-715c-4b21-9793-656a5dd505dd) [OkHttp http://localhost:8096/...] WARN org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has failed java.io.EOFException at okio.RealBufferedSource.require(RealBufferedSource.kt:199) at okio.RealBufferedSource.readByte(RealBufferedSource.kt:209) at okhttp3.internal.ws.WebSocketReader.readHeader(WebSocketReader.kt:119) at okhttp3.internal.ws.WebSocketReader.processNextFrame(WebSocketReader.kt:102) at okhttp3.internal.ws.RealWebSocket.loopReader(RealWebSocket.kt:293) at okhttp3.internal.ws.RealWebSocket$connect$1.onResponse(RealWebSocket.kt:195) at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829) State DISCONNECTED ``` **Kotlin SDK logs after:** ``` Starting subscription [main] INFO org.jellyfin.sdk.api.client.KtorClient - POST http://localhost:8096/Users/AuthenticateByName [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - ReconnectingState DISCONNECTED State CONNECTING [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has opened [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"ForceKeepAlive","MessageId":"00000000000000000000000000000000","Data":60} [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for SessionsMessage [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"SessionsStart","Data":"0,1000"} [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ActivityLogEntryMessage [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.KeepAliveHelper - Using a KeepAlive message delay of 30 seconds [DefaultDispatcher-worker-2] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Adding subscription for ScheduledTasksInfoMessage [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ActivityLogEntryStart","Data":"0,1000"} [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"KeepAlive","Data":{}} [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Receiving (raw) message {"MessageType":"KeepAlive","MessageId":"3d76f92db07b4be9b631d710c33d9c00"} State CONNECTED [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - Sending (raw) message {"MessageType":"ScheduledTasksInfoStart","Data":"0,1000"} [OkHttp http://localhost:8096/...] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket is closing, code=1000, reason=System Shutdown State DISCONNECTED [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.helper.ReconnectHelper - Reconnect scheduled in 5s (error=false) [DefaultDispatcher-worker-1] INFO org.jellyfin.sdk.api.sockets.SocketInstance - Reconnecting State CONNECTING [OkHttp WebSocket http://localhost:8096/... writer] INFO org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has closed, code=1000, reason=System Shutdown [OkHttp http://localhost:8096/...] WARN org.jellyfin.sdk.api.sockets.OkHttpWebsocketSession - WebSocket has failed java.net.SocketTimeoutException: timeout at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:143) at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:162) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:335) at okio.RealBufferedSource.indexOf(RealBufferedSource.kt:427) at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:320) at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29) at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178) at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106) at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:517) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829) Caused by: java.net.SocketException: Socket closed at java.base/java.net.SocketInputStream.read(SocketInputStream.java:183) at java.base/java.net.SocketInputStream.read(SocketInputStream.java:140) at okio.InputStreamSource.read(JvmOkio.kt:90) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:129) ... 20 more ```
OVERLORD added the pull-request label 2026-02-07 06:39:44 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/jellyfin#11508