zeroclaw-labs/zeroclaw · error · anyhow::Error

Mattermost WebSocket stream ended

Error message

Mattermost WebSocket stream ended

What it means

While listening, the WebSocket frame stream returned `None` — the connection ended without a Close frame. The listener surfaces this as an error so the caller (usually the runtime's supervisor loop) can reconnect. It is the classic silent connection drop during an established session.

Source

Thrown at crates/zeroclaw-channels/src/mattermost.rs:1059

                }
                _ = ping_interval.tick() => {
                    let ping = serde_json::json!({"seq": seq, "action": "ping"});
                    write
                        .send(WsMessage::Text(ping.to_string().into()))
                        .await
                        .context("Mattermost WebSocket ping send failed")?;
                    seq = seq.wrapping_add(1);
                }
                frame = read.next() => {
                    let frame = match frame {
                        Some(Ok(frame)) => {
                            last_frame = tokio::time::Instant::now();
                            frame
                        }
                        Some(Err(error)) => {
                            return Err(error).context("Mattermost WebSocket read failed");
                        }
                        None => bail!("Mattermost WebSocket stream ended"),
                    };

                    let text = match frame {
                        WsMessage::Text(text) => text,
                        WsMessage::Ping(payload) => {
                            write
                                .send(WsMessage::Pong(payload))
                                .await
                                .context("Mattermost WebSocket pong send failed")?;
                            continue;
                        }
                        WsMessage::Close(frame) => {
                            let reason = frame
                                .as_ref()
                                .map(|frame| frame.reason.as_ref())
                                .unwrap_or("");
                            bail!("Mattermost WebSocket closed: {reason}");
                        }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Wrap `listen()` in a reconnect loop with exponential backoff — the runtime normally restarts channel listeners on error
  2. Raise proxy/LB WebSocket idle timeout above Mattermost's ~30s ping interval
  3. Check Mattermost logs for restarts matching the drop time
Defensive patterns

Strategy: retry

Try / catch

loop {
    if let Err(e) = mm_channel.listen(tx.clone()).await {
        let transient = e.to_string().contains("stream ended")
            || e.to_string().contains("WebSocket closed");
        if !transient {
            return Err(e);
        }
        tokio::time::sleep(backoff.next()).await;
    }
}

Prevention

When it happens

Trigger: During an active `listen_websocket` session: network drop, Mattermost restart, or a proxy/LB killing the socket without a close handshake.

Common situations: Long-running bots on flaky networks; server deploys; load-balancer idle timeouts shorter than the server's ping interval.

Related errors


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