fix(zap-server): race-free client cleanup when client_id is reused

When an extension reuses its client_id across consecutive ZAP connections
(probe followed by connectZap, both with mgr.state.extensionId), the
older websocket's finally clause was unconditionally popping the
client_id from _clients — deleting the new connection's freshly-installed
registration. The extension would then immediately disconnect because the
server-side state no longer had it, hammering reconnect loops.

Now the finally clause only removes the entry when it still points at
the closing websocket. Verified end-to-end: Firefox 1.9.x ZAP-native
connection holds steady; tabs/screenshot/evaluate round-trip in
11/73/5 ms.
This commit is contained in:
2026-05-08 09:54:12 -07:00
parent 0dc6c65566
commit 1ee67a2e13
@@ -550,9 +550,17 @@ class ZapServer:
finally:
cid = self._ws_to_id.pop(websocket, None)
if cid:
self._clients.pop(cid, None)
self._leases.pop(cid, None)
logger.info("zap: client disconnected %s", cid)
existing = self._clients.get(cid)
if existing is not None and existing.ws is websocket:
self._clients.pop(cid, None)
self._leases.pop(cid, None)
logger.info("zap: client disconnected %s", cid)
else:
logger.debug(
"zap: stale ws %s for client %s — newer connection holds the slot",
websocket,
cid,
)
async def _dispatch(self, websocket: Any, msg_type: int, payload: dict) -> None:
if msg_type == MSG_HANDSHAKE: