zeroclaw-labs/zeroclaw · error

QQ WebSocket connection closed: close_code={code}, reason="{

Error message

QQ WebSocket connection closed: close_code={code}, reason="{reason}"

What it means

Raised by QQChannel::listen when the WebSocket read loop receives a Close frame (ExitReason::Close). The handler extracts the numeric close code and reason string from the frame ('unknown'/'none' if the frame is absent) and emits a WARN log before bailing, so the tuple of (code, reason) is available in both the structured log and the error message. Recovery follows the reconnect path — Resume is attempted on the next listen() since session state is kept.

Source

Thrown at crates/zeroclaw-channels/src/qq.rs:1789

                *self.session_id.write().await = None;
                *self.last_sequence.write().await = None;
                anyhow::bail!(
                    "QQ WebSocket connection closed: invalid session (fresh auth required)"
                )
            }
            ExitReason::Reconnect => {
                // Session state preserved — supervisor will reconnect and we'll attempt Resume
                anyhow::bail!(
                    "QQ WebSocket connection closed: server requested reconnect (resume will be attempted)"
                )
            }
            ExitReason::Close(ref frame) => {
                let (code, reason) = frame
                    .as_ref()
                    .map(|f| (f.code.to_string(), f.reason.to_string()))
                    .unwrap_or_else(|| ("unknown".into(), "none".into()));
                ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Unknown).with_attrs(::serde_json::json!({"code": code.to_string(), "reason": reason.to_string()})), "WebSocket closed with code=, reason=\"\"; resume will be attempted on reconnect");
                anyhow::bail!(
                    "QQ WebSocket connection closed: close_code={code}, reason=\"{reason}\""
                )
            }
            ExitReason::StreamEnded => {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
                    "WebSocket stream ended unexpectedly; resume will be attempted on reconnect"
                );
                anyhow::bail!("QQ WebSocket connection closed: stream ended unexpectedly")
            }
            ExitReason::HeartbeatTimeout => {
                ::zeroclaw_log::record!(
                    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})),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the close code: 1000/1001 and server-maintenance reasons just need a reconnect; auth codes (4004-style) mean credentials must be fixed first
  2. For auth close codes, verify AppID/AppSecret and token acquisition, then reconnect
  3. Keep the supervisor reconnecting with backoff — Resume preserves the session across most closes
  4. If closes repeat immediately after connect, check for a competing connection with the same credentials or a gateway protocol version change
Defensive patterns

Strategy: try-catch

Try / catch

if let Err(err) = channel.listen(&tx).await {
    let msg = format!("{err:#}");
    if msg.contains("close_code=4004") || msg.contains("close_code=4010") {
        return Err(err); // auth/shard close codes: stop reconnecting until credentials are fixed
    }
    tokio::time::sleep(backoff.next()).await;
    continue; // other codes: reconnect and Resume
}

Prevention

When it happens

Trigger: The QQ gateway closes the socket with a Close frame: authentication close codes (e.g. 4004 invalid token / 4010-4012 shard or version problems in QQ-style gateways), rate-limit closes, or a normal 1000 server shutdown. frame.as_ref() Some => (code, reason); None => 'unknown'/'none'.

Common situations: Invalid or expired QQ bot credentials producing an auth close code right after connecting; gateway maintenance closes; intermediaries (proxies, load balancers) injecting close frames on idle; version mismatch between the client's gateway protocol and the server.

Understand the failure class

Related errors


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