From 47ea05051514d56b1a981a8ce05373ad4b1aa21d Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 16 Jun 2026 11:32:52 -0400 Subject: [PATCH 1/5] fix: detach error handlers before closing data channel --- src/room/RTCEngine.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 130f39de9c..2df9b74b20 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -456,12 +456,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit } async cleanupPeerConnections() { - await this.pcManager?.close(); - this.pcManager = undefined; - - const dcCleanup = (dc: RTCDataChannel | undefined) => { + // Detach the data channel handlers before closing the peer connections. Closing a peer + // connection tears down the SCTP transport, which can dispatch `error`/`close` events on + // the still-open data channels; if our handlers are still attached at that point, + // handleDataError logs a spurious "Unknown DataChannel error" during an otherwise graceful + // disconnect. Detaching first makes this deterministic regardless of how/when the browser + // dispatches those teardown events. See livekit/client-sdk-js#1953. + const detachHandlers = (dc: RTCDataChannel | undefined) => { if (!dc) return; - dc.close(); dc.onbufferedamountlow = null; dc.onclose = null; dc.onclosing = null; @@ -469,6 +471,20 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit dc.onmessage = null; dc.onopen = null; }; + detachHandlers(this.lossyDC); + detachHandlers(this.lossyDCSub); + detachHandlers(this.reliableDC); + detachHandlers(this.reliableDCSub); + detachHandlers(this.dataTrackDC); + detachHandlers(this.dataTrackDCSub); + + await this.pcManager?.close(); + this.pcManager = undefined; + + const dcCleanup = (dc: RTCDataChannel | undefined) => { + if (!dc) return; + dc.close(); + }; dcCleanup(this.lossyDC); dcCleanup(this.lossyDCSub); dcCleanup(this.reliableDC); From ea1287698dbad3b22c59bf25e24cbf330e87b7f6 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 16 Jun 2026 11:33:31 -0400 Subject: [PATCH 2/5] fix: ignore errors when data channel is closed --- src/room/RTCEngine.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 2df9b74b20..5a34f0e68a 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -1054,6 +1054,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit }; private handleDataError = (event: Event) => { + // Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as + // the peer connection closes) carry no actionable information — the channel is going away + // regardless. Suppress them so a graceful disconnect doesn't surface spurious errors. + // See livekit/client-sdk-js#1953. + if (this._isClosed) { + return; + } + const channel = event.currentTarget as RTCDataChannel; const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable'; From 3678e7b872344d435bcbd5330c96ab8eaf0f2e99 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 16 Jun 2026 11:36:14 -0400 Subject: [PATCH 3/5] fix: unify detachHandlers / dcCleanup again, but run close after removing handlers So now the order is: 1. Detach handlers 2. Run dc.close() 3. After 1+2 have run for all data channels, THEN call pcManager.close() --- src/room/RTCEngine.ts | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 5a34f0e68a..d31de88251 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -456,33 +456,24 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit } async cleanupPeerConnections() { - // Detach the data channel handlers before closing the peer connections. Closing a peer - // connection tears down the SCTP transport, which can dispatch `error`/`close` events on - // the still-open data channels; if our handlers are still attached at that point, - // handleDataError logs a spurious "Unknown DataChannel error" during an otherwise graceful - // disconnect. Detaching first makes this deterministic regardless of how/when the browser - // dispatches those teardown events. See livekit/client-sdk-js#1953. - const detachHandlers = (dc: RTCDataChannel | undefined) => { - if (!dc) return; + const dcCleanup = (dc: RTCDataChannel | undefined) => { + if (!dc) { + return; + } + + // Detach the data channel handlers before closing anything. Closing a peer connection tears + // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data + // channels; if our handlers are still attached at that point, handleDataError logs a spurious + // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers + // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the + // browser dispatches those teardown events. See livekit/client-sdk-js#1953. dc.onbufferedamountlow = null; dc.onclose = null; dc.onclosing = null; dc.onerror = null; dc.onmessage = null; dc.onopen = null; - }; - detachHandlers(this.lossyDC); - detachHandlers(this.lossyDCSub); - detachHandlers(this.reliableDC); - detachHandlers(this.reliableDCSub); - detachHandlers(this.dataTrackDC); - detachHandlers(this.dataTrackDCSub); - - await this.pcManager?.close(); - this.pcManager = undefined; - const dcCleanup = (dc: RTCDataChannel | undefined) => { - if (!dc) return; dc.close(); }; dcCleanup(this.lossyDC); @@ -492,6 +483,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit dcCleanup(this.dataTrackDC); dcCleanup(this.dataTrackDCSub); + await this.pcManager?.close(); + this.pcManager = undefined; + this.lossyDC = undefined; this.lossyDCSub = undefined; this.reliableDC = undefined; From 82f192a15c166997920194db948cd4dd96d53f34 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 16 Jun 2026 11:41:40 -0400 Subject: [PATCH 4/5] fix: address issue with ErrorEvent being checked when RTCErrorEvent is actually what should be checked --- src/room/RTCEngine.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index d31de88251..6dae5cfab8 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -1059,9 +1059,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit const channel = event.currentTarget as RTCDataChannel; const channelKind = channel.maxRetransmits === 0 ? 'lossy' : 'reliable'; - if (event instanceof ErrorEvent && event.error) { - const { error } = event.error; - this.log.error(`DataChannel error on ${channelKind}: ${event.message}`, { error }); + if (typeof RTCErrorEvent !== 'undefined' && event instanceof RTCErrorEvent && event.error) { + const { error } = event; + this.log.error(`DataChannel error on ${channelKind}: ${error.message}`, { + error, + errorDetail: error.errorDetail, + sctpCauseCode: error.sctpCauseCode, + }); } else { this.log.error(`Unknown DataChannel error on ${channelKind}`, { event }); } From 8db768ca4c395e96b0e63da065437ffcaeea9626 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 17 Jun 2026 09:23:35 -0400 Subject: [PATCH 5/5] fix: add missing changeset --- .changeset/gold-candies-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gold-candies-switch.md diff --git a/.changeset/gold-candies-switch.md b/.changeset/gold-candies-switch.md new file mode 100644 index 0000000000..d47421233b --- /dev/null +++ b/.changeset/gold-candies-switch.md @@ -0,0 +1,5 @@ +--- +'livekit-client': patch +--- + +Fix data channel close race condition