From 523a5f97a860daf3af5dd6bb0f4dc4fa548a8bdc Mon Sep 17 00:00:00 2001 From: Faisal Fehad Date: Mon, 1 Jun 2026 15:46:30 +0100 Subject: [PATCH 1/2] fix: suppress CDP error -32000 to prevent session corruption When Browser.getWindowForTarget returns error -32000 ('Browser window not found'), the CDP client raises RuntimeError which crashes the event handler and corrupts the browser session. This occurs during race conditions when a target exists in CDP but doesn't yet have an associated window (e.g., during file uploads, page transitions, or new tab creation). The fix returns a default window result instead of raising an exception, allowing the session to continue normally. Other CDP errors are still raised as before. Root cause analysis: - _on_target_info_changed fires when a new target is discovered - _trigger_page_target_discovered calls getWindowForTarget for the new target - The target doesn't have a window yet, so CDP returns -32000 - The unhandled RuntimeError propagates up and corrupts the session - All subsequent commands fail with 'Browser window not found' Verified: file uploads, page navigation, and wait stable all work after this fix. --- cdp_use/client.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/cdp_use/client.py b/cdp_use/client.py index 893215d..b4f5e46 100644 --- a/cdp_use/client.py +++ b/cdp_use/client.py @@ -315,10 +315,36 @@ async def _handle_messages(self): # Check if future is already done to avoid InvalidStateError if not future.done(): if "error" in data: - logger.debug( - f"CDP Error for request {data['id']}: {data['error']}" - ) - future.set_exception(RuntimeError(data["error"])) + error = data["error"] + # Suppress CDP error -32000 "Browser window not found". + # This error occurs during race conditions when getWindowForTarget + # is called for a target that doesn't yet have an associated window + # (e.g., during file uploads, page transitions, or new tab creation). + # The target exists in CDP but has no window binding yet, so + # Browser.getWindowForTarget returns -32000. Returning a default + # result prevents the event handler from crashing and corrupting + # the session. + if isinstance(error, dict) and error.get("code") == -32000: + logger.info( + f"CDP error {error.get('code')} suppressed for request " + f"{data['id']}: {error.get('message', 'unknown')} - " + f"returning default window result" + ) + future.set_result({ + "windowId": 0, + "bounds": { + "left": 0, + "top": 0, + "width": 1920, + "height": 1080, + "windowState": "normal", + }, + }) + else: + logger.debug( + f"CDP Error for request {data['id']}: {error}" + ) + future.set_exception(RuntimeError(error)) else: future.set_result(data["result"]) else: From 1e7577e247eafeda474ad0d12ced636fada19fb1 Mon Sep 17 00:00:00 2001 From: Faisal Fehad Date: Mon, 1 Jun 2026 21:55:50 +0100 Subject: [PATCH 2/2] Scope -32000 error suppression to Browser.getWindowForTarget only Track the CDP method name alongside each pending request future so that error -32000 suppression only applies to Browser.getWindowForTarget. Previously, any CDP method returning -32000 would receive a fabricated window-shaped result, silently corrupting downstream logic for unrelated methods. Changes: - pending_requests now stores (future, method) tuples instead of bare futures - send_raw stores the method name with the future - _handle_messages unpacks (future, request_method) and checks request_method == 'Browser.getWindowForTarget' before suppressing -32000 - Exception handlers updated to unpack (future, _) tuples --- cdp_use/client.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/cdp_use/client.py b/cdp_use/client.py index b4f5e46..80dd02e 100644 --- a/cdp_use/client.py +++ b/cdp_use/client.py @@ -238,7 +238,7 @@ def __init__( self.max_ws_frame_size = max_ws_frame_size self.ws: Optional[websockets.ClientConnection] = None self.msg_id: int = 0 - self.pending_requests: Dict[int, asyncio.Future] = {} + self.pending_requests: Dict[int, tuple[asyncio.Future, str]] = {} self._message_handler_task = None # Initialize the type-safe CDP library @@ -311,23 +311,30 @@ async def _handle_messages(self): # Handle response messages (with id) if "id" in data and data["id"] in self.pending_requests: - future = self.pending_requests.pop(data["id"]) + future, request_method = self.pending_requests.pop(data["id"]) # Check if future is already done to avoid InvalidStateError if not future.done(): if "error" in data: error = data["error"] - # Suppress CDP error -32000 "Browser window not found". - # This error occurs during race conditions when getWindowForTarget - # is called for a target that doesn't yet have an associated window - # (e.g., during file uploads, page transitions, or new tab creation). - # The target exists in CDP but has no window binding yet, so - # Browser.getWindowForTarget returns -32000. Returning a default - # result prevents the event handler from crashing and corrupting - # the session. - if isinstance(error, dict) and error.get("code") == -32000: + # Suppress CDP error -32000 "Browser window not found" + # only for Browser.getWindowForTarget. + # This error occurs during race conditions when + # getWindowForTarget is called for a target that doesn't + # yet have an associated window (e.g., during file uploads, + # page transitions, or new tab creation). The target exists + # in CDP but has no window binding yet, so + # Browser.getWindowForTarget returns -32000. Returning a + # default result prevents the event handler from crashing and + # corrupting the session. + if ( + isinstance(error, dict) + and error.get("code") == -32000 + and request_method == "Browser.getWindowForTarget" + ): logger.info( - f"CDP error {error.get('code')} suppressed for request " - f"{data['id']}: {error.get('message', 'unknown')} - " + f"CDP error {error.get('code')} suppressed for " + f"Browser.getWindowForTarget request {data['id']}: " + f"{error.get('message', 'unknown')} - " f"returning default window result" ) future.set_result({ @@ -372,14 +379,14 @@ async def _handle_messages(self): except websockets.exceptions.ConnectionClosed as e: logger.debug(f"WebSocket connection closed: {e}") # Connection closed, resolve all pending futures with an exception - for future in self.pending_requests.values(): + for future, _ in self.pending_requests.values(): if not future.done(): future.set_exception(ConnectionError("WebSocket connection closed")) self.pending_requests.clear() except Exception as e: logger.error(f"Error in message handler: {e}") # Handle other exceptions - for future in self.pending_requests.values(): + for future, _ in self.pending_requests.values(): if not future.done(): future.set_exception(e) self.pending_requests.clear() @@ -405,9 +412,9 @@ async def send_raw( if session_id: msg["sessionId"] = session_id - # Create a future for this request + # Create a future for this request, storing method for error handling future = asyncio.Future() - self.pending_requests[self.msg_id] = future + self.pending_requests[self.msg_id] = (future, method) await self.ws.send(json.dumps(msg))