diff --git a/Lib/http/client.py b/Lib/http/client.py index 4b9a61cfc1159f..f939037a285835 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -887,8 +887,6 @@ def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, self._validate_host(self.host) - # This is stored as an instance variable to allow unit - # tests to replace it with a suitable mockup self._create_connection = socket.create_connection def set_tunnel(self, host, port=None, headers=None): @@ -1339,7 +1337,14 @@ def endheaders(self, message_body=None, *, encode_chunked=False): def request(self, method, url, body=None, headers={}, *, encode_chunked=False): """Send a complete request to the server.""" - self._send_request(method, url, body, headers, encode_chunked) + try: + self._send_request(method, url, body, headers, encode_chunked) + except OSError as e: + # If the transmission fails (e.g. timeout), close the connection + # to reset the state machine to _CS_IDLE + if getattr(e, "errno", None) != errno.EPIPE: + self.close() + raise def _send_request(self, method, url, body, headers, encode_chunked): # Honor explicitly requested Host: and Accept-Encoding: headers. diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 47e3914d1dd62e..171cd144f75975 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -2568,6 +2568,28 @@ def _create_connection(address, timeout=None, source_address=None): self.assertIsNotNone(exc) self.assertTrue(sock.file_closed) +class HTTPConnectionStateTests(TestCase): + def test_connect_timeout_resets_state(self): + with mock.patch('socket.create_connection', side_effect=TimeoutError("timed out")): + conn = client.HTTPConnection('10.255.255.1', 80, timeout=0.01) + + with self.assertRaises(TimeoutError): + conn.request('GET', '/') + + self.assertEqual(conn._HTTPConnection__state, client._CS_IDLE) + self.assertIsNone(conn.sock) + + with mock.patch('socket.create_connection') as mock_cc: + fake_sock = mock.Mock() + mock_cc.return_value = fake_sock + # Provide a working socket for the second request. + conn.request("GET", "/second") + + # Ensure the connection is in a usable state (either idle or + # request-sent depending on response handling). + self.assertIn(conn._HTTPConnection__state, + (client._CS_REQ_SENT, client._CS_IDLE)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Misc/NEWS.d/next/Library/2025-11-27-09-32-38.gh-issue-141938.AT4fBQ.rst b/Misc/NEWS.d/next/Library/2025-11-27-09-32-38.gh-issue-141938.AT4fBQ.rst new file mode 100644 index 00000000000000..f1048609764665 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-27-09-32-38.gh-issue-141938.AT4fBQ.rst @@ -0,0 +1,5 @@ +Fix :mod:`http.client` to properly reset the connection state when an +:exc:`OSError` (such as a :exc:`TimeoutError`) occurs during request sending. +Previously, the state remained inconsistent, causing subsequent +:meth:`~http.client.HTTPConnection.request` calls to raise +:exc:`~http.client.CannotSendRequest`. Patch by pareshjoshij.