{"record":{"id":"bd18e2d3b1be2085","repo":"zeroclaw-labs/zeroclaw","slug":"websocket-stream-ended","errorCode":null,"errorMessage":"WebSocket stream ended","messagePattern":"WebSocket stream ended","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/dingtalk.rs","lineNumber":400,"sourceCode":"\n                    if tx.send(channel_msg).await.is_err() {\n                        ::zeroclaw_log::record!(\n                            WARN,\n                            ::zeroclaw_log::Event::new(\n                                module_path!(),\n                                ::zeroclaw_log::Action::Note\n                            )\n                            .with_outcome(::zeroclaw_log::EventOutcome::Unknown),\n                            \"message channel closed\"\n                        );\n                        break;\n                    }\n                }\n                _ => {}\n            }\n        }\n\n        anyhow::bail!(\"WebSocket stream ended\")\n    }\n\n    async fn health_check(&self) -> bool {\n        self.register_connection().await.is_ok()\n    }\n\n    async fn start_typing(&self, _recipient: &str) -> anyhow::Result<()> {\n        // No typing-indicator API in the DingTalk Open Platform.\n        Ok(())\n    }\n\n    async fn stop_typing(&self, _recipient: &str) -> anyhow::Result<()> {\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/dingtalk.rs#L382-L418","documentation":"DingTalkChannel::listen ends every WebSocket read-loop exit with this bail: a Close frame from the server, a protocol/IO error, a failed pong send, or the internal mpsc sender closing all break the loop and fall through to this line. It is a lifecycle signal (\"stream is gone, reconnect\"), not the fault description — the underlying WebSocket error is logged separately at WARN just before. DingTalk's stream gateway routinely closes long-lived connections, so a supervisor should expect this error periodically.","triggerScenarios":"Server-side close after a missed SYSTEM ping/pong exchange (GC pause, blocked network); TCP reset from network drops or NAT idle timeouts; proxy killing idle connections; failed pong write breaking the loop; process shutdown closing the mpsc receiver.","commonSituations":"Bots behind proxies with short idle timeouts; event bursts stalling the pong loop; laptop-sleep interruptions; deployments that called listen() once with no reconnect handling.","solutions":["Wrap listen() in a reconnect supervisor with exponential backoff — each iteration re-registers via register_connection and reconnects","Check the preceding WARN logs (\"WebSocket error\", \"failed to send pong\") for the real cause","If proxies kill idle sockets, keep the ping/pong path responsive or raise the proxy idle timeout","Distinguish shutdown (tx closed) from server disconnect so exit does not trigger reconnect storms"],"exampleFix":"// before\nchannel.listen(tx).await?;\n\n// after: reconnect loop with backoff\nlet mut backoff = backoff::ExponentialBackoff::default();\nloop {\n    if let Err(e) = channel.listen(tx.clone()).await {\n        tracing::warn!(\"dingtalk stream ended: {e}; reconnecting\");\n    }\n    tokio::time::sleep(backoff.next()).await;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut delay = Duration::from_secs(1);\nloop {\n    let _ = channel.listen(tx.clone()).await; // ends with \"WebSocket stream ended\"\n    tokio::time::sleep(delay).await;\n    delay = (delay * 2).min(Duration::from_secs(60)); // capped backoff, then re-listen\n}","preventionTips":["Assume the stream will end: build a reconnect supervisor, not a one-shot listen","Read the WARN WebSocket logs preceding the bail — they carry the real cause","Cap and jitter backoff to avoid hammering the gateway after outages"],"tags":["dingtalk","websocket","reconnect","lifecycle"],"backgroundTag":"websocket-disconnect","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}