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
- Reconnect via the connect command/function to re-establish the WebSocket and retry the operation.
- Verify Chrome is still running and the remote debugging port is alive (curl http://localhost:9222/json/version).
- Re-launch Chrome with --remote-debugging-port and retry the navigation/screenshot command.
- 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
- Verify Chrome is running with --remote-debugging-port before automating
- Ping /json/version between long operations to detect dead sessions
- Avoid operating on tabs that will close or navigate away mid-command
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
- WebSocket closed by Chrome
- WebSocket connect to
- No debuggable page found on port
- CDP error
- No screenshot data in response
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)