tokio-rs/tokio · error · io::Error
early eof
Error message
early eof
What it means
ReadExact::poll returns this io::ErrorKind::UnexpectedEof (built by the eof() helper) when the underlying AsyncRead signals EOF before the requested buffer is fully filled. read_exact guarantees either a full buffer or an error, so a short read is treated as failure. The number of bytes read so far is consumed and lost to the caller (only the error is returned).
Source
Thrown at tokio/src/io/util/read_exact.rs:44
pin_project! {
/// Creates a future which will read exactly enough bytes to fill `buf`,
/// returning an error if EOF is hit sooner.
///
/// On success the number of bytes is returned
#[derive(Debug)]
#[must_use = "futures do nothing unless you `.await` or poll them"]
pub struct ReadExact<'a, A: ?Sized> {
reader: &'a mut A,
buf: ReadBuf<'a>,
// Make this future `!Unpin` for compatibility with async trait methods.
#[pin]
_pin: PhantomPinned,
}
}
fn eof() -> io::Error {
io::Error::new(io::ErrorKind::UnexpectedEof, "early eof")
}
impl<A> Future for ReadExact<'_, A>
where
A: AsyncRead + Unpin + ?Sized,
{
type Output = io::Result<usize>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<usize>> {
let me = self.project();
loop {
// if our buffer is empty, then we need to read some data to continue.
let rem = me.buf.remaining();
if rem != 0 {
match ready!(Pin::new(&mut *me.reader).poll_read(cx, me.buf)) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,View on GitHub (pinned to 7d0d729d8f)
Solutions
- Treat UnexpectedEof as 'clean close / partial message' and decide whether the partial bytes are recoverable (use read instead of read_exact if short reads are acceptable).
- Validate message completeness at the protocol layer before issuing a fixed-length read_exact.
- If you need the partial bytes, switch to manual loop with read() and accumulate, tracking bytes read.
- Confirm the peer is expected to keep the connection open for the full frame; add a keepalive/protocol-level handshake.
Example fix
// before
let mut hdr = [0u8; 8];
reader.read_exact(&mut hdr).await?; // fails on short msg
// after
let mut hdr = [0u8; 8];
match reader.read_exact(&mut hdr).await {
Ok(_) => Ok(hdr),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
// peer closed before full header; treat as end of stream
Err(anyhow::anyhow!("connection closed mid-frame"))
}
Err(e) => Err(e.into()),
} Defensive patterns
Strategy: try-catch
Validate before calling
// If short reads are acceptable, prefer read() over read_exact() and accumulate:
let mut filled = 0;
while filled < buf.len() {
match reader.read(&mut buf[filled..]).await? {
0 => break, // EOF
n => filled += n,
}
} Type guard
fn is_unexpected_eof(e: &io::Error) -> bool {
e.kind() == io::ErrorKind::UnexpectedEof
} Try / catch
match reader.read_exact(&mut buf).await {
Ok(_) => Ok(buf),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
Err(anyhow::anyhow!("peer closed before frame complete"))
}
Err(e) => Err(e.into()),
} Prevention
- Use read (not read_exact) when partial data is acceptable.
- Frame messages with an explicit length prefix so peers always send complete units.
- Add a protocol-level heartbeat so unexpected EOFs are detected quickly.
- Track bytes read manually if you need the partial payload after EOF.
When it happens
Trigger: Calling AsyncReadExt::read_exact(&mut buf[..N]) (or read_buf_exact) on a reader that returns 0 bytes (EOF) before N bytes have been read. Also reachable via any future built on read_exact_internal such as reading fixed-length protocol frames.
Common situations: Reading a length-prefixed header where the peer sent fewer bytes then closed the connection; truncated file reads; a stream that hit EOF mid-record; network peer that sent a partial message then RST'd. Extremely common in protocol decoders.
Related errors
- early eof
- stream did not contain valid UTF-8
- failed to write the buffered data
- write zero byte into writer
- utf-8 error (io::Error::new(io::ErrorKind::InvalidData, err)
AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11).
Data as JSON: /api/errors/fd2eda6359c46568.
Report an issue: GitHub.