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

  1. Treat UnexpectedEof from read_frame as a disconnect: tear down the session and reconnect/restart the peer
  2. Ensure the writer side always writes complete frames atomically (build with encode_frame and write_all, never partial writes)
  3. Check why the peer died: its logs, OOM killer, or restart policy
  4. 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

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


AI-assisted analysis of atuinsh/atuin@202f6ad98e (2026-08-16). Data as JSON: /api/errors/3e88a64a7c70aad0. Report an issue: GitHub.