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.
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: heartbeat ACK timeout
- QQ WebSocket connection closed: stream ended unexpectedly
- QQ gateway request failed
- QQ WebSocket connection closed: close_code=
- QQ WebSocket connection closed: invalid session (fresh auth…
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/fc2930defc867b7d.
Report an issue: GitHub.
Appendix: 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)