Skip to content

Commit c7253e7

Browse files
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 <noreply@anthropic.com>
1 parent a0fa552 commit c7253e7

1 file changed

Lines changed: 56 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,16 @@ fn parse_headers_iter_uninit<'a>(
11021102
break;
11031103
}
11041104
}
1105+
// A line consisting only of whitespace has no header to parse. Falling
1106+
// through to `continue 'headers` here would let this loop's own CR/LF
1107+
// check (above) treat it as the blank line terminating the header
1108+
// section, silently discarding any real headers that follow in the
1109+
// buffer.
1110+
match bytes.peek() {
1111+
None => return Ok(Status::Partial),
1112+
Some(b'\r') | Some(b'\n') => break 'header Error::HeaderName,
1113+
_ => {}
1114+
}
11051115
bytes.slice();
11061116
continue 'headers;
11071117
} else {
@@ -2719,6 +2729,52 @@ mod tests {
27192729
assert_eq!(response.headers[0].value, &b"hello there"[..]);
27202730
}
27212731

2732+
// Regression test: a line consisting only of whitespace (no header name/value follows before
2733+
// the line terminator) must not be silently treated as the blank line ending the header
2734+
// section -- that would discard every subsequent header from the parsed result while leaving
2735+
// their bytes unconsumed in the buffer for the caller to misinterpret as body/next-request
2736+
// data. This is the whitespace-only-line degenerate case that
2737+
// `test_allow_response_response_with_space_before_first_header` (above) never exercises, since
2738+
// its fixture always has real header content after the leading space.
2739+
#[test]
2740+
fn test_allow_space_before_first_header_name_rejects_whitespace_only_line() {
2741+
const REQUEST_WITH_WHITESPACE_ONLY_FIRST_LINE: &[u8] =
2742+
b"GET / HTTP/1.1\r\n \r\nHost: example.com\r\n\r\n";
2743+
2744+
let mut headers = [EMPTY_HEADER; 16];
2745+
let mut req = Request::new(&mut headers[..]);
2746+
let result = crate::ParserConfig::default()
2747+
.allow_space_before_first_header_name(true)
2748+
.parse_request(&mut req, REQUEST_WITH_WHITESPACE_ONLY_FIRST_LINE);
2749+
2750+
assert_eq!(result, Err(crate::Error::HeaderName));
2751+
2752+
const RESPONSE_WITH_WHITESPACE_ONLY_FIRST_LINE: &[u8] =
2753+
b"HTTP/1.1 200 OK\r\n \r\nContent-Length: 500\r\n\r\n";
2754+
2755+
let mut headers = [EMPTY_HEADER; 16];
2756+
let mut resp = Response::new(&mut headers[..]);
2757+
let result = crate::ParserConfig::default()
2758+
.allow_space_before_first_header_name(true)
2759+
.parse_response(&mut resp, RESPONSE_WITH_WHITESPACE_ONLY_FIRST_LINE);
2760+
2761+
assert_eq!(result, Err(crate::Error::HeaderName));
2762+
}
2763+
2764+
// The fix must not regress the streaming/incremental case: a buffer that ends right after the
2765+
// whitespace run (before the caller has fed the byte that would decide whitespace-only vs a
2766+
// real header) must return `Partial`, not error or complete prematurely.
2767+
#[test]
2768+
fn test_allow_space_before_first_header_name_partial_after_whitespace() {
2769+
let mut headers = [EMPTY_HEADER; 16];
2770+
let mut req = Request::new(&mut headers[..]);
2771+
let result = crate::ParserConfig::default()
2772+
.allow_space_before_first_header_name(true)
2773+
.parse_request(&mut req, b"GET / HTTP/1.1\r\n ");
2774+
2775+
assert_eq!(result, Ok(Status::Partial));
2776+
}
2777+
27222778
#[test]
27232779
fn test_no_space_after_colon() {
27242780
let mut headers = [EMPTY_HEADER; 1];

0 commit comments

Comments
 (0)