Fix missing tab storage creation in the On Air handshake
Description
On the On Air transport, app.storage.tab raises AssertionError: tab storage for <tab_id> should be created before accessing it on a page's first visit.
Air._handle_handshake (nicegui/air.py) never calls Storage._create_tab_storage, while the local Socket.IO handler _on_handshake (nicegui/nicegui.py) ends with exactly that call. The only thing that populates Storage._tabs on the Air path is copy_tab, and copy_tab is both conditional on data.get('old_tab_id') being present and a no-op unless the old ID is already in _tabs:
def copy_tab(self, old_tab_id: str, tab_id: str) -> None:
if old_tab_id in self._tabs: # <- silently does nothing otherwise
...So a first-ever Air handshake for a tab leaves _tabs without an entry, and Storage.tab then trips its own assertion:
assert client.tab_id in self._tabs, f'tab storage for {client.tab_id} should be created before accessing it'Spotted by @falkoschindler while reviewing #6279 (finding 4 there), and filed separately at his request because it is not caused by that PR.
Reproduction — the Air handshake sequence, run without the relayI do not have an On Air relay to drive, so this runs the statements Air._handle_handshake executes, in order, for a handshake without a known old_tab_id, and then touches app.storage.tab the way page code would. Everything below is real library code; only the relay frame is stood in for.
async def test_air_handshake_leaves_no_tab_storage(user: User):
@ui.page('/')
def index():
pass
template = await user.open('/')
client = Client(template.page, request=template.request)
# exactly the sequence nicegui/air.py::Air._handle_handshake runs
client.environ = {'QUERY_STRING': f'client_id={client.id}'}
client.tab_id = 'air-tab' # no old_tab_id -> no copy_tab, and no _create_tab_storage anywhere
client.on_air = True
client.handle_handshake('air-sid', 'air-doc', None)
print('tab_id in storage._tabs:', client.tab_id in core.app.storage._tabs)
with client:
app.storage.tab['x'] = 1tab_id in storage._tabs: False
AssertionError: tab storage for air-tab should be created before accessing itThe same sequence through _on_handshake ends in await core.app.storage._create_tab_storage(client.tab_id), so the local transport does not have the problem.
tests/test_air.py does not exercise a handshake through the relay, and every tab-storage test goes through _on_handshake (directly, or via the User/Screen fixtures), which is the arm that does create the storage. So the asymmetry between the two handshake handlers is invisible to the suite.
Possible fix
Await _create_tab_storage(data['tab_id']) in Air._handle_handshake, next to the existing copy_tab call, mirroring _on_handshake. That makes the handler async, which python-socketio's relay client supports. Folding the shared register → copy_tab → tab_id → handle_handshake sequence into one Client method would keep the two transports from drifting again — @falkoschindler raised that as a leave-or-take in #6279 and it was left, so it may be worth taking here instead.
I have not tried either, because I cannot verify a fix on the Air path without a relay.
Source: zauberzeug/nicegui