Kuberwastaken/claurst · error

WebSocket closed unexpectedly

Error message

WebSocket closed unexpectedly

What it means

In cdp_call, the WebSocket stream returned None from ws.next(), i.e. the stream ended without a Close frame being surfaced as a message. The code maps that to this error while draining messages to find the response with the matching request id. It means the DevTools WebSocket connection was torn down before Chrome answered the CDP command.

Solutions

  1. Reconnect via the connect command/function to re-establish the WebSocket and retry the operation.
  2. Verify Chrome is still running and the remote debugging port is alive (curl http://localhost:9222/json/version).
  3. Re-launch Chrome with --remote-debugging-port and retry the navigation/screenshot command.
  4. Check whether the target tab was closed; operate on a stable tab instead of one that navigates away.

Example fix

// before
let raw = ws.next().await.ok_or_else(|| anyhow::anyhow!("WebSocket closed unexpectedly"))??;
// after
match ws.next().await {
    Some(Ok(msg)) => { /* continue processing */ }
    Some(Err(e)) => return Err(anyhow::anyhow!("WebSocket error: {}", e)),
    None => return Err(anyhow::anyhow!("WebSocket closed unexpectedly")),
}
Defensive patterns

Strategy: retry

Validate before calling

// check the debug endpoint is alive before sending CDP commands
reqwest::get("http://localhost:9222/json/version").await?;

Try / catch

// reconnect once and retry the CDP call
match cdp_call(&mut ws, method, params).await {
    Err(e) if e.to_string().contains("closed") => {
        ws = connect(ws_url).await?;
        cdp_call(&mut ws, method, params).await
    }
    other => other,
}

Prevention

When it happens

Trigger: During the drain loop in cdp_call, StreamExt::next() yields None: the underlying tungstenite socket was closed by the peer at the transport level, or the connection task ended, before a Text message with the request id arrived.

Common situations: Chrome/Chromium crashed or was killed mid-session; the page/tab navigated or closed, destroying the target; DevTools port (e.g. 9222) proxy dropped the connection; network interruption to the debug endpoint.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/20bae905790f086b. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/commands/src/chrome.rs:70

    // -----------------------------------------------------------------------

    /// Send a CDP method call and wait for the matching response.
    /// Returns the full response object (including `result` / `error`).
    async fn cdp_call(
        ws: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
        method: &str,
        params: Value,
    ) -> anyhow::Result<Value> {
        let id = next_id();
        let request = json!({ "id": id, "method": method, "params": params });
        ws.send(WsMessage::Text(request.to_string())).await?;

        // Drain messages until we get the one with our id (ignore events).
        loop {
            let raw = ws
                .next()
                .await
                .ok_or_else(|| anyhow::anyhow!("WebSocket closed unexpectedly"))??;
            let text: String = match raw {
                WsMessage::Text(t) => t.to_string(),
                WsMessage::Ping(_) | WsMessage::Pong(_) => continue,
                WsMessage::Close(_) => {
                    return Err(anyhow::anyhow!("WebSocket closed by Chrome"));
                }
                _ => continue,
            };
            let val: Value = serde_json::from_str(&text)?;
            if val["id"] == id {
                if let Some(err) = val.get("error") {
                    return Err(anyhow::anyhow!("CDP error: {}", err));
                }
                return Ok(val);
            }
            // It's an event or different response — keep waiting.
        }
    }

View on GitHub (pinned to b0637c97ec)