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

invalid chunk extension contains newline

Error message

invalid chunk extension contains newline

What it means

Thrown in the Extension state (src/proto/h1/decode.rs:437) when a chunk extension contains a bare line-feed ('\n') not preceded by '\r'. hyper ignores chunk extension contents but defensively rejects a lone LF (which some broken senders use instead of CRLF) to avoid smuggling ambiguity. Reported as io::ErrorKind::InvalidData.

Source

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

                "Invalid chunk size linear white space",
            ))),
        }
    }
    fn read_extension<R: MemRead>(
        cx: &mut Context<'_>,
        rdr: &mut R,
        extensions_cnt: &mut u64,
    ) -> Poll<Result<ChunkedState, io::Error>> {
        trace!("read_extension");
        // We don't care about extensions really at all. Just ignore them.
        // They "end" at the next CRLF.
        //
        // However, some implementations may not check for the CR, so to save
        // them from themselves, we reject extensions containing plain LF as
        // well.
        match byte!(rdr, cx) {
            b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
            b'\n' => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid chunk extension contains newline",
            ))),
            _ => {
                *extensions_cnt += 1;
                if *extensions_cnt >= CHUNKED_EXTENSIONS_LIMIT {
                    Poll::Ready(Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "chunk extensions over limit",
                    )))
                } else {
                    Poll::Ready(Ok(ChunkedState::Extension))
                }
            } // no supported extensions
        }
    }
    fn read_size_lf<R: MemRead>(
        cx: &mut Context<'_>,

View on GitHub (pinned to 084473f728)

Solutions

  1. Ensure chunk extensions are terminated by CRLF (\r\n), never a lone LF.
  2. Strip CR/LF from any dynamic value placed into a chunk extension before writing it.
  3. If you do not need chunk extensions, omit the ';' entirely.

Example fix

// before: extension value contains a newline
let trace = "a\nb";
write!(w, "1;t={}\r\nX\r\n", trace).await?; // -> error 23

// after: sanitize extension bytes
let trace = trace.replace(['\r', '\n'], "_");
write!(w, "1;t={}\r\nX\r\n", trace).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Before writing a chunk extension, strip CR/LF so no bare LF can reach the wire.
fn safe_ext(value: &str) -> String {
    value.chars().map(|c| match c {
        '\r' | '\n' => '_',
        c => c,
    }).collect()
}
write!(w, "{:x};t={}\r\n", len, safe_ext(&trace_id)).await?;

Type guard

fn has_no_newline(s: &str) -> bool { !s.as_bytes().iter().any(|b| matches!(b, b'\r' | b'\n')) }

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::InvalidData))
        && e.to_string().contains("newline") {
        tracing::warn!(error=%e, "peer sent LF inside chunk extension; possible smuggling");
        break;
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: A chunk extension that uses bare LF as a separator, e.g. a chunk line "1;reject\nnewlines\r\n" where the extension text contains '\n' before the terminating CRLF.

Common situations: A peer that normalizes CRLF to LF in the body; a logging/trace value embedded in a chunk extension that contains newlines; request smuggling attempts that exploit LF handling.

Related errors


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