zeroclaw-labs/zeroclaw · error · anyhow::Error
QQ WebSocket connection closed: internal message channel clo
Error message
QQ WebSocket connection closed: internal message channel closed
What it means
Raised by QQChannel::listen when the internal broadcast channel that carries parsed ChannelMessages to the consumer is closed (ExitReason::ChannelClosed) — tx.send(...) failed at qq.rs:1753 because the receiver was dropped. This is not a network or QQ problem: the downstream consumer (orchestrator/supervisor event loop) shut down while the gateway connection was still healthy. Unlike other exits, there is no WARN log, only the bail.
Source
Thrown at crates/zeroclaw-channels/src/qq.rs:1825
.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
- Treat it as a shutdown signal, not a failure — do not reconnect-loop on it; propagate so the caller can finish terminating
- On graceful shutdown, drop/cancel the listener task at the same time as the consumer so this path is expected and quiet
- If it appears unexpectedly mid-run, find which task owned the receiver and why it exited (check its logs/panics)
Defensive patterns
Strategy: try-catch
Try / catch
if let Err(err) = channel.listen(&tx).await {
if format!("{err:#}").contains("internal message channel closed") {
break; // consumer is gone: treat as shutdown, do NOT reconnect-loop
}
return Err(err);
} Prevention
- Drop the listener task together with the consumer on shutdown so this exit is expected
- Never auto-reconnect on this error — the new listener's messages would have nowhere to go
- Investigate consumer panics/cancellations if this fires outside intentional shutdown
When it happens
Trigger: listen()'s select arm forwards a composed ChannelMessage into tx; the receiving end of the mpsc channel has already been dropped (listener task cancelled, orchestrator shutting down, consumer erroring out), so send returns Err and the loop breaks with ExitReason::ChannelClosed.
Common situations: Graceful bot shutdown where the supervisor stops consuming before the channel task finishes; a consumer task that panicked or was cancelled; reconnect logic dropping the old listener's receiver without draining it.
Understand the failure class
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- QQ WebSocket connection closed: server requested reconnect (
- WebSocket stream ended
- QQ gateway request failed ({status}): {err}
- QQ WebSocket connection closed: invalid session (fresh auth
- QQ WebSocket connection closed: close_code={code}, reason="{
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9bf16e53c8beaeca.
Report an issue: GitHub.