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
- Verify the peer sends complete frames and closes cleanly.
- If trailing partial bytes are acceptable, override `decode_eof` in your `Decoder` to return `Ok(None)` instead of erroring.
- Confirm the codec framing matches the wire protocol (field length, endianness, offsets, delimiters).
- 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
- Confirm the codec framing exactly matches the wire protocol before relying on the default `decode_eof`.
- Override `decode_eof` if trailing partial bytes are acceptable for your protocol.
- Treat peer disconnects mid-frame as expected on untrusted networks.
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
- failed to write frame to transport
- frame size too big
- failed to write entire datagram to socket
- provided length would overflow after adjustment
- Unable to decode input as UTF8
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/840989b3594bab93.
Report an issue: GitHub.