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

  1. 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).
  2. Validate message completeness at the protocol layer before issuing a fixed-length read_exact.
  3. If you need the partial bytes, switch to manual loop with read() and accumulate, tracking bytes read.
  4. 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

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


AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11). Data as JSON: /api/errors/fd2eda6359c46568. Report an issue: GitHub.