diff --git a/Lib/http/client.py b/Lib/http/client.py index 7ef99e7201c005c..3a233872761ae97 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -1067,6 +1067,7 @@ def connect(self): self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) except OSError as e: if e.errno != errno.ENOPROTOOPT: + self.close() raise if self._tunnel_host: diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 5b1d6e0aa520794..ef21b9269a650dd 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -2495,6 +2495,55 @@ def test_getting_header_defaultint(self): header = self.resp.getheader('No-Such-Header',default=42) self.assertEqual(header, 42) +class ConnectTests(TestCase): + + class Socket(FakeSocket): + def __init__(self, setsockopt_error=None): + super().__init__(b'') + self.setsockopt_error = setsockopt_error + self.closed = False + + def setsockopt(self, level, optname, value): + if self.setsockopt_error is not None: + raise self.setsockopt_error + + def close(self): + self.closed = True + + def make_connection(self, sock): + conn = client.HTTPConnection('example.com') + conn._create_connection = lambda *args, **kwargs: sock + return conn + + def test_connect(self): + sock = self.Socket() + conn = self.make_connection(sock) + conn.connect() + self.assertIs(conn.sock, sock) + self.assertFalse(sock.closed) + + def test_connect_tcp_nodelay_unsupported(self): + # An OS without TCP_NODELAY leaves the connection usable. + error = OSError(errno.ENOPROTOOPT, 'Protocol not available') + sock = self.Socket(setsockopt_error=error) + conn = self.make_connection(sock) + conn.connect() + self.assertIs(conn.sock, sock) + self.assertFalse(sock.closed) + + def test_connect_tcp_nodelay_error_closes_socket(self): + # gh-157174: any other error setting TCP_NODELAY (macOS raises EINVAL + # once the peer has reset the connection) must not leak the socket. + error = OSError(errno.EINVAL, 'Invalid argument') + sock = self.Socket(setsockopt_error=error) + conn = self.make_connection(sock) + with self.assertRaises(OSError) as cm: + conn.connect() + self.assertIs(cm.exception, error) + self.assertIsNone(conn.sock) + self.assertTrue(sock.closed) + + class TunnelTests(TestCase): def setUp(self): response_text = ( diff --git a/Misc/NEWS.d/next/Library/2026-09-08-13-10-00.gh-issue-157174.7J6qt6.rst b/Misc/NEWS.d/next/Library/2026-09-08-13-10-00.gh-issue-157174.7J6qt6.rst new file mode 100644 index 000000000000000..f101d2ba8714d93 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-08-13-10-00.gh-issue-157174.7J6qt6.rst @@ -0,0 +1,3 @@ +Fix :class:`http.client.HTTPConnection` leaking its socket when connecting +fails at setting the ``TCP_NODELAY`` option, which happens on macOS when the +server resets the connection right after accepting it.