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
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