{"record":{"id":"3e88a64a7c70aad0","repo":"atuinsh/atuin","slug":"eof-in-the-middle-of-a-frame","errorCode":null,"errorMessage":"eof in the middle of a frame","messagePattern":"eof in the middle of a frame","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/atuin-pty-proxy/src/protocol.rs","lineNumber":172,"sourceCode":"            \"frame exceeds maximum length\",\n        ));\n    }\n    let mut payload = vec![0u8; len];\n    reader.read_exact(&mut payload)?;\n    Ok(Some((frame_type, payload)))\n}\n\n/// Fill `buf` completely. Returns `Ok(false)` on EOF before the first byte,\n/// `Ok(true)` when full; EOF partway through is an [`io::ErrorKind::UnexpectedEof`].\nfn read_exact_or_eof(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<bool> {\n    let mut filled = 0;\n    while filled < buf.len() {\n        match reader.read(&mut buf[filled..]) {\n            Ok(0) => {\n                if filled == 0 {\n                    return Ok(false);\n                }\n                return Err(io::Error::new(\n                    io::ErrorKind::UnexpectedEof,\n                    \"eof in the middle of a frame\",\n                ));\n            }\n            Ok(n) => filled += n,\n            Err(e) if e.kind() == io::ErrorKind::Interrupted => {}\n            Err(e) => return Err(e),\n        }\n    }\n    Ok(true)\n}\n\n/// Build a complete Subscribe frame.\n///\n/// # Panics\n///\n/// Panics if `token` is longer than `u16::MAX` bytes; real tokens are 64\n/// ASCII characters.","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin-pty-proxy/src/protocol.rs#L154-L190","documentation":"Returned by the read_exact_or_eof helper (and, via std's read_exact, by read_frame's payload read) when the stream delivers at least one byte of the current buffer and then hits EOF before it is full. A clean EOF exactly at a frame boundary returns Ok(None) instead; this error means the peer died or closed the connection partway through writing a frame, leaving a truncated header or payload. Kind is io::ErrorKind::UnexpectedEof.","triggerScenarios":"The peer process crashing between writing a frame header (5 bytes) and its payload; the socket being half-closed after a partial write; the peer's output pipe breaking mid-snapshot (large screen-state frames are the most likely to be interrupted); a proxy in the middle dropping the connection.","commonSituations":"The pty proxy server being killed (OOM, crash, systemctl restart) while streaming screen updates; network disruption to a remote proxy; a peer that shutdown(Send) after a partial write; test harnesses that close the stream after writing N bytes of a longer frame.","solutions":["Treat UnexpectedEof from read_frame as a disconnect: tear down the session and reconnect/restart the peer","Ensure the writer side always writes complete frames atomically (build with encode_frame and write_all, never partial writes)","Check why the peer died: its logs, OOM killer, or restart policy","In tests, write full encoded frames (header + payload) before closing the stream"],"exampleFix":"// before\nlet frame = read_frame(&mut stream)?.unwrap();\n\n// after\nmatch read_frame(&mut stream) {\n    Ok(Some(frame)) => { /* handle */ }\n    Ok(None) => { /* peer closed cleanly at a frame boundary */ }\n    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {\n        // truncated frame: peer died mid-write; reconnect\n    }\n    Err(e) => return Err(e.into()),\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match read_frame(&mut stream) {\n    Ok(Some(frame)) => { /* handle */ }\n    Ok(None) => { /* clean EOF at frame boundary: normal shutdown */ }\n    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {\n        // peer died mid-frame: treat as disconnect, rebuild the session\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Write whole frames atomically: build with encode_frame and use write_all","Never shutdown(Write) mid-frame on the sender side","Distinguish Ok(None) (clean close) from UnexpectedEof (truncated) in handlers","Add restart/reconnect logic around long-lived proxy connections since peers can crash mid-stream"],"tags":["protocol","eof","io","disconnection","rust","atuin","pty"],"backgroundTag":"unexpected-eof","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}