Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
gh-150751: Handle duplicate Content-Length headers in http.client
HTTPResponse.begin() framed the body with the first Content-Length
header and ignored any others, so a response with two differing values
was read using the first one while the rest of the body stayed on the
connection and corrupted the next response on a keep-alive connection.
A comma-separated list of identical values ("7, 7"), which a proxy
produces when it combines repeated fields, was treated as malformed and
the body was read until the connection closed.

RFC 9112, section 6.3 says that an invalid Content-Length makes the
message framing invalid, unless it is a comma-separated list of
identical values, which is then used as that single value; repeated
fields are equivalent to such a list (RFC 9110, section 5.3). Collect
the distinct values: a single one is used as before, several are
treated like an invalid header (the body is read until close and the
connection is not reused), as Go and h11 do.
  • Loading branch information
danilablagorodniy37 committed Oct 15, 2026
commit 63a539fa82cb322619f075b4a1dd8d24887db548
15 changes: 10 additions & 5 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,11 +396,16 @@ def begin(self, *, _max_headers=None):
# do we have a Content-Length?
# NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
self.length = None
length = self.headers.get("content-length")
if length is not None:
# RFC 9112, section 5.1: optional whitespace (SP / HTAB) around
# the field value is not part of the value.
length = length.strip(' \t')
# RFC 9112, section 6.3: a Content-Length that is a comma-separated
# list (or, per RFC 9110, section 5.3, a repeated field) is only
# valid if all the values are the same, and is then used as that
# single value; otherwise the message framing is invalid.
# RFC 9112, section 5.1: optional whitespace (SP / HTAB) around
# the field value is not part of the value.
lengths = {value.strip(' \t')
for field in self.headers.get_all("content-length", ())
for value in field.split(',')}
length = lengths.pop() if len(lengths) == 1 else None
if length and not self.chunked and _is_legal_content_length(length):
self.length = int(length)
else:
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,40 @@ def test_content_length_with_whitespace(self):
self.assertTrue(resp.isclosed())
resp.close()

def test_duplicate_content_length(self):
# RFC 9112, section 6.3: a comma-separated list of identical
# Content-Length values, or (RFC 9110, section 5.3) a repeated
# field, is processed with that single value.
for fields in ('Content-Length: 7\r\nContent-Length: 7\r\n',
'Content-Length: 7, 7\r\n',
'Content-Length: 7 ,7\r\nContent-Length: 7\r\n'):
with self.subTest(fields=fields):
sock = FakeSocket(
'HTTP/1.1 200 OK\r\n%s\r\nHello\r\nextra' % fields)
resp = client.HTTPResponse(sock, method="GET")
resp.begin()
self.assertEqual(resp.length, 7)
self.assertEqual(resp.read(), b'Hello\r\n')
self.assertTrue(resp.isclosed())
resp.close()

def test_conflicting_content_length(self):
# RFC 9112, section 6.3: differing Content-Length values make the
# message framing invalid, so none of them may be used to frame
# the body, and the connection must not be reused.
for fields in ('Content-Length: 5\r\nContent-Length: 7\r\n',
'Content-Length: 7\r\nContent-Length: 5\r\n',
'Content-Length: 5, 7\r\n'):
with self.subTest(fields=fields):
sock = FakeSocket(
'HTTP/1.1 200 OK\r\n%s\r\nHello\r\n' % fields)
resp = client.HTTPResponse(sock, method="GET")
resp.begin()
self.assertIsNone(resp.length)
self.assertTrue(resp.will_close)
self.assertEqual(resp.read(), b'Hello\r\n')
resp.close()

def test_malformed_chunk_size(self):
# RFC 9112: chunk-size = 1*HEXDIG. Reject sizes that int(_, 16) accepts
# but the grammar forbids (a sign, an "0x" prefix, underscores or
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:mod:`http.client` now handles a repeated or comma-separated
``Content-Length`` header as required by :rfc:`9112`: identical values
frame the response body like a single header, while differing values are
treated as an invalid header and the body is read until the connection is
closed. Previously the first of several ``Content-Length`` headers was
used, which could leave part of the body unread on a reused connection.
Loading