Skip to content

Commit 2dbb047

Browse files
tmkarthiclaude
andcommitted
fix(rtc): avoid KeyError in local_track_unpublished handler during teardown
Room._on_room_event did an unchecked `self.local_participant.track_publications[sid]` lookup on the `local_track_unpublished` branch. LocalParticipant.unpublish_track removes the publication from `_track_publications` when its FFI async response is processed, and that response races the `local_track_unpublished` room event. When the response is handled first, the SID is already gone and the handler raises KeyError, which _listen_task logs as a spurious ERROR with a full traceback on every affected disconnect cleanup. Pop the publication defensively in the handler and only emit when it is still tracked, mirroring the remote `track_unpublished` and `local_track_republished` handlers. This also folds removal into the handler, fixing a latent leak where a server-forced unpublish (no unpublish_track call) left the publication in the dict. Because the handler now owns removal, the common (non-racing) ordering is that the room event is processed before unpublish_track's pop. Make unpublish_track's pop tolerant (`pop(track_sid, None)` + guarded `_track = None`) so it does not raise a new KeyError once the handler has already removed the publication. Fixes #681 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b15518a commit 2dbb047

2 files changed

Lines changed: 20 additions & 4 deletions

File tree

livekit-rtc/livekit/rtc/participant.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -799,8 +799,12 @@ async def unpublish_track(self, track_sid: str) -> None:
799799
if cb.unpublish_track.error:
800800
raise UnpublishTrackError(cb.unpublish_track.error)
801801

802-
publication = self._track_publications.pop(track_sid)
803-
publication._track = None
802+
# The local_track_unpublished room event may have already removed
803+
# this publication from the dict (the FFI event and this async
804+
# response race during teardown), so pop defensively.
805+
publication = self._track_publications.pop(track_sid, None)
806+
if publication is not None:
807+
publication._track = None
804808
queue.task_done()
805809
finally:
806810
self._room_queue.unsubscribe(queue)

livekit-rtc/livekit/rtc/room.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,9 +735,21 @@ def _on_room_event(self, event: proto_room.RoomEvent) -> None:
735735
ltrack = lpublication.track
736736
self.emit("local_track_published", lpublication, ltrack)
737737
elif which == "local_track_unpublished":
738+
# During teardown the publication may already have been removed
739+
# from the participant's dict by LocalParticipant.unpublish_track
740+
# (or by a previously processed event), so the SID can be gone by
741+
# the time this event is dispatched. Pop defensively and skip the
742+
# emit when it is no longer tracked, mirroring the remote
743+
# track_unpublished and local_track_republished handlers, instead
744+
# of raising a KeyError that _listen_task logs as an error.
738745
sid = event.local_track_unpublished.publication_sid
739-
lpublication = self.local_participant.track_publications[sid]
740-
self.emit("local_track_unpublished", lpublication)
746+
lpublication = self.local_participant._track_publications.pop(sid, None)
747+
if lpublication is not None:
748+
self.emit("local_track_unpublished", lpublication)
749+
else:
750+
logging.debug(
751+
"local_track_unpublished for untracked publication sid %s", sid
752+
)
741753
elif which == "local_track_republished":
742754
# The SDK auto-republished a local track during a full
743755
# reconnect: the underlying Track (and its bound source) is

0 commit comments

Comments
 (0)