Skip to content

Commit 8dc377b

Browse files
YashJainSCclaude
andcommitted
fix: resume joinContinuation when LEAVE received during reconnect handshake
When the server sends a LEAVE as the initial response to a soft reconnect (e.g. STATE_MISMATCH), handleSignalResponse processed it without resuming joinContinuation, leaving client.reconnect() suspended forever. This prevented the reconnect loop from advancing to a full reconnect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent baf1ff6 commit 8dc377b

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,11 @@ constructor(
691691
} else if (response.hasLeave()) {
692692
// Some reconnects may immediately send leave back without a join response first.
693693
handleSignalResponseImpl(ws, response)
694+
val cont = joinContinuation
695+
joinContinuation = null
696+
cont?.resumeWithException(
697+
RoomException.ConnectException("Received leave during reconnect: ${response.leave.reason}"),
698+
)
694699
} else if (isReconnecting) {
695700
// When reconnecting, any message received means signal reconnected.
696701
// Newer servers will send a reconnect response first

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import kotlinx.coroutines.async
3333
import kotlinx.coroutines.supervisorScope
3434
import kotlinx.coroutines.yield
3535
import kotlinx.serialization.json.Json
36+
import livekit.LivekitModels
3637
import livekit.LivekitRtc
3738
import livekit.org.webrtc.SessionDescription
3839
import okhttp3.OkHttpClient
@@ -429,6 +430,77 @@ class SignalClientTest : BaseTest() {
429430
assertTrue(originalWs.isClosed)
430431
}
431432

433+
/**
434+
* Reconnect fails cleanly when server sends LEAVE as the initial response and closes WS cleanly.
435+
*/
436+
@Test
437+
fun reconnectDoesNotHangOnLeaveStateMismatchWithCleanClose() = runTest {
438+
val joinJob = async { client.join(EXAMPLE_URL, "") }
439+
connectWebsocketAndJoin()
440+
joinJob.await()
441+
442+
wsFactory.ws.cancel()
443+
444+
supervisorScope {
445+
val reconnectJob = async {
446+
client.reconnect(EXAMPLE_URL, "", "participant_sid")
447+
}
448+
yield()
449+
450+
client.onOpen(wsFactory.ws, createOpenResponse(wsFactory.request))
451+
val leaveResponse = with(LivekitRtc.SignalResponse.newBuilder()) {
452+
leave = with(LivekitRtc.LeaveRequest.newBuilder()) {
453+
reason = LivekitModels.DisconnectReason.STATE_MISMATCH
454+
action = LivekitRtc.LeaveRequest.Action.RECONNECT
455+
build()
456+
}
457+
build()
458+
}
459+
wsFactory.receiveMessage(leaveResponse)
460+
461+
wsFactory.ws.close(1000, "normal")
462+
463+
val result = runCatching { reconnectJob.await() }
464+
assertTrue("reconnect should have failed", result.isFailure)
465+
}
466+
}
467+
468+
/**
469+
* Reconnect fails cleanly when server sends LEAVE as the initial response and drops the connection.
470+
*/
471+
@Test
472+
fun reconnectDoesNotHangOnLeaveStateMismatchWithFailure() = runTest {
473+
val joinJob = async { client.join(EXAMPLE_URL, "") }
474+
connectWebsocketAndJoin()
475+
joinJob.await()
476+
477+
wsFactory.ws.cancel()
478+
479+
supervisorScope {
480+
val reconnectJob = async {
481+
client.reconnect(EXAMPLE_URL, "", "participant_sid")
482+
}
483+
yield()
484+
485+
client.onOpen(wsFactory.ws, createOpenResponse(wsFactory.request))
486+
val leaveResponse = with(LivekitRtc.SignalResponse.newBuilder()) {
487+
leave = with(LivekitRtc.LeaveRequest.newBuilder()) {
488+
reason = LivekitModels.DisconnectReason.STATE_MISMATCH
489+
action = LivekitRtc.LeaveRequest.Action.RECONNECT
490+
build()
491+
}
492+
build()
493+
}
494+
wsFactory.receiveMessage(leaveResponse)
495+
496+
// 5. Server drops connection (unclean close)
497+
wsFactory.ws.cancel()
498+
499+
val result = runCatching { reconnectJob.await() }
500+
assertTrue("reconnect should have failed", result.isFailure)
501+
}
502+
}
503+
432504
// mock data
433505
companion object
434506
}

0 commit comments

Comments
 (0)