zeroclaw-labs/zeroclaw · error · anyhow::Error
QQ WebSocket connection closed: write failed
Error message
QQ WebSocket connection closed: write failed
What it means
Raised by QQChannel::listen when writing to the WebSocket fails (ExitReason::WriteFailed) — the outbound sink rejected a heartbeat Message::Text, a Pong reply to a server Ping, or another frame. A WARN is logged ('WebSocket write failed; resume will be attempted on reconnect') before the bail. It indicates the send side of the connection is broken even though reads may not have noticed yet.
Source
Thrown at crates/zeroclaw-channels/src/qq.rs:1822
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown)
.with_attrs(::serde_json::json!({"MAX_MISSED_ACKS": MAX_MISSED_ACKS})),
"heartbeat timeout after consecutive missed ACKs; resume will be attempted on reconnect"
);
anyhow::bail!(
"QQ WebSocket connection closed: heartbeat ACK timeout \
({MAX_MISSED_ACKS} consecutive missed ACKs)"
)
}
ExitReason::WriteFailed => {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_outcome(::zeroclaw_log::EventOutcome::Unknown),
"WebSocket write failed; resume will be attempted on reconnect"
);
anyhow::bail!("QQ WebSocket connection closed: write failed")
}
ExitReason::ChannelClosed => {
anyhow::bail!("QQ WebSocket connection closed: internal message channel closed")
}
}
}
async fn health_check(&self) -> bool {
self.fetch_access_token_with_retry().await.is_ok()
}
async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {
// No typing-indicator API on the QQ Bot Open Platform.
Ok(())
}
async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {
Ok(())View on GitHub (pinned to 88bb9c8533)
Solutions
- No intervention needed per occurrence — the supervisor reconnects and Resume is attempted
- If it appears in bursts, look for an intermediary (reverse proxy, VPN) killing long-lived WebSocket connections
- Correlate with the heartbeat-timeout and close-frame errors to find who is dropping the connection first
Defensive patterns
Strategy: retry
Try / catch
if let Err(err) = channel.listen(&tx).await {
if format!("{err:#}").contains("write failed") {
tokio::time::sleep(backoff.next()).await;
continue; // send side broke; reconnect and Resume
}
return Err(err);
} Prevention
- Correlate with close-frame and heartbeat-timeout errors to find which side drops the connection first
- Check intermediary WebSocket idle/timeout policies when write failures cluster
- Reconnect promptly — a broken write side usually precedes a broken read side
When it happens
Trigger: write.send(...) returns Err while answering a Ping with Pong (qq.rs:1523) or sending the op 1 heartbeat (qq.rs:1509): connection reset, TLS session torn down, buffer overflow after the peer stopped reading, or socket closed by an intermediary.
Common situations: Server or proxy closing the connection a moment before the client writes; network interface loss mid-write; long GC-like stalls in the host process causing the peer to give up and reset.
Understand the failure class
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- QQ WebSocket connection closed: stream ended unexpectedly
- QQ WebSocket connection closed: heartbeat ACK timeout ({MAX_
- QQ gateway request failed ({status}): {err}
- QQ WebSocket connection closed: invalid session (fresh auth
- QQ WebSocket connection closed: server requested reconnect (
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/fc2930defc867b7d.
Report an issue: GitHub.