From c7253e738961575d2de6160fcc20ca631367c8f3 Mon Sep 17 00:00:00 2001 From: scadastrangelove Date: Sun, 19 Jul 2026 16:15:03 +0300 Subject: [PATCH] Fix whitespace-only header line silently truncating headers (fix #222) `ParserConfig::allow_space_before_first_header_name(true)` + a line consisting only of whitespace right after the request/status-line silently dropped the entire header block: the parser returned Ok(Status::Complete) with headers == [], leaving every real header unconsumed in the buffer for the caller to misinterpret as body/ next-request bytes. After stripping the leading whitespace run, peek at the next byte before continuing: whitespace-only (next byte is CR/LF) now rejects with the same Error::HeaderName the non-leniency path already uses; no more buffered data returns Status::Partial (preserves streaming correctness). The documented, tested, non-degenerate case (space then a real header) is unaffected. Adds two regression tests (whitespace-only rejection on both request and response sides; the Partial streaming case). Co-Authored-By: Claude Sonnet 5 --- src/lib.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 98b37c7..429a3b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1102,6 +1102,16 @@ fn parse_headers_iter_uninit<'a>( break; } } + // A line consisting only of whitespace has no header to parse. Falling + // through to `continue 'headers` here would let this loop's own CR/LF + // check (above) treat it as the blank line terminating the header + // section, silently discarding any real headers that follow in the + // buffer. + match bytes.peek() { + None => return Ok(Status::Partial), + Some(b'\r') | Some(b'\n') => break 'header Error::HeaderName, + _ => {} + } bytes.slice(); continue 'headers; } else { @@ -2719,6 +2729,52 @@ mod tests { assert_eq!(response.headers[0].value, &b"hello there"[..]); } + // Regression test: a line consisting only of whitespace (no header name/value follows before + // the line terminator) must not be silently treated as the blank line ending the header + // section -- that would discard every subsequent header from the parsed result while leaving + // their bytes unconsumed in the buffer for the caller to misinterpret as body/next-request + // data. This is the whitespace-only-line degenerate case that + // `test_allow_response_response_with_space_before_first_header` (above) never exercises, since + // its fixture always has real header content after the leading space. + #[test] + fn test_allow_space_before_first_header_name_rejects_whitespace_only_line() { + const REQUEST_WITH_WHITESPACE_ONLY_FIRST_LINE: &[u8] = + b"GET / HTTP/1.1\r\n \r\nHost: example.com\r\n\r\n"; + + let mut headers = [EMPTY_HEADER; 16]; + let mut req = Request::new(&mut headers[..]); + let result = crate::ParserConfig::default() + .allow_space_before_first_header_name(true) + .parse_request(&mut req, REQUEST_WITH_WHITESPACE_ONLY_FIRST_LINE); + + assert_eq!(result, Err(crate::Error::HeaderName)); + + const RESPONSE_WITH_WHITESPACE_ONLY_FIRST_LINE: &[u8] = + b"HTTP/1.1 200 OK\r\n \r\nContent-Length: 500\r\n\r\n"; + + let mut headers = [EMPTY_HEADER; 16]; + let mut resp = Response::new(&mut headers[..]); + let result = crate::ParserConfig::default() + .allow_space_before_first_header_name(true) + .parse_response(&mut resp, RESPONSE_WITH_WHITESPACE_ONLY_FIRST_LINE); + + assert_eq!(result, Err(crate::Error::HeaderName)); + } + + // The fix must not regress the streaming/incremental case: a buffer that ends right after the + // whitespace run (before the caller has fed the byte that would decide whitespace-only vs a + // real header) must return `Partial`, not error or complete prematurely. + #[test] + fn test_allow_space_before_first_header_name_partial_after_whitespace() { + let mut headers = [EMPTY_HEADER; 16]; + let mut req = Request::new(&mut headers[..]); + let result = crate::ParserConfig::default() + .allow_space_before_first_header_name(true) + .parse_request(&mut req, b"GET / HTTP/1.1\r\n "); + + assert_eq!(result, Ok(Status::Partial)); + } + #[test] fn test_no_space_after_colon() { let mut headers = [EMPTY_HEADER; 1];