From b26ceecf7b97398c8a7b77960c049ee0e5dd9606 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Fri, 13 Mar 2026 06:58:47 +0100 Subject: [PATCH] Fix case-insensitive header matching in body strategy detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The determine_body_strategy function only matched two case variants of Transfer-Encoding and Content-Length headers. HTTP headers are case-insensitive per RFC 7230 ยง3.2, so servers sending differently cased headers (e.g. CONTENT-LENGTH) would cause incorrect body reading strategy (falling through to ReadUntilClose). Extract a header_value() helper that does case-insensitive name matching via eq_ignore_ascii_case. Add tests for mixed-case headers. Co-Authored-By: Claude Opus 4.6 --- crates/net/src/client.rs | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/net/src/client.rs b/crates/net/src/client.rs index 90f6f27..11ecae9 100644 --- a/crates/net/src/client.rs +++ b/crates/net/src/client.rs @@ -477,6 +477,16 @@ enum BodyStrategy { ReadUntilClose, } +/// Extract the value for a header name (case-insensitive match). +fn header_value<'a>(line: &'a str, name: &str) -> Option<&'a str> { + let colon = line.find(':')?; + if line[..colon].eq_ignore_ascii_case(name) { + Some(line[colon + 1..].trim()) + } else { + None + } +} + /// Determine how to read the body from headers. fn determine_body_strategy(headers: &str, status_code: u16) -> BodyStrategy { // 1xx, 204, 304 have no body @@ -486,11 +496,8 @@ fn determine_body_strategy(headers: &str, status_code: u16) -> BodyStrategy { // Check for Transfer-Encoding: chunked for line in headers.split("\r\n").skip(1) { - if let Some(val) = line - .strip_prefix("Transfer-Encoding:") - .or_else(|| line.strip_prefix("transfer-encoding:")) - { - if val.trim().eq_ignore_ascii_case("chunked") { + if let Some(val) = header_value(line, "transfer-encoding") { + if val.eq_ignore_ascii_case("chunked") { return BodyStrategy::Chunked; } } @@ -498,11 +505,8 @@ fn determine_body_strategy(headers: &str, status_code: u16) -> BodyStrategy { // Check for Content-Length for line in headers.split("\r\n").skip(1) { - if let Some(val) = line - .strip_prefix("Content-Length:") - .or_else(|| line.strip_prefix("content-length:")) - { - if let Ok(len) = val.trim().parse::() { + if let Some(val) = header_value(line, "content-length") { + if let Ok(len) = val.parse::() { return BodyStrategy::ContentLength(len); } } @@ -846,6 +850,24 @@ mod tests { )); } + #[test] + fn strategy_content_length_mixed_case() { + let headers = "HTTP/1.1 200 OK\r\nCONTENT-LENGTH: 99"; + match determine_body_strategy(headers, 200) { + BodyStrategy::ContentLength(99) => {} + _ => panic!("expected ContentLength(99)"), + } + } + + #[test] + fn strategy_chunked_mixed_case_name() { + let headers = "HTTP/1.1 200 OK\r\nTRANSFER-ENCODING: chunked"; + assert!(matches!( + determine_body_strategy(headers, 200), + BodyStrategy::Chunked + )); + } + // -- URL scheme handling -- #[test] -- 2.51.2