tokio-rs/tokio · error · io::Error

bytes remaining on stream

Error message

bytes remaining on stream

What it means

Runtime error from the default `Decoder::decode_eof` in tokio-util. When the stream reaches EOF and `decode` returns `Ok(None)` (no full frame) but the buffer still holds unconsumed bytes, this error is returned. It signals a truncated/partial frame at end of stream — a protocol framing mismatch or a peer that disconnected mid-message.

Source

Thrown at tokio-util/src/codec/decoder.rs:151

    /// frames _across_ eof boundaries on sources that can be resumed.
    ///
    /// Note that the `buf` argument may be empty. If a previous call to
    /// `decode_eof` consumed all the bytes in the buffer, `decode_eof` will be
    /// called again until it returns `None`, indicating that there are no more
    /// frames to yield. This behavior enables returning finalization frames
    /// that may not be based on inbound data.
    ///
    /// Once `None` has been returned, `decode_eof` won't be called again until
    /// an attempt to resume the stream has been made, where the underlying stream
    /// actually returned more data.
    fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
        match self.decode(buf)? {
            Some(frame) => Ok(Some(frame)),
            None => {
                if buf.is_empty() {
                    Ok(None)
                } else {
                    Err(io::Error::new(io::ErrorKind::Other, "bytes remaining on stream").into())
                }
            }
        }
    }

    /// Provides a [`Stream`] and [`Sink`] interface for reading and writing to this
    /// `Io` object, using `Decode` and `Encode` to read and write the raw data.
    ///
    /// Raw I/O objects work with byte sequences, but higher-level code usually
    /// wants to batch these into meaningful chunks, called "frames". This
    /// method layers framing on top of an I/O object, by using the `Codec`
    /// traits to handle encoding and decoding of messages frames. Note that
    /// the incoming and outgoing frame types may be distinct.
    ///
    /// This function returns a *single* object that is both `Stream` and
    /// `Sink`; grouping this into a single object is often useful for layering
    /// things like gzip or TLS, which require both read and write access to the
    /// underlying object.

View on GitHub (pinned to 625954f365)

Solutions

  1. Verify the peer sends complete frames and closes cleanly.
  2. If trailing partial bytes are acceptable, override `decode_eof` in your `Decoder` to return `Ok(None)` instead of erroring.
  3. Confirm the codec framing matches the wire protocol (field length, endianness, offsets, delimiters).
  4. Handle the `io::Error` (kind `Other`) on the stream and decide whether to log, discard, or reconnect.

Example fix

// before: relying on default decode_eof, partial frame aborts the stream
impl Decoder for MyCodec {
    type Item = Frame; type Error = io::Error;
    fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Frame>, io::Error> { /* ... */ }
}

// after: tolerate trailing partial bytes at EOF
impl Decoder for MyCodec {
    type Item = Frame; type Error = io::Error;
    fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Frame>, io::Error> { /* ... */ }
    fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Frame>, io::Error> {
        if buf.is_empty() { Ok(None) } else { /* drain/return last partial, or */ Ok(None) }
    }
}
Defensive patterns

Strategy: try-catch

Type guard

// Narrow the partial-frame-at-EOF error
fn is_bytes_remaining(e: &io::Error) -> bool {
    e.kind() == io::ErrorKind::Other && e.to_string() == "bytes remaining on stream"
}

Try / catch

while let Some(item) = framed.next().await {
    match item {
        Ok(frame) => handle(frame),
        Err(e) if is_bytes_remaining(&e) => { /* partial frame at EOF: log and stop */ break; }
        Err(e) => return Err(e.into()),
    }
}

Prevention

When it happens

Trigger: A `FramedRead`/`Decoder` reaches EOF with leftover bytes that do not form a complete frame: e.g. a length-delimited stream whose last header promised more bytes than arrived, or a custom codec with a partial frame buffered at EOF.

Common situations: Peer closed the TCP connection mid-message; corrupt/truncated stream; codec framing (length-field size, endianness, delimiter) not matching the wire protocol; reading a file that was cut off; `LinesCodec` overrides `decode_eof` so this does not fire for plain lines.

Related errors


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/840989b3594bab93. Report an issue: GitHub.