From b606eee9a1072c16f4aeefedd0e2ad29a7d9b744 Mon Sep 17 00:00:00 2001 From: Adrian Niculescu <15037449+adrian-niculescu@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:53:53 +0200 Subject: [PATCH] Fixed Room getting stuck in CONNECTING state after failed connect attempts. Previously, if Room.connect() failed (due to network errors, invalid URL/token, TLS errors, etc.), the Room would remain in CONNECTING state, causing subsequent connect attempts to throw IllegalStateException. Now the Room properly resets to DISCONNECTED state and emits a Disconnected event with JOIN_FAILURE reason, allowing retry attempts. --- .changeset/fix-room-connect-failure-state.md | 5 ++ .../main/java/io/livekit/android/room/Room.kt | 7 +- .../java/io/livekit/android/room/RoomTest.kt | 86 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-room-connect-failure-state.md diff --git a/.changeset/fix-room-connect-failure-state.md b/.changeset/fix-room-connect-failure-state.md new file mode 100644 index 00000000..57cf6b12 --- /dev/null +++ b/.changeset/fix-room-connect-failure-state.md @@ -0,0 +1,5 @@ +--- +"client-sdk-android": patch +--- + +Fixed Room getting stuck in CONNECTING state after failed connect attempts. diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt index 658d910b..700643c2 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt @@ -566,7 +566,12 @@ constructor( } connectJob.join() - error?.let { throw it } + error?.let { + if (it !is CancellationException) { + handleDisconnect(DisconnectReason.JOIN_FAILURE) + } + throw it + } } /** diff --git a/livekit-android-test/src/test/java/io/livekit/android/room/RoomTest.kt b/livekit-android-test/src/test/java/io/livekit/android/room/RoomTest.kt index 37296965..a6ffc0d4 100644 --- a/livekit-android-test/src/test/java/io/livekit/android/room/RoomTest.kt +++ b/livekit-android-test/src/test/java/io/livekit/android/room/RoomTest.kt @@ -269,4 +269,90 @@ class RoomTest { assertEquals(update.sid, sid.sid) } + + @Test + fun connectFailureResetsStateToDisconnected() = runTest { + val connectException = RuntimeException("Connection failed") + rtcEngine.stub { + onBlocking { rtcEngine.join(any(), any(), anyOrNull(), anyOrNull()) } + .doSuspendableAnswer { + throw connectException + } + } + rtcEngine.stub { + onBlocking { rtcEngine.client } + .doReturn(Mockito.mock(SignalClient::class.java)) + } + + val eventCollector = EventCollector(room.events, coroutineRule.scope) + + var caughtException: Throwable? = null + try { + room.connect( + url = TestData.EXAMPLE_URL, + token = "", + ) + } catch (e: Throwable) { + caughtException = e + } + + val events = eventCollector.stopCollecting() + + // Verify exception was thrown (check message since coroutines may wrap exceptions) + assertEquals("Connection failed", caughtException?.message) + + // Verify room state is reset to DISCONNECTED + assertEquals(Room.State.DISCONNECTED, room.state) + + // Verify Disconnected event was posted with JOIN_FAILURE reason + val disconnectedEvents = events.filterIsInstance() + assertEquals(1, disconnectedEvents.size) + assertEquals(DisconnectReason.JOIN_FAILURE, disconnectedEvents[0].reason) + } + + @Test + fun connectRetryAfterFailureSucceeds() = runTest { + val connectException = RuntimeException("Connection failed") + var shouldFail = true + + rtcEngine.stub { + onBlocking { rtcEngine.join(any(), any(), anyOrNull(), anyOrNull()) } + .doSuspendableAnswer { + if (shouldFail) { + throw connectException + } + room.onJoinResponse(TestData.JOIN.join) + TestData.JOIN.join + } + } + rtcEngine.stub { + onBlocking { rtcEngine.client } + .doReturn(Mockito.mock(SignalClient::class.java)) + } + + // First connect attempt fails + try { + room.connect( + url = TestData.EXAMPLE_URL, + token = "", + ) + } catch (e: Throwable) { + // Expected + } + + // Verify room is in DISCONNECTED state after failure + assertEquals(Room.State.DISCONNECTED, room.state) + + // Second connect attempt should succeed + shouldFail = false + room.connect( + url = TestData.EXAMPLE_URL, + token = "", + ) + + // Verify room connected successfully + val roomInfo = TestData.JOIN.join.room + assertEquals(roomInfo.name, room.name) + assertEquals(Room.Sid(roomInfo.sid), room.sid) + } }