Skip to content
Closed
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
9 changes: 6 additions & 3 deletions Lib/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,15 @@ def begin(self, *, _max_headers=None):
# for _read_and_discard_trailer() to enforce.
self._max_headers = _max_headers

# read until we get a non-100 response
# read until we get a final response, skipping 1xx (informational)
# responses such as "100 Continue" or "103 Early Hints".
# "101 Switching Protocols" is returned to the caller since it ends
# the HTTP/1.1 exchange on this connection.
for _ in range(_MAXINTERIMRESPONSES):
version, status, reason = self._read_status()
if status != CONTINUE:
if not 100 <= status < 200 or status == SWITCHING_PROTOCOLS:
break
# skip the header from the 100 response
# skip the header from the 1xx response
skipped_headers = _read_headers(self.fp, _max_headers)
if self.debuglevel > 0:
print("headers:", skipped_headers)
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,46 @@ def test_multiple_interim_responses(self):
self.assertEqual(resp.read(), b'hello')
resp.close()

def test_informational_responses(self):
# gh-72756: every 1xx response other than "101 Switching Protocols"
# is an interim response and must be skipped, like "100 Continue".
for interim in ('102 Processing', '103 Early Hints', '199 Unknown',
'103 Early Hints\r\n\r\nHTTP/1.1 100 Continue'):
with self.subTest(interim=interim):
body = (
f'HTTP/1.1 {interim}\r\n'
'Link: </style.css>; rel=preload; as=style\r\n'
'\r\n'
'HTTP/1.1 200 OK\r\n'
'Content-Length: 5\r\n'
'\r\n'
'hello'
)
resp = client.HTTPResponse(FakeSocket(body), method="GET")
resp.begin()
self.assertEqual(resp.status, 200)
self.assertEqual(resp.reason, 'OK')
self.assertIsNone(resp.getheader('Link'))
self.assertEqual(resp.read(), b'hello')
resp.close()

def test_switching_protocols_response(self):
# "101 Switching Protocols" ends the HTTP/1.1 exchange, so it is
# returned to the caller instead of being skipped.
body = (
'HTTP/1.1 101 Switching Protocols\r\n'
'Upgrade: websocket\r\n'
'Connection: Upgrade\r\n'
'\r\n'
'not HTTP'
)
resp = client.HTTPResponse(FakeSocket(body), method="GET")
resp.begin()
self.assertEqual(resp.status, 101)
self.assertEqual(resp.getheader('Upgrade'), 'websocket')
self.assertEqual(resp.read(), b'')
resp.close()

def test_overflowing_chunked_line(self):
body = (
'HTTP/1.1 200 OK\r\n'
Expand Down
19 changes: 18 additions & 1 deletion Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def test_send_error(self):
allow_transfer_encoding_codes = (HTTPStatus.NOT_MODIFIED,
HTTPStatus.RESET_CONTENT)
for code in (HTTPStatus.NO_CONTENT, HTTPStatus.NOT_MODIFIED,
HTTPStatus.PROCESSING, HTTPStatus.RESET_CONTENT,
HTTPStatus.RESET_CONTENT,
HTTPStatus.SWITCHING_PROTOCOLS):
self.con.request('SEND_ERROR', '/{}'.format(code))
res = self.con.getresponse()
Expand All @@ -342,6 +342,23 @@ def test_send_error(self):
data = res.read()
self.assertEqual(b'', data)

def test_send_error_informational(self):
# http.client skips 1xx (informational) responses other than
# "101 Switching Protocols", so read this one from the socket.
self.con.close()
with socket.create_connection((self.HOST, self.PORT)) as sock:
sock.sendall(b'SEND_ERROR /102 HTTP/1.1\r\n\r\n')
response = b''.join(iter(lambda: sock.recv(1024), b''))
head, sep, data = response.partition(b'\r\n\r\n')
self.assertEqual(sep, b'\r\n\r\n')
status_line, *header_lines = head.split(b'\r\n')
self.assertStartsWith(status_line, b'HTTP/1.1 102 ')
names = [line.partition(b':')[0].lower() for line in header_lines]
self.assertNotIn(b'content-length', names)
self.assertNotIn(b'content-type', names)
self.assertNotIn(b'transfer-encoding', names)
self.assertEqual(b'', data)

def test_head_via_send_error(self):
allow_transfer_encoding_codes = (HTTPStatus.NOT_MODIFIED,
HTTPStatus.RESET_CONTENT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :mod:`http.client` returning a 1xx informational response such as
``103 Early Hints`` as the final response. All 1xx responses except
``101 Switching Protocols`` are now skipped, like ``100 Continue``.
Loading