From f1111f5b83b1e6c70728d93535d36db3a1fd83ce Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Mon, 6 Jul 2026 08:09:55 -0400 Subject: [PATCH] fix(http1): discard content-length header when received before transfer-encoding While hyper currently recognizes the correct message payload semantics when both content-length and transfer-encoding headers are sent, there are cases where it doesn't always strip the canceled content-length header. This fix now correctly removes the header if it was seen before transfer-encoding. It also now sets the connection to close at the end of the message, as recommended in the new RFC 9112. (To be clear, the semantics have always been correct. This reduces the possibility of forwarding confusing headers to a remote that doesn't know how to act correctly.) Closes #4123 --- src/proto/h1/role.rs | 87 +++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/src/proto/h1/role.rs b/src/proto/h1/role.rs index 740c8e1d56..f467af8286 100644 --- a/src/proto/h1/role.rs +++ b/src/proto/h1/role.rs @@ -234,6 +234,7 @@ impl Http1Transaction for Server { let mut decoder = DecodedLength::ZERO; let mut expect_continue = false; let mut con_len = None; + let mut is_cl = false; let mut is_te = false; let mut is_te_chunked = false; let mut wants_upgrade = subject.0 == Method::CONNECT; @@ -272,6 +273,9 @@ impl Http1Transaction for Server { return Err(Parse::transfer_encoding_unexpected()); } is_te = true; + if is_cl && con_len.take().is_some() { + headers.remove(header::CONTENT_LENGTH); + } if headers::is_chunked_(&value) { is_te_chunked = true; decoder = DecodedLength::CHUNKED; @@ -280,6 +284,7 @@ impl Http1Transaction for Server { } } header::CONTENT_LENGTH => { + is_cl = true; if is_te { continue; } @@ -340,6 +345,10 @@ impl Http1Transaction for Server { return Err(Parse::transfer_encoding_invalid()); } + if is_te && is_cl { + keep_alive = false; + } + let mut extensions = http::Extensions::default(); if let Some(header_case_map) = header_case_map { @@ -2048,45 +2057,55 @@ mod tests { ); // transfer-encoding and content-length = chunked - assert_eq!( - parse( - "\ - POST / HTTP/1.1\r\n\ - content-length: 10\r\n\ - transfer-encoding: chunked\r\n\ - \r\n\ - " - ) - .decode, - DecodedLength::CHUNKED + let msg = parse( + "\ + POST / HTTP/1.1\r\n\ + content-length: 10\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + ", ); + assert_eq!(msg.decode, DecodedLength::CHUNKED); + assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH)); + assert!(!msg.keep_alive); - assert_eq!( - parse( - "\ - POST / HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\ - content-length: 10\r\n\ - \r\n\ - " - ) - .decode, - DecodedLength::CHUNKED + let msg = parse( + "\ + POST / HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + content-length: 10\r\n\ + \r\n\ + ", ); + assert_eq!(msg.decode, DecodedLength::CHUNKED); + assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH)); + assert!(!msg.keep_alive); - assert_eq!( - parse( - "\ - POST / HTTP/1.1\r\n\ - transfer-encoding: gzip\r\n\ - content-length: 10\r\n\ - transfer-encoding: chunked\r\n\ - \r\n\ - " - ) - .decode, - DecodedLength::CHUNKED + let msg = parse( + "\ + POST / HTTP/1.1\r\n\ + transfer-encoding: gzip\r\n\ + content-length: 10\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + ", + ); + assert_eq!(msg.decode, DecodedLength::CHUNKED); + assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH)); + assert!(!msg.keep_alive); + + let msg = parse( + "\ + POST / HTTP/1.1\r\n\ + connection: keep-alive\r\n\ + content-length: 10\r\n\ + transfer-encoding: chunked\r\n\ + \r\n\ + ", ); + assert_eq!(msg.decode, DecodedLength::CHUNKED); + assert!(!msg.head.headers.contains_key(header::CONTENT_LENGTH)); + assert!(!msg.keep_alive); // multiple content-lengths of same value are fine assert_eq!(