Sendspin web player race condition losing `client/hello` (#2946)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Mon, 12 Jan 2026 12:22:42 +0000 (13:22 +0100)
committerGitHub <noreply@github.com>
Mon, 12 Jan 2026 12:22:42 +0000 (13:22 +0100)
fix: Sendspin DataChannel race condition losing `client/hello`

Register message/close handlers before ws_connect() to ensure early
messages are queued. Change condition to also queue when task is None
(during setup), not just when running.

music_assistant/controllers/webserver/remote_access/gateway.py

index 8d6343c363ebfb6d41778f4c8c366feb9ef05fc1..4ded35556d105017a1fe9a127514fcb64ca95207 100644 (file)
@@ -712,21 +712,16 @@ class WebRTCGateway:
             return
 
         try:
-            session.sendspin_ws = await self.http_session.ws_connect("ws://127.0.0.1:8927/sendspin")
-            self.logger.debug("Sendspin channel connected for session %s", session.session_id)
-
             loop = asyncio.get_event_loop()
 
-            session.sendspin_to_local_task = asyncio.create_task(
-                self._forward_sendspin_to_local(session)
-            )
-            session.sendspin_from_local_task = asyncio.create_task(
-                self._forward_sendspin_from_local(session)
-            )
-
             @channel.on("message")  # type: ignore[untyped-decorator]
             def on_message(message: str | bytes) -> None:
-                if session.sendspin_to_local_task and not session.sendspin_to_local_task.done():
+                # Queue if task not yet created (None) or still running.
+                # Only drop when task exists and is done (shutdown).
+                if (
+                    session.sendspin_to_local_task is None
+                    or not session.sendspin_to_local_task.done()
+                ):
                     loop.call_soon_threadsafe(session.sendspin_queue.put_nowait, message)
 
             @channel.on("close")  # type: ignore[untyped-decorator]
@@ -734,6 +729,17 @@ class WebRTCGateway:
                 if session.sendspin_ws and not session.sendspin_ws.closed:
                     asyncio.run_coroutine_threadsafe(session.sendspin_ws.close(), loop)
 
+            session.sendspin_ws = await self.http_session.ws_connect("ws://127.0.0.1:8927/sendspin")
+            self.logger.debug("Sendspin channel connected for session %s", session.session_id)
+
+            # Start forwarding tasks - queued messages will be processed
+            session.sendspin_to_local_task = asyncio.create_task(
+                self._forward_sendspin_to_local(session)
+            )
+            session.sendspin_from_local_task = asyncio.create_task(
+                self._forward_sendspin_from_local(session)
+            )
+
         except Exception:
             self.logger.exception(
                 "Failed to connect sendspin channel to internal server for session %s",