diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 67f3074e63c3c6..c8b84e66a747fa 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -1485,28 +1485,36 @@ to sockets. .. availability:: not WASI. -.. method:: socket.close() +.. method:: socket.close(shutdown=False) - Mark the socket closed. The underlying system resource (e.g. a file - descriptor) is also closed when all file objects from :meth:`makefile` - are closed. Once that happens, all future operations on the socket - object will fail. The remote end will receive no more data (after - queued data is flushed). + Mark the socket closed. The underlying system resource (e.g., a file descriptor) is also + closed when all file objects from :meth:`makefile` are closed. Once that happens, all future + operations on the socket object will fail. The remote end will receive no more data + (after queued data is flushed). + + If the ``shutdown`` parameter is set to ``True``, the socket will first be + shut down before closing, ensuring that no further data can be sent or received. + This is useful for properly releasing resources and preventing issues like persistent + connections or reset by peer (RST) errors in some network conditions. If the parameter is + ommited or set to false, the function will continue its normal behavior Sockets are automatically closed when they are garbage-collected, but it is recommended to :meth:`close` them explicitly, or to use a :keyword:`with` statement around them. + .. versionadded:: 3.14 + Added an optional ``shutdown`` parameter to allow explicit socket shutdown before closing. + .. versionchanged:: 3.6 :exc:`OSError` is now raised if an error occurs when the underlying :c:func:`close` call is made. .. note:: - :meth:`close` releases the resource associated with a connection but - does not necessarily close the connection immediately. If you want - to close the connection in a timely fashion, call :meth:`shutdown` - before :meth:`close`. + :meth:`close` releases the resource associated with a connection + but does not necessarily close the connection immediately. If you want to close the connection in a + timely fashion, call :meth:`shutdown` before :meth:`close`, or + use this function with the shutdown parameter like this ``socket.close(shutdown=True)`` .. method:: socket.connect(address) diff --git a/Lib/http/client.py b/Lib/http/client.py index 33a858d34ae1ba..3ad3c270cb91e6 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -1012,19 +1012,39 @@ def connect(self): if self._tunnel_host: self._tunnel() - def close(self): - """Close the connection to the HTTP server.""" + def close(self, shutdown=False): + """Close the connection to the HTTP server. + :param shutdown: If True, perform an explicit shutdown of the socket before closing. Defaults to False. + :type shutdown: bool, optional + """ self.__state = _CS_IDLE try: sock = self.sock if sock: self.sock = None - sock.close() # close it manually... there may be other refs + if shutdown: + try: + sock.shutdown(socket.SHUT_RDWR) + except OSError as e: + print(f"Socket shutdown error: {e}", file=sys.stderr) + except Exception as e: + print(f"Unexpected error during socket shutdown: {e}", file=sys.stderr) + try: + sock.close() + except OSError as e: + print(f"Socket close error: {e}", file=sys.stderr) + except Exception as e: + print(f"Unexpected error during socket close: {e}", file=sys.stderr) finally: response = self.__response if response: self.__response = None - response.close() + try: + response.close() + except OSError as e: + print(f"Response close error: {e}", file=sys.stderr) + except Exception as e: + print(f"Unexpected error during response close: {e}", file=sys.stderr) def send(self, data): """Send 'data' to the server. diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 75b748aee05940..c1b44632d52cd4 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1,12 +1,15 @@ import enum import errno from http import client, HTTPStatus +from http.client import HTTPConnection import io +from io import StringIO import itertools import os import array import re import socket +import sys import threading import unittest @@ -16,6 +19,7 @@ from test import support from test.support import os_helper from test.support import socket_helper +from unittest.mock import MagicMock support.requires_working_socket(module=True) @@ -2525,5 +2529,75 @@ def _create_connection(address, timeout=None, source_address=None): self.assertTrue(sock.file_closed) + +class TestHTTPSocketShutdown(TestCase): + def test_close_with_shutdown(self): + mock_socket = MagicMock() + mock_socket.close = MagicMock() + mock_socket.shutdown = MagicMock() + connection = HTTPConnection('www.example.com') + connection.sock = mock_socket + original_stderr = sys.stderr + sys.stderr = StringIO() + try: + connection.close(shutdown=True) + mock_socket.shutdown.assert_called_once_with(2) # SHUT_RDWR + mock_socket.close.assert_called_once() + error_output = sys.stderr.getvalue() + self.assertEqual(error_output, "") + finally: + sys.stderr = original_stderr + + def test_close_without_shutdown(self): + mock_socket = MagicMock() + mock_socket.close = MagicMock() + mock_socket.shutdown = MagicMock() + connection = HTTPConnection('www.example.com') + connection.sock = mock_socket + original_stderr = sys.stderr + sys.stderr = StringIO() + try: + connection.close(shutdown=False) + mock_socket.shutdown.assert_not_called() + mock_socket.close.assert_called_once() + error_output = sys.stderr.getvalue() + self.assertEqual(error_output, "") + finally: + sys.stderr = original_stderr + + def test_close_shutdown_error(self): + mock_socket = MagicMock() + mock_socket.close = MagicMock() + mock_socket.shutdown = MagicMock(side_effect=OSError("Shutdown error")) + connection = HTTPConnection('www.example.com') + connection.sock = mock_socket + original_stderr = sys.stderr + sys.stderr = StringIO() + try: + connection.close(shutdown=True) + mock_socket.shutdown.assert_called_once_with(2) # SHUT_RDWR + mock_socket.close.assert_called_once() + error_output = sys.stderr.getvalue() + self.assertIn("Socket shutdown error: Shutdown error", error_output) + finally: + sys.stderr = original_stderr + + def test_close_unexpected_error(self): + mock_socket = MagicMock() + mock_socket.close = MagicMock() + mock_socket.shutdown = MagicMock(side_effect=Exception("Unexpected error")) + connection = HTTPConnection('www.example.com') + connection.sock = mock_socket + original_stderr = sys.stderr + sys.stderr = StringIO() + try: + connection.close(shutdown=True) + mock_socket.shutdown.assert_called_once_with(2) # SHUT_RDWR + mock_socket.close.assert_called_once() + error_output = sys.stderr.getvalue() + self.assertIn("Unexpected error during socket shutdown: Unexpected error", error_output) + finally: + sys.stderr = original_stderr + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Misc/NEWS.d/next/Library/2025-03-06-16-31-08.gh-issue-130902.mHAtcq.rst b/Misc/NEWS.d/next/Library/2025-03-06-16-31-08.gh-issue-130902.mHAtcq.rst new file mode 100644 index 00000000000000..cc30ee97c63a82 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-06-16-31-08.gh-issue-130902.mHAtcq.rst @@ -0,0 +1,4 @@ +Issue: :gh:`130902` + + +Modifies the :meth:`http.client.HTTPConnection.close` of :class:`http.client.HTTPConnection` to include an optional socket ``shutdown``, improving functionality with error handling.