{"record":{"id":"6ef0d714b2153d01","repo":"asciinema/asciinema","slug":"splitstream-closed","errorCode":null,"errorMessage":"SplitStream closed","messagePattern":"SplitStream closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/forwarder.rs","lineNumber":216,"sourceCode":"\n            _ = &mut ping_timeout => bail!(\"ping timeout\"),\n\n            message = stream.next() => {\n                match message {\n                    Some(Ok(Message::Close(close_frame))) => {\n                        handle_close_frame(close_frame)?;\n                        return Ok(true);\n                    },\n\n                    Some(Ok(Message::Ping(_))) => (),\n\n                    Some(Ok(Message::Pong(_))) => {\n                        ping_timeout = Box::pin(future::pending());\n                    },\n\n                    Some(Ok(msg)) => debug!(\"unexpected message from the server: {msg:?}\"),\n                    Some(Err(e)) => bail!(e),\n                    None => bail!(\"SplitStream closed\")\n                }\n            }\n        }\n    }\n}\n\nasync fn send_with_timeout(\n    sink: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,\n    message: Message,\n) -> anyhow::Result<Result<(), tungstenite::Error>> {\n    time::timeout(SEND_TIMEOUT, sink.send(message))\n        .await\n        .map_err(|_| anyhow!(\"send timeout\"))\n}\n\nfn handle_close_frame(frame: Option<CloseFrame>) -> anyhow::Result<()> {\n    match frame {\n        Some(CloseFrame { code, reason }) => {","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/asciinema/asciinema/blob/77498061982889f68ba1f6e8b666dd8d13246250/src/forwarder.rs#L198-L234","documentation":"handle_socket treats the WebSocket stream ending without a Close frame as fatal: when stream.next() returns None it bails with \"SplitStream closed\". A properly closed connection should deliver Message::Close first; hitting EOF means the peer (or transport) terminated abruptly.","triggerScenarios":"The remote end drops the TCP connection without a WebSocket Close handshake while connect_and_forward is streaming — server kill -9, network outage, LB idle eviction — so the split stream yields None.","commonSituations":"Long uploads interrupted by network drops; container/server restarts; load balancers that cut connections at idle timeout without a graceful close; MTU issues truncating frames.","solutions":["Retry the stream/upload once connectivity is restored (handle_socket's caller handles reconnection)","Check server/proxy logs for abrupt disconnects and raise idle timeouts","If reproducible locally, test the server with a WebSocket client (wscat) to confirm it closes gracefully"],"exampleFix":"// before\nNone => bail!(\"SplitStream closed\"),\n// after (caller side)\nmatch forward().await { Err(e) if e.to_string().contains(\"SplitStream closed\") => reconnect_and_resume(), other => other }","handlingStrategy":"retry","validationCode":"// preflight TCP+TLS reachability to reduce abrupt EOF risk\nTcpStream::connect_timeout(&addr, Duration::from_secs(5))\n    .context(\"server not reachable, aborting before stream\")?;","typeGuard":null,"tryCatchPattern":"match forward().await {\n    Err(e) if e.to_string().contains(\"SplitStream closed\") => {\n        warn!(\"server dropped connection; reconnecting\");\n        reconnect().await\n    }\n    other => other,\n}","preventionTips":["Configure LBs/proxies to close WebSockets gracefully with a Close frame","Raise idle eviction timeouts beyond the session length","Retry/resume uploads rather than treating EOF as fatal"],"tags":["network","websocket","eof"],"backgroundTag":"websocket-abrupt-disconnect","analyzedSha":"77498061982889f68ba1f6e8b666dd8d13246250","analyzedAt":"2026-09-03T06:12:25.119Z","contentChangedAt":"2026-09-03T06:12:25.119Z","schemaVersion":2},"datasetVersion":"2026-09-10T12:17:11.382Z"}