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

  1. No intervention needed per occurrence — the supervisor reconnects and Resume is attempted
  2. If it appears in bursts, look for an intermediary (reverse proxy, VPN) killing long-lived WebSocket connections
  3. 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

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

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/fc2930defc867b7d. Report an issue: GitHub.