Skip to content

Commit 1c734e8

Browse files
Fixed Room getting stuck in CONNECTING state after failed connect attempts. (#836)
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.
1 parent c91c476 commit 1c734e8

3 files changed

Lines changed: 97 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Fixed Room getting stuck in CONNECTING state after failed connect attempts.

livekit-android-sdk/src/main/java/io/livekit/android/room/Room.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,12 @@ constructor(
567567
}
568568
connectJob.join()
569569

570-
error?.let { throw it }
570+
error?.let {
571+
if (it !is CancellationException) {
572+
handleDisconnect(DisconnectReason.JOIN_FAILURE)
573+
}
574+
throw it
575+
}
571576
}
572577

573578
/**

livekit-android-test/src/test/java/io/livekit/android/room/RoomTest.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,90 @@ class RoomTest {
269269

270270
assertEquals(update.sid, sid.sid)
271271
}
272+
273+
@Test
274+
fun connectFailureResetsStateToDisconnected() = runTest {
275+
val connectException = RuntimeException("Connection failed")
276+
rtcEngine.stub {
277+
onBlocking { rtcEngine.join(any(), any(), anyOrNull(), anyOrNull()) }
278+
.doSuspendableAnswer {
279+
throw connectException
280+
}
281+
}
282+
rtcEngine.stub {
283+
onBlocking { rtcEngine.client }
284+
.doReturn(Mockito.mock(SignalClient::class.java))
285+
}
286+
287+
val eventCollector = EventCollector(room.events, coroutineRule.scope)
288+
289+
var caughtException: Throwable? = null
290+
try {
291+
room.connect(
292+
url = TestData.EXAMPLE_URL,
293+
token = "",
294+
)
295+
} catch (e: Throwable) {
296+
caughtException = e
297+
}
298+
299+
val events = eventCollector.stopCollecting()
300+
301+
// Verify exception was thrown (check message since coroutines may wrap exceptions)
302+
assertEquals("Connection failed", caughtException?.message)
303+
304+
// Verify room state is reset to DISCONNECTED
305+
assertEquals(Room.State.DISCONNECTED, room.state)
306+
307+
// Verify Disconnected event was posted with JOIN_FAILURE reason
308+
val disconnectedEvents = events.filterIsInstance<RoomEvent.Disconnected>()
309+
assertEquals(1, disconnectedEvents.size)
310+
assertEquals(DisconnectReason.JOIN_FAILURE, disconnectedEvents[0].reason)
311+
}
312+
313+
@Test
314+
fun connectRetryAfterFailureSucceeds() = runTest {
315+
val connectException = RuntimeException("Connection failed")
316+
var shouldFail = true
317+
318+
rtcEngine.stub {
319+
onBlocking { rtcEngine.join(any(), any(), anyOrNull(), anyOrNull()) }
320+
.doSuspendableAnswer {
321+
if (shouldFail) {
322+
throw connectException
323+
}
324+
room.onJoinResponse(TestData.JOIN.join)
325+
TestData.JOIN.join
326+
}
327+
}
328+
rtcEngine.stub {
329+
onBlocking { rtcEngine.client }
330+
.doReturn(Mockito.mock(SignalClient::class.java))
331+
}
332+
333+
// First connect attempt fails
334+
try {
335+
room.connect(
336+
url = TestData.EXAMPLE_URL,
337+
token = "",
338+
)
339+
} catch (e: Throwable) {
340+
// Expected
341+
}
342+
343+
// Verify room is in DISCONNECTED state after failure
344+
assertEquals(Room.State.DISCONNECTED, room.state)
345+
346+
// Second connect attempt should succeed
347+
shouldFail = false
348+
room.connect(
349+
url = TestData.EXAMPLE_URL,
350+
token = "",
351+
)
352+
353+
// Verify room connected successfully
354+
val roomInfo = TestData.JOIN.join.room
355+
assertEquals(roomInfo.name, room.name)
356+
assertEquals(Room.Sid(roomInfo.sid), room.sid)
357+
}
272358
}

0 commit comments

Comments
 (0)