Skip to content

Commit c25910a

Browse files
authored
bpo-43332: Buffer proxy connection setup packets before sending. (GH-24780)
We now buffer the CONNECT request + tunnel HTTP headers into a single send call. This prevents the OS from generating multiple network packets for connection setup when not necessary, improving efficiency.
1 parent 8d00462 commit c25910a

File tree

3 files changed

+32
-10
lines changed

3 files changed

+32
-10
lines changed

Lib/http/client.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -901,23 +901,24 @@ def set_debuglevel(self, level):
901901
self.debuglevel = level
902902

903903
def _tunnel(self):
904-
connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self._tunnel_host,
905-
self._tunnel_port)
906-
connect_bytes = connect_str.encode("ascii")
907-
self.send(connect_bytes)
904+
connect = b"CONNECT %s:%d HTTP/1.0\r\n" % (
905+
self._tunnel_host.encode("ascii"), self._tunnel_port)
906+
headers = [connect]
908907
for header, value in self._tunnel_headers.items():
909-
header_str = "%s: %s\r\n" % (header, value)
910-
header_bytes = header_str.encode("latin-1")
911-
self.send(header_bytes)
912-
self.send(b'\r\n')
908+
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
909+
headers.append(b"\r\n")
910+
# Making a single send() call instead of one per line encourages
911+
# the host OS to use a more optimal packet size instead of
912+
# potentially emitting a series of small packets.
913+
self.send(b"".join(headers))
914+
del headers
913915

914916
response = self.response_class(self.sock, method=self._method)
915917
(version, code, message) = response._read_status()
916918

917919
if code != http.HTTPStatus.OK:
918920
self.close()
919-
raise OSError("Tunnel connection failed: %d %s" % (code,
920-
message.strip()))
921+
raise OSError(f"Tunnel connection failed: {code} {message.strip()}")
921922
while True:
922923
line = response.fp.readline(_MAXLINE + 1)
923924
if len(line) > _MAXLINE:

Lib/test/test_httplib.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import warnings
1111

1212
import unittest
13+
from unittest import mock
1314
TestCase = unittest.TestCase
1415

1516
from test import support
@@ -2051,6 +2052,23 @@ def test_connect_with_tunnel(self):
20512052
# This test should be removed when CONNECT gets the HTTP/1.1 blessing
20522053
self.assertNotIn(b'Host: proxy.com', self.conn.sock.data)
20532054

2055+
def test_tunnel_connect_single_send_connection_setup(self):
2056+
"""Regresstion test for https://bugs.python.org/issue43332."""
2057+
with mock.patch.object(self.conn, 'send') as mock_send:
2058+
self.conn.set_tunnel('destination.com')
2059+
self.conn.connect()
2060+
self.conn.request('GET', '/')
2061+
mock_send.assert_called()
2062+
# Likely 2, but this test only cares about the first.
2063+
self.assertGreater(
2064+
len(mock_send.mock_calls), 1,
2065+
msg=f'unexpected number of send calls: {mock_send.mock_calls}')
2066+
proxy_setup_data_sent = mock_send.mock_calls[0][1][0]
2067+
self.assertIn(b'CONNECT destination.com', proxy_setup_data_sent)
2068+
self.assertTrue(
2069+
proxy_setup_data_sent.endswith(b'\r\n\r\n'),
2070+
msg=f'unexpected proxy data sent {proxy_setup_data_sent!r}')
2071+
20542072
def test_connect_put_request(self):
20552073
self.conn.set_tunnel('destination.com')
20562074
self.conn.request('PUT', '/', '')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Improves the networking efficiency of :mod:`http.client` when using a proxy
2+
via :meth:`~HTTPConnection.set_tunnel`. Fewer small send calls are made
3+
during connection setup.

0 commit comments

Comments
 (0)