Skip to content

Commit 9698ad2

Browse files
committed
gh-157174: Close the socket when HTTPConnection.connect() fails to set TCP_NODELAY
connect() left self.sock holding the open socket when setsockopt() raised anything but ENOPROTOOPT, so a caller that dropped the connection leaked it. On macOS, setsockopt(TCP_NODELAY) raises EINVAL once the peer has reset the connection, which is what test_ssl.test_https_client_non_tls_response_ignored provokes on purpose; on a slow machine the reset wins the race and the test leaves an unclosed socket behind. Close the connection before re-raising, as _tunnel() already does.
1 parent 024b6bc commit 9698ad2

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

Lib/http/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ def connect(self):
10671067
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
10681068
except OSError as e:
10691069
if e.errno != errno.ENOPROTOOPT:
1070+
self.close()
10701071
raise
10711072

10721073
if self._tunnel_host:

Lib/test/test_httplib.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,55 @@ def test_getting_header_defaultint(self):
24952495
header = self.resp.getheader('No-Such-Header',default=42)
24962496
self.assertEqual(header, 42)
24972497

2498+
class ConnectTests(TestCase):
2499+
2500+
class Socket(FakeSocket):
2501+
def __init__(self, setsockopt_error=None):
2502+
super().__init__(b'')
2503+
self.setsockopt_error = setsockopt_error
2504+
self.closed = False
2505+
2506+
def setsockopt(self, level, optname, value):
2507+
if self.setsockopt_error is not None:
2508+
raise self.setsockopt_error
2509+
2510+
def close(self):
2511+
self.closed = True
2512+
2513+
def make_connection(self, sock):
2514+
conn = client.HTTPConnection('example.com')
2515+
conn._create_connection = lambda *args, **kwargs: sock
2516+
return conn
2517+
2518+
def test_connect(self):
2519+
sock = self.Socket()
2520+
conn = self.make_connection(sock)
2521+
conn.connect()
2522+
self.assertIs(conn.sock, sock)
2523+
self.assertFalse(sock.closed)
2524+
2525+
def test_connect_tcp_nodelay_unsupported(self):
2526+
# An OS without TCP_NODELAY leaves the connection usable.
2527+
error = OSError(errno.ENOPROTOOPT, 'Protocol not available')
2528+
sock = self.Socket(setsockopt_error=error)
2529+
conn = self.make_connection(sock)
2530+
conn.connect()
2531+
self.assertIs(conn.sock, sock)
2532+
self.assertFalse(sock.closed)
2533+
2534+
def test_connect_tcp_nodelay_error_closes_socket(self):
2535+
# gh-157174: any other error setting TCP_NODELAY (macOS raises EINVAL
2536+
# once the peer has reset the connection) must not leak the socket.
2537+
error = OSError(errno.EINVAL, 'Invalid argument')
2538+
sock = self.Socket(setsockopt_error=error)
2539+
conn = self.make_connection(sock)
2540+
with self.assertRaises(OSError) as cm:
2541+
conn.connect()
2542+
self.assertIs(cm.exception, error)
2543+
self.assertIsNone(conn.sock)
2544+
self.assertTrue(sock.closed)
2545+
2546+
24982547
class TunnelTests(TestCase):
24992548
def setUp(self):
25002549
response_text = (
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :class:`http.client.HTTPConnection` leaking its socket when connecting
2+
fails at setting the ``TCP_NODELAY`` option, which happens on macOS when the
3+
server resets the connection right after accepting it.

0 commit comments

Comments
 (0)