hyperium/hyper · error · std::io::Error

unexpected EOF during chunk size line

Error message

unexpected EOF during chunk size line

What it means

Thrown in the HTTP/1 chunked decoder via the byte! macro (src/proto/h1/decode.rs:258) as io::Error::new(UnexpectedEof, "unexpected EOF during chunk size line"). It fires when the connection EOFs while the parser is still reading the hex chunk-size line of a Transfer-Encoding: chunked body — i.e. the chunked stream was truncated before the next size token was complete.

Source

Thrown at src/proto/h1/decode.rs:258

    #[cfg(test)]
    async fn decode_fut<R: MemRead>(&mut self, body: &mut R) -> Result<Frame<Bytes>, io::Error> {
        futures_util::future::poll_fn(move |cx| self.decode(cx, body)).await
    }
}

impl fmt::Debug for Decoder {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&self.kind, f)
    }
}

macro_rules! byte (
    ($rdr:ident, $cx:expr) => ({
        let buf = ready!($rdr.read_mem($cx, 1))?;
        if !buf.is_empty() {
            buf[0]
        } else {
            return Poll::Ready(Err(io::Error::new(io::ErrorKind::UnexpectedEof,
                                      "unexpected EOF during chunk size line")));
        }
    })
);

macro_rules! or_overflow {
    ($e:expr) => (
        match $e {
            Some(val) => val,
            None => return Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid chunk size: overflow",
            ))),
        }
    )
}

macro_rules! put_u8 {

View on GitHub (pinned to 084473f728)

Solutions

  1. Confirm the peer sends the full chunked body including the terminating zero-length chunk and trailer CRLF.
  2. On the receiving side, treat this UnexpectedEof as a truncated body — discard partial data; retry idempotent requests on a new connection.
  3. If you generate chunked output, ensure the stream always emits the final 0-length chunk even on error paths.

Example fix

// before: producer can drop without finishing the chunked stream
let body = Body::wrap_stream(producer_that_may_panic());

// after: guarantee the terminal chunk by completing or converting to an error status
let body = Body::wrap_stream(async_stream::stream! {
    let mut s = producer_that_may_panic();
    while let Some(item) = s.next().await {
        match item {
            Ok(b) => yield Ok::<_, std::io::Error>(b),
            Err(e) => { tracing::error!("producer failed: {e}"); break; }
        }
    }
});
Defensive patterns

Strategy: retry

Type guard

fn is_chunk_eof(err: &hyper::Error) -> bool {
    matches!(
        err.source().and_then(|s| s.downcast_ref::<std::io::Error>()).map(|io| io.kind()),
        Some(std::io::ErrorKind::UnexpectedEof)
    )
}

Try / catch

match hyper::body::to_bytes(resp.into_body()).await {
    Ok(b) => Ok(b),
    Err(e) if is_chunk_eof(&e) && idempotent => { /* truncated chunked body: retry */ }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: The byte! macro (decode.rs:252-262) calls read_mem(1) and gets an empty buffer (EOF) while in Size/SizeLws/Extension/SizeLf/BodyCr/BodyLf/Trailer states — i.e. anywhere a chunk framing byte is expected. Happens when the peer closes the connection between chunks instead of sending the terminating 0-size chunk.

Common situations: A chunked response is cut off before the final '0\r\n\r\n'; a streaming producer crashes mid-chunk; a proxy times out and drops a chunked body; network reset between chunks. The decoder reached the end of stream before it saw the chunk-size digits it needed.

Related errors


AI-assisted analysis of hyperium/hyper@084473f728 (2026-08-06). Data as JSON: /data/errors/958d537ff3ff5e2f.json. Report an issue: GitHub.