atuinsh/atuin · error · std::io::Error
eof in the middle of a frame
Error message
eof in the middle of a frame
What it means
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.
Source
Thrown at crates/atuin-pty-proxy/src/protocol.rs:172
"frame exceeds maximum length",
));
}
let mut payload = vec![0u8; len];
reader.read_exact(&mut payload)?;
Ok(Some((frame_type, payload)))
}
/// Fill `buf` completely. Returns `Ok(false)` on EOF before the first byte,
/// `Ok(true)` when full; EOF partway through is an [`io::ErrorKind::UnexpectedEof`].
fn read_exact_or_eof(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<bool> {
let mut filled = 0;
while filled < buf.len() {
match reader.read(&mut buf[filled..]) {
Ok(0) => {
if filled == 0 {
return Ok(false);
}
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"eof in the middle of a frame",
));
}
Ok(n) => filled += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(true)
}
/// Build a complete Subscribe frame.
///
/// # Panics
///
/// Panics if `token` is longer than `u16::MAX` bytes; real tokens are 64
/// ASCII characters.View on GitHub (pinned to 202f6ad98e)
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
Example fix
// before
let frame = read_frame(&mut stream)?.unwrap();
// after
match read_frame(&mut stream) {
Ok(Some(frame)) => { /* handle */ }
Ok(None) => { /* peer closed cleanly at a frame boundary */ }
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
// truncated frame: peer died mid-write; reconnect
}
Err(e) => return Err(e.into()),
} Defensive patterns
Strategy: try-catch
Try / catch
match read_frame(&mut stream) {
Ok(Some(frame)) => { /* handle */ }
Ok(None) => { /* clean EOF at frame boundary: normal shutdown */ }
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
// peer died mid-frame: treat as disconnect, rebuild the session
}
Err(e) => return Err(e.into()),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- frame exceeds maximum length
- Empty theme directory override and could not find theme else
- Failed to deserialize theme: {}
- Parent requested but we hit the recursion limit
- key file vanished immediately after a concurrent write
AI-assisted analysis of atuinsh/atuin@202f6ad98e (2026-08-16).
Data as JSON: /api/errors/3e88a64a7c70aad0.
Report an issue: GitHub.