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

Invalid chunk end LF

Error message

Invalid chunk end LF

What it means

Thrown in read_end_lf (src/proto/h1/decode.rs:630) for the final terminator of the chunked stream: after the closing '\r' (the blank line that ends the trailer block / body), the next byte must be '\n'. Any other byte yields io::ErrorKind::InvalidInput — the chunked body's terminating CRLF is broken.

Source

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

                Poll::Ready(Ok(ChunkedState::Trailer))
            }
        }
    }
    fn read_end_lf<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        trailers_buf: &mut Option<BytesMut>,
        h1_max_header_size: usize,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        let byte = byte!(rdr, cx);
        match byte {
            b'\n' => {
                if let Some(trailers_buf) = trailers_buf {
                    put_u8!(trailers_buf, byte, h1_max_header_size);
                }
                Poll::Ready(Ok(ChunkedState::End))
            }
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Invalid chunk end LF",
            ))),
        }
    }
}

// TODO: disallow Transfer-Encoding, Content-Length, Trailer, etc in trailers ??
fn decode_trailers(buf: &mut BytesMut, count: usize) -> Result<HeaderMap, io::Error> {
    let mut trailers = HeaderMap::new();
    let mut headers = vec![httparse::EMPTY_HEADER; count];
    let res = httparse::parse_headers(buf, &mut headers);
    match res {
        Ok(httparse::Status::Complete((_, headers))) => {
            for header in headers {
                use std::convert::TryFrom;
                let name = match HeaderName::try_from(header.name) {
                    Ok(name) => name,

View on GitHub (pinned to 084473f728)

Solutions

  1. Confirm the chunked stream ends with the full "0\r\n\r\n" (zero chunk + blank line).
  2. Make sure the sender flushes and only then closes the connection.
  3. Check for proxies/load-balancers known to truncate the final byte of a streamed response.

Example fix

// before: closing after the CR
write!(w, "0\r\n\r").await?; // -> error 31

// after: full terminating sequence
write!(w, "0\r\n\r\n").await?;
Defensive patterns

Strategy: try-catch

Try / catch

Some(Err(e)) => {
    let kind = e.source()
        .and_then(|s| s.downcast_ref::<std::io::Error>())
        .map(|io| io.kind());
    if matches!(kind, Some(std::io::ErrorKind::InvalidInput)) {
        tracing::warn!(error=%e, "chunked terminator CR not followed by LF");
        break;
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: A chunked body that ends with "0\r\n\r" but never sends the final LF, or sends a different byte there — e.g. "0\r\n\rX". The EndLf state is reached from EndCr after the final CR.

Common situations: A sender that closes the socket immediately after the final CR without the LF; a transport bug that drops the very last byte; a proxy that truncates the response tail.

Related errors


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