From 9f7b80436cd6cdec4b44fc980f39c7881662334f Mon Sep 17 00:00:00 2001 From: Arkadiusz Grzelka Date: Fri, 10 Jul 2026 21:41:35 +0200 Subject: [PATCH] Don't mask BIG (sync) establishment failure with a KeyError On a BIG or BIG-sync establishment failure the event handler removes the bigs / big_syncs entry and emits ESTABLISHMENT_FAILURE, which makes the awaited future raise hci.HCI_Error. create_big()/create_big_sync() then ran `del self.bigs[big_handle]` / `del self.big_syncs[big_handle]` in their `except hci.HCI_Error` clause on an entry that was already gone, raising `KeyError: 0` and discarding the real HCI status. Pop with a default instead of deleting, matching the idempotent pattern already used in on_big_termination(). The HCI_Error now propagates with its status intact. Fixes #954. --- bumble/device.py | 12 ++++++++++-- tests/device_test.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/bumble/device.py b/bumble/device.py index 7eb5dc15..6d4056c7 100644 --- a/bumble/device.py +++ b/bumble/device.py @@ -5229,7 +5229,11 @@ async def create_big( ) await established except hci.HCI_Error: - del self.bigs[big_handle] + # The establishment-failure event handler may have already + # removed the entry; pop so this cleanup cannot raise KeyError + # and mask the HCI_Error (see on_big_termination for the same + # idempotent pattern). + self.bigs.pop(big_handle, None) raise return big @@ -5278,7 +5282,11 @@ async def create_big_sync( ) await established except hci.HCI_Error: - del self.big_syncs[big_handle] + # The establishment-failure event handler may have already + # removed the entry; pop so this cleanup cannot raise KeyError + # and mask the HCI_Error (see on_big_termination for the same + # idempotent pattern). + self.big_syncs.pop(big_handle, None) raise return big_sync diff --git a/tests/device_test.py b/tests/device_test.py index 7df82802..9e555126 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -30,6 +30,7 @@ Advertisement, AdvertisingEventProperties, AdvertisingParameters, + BigSyncParameters, CigParameters, CisLink, Connection, @@ -881,6 +882,45 @@ async def test_get_remote_classic_features(): ) +# ----------------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_create_big_sync_failure_propagates_hci_error(): + # Regression: on a BIG sync establishment failure the event handler removes + # the big_syncs entry, and create_big_sync's except clause then removed it + # again with `del`, raising KeyError and masking the real HCI status. + device = Device(host=Host(None, None)) + status = HCI_CONNECTION_FAILED_TO_BE_ESTABLISHED_ERROR + + async def fake_send_async_command(command, check_status=True): + # The controller accepted the command; a failure event then arrives and + # its handler removes the entry before create_big_sync's except runs. + big_handle = next(iter(device.big_syncs)) + asyncio.get_running_loop().call_soon( + device.on_big_sync_establishment, + status, + big_handle, + 0, # transport_latency_big + 0, # nse + 0, # bn + 0, # pto + 0, # irc + 0, # max_pdu + 0, # iso_interval + [], # bis_handles + ) + return HCI_SUCCESS + + device.send_async_command = fake_send_async_command # type: ignore[assignment] + pa_sync = mock.Mock(sync_handle=0) + parameters = BigSyncParameters(big_sync_timeout=1000, bis=[1]) + + with pytest.raises(HCI_Error) as exc_info: + await device.create_big_sync(pa_sync, parameters) + + assert exc_info.value.error_code == status + assert device.big_syncs == {} + + # ----------------------------------------------------------------------------- async def run_test_device(): await test_device_connect_parallel()