diff --git a/CHANGES.rst b/CHANGES.rst index 3eee9e685c6..1ae7c2de6a2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,59 @@ .. towncrier release notes start +3.11.13 (2025-02-24) +==================== + +Bug fixes +--------- + +- Removed a break statement inside the finally block in :py:class:`~aiohttp.web.RequestHandler` + -- by :user:`Cycloctane`. + + + *Related issues and pull requests on GitHub:* + :issue:`10434`. + + + +- Changed connection creation to explicitly close sockets if an exception is raised in the event loop's ``create_connection`` method -- by :user:`top-oai`. + + + *Related issues and pull requests on GitHub:* + :issue:`10464`. + + + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Fixed test ``test_write_large_payload_deflate_compression_data_in_eof_writelines`` failing with Python 3.12.9+ or 3.13.2+ -- by :user:`bdraco`. + + + *Related issues and pull requests on GitHub:* + :issue:`10423`. + + + + +Miscellaneous internal changes +------------------------------ + +- Added human-readable error messages to the exceptions for WebSocket disconnects due to PONG not being received -- by :user:`bdraco`. + + Previously, the error messages were empty strings, which made it hard to determine what went wrong. + + + *Related issues and pull requests on GitHub:* + :issue:`10422`. + + + + +---- + + 3.11.12 (2025-02-05) ==================== diff --git a/CHANGES/10422.misc.rst b/CHANGES/10422.misc.rst deleted file mode 100644 index 7ecb1c0e2e2..00000000000 --- a/CHANGES/10422.misc.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added human-readable error messages to the exceptions for WebSocket disconnects due to PONG not being received -- by :user:`bdraco`. - -Previously, the error messages were empty strings, which made it hard to determine what went wrong. diff --git a/CHANGES/10423.packaging.rst b/CHANGES/10423.packaging.rst deleted file mode 100644 index 6cf58c5a10b..00000000000 --- a/CHANGES/10423.packaging.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed test ``test_write_large_payload_deflate_compression_data_in_eof_writelines`` failing with Python 3.12.9+ or 3.13.2+ -- by :user:`bdraco`. diff --git a/CHANGES/10434.bugfix.rst b/CHANGES/10434.bugfix.rst deleted file mode 100644 index c4bc50dc6aa..00000000000 --- a/CHANGES/10434.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid break statement inside the finally block in :py:class:`~aiohttp.web.RequestHandler` --- by :user:`Cycloctane`. diff --git a/CHANGES/10464.bugfix.rst b/CHANGES/10464.bugfix.rst deleted file mode 100644 index 4e21000a317..00000000000 --- a/CHANGES/10464.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Changed connection creation to explicitly close sockets if an exception is raised in the event loop's ``create_connection`` method -- by :user:`top-oai`. diff --git a/CHANGES/10474.feature.rst b/CHANGES/10474.feature.rst new file mode 100644 index 00000000000..d5d6e4b40b9 --- /dev/null +++ b/CHANGES/10474.feature.rst @@ -0,0 +1,2 @@ +Added ``tcp_sockopts`` to ``TCPConnector`` to allow specifying custom socket options +-- by :user:`TimMenninger`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 42062c972c8..3004ee5cd18 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -341,6 +341,7 @@ Thanos Lefteris Thijs Vermeir Thomas Forbes Thomas Grainger +Tim Menninger Tolga Tezel Tomasz Trebski Toshiaki Tanaka diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 99b281b24a5..8a3f1bcbf2b 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -20,6 +20,7 @@ DefaultDict, Deque, Dict, + Iterable, Iterator, List, Literal, @@ -61,6 +62,11 @@ ) from .resolver import DefaultResolver +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + if TYPE_CHECKING: import ssl @@ -820,6 +826,8 @@ class TCPConnector(BaseConnector): the happy eyeballs algorithm, set to None. interleave - “First Address Family Count” as defined in RFC 8305 loop - Optional event loop. + tcp_sockopts - List of tuples of sockopts applied to underlying + socket """ allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"}) @@ -841,6 +849,7 @@ def __init__( timeout_ceil_threshold: float = 5, happy_eyeballs_delay: Optional[float] = 0.25, interleave: Optional[int] = None, + tcp_sockopts: Iterable[Tuple[int, int, Union[int, Buffer]]] = [], ): super().__init__( keepalive_timeout=keepalive_timeout, @@ -871,6 +880,7 @@ def __init__( self._happy_eyeballs_delay = happy_eyeballs_delay self._interleave = interleave self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set() + self._tcp_sockopts = tcp_sockopts def _close_immediately(self) -> List[Awaitable[object]]: for fut in chain.from_iterable(self._throttle_dns_futures.values()): @@ -1113,6 +1123,8 @@ async def _wrap_create_connection( interleave=self._interleave, loop=self._loop, ) + for sockopt in self._tcp_sockopts: + sock.setsockopt(*sockopt) connection = await self._loop.create_connection( *args, **kwargs, sock=sock ) diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 01ea3e9dc73..8f34fefaf81 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -468,6 +468,21 @@ If your HTTP server uses UNIX domain sockets you can use session = aiohttp.ClientSession(connector=conn) +Setting socket options +^^^^^^^^^^^^^^^^^^^^^^ + +Socket options passed to the :class:`~aiohttp.TCPConnector` will be passed +to the underlying socket when creating a connection. For example, we may +want to change the conditions under which we consider a connection dead. +The following would change that to 9*7200 = 18 hours:: + + import socket + + conn = aiohttp.TCPConnector(tcp_sockopts=[(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True), + (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7200), + (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 9) ]) + + Named pipes in Windows ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 8f15948f34b..e1128934631 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1128,7 +1128,8 @@ is controlled by *force_close* constructor's parameter). resolver=None, keepalive_timeout=sentinel, \ force_close=False, limit=100, limit_per_host=0, \ enable_cleanup_closed=False, timeout_ceil_threshold=5, \ - happy_eyeballs_delay=0.25, interleave=None, loop=None) + happy_eyeballs_delay=0.25, interleave=None, loop=None, \ + tcp_sockopts=[]) Connector for working with *HTTP* and *HTTPS* via *TCP* sockets. @@ -1249,6 +1250,12 @@ is controlled by *force_close* constructor's parameter). .. versionadded:: 3.10 + :param list tcp_sockopts: options applied to the socket when a connection is + created. This should be a list of 3-tuples, each a ``(level, optname, value)``. + Each tuple is deconstructed and passed verbatim to ``.setsockopt``. + + .. versionadded:: 3.12 + .. attribute:: family *TCP* socket family e.g. :data:`socket.AF_INET` or diff --git a/docs/contributing-admins.rst b/docs/contributing-admins.rst index acfaebc0e97..b17cbe1019a 100644 --- a/docs/contributing-admins.rst +++ b/docs/contributing-admins.rst @@ -21,9 +21,9 @@ To create a new release: #. Run ``towncrier``. #. Check and cleanup the changes in ``CHANGES.rst``. #. Checkout a new branch: e.g. ``git checkout -b release/v3.8.6`` -#. Commit and create a PR. Once PR is merged, continue. +#. Commit and create a PR. Verify the changelog and release notes look good on Read the Docs. Once PR is merged, continue. #. Go back to the release branch: e.g. ``git checkout 3.8 && git pull`` -#. Add a tag: e.g. ``git tag -a v3.8.6 -m 'Release 3.8.6'`` +#. Add a tag: e.g. ``git tag -a v3.8.6 -m 'Release 3.8.6' -s`` #. Push the tag: e.g. ``git push origin v3.8.6`` #. Monitor CI to ensure release process completes without errors. @@ -49,6 +49,10 @@ first merge into the newer release branch (e.g. 3.8 into 3.9) and then to master Back on the original release branch, bump the version number and append ``.dev0`` in ``__init__.py``. +Post the release announcement to social media: + - BlueSky: https://bsky.app/profile/aiohttp.org and re-post to https://bsky.app/profile/aio-libs.org + - Mastodon: https://fosstodon.org/@aiohttp and re-post to https://fosstodon.org/@aio_libs + If doing a minor release: #. Create a new release branch for future features to go to: e.g. ``git checkout -b 3.10 3.9 && git push`` diff --git a/requirements/base.txt b/requirements/base.txt index 49891cf5b39..22a0288cb5f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -30,7 +30,7 @@ multidict==6.1.0 # yarl packaging==24.2 # via gunicorn -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 56de66cc419..9e05c4e7624 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -24,7 +24,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # valkey babel==2.17.0 # via sphinx -blockbuster==1.5.21 +blockbuster==1.5.23 # via # -r requirements/lint.in # -r requirements/test.in @@ -86,7 +86,7 @@ gidgethub==5.3.0 # via cherry-picker gunicorn==23.0.0 # via -r requirements/base.in -identify==2.6.7 +identify==2.6.8 # via pre-commit idna==3.6 # via @@ -136,7 +136,7 @@ pluggy==1.5.0 # via pytest pre-commit==4.1.0 # via -r requirements/lint.in -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/runtime-deps.in # yarl @@ -284,7 +284,7 @@ yarl==1.18.3 # The following packages are considered to be unsafe in a requirements file: pip==25.0.1 # via pip-tools -setuptools==75.8.0 +setuptools==75.8.1 # via # incremental # pip-tools diff --git a/requirements/dev.txt b/requirements/dev.txt index b6f1e849123..82b9e83e647 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -24,7 +24,7 @@ async-timeout==5.0.1 ; python_version < "3.11" # valkey babel==2.17.0 # via sphinx -blockbuster==1.5.21 +blockbuster==1.5.23 # via # -r requirements/lint.in # -r requirements/test.in @@ -84,7 +84,7 @@ gidgethub==5.3.0 # via cherry-picker gunicorn==23.0.0 # via -r requirements/base.in -identify==2.6.7 +identify==2.6.8 # via pre-commit idna==3.6 # via @@ -133,7 +133,7 @@ pluggy==1.5.0 # via pytest pre-commit==4.1.0 # via -r requirements/lint.in -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/runtime-deps.in # yarl @@ -275,7 +275,7 @@ yarl==1.18.3 # The following packages are considered to be unsafe in a requirements file: pip==25.0.1 # via pip-tools -setuptools==75.8.0 +setuptools==75.8.1 # via # incremental # pip-tools diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index c22df39a396..055e18cd48f 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -76,5 +76,5 @@ urllib3==2.3.0 # via requests # The following packages are considered to be unsafe in a requirements file: -setuptools==75.8.0 +setuptools==75.8.1 # via incremental diff --git a/requirements/doc.txt b/requirements/doc.txt index 491eb3453de..8626f5f1b67 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -69,5 +69,5 @@ urllib3==2.3.0 # via requests # The following packages are considered to be unsafe in a requirements file: -setuptools==75.8.0 +setuptools==75.8.1 # via incremental diff --git a/requirements/lint.txt b/requirements/lint.txt index 039698a23cc..c186db8ac84 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -10,7 +10,7 @@ annotated-types==0.7.0 # via pydantic async-timeout==5.0.1 # via valkey -blockbuster==1.5.22 +blockbuster==1.5.23 # via -r requirements/lint.in cffi==1.17.1 # via @@ -33,7 +33,7 @@ forbiddenfruit==0.1.4 # via blockbuster freezegun==1.5.1 # via -r requirements/lint.in -identify==2.6.7 +identify==2.6.8 # via pre-commit idna==3.7 # via trustme diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 8ab8f3f43b9..75830bf4190 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -26,7 +26,7 @@ multidict==6.1.0 # via # -r requirements/runtime-deps.in # yarl -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/runtime-deps.in # yarl diff --git a/requirements/test.txt b/requirements/test.txt index c27b4271167..b728abf4ee3 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -14,7 +14,7 @@ annotated-types==0.7.0 # via pydantic async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in -blockbuster==1.5.22 +blockbuster==1.5.23 # via -r requirements/test.in brotli==1.1.0 ; platform_python_implementation == "CPython" # via -r requirements/runtime-deps.in @@ -69,7 +69,7 @@ packaging==24.2 # pytest pluggy==1.5.0 # via pytest -propcache==0.2.1 +propcache==0.3.0 # via # -r requirements/runtime-deps.in # yarl diff --git a/tests/test_connector.py b/tests/test_connector.py index 80fb9ba0c0b..076ed556971 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -3767,6 +3767,29 @@ def test_connect() -> Literal[True]: assert raw_response_list == [True, True] +async def test_tcp_connector_setsockopts( + loop: asyncio.AbstractEventLoop, start_connection: mock.AsyncMock +) -> None: + """Check that sockopts get passed to socket""" + conn = aiohttp.TCPConnector( + tcp_sockopts=[(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2)] + ) + + with mock.patch.object( + conn._loop, "create_connection", autospec=True, spec_set=True + ) as create_connection: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + start_connection.return_value = s + create_connection.return_value = mock.Mock(), mock.Mock() + + req = ClientRequest("GET", URL("https://127.0.0.1:443"), loop=loop) + + with closing(await conn.connect(req, [], ClientTimeout())): + assert s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT) == 2 + + await conn.close() + + def test_default_ssl_context_creation_without_ssl() -> None: """Verify _make_ssl_context does not raise when ssl is not available.""" with mock.patch.object(connector_module, "ssl", None):