Skip to content

Commit ca39f6a

Browse files
authored
[PR #11148/86a9a38 backport][3.12] Abort ssl connections on close when ssl_shutdown_timeout is 0 (#11155)
1 parent dcc0ba2 commit ca39f6a

11 files changed

+670
-91
lines changed

CHANGES/11148.deprecation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
11148.feature.rst

CHANGES/11148.feature.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Improved SSL connection handling by changing the default ``ssl_shutdown_timeout``
2+
from ``0.1`` to ``0`` seconds. SSL connections now use Python's default graceful
3+
shutdown during normal operation but are aborted immediately when the connector
4+
is closed, providing optimal behavior for both cases. Also added support for
5+
``ssl_shutdown_timeout=0`` on all Python versions. Previously, this value was
6+
rejected on Python 3.11+ and ignored on earlier versions. Non-zero values on
7+
Python < 3.11 now trigger a ``RuntimeWarning`` -- by :user:`bdraco`.
8+
9+
The ``ssl_shutdown_timeout`` parameter is now deprecated and will be removed in
10+
aiohttp 4.0 as there is no clear use case for changing the default.

aiohttp/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def __init__(
303303
max_field_size: int = 8190,
304304
fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8",
305305
middlewares: Sequence[ClientMiddlewareType] = (),
306-
ssl_shutdown_timeout: Optional[float] = 0.1,
306+
ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
307307
) -> None:
308308
# We initialise _connector to None immediately, as it's referenced in __del__()
309309
# and could cause issues if an exception occurs during initialisation.
@@ -361,6 +361,13 @@ def __init__(
361361
"timeout.connect"
362362
)
363363

364+
if ssl_shutdown_timeout is not sentinel:
365+
warnings.warn(
366+
"The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
367+
DeprecationWarning,
368+
stacklevel=2,
369+
)
370+
364371
if connector is None:
365372
connector = TCPConnector(ssl_shutdown_timeout=ssl_shutdown_timeout)
366373

aiohttp/client_proto.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ def close(self) -> None:
9595
self._payload = None
9696
self._drop_timeout()
9797

98+
def abort(self) -> None:
99+
self._exception = None # Break cyclic references
100+
transport = self.transport
101+
if transport is not None:
102+
transport.abort()
103+
self.transport = None
104+
self._payload = None
105+
self._drop_timeout()
106+
98107
def is_connected(self) -> bool:
99108
return self.transport is not None and not self.transport.is_closing()
100109

aiohttp/connector.py

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .client_proto import ResponseHandler
5353
from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params
5454
from .helpers import (
55+
_SENTINEL,
5556
ceil_timeout,
5657
is_ip_address,
5758
noop,
@@ -231,15 +232,19 @@ def closed(self) -> bool:
231232
class _TransportPlaceholder:
232233
"""placeholder for BaseConnector.connect function"""
233234

234-
__slots__ = ("closed",)
235+
__slots__ = ("closed", "transport")
235236

236237
def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None:
237238
"""Initialize a placeholder for a transport."""
238239
self.closed = closed_future
240+
self.transport = None
239241

240242
def close(self) -> None:
241243
"""Close the placeholder."""
242244

245+
def abort(self) -> None:
246+
"""Abort the placeholder (does nothing)."""
247+
243248

244249
class BaseConnector:
245250
"""Base connector class.
@@ -469,9 +474,14 @@ def _cleanup_closed(self) -> None:
469474
timeout_ceil_threshold=self._timeout_ceil_threshold,
470475
)
471476

472-
def close(self) -> Awaitable[None]:
473-
"""Close all opened transports."""
474-
if not (waiters := self._close()):
477+
def close(self, *, abort_ssl: bool = False) -> Awaitable[None]:
478+
"""Close all opened transports.
479+
480+
:param abort_ssl: If True, SSL connections will be aborted immediately
481+
without performing the shutdown handshake. This provides
482+
faster cleanup at the cost of less graceful disconnection.
483+
"""
484+
if not (waiters := self._close(abort_ssl=abort_ssl)):
475485
# If there are no connections to close, we can return a noop
476486
# awaitable to avoid scheduling a task on the event loop.
477487
return _DeprecationWaiter(noop())
@@ -484,7 +494,7 @@ def close(self) -> Awaitable[None]:
484494
task = self._loop.create_task(coro)
485495
return _DeprecationWaiter(task)
486496

487-
def _close(self) -> List[Awaitable[object]]:
497+
def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
488498
waiters: List[Awaitable[object]] = []
489499

490500
if self._closed:
@@ -506,12 +516,26 @@ def _close(self) -> List[Awaitable[object]]:
506516

507517
for data in self._conns.values():
508518
for proto, _ in data:
509-
proto.close()
519+
if (
520+
abort_ssl
521+
and proto.transport
522+
and proto.transport.get_extra_info("sslcontext") is not None
523+
):
524+
proto.abort()
525+
else:
526+
proto.close()
510527
if closed := proto.closed:
511528
waiters.append(closed)
512529

513530
for proto in self._acquired:
514-
proto.close()
531+
if (
532+
abort_ssl
533+
and proto.transport
534+
and proto.transport.get_extra_info("sslcontext") is not None
535+
):
536+
proto.abort()
537+
else:
538+
proto.close()
515539
if closed := proto.closed:
516540
waiters.append(closed)
517541

@@ -881,11 +905,12 @@ class TCPConnector(BaseConnector):
881905
socket_factory - A SocketFactoryType function that, if supplied,
882906
will be used to create sockets given an
883907
AddrInfoType.
884-
ssl_shutdown_timeout - Grace period for SSL shutdown handshake on TLS
885-
connections. Default is 0.1 seconds. This usually
886-
allows for a clean SSL shutdown by notifying the
887-
remote peer of connection closure, while avoiding
888-
excessive delays during connector cleanup.
908+
ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0.
909+
Grace period for SSL shutdown handshake on TLS
910+
connections. Default is 0 seconds (immediate abort).
911+
This parameter allowed for a clean SSL shutdown by
912+
notifying the remote peer of connection closure,
913+
while avoiding excessive delays during connector cleanup.
889914
Note: Only takes effect on Python 3.11+.
890915
"""
891916

@@ -913,7 +938,7 @@ def __init__(
913938
happy_eyeballs_delay: Optional[float] = 0.25,
914939
interleave: Optional[int] = None,
915940
socket_factory: Optional[SocketFactoryType] = None,
916-
ssl_shutdown_timeout: Optional[float] = 0.1,
941+
ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
917942
):
918943
super().__init__(
919944
keepalive_timeout=keepalive_timeout,
@@ -946,26 +971,57 @@ def __init__(
946971
self._interleave = interleave
947972
self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
948973
self._socket_factory = socket_factory
949-
self._ssl_shutdown_timeout = ssl_shutdown_timeout
974+
self._ssl_shutdown_timeout: Optional[float]
975+
# Handle ssl_shutdown_timeout with warning for Python < 3.11
976+
if ssl_shutdown_timeout is sentinel:
977+
self._ssl_shutdown_timeout = 0
978+
else:
979+
# Deprecation warning for ssl_shutdown_timeout parameter
980+
warnings.warn(
981+
"The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
982+
DeprecationWarning,
983+
stacklevel=2,
984+
)
985+
if (
986+
sys.version_info < (3, 11)
987+
and ssl_shutdown_timeout is not None
988+
and ssl_shutdown_timeout != 0
989+
):
990+
warnings.warn(
991+
f"ssl_shutdown_timeout={ssl_shutdown_timeout} is ignored on Python < 3.11; "
992+
"only ssl_shutdown_timeout=0 is supported. The timeout will be ignored.",
993+
RuntimeWarning,
994+
stacklevel=2,
995+
)
996+
self._ssl_shutdown_timeout = ssl_shutdown_timeout
950997

951-
def _close(self) -> List[Awaitable[object]]:
998+
def _close(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
952999
"""Close all ongoing DNS calls."""
9531000
for fut in chain.from_iterable(self._throttle_dns_futures.values()):
9541001
fut.cancel()
9551002

956-
waiters = super()._close()
1003+
waiters = super()._close(abort_ssl=abort_ssl)
9571004

9581005
for t in self._resolve_host_tasks:
9591006
t.cancel()
9601007
waiters.append(t)
9611008

9621009
return waiters
9631010

964-
async def close(self) -> None:
965-
"""Close all opened transports."""
1011+
async def close(self, *, abort_ssl: bool = False) -> None:
1012+
"""
1013+
Close all opened transports.
1014+
1015+
:param abort_ssl: If True, SSL connections will be aborted immediately
1016+
without performing the shutdown handshake. If False (default),
1017+
the behavior is determined by ssl_shutdown_timeout:
1018+
- If ssl_shutdown_timeout=0: connections are aborted
1019+
- If ssl_shutdown_timeout>0: graceful shutdown is performed
1020+
"""
9661021
if self._resolver_owner:
9671022
await self._resolver.close()
968-
await super().close()
1023+
# Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default
1024+
await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0)
9691025

9701026
@property
9711027
def family(self) -> int:
@@ -1200,7 +1256,7 @@ async def _wrap_create_connection(
12001256
# Add ssl_shutdown_timeout for Python 3.11+ when SSL is used
12011257
if (
12021258
kwargs.get("ssl")
1203-
and self._ssl_shutdown_timeout is not None
1259+
and self._ssl_shutdown_timeout
12041260
and sys.version_info >= (3, 11)
12051261
):
12061262
kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
@@ -1343,10 +1399,7 @@ async def _start_tls_connection(
13431399
):
13441400
try:
13451401
# ssl_shutdown_timeout is only available in Python 3.11+
1346-
if (
1347-
sys.version_info >= (3, 11)
1348-
and self._ssl_shutdown_timeout is not None
1349-
):
1402+
if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
13501403
tls_transport = await self._loop.start_tls(
13511404
underlying_transport,
13521405
tls_proto,
@@ -1367,7 +1420,10 @@ async def _start_tls_connection(
13671420
# We need to close the underlying transport since
13681421
# `start_tls()` probably failed before it had a
13691422
# chance to do this:
1370-
underlying_transport.close()
1423+
if self._ssl_shutdown_timeout == 0:
1424+
underlying_transport.abort()
1425+
else:
1426+
underlying_transport.close()
13711427
raise
13721428
if isinstance(tls_transport, asyncio.Transport):
13731429
fingerprint = self._get_fingerprint(req)

docs/client_reference.rst

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The client session supports the context manager protocol for self closing.
5858
max_line_size=8190, \
5959
max_field_size=8190, \
6060
fallback_charset_resolver=lambda r, b: "utf-8", \
61-
ssl_shutdown_timeout=0.1)
61+
ssl_shutdown_timeout=0)
6262

6363
The class for creating client sessions and making requests.
6464

@@ -257,16 +257,31 @@ The client session supports the context manager protocol for self closing.
257257

258258
.. versionadded:: 3.8.6
259259

260-
:param float ssl_shutdown_timeout: Grace period for SSL shutdown handshake on TLS
261-
connections (``0.1`` seconds by default). This usually provides sufficient time
262-
to notify the remote peer of connection closure, helping prevent broken
263-
connections on the server side, while minimizing delays during connector
264-
cleanup. This timeout is passed to the underlying :class:`TCPConnector`
265-
when one is created automatically. Note: This parameter only takes effect
266-
on Python 3.11+.
260+
:param float ssl_shutdown_timeout: **(DEPRECATED)** This parameter is deprecated
261+
and will be removed in aiohttp 4.0. Grace period for SSL shutdown handshake on
262+
TLS connections when the connector is closed (``0`` seconds by default).
263+
By default (``0``), SSL connections are aborted immediately when the
264+
connector is closed, without performing the shutdown handshake. During
265+
normal operation, SSL connections use Python's default SSL shutdown
266+
behavior. Setting this to a positive value (e.g., ``0.1``) will perform
267+
a graceful shutdown when closing the connector, notifying the remote
268+
peer which can help prevent "connection reset" errors at the cost of
269+
additional cleanup time. This timeout is passed to the underlying
270+
:class:`TCPConnector` when one is created automatically.
271+
Note: On Python versions prior to 3.11, only a value of ``0`` is supported;
272+
other values will trigger a warning.
267273

268274
.. versionadded:: 3.12.5
269275

276+
.. versionchanged:: 3.12.11
277+
Changed default from ``0.1`` to ``0`` to abort SSL connections
278+
immediately when the connector is closed. Added support for
279+
``ssl_shutdown_timeout=0`` on all Python versions. A :exc:`RuntimeWarning`
280+
is issued when non-zero values are passed on Python < 3.11.
281+
282+
.. deprecated:: 3.12.11
283+
This parameter is deprecated and will be removed in aiohttp 4.0.
284+
270285
.. attribute:: closed
271286

272287
``True`` if the session has been closed, ``False`` otherwise.
@@ -1196,7 +1211,7 @@ is controlled by *force_close* constructor's parameter).
11961211
force_close=False, limit=100, limit_per_host=0, \
11971212
enable_cleanup_closed=False, timeout_ceil_threshold=5, \
11981213
happy_eyeballs_delay=0.25, interleave=None, loop=None, \
1199-
socket_factory=None, ssl_shutdown_timeout=0.1)
1214+
socket_factory=None, ssl_shutdown_timeout=0)
12001215

12011216
Connector for working with *HTTP* and *HTTPS* via *TCP* sockets.
12021217

@@ -1323,16 +1338,29 @@ is controlled by *force_close* constructor's parameter).
13231338

13241339
.. versionadded:: 3.12
13251340

1326-
:param float ssl_shutdown_timeout: Grace period for SSL shutdown on TLS
1327-
connections (``0.1`` seconds by default). This parameter balances two
1328-
important considerations: usually providing sufficient time to notify
1329-
the remote server (which helps prevent "connection reset" errors),
1330-
while avoiding unnecessary delays during connector cleanup.
1331-
The default value provides a reasonable compromise for most use cases.
1332-
Note: This parameter only takes effect on Python 3.11+.
1341+
:param float ssl_shutdown_timeout: **(DEPRECATED)** This parameter is deprecated
1342+
and will be removed in aiohttp 4.0. Grace period for SSL shutdown on TLS
1343+
connections when the connector is closed (``0`` seconds by default).
1344+
By default (``0``), SSL connections are aborted immediately when the
1345+
connector is closed, without performing the shutdown handshake. During
1346+
normal operation, SSL connections use Python's default SSL shutdown
1347+
behavior. Setting this to a positive value (e.g., ``0.1``) will perform
1348+
a graceful shutdown when closing the connector, notifying the remote
1349+
server which can help prevent "connection reset" errors at the cost of
1350+
additional cleanup time. Note: On Python versions prior to 3.11, only
1351+
a value of ``0`` is supported; other values will trigger a warning.
13331352

13341353
.. versionadded:: 3.12.5
13351354

1355+
.. versionchanged:: 3.12.11
1356+
Changed default from ``0.1`` to ``0`` to abort SSL connections
1357+
immediately when the connector is closed. Added support for
1358+
``ssl_shutdown_timeout=0`` on all Python versions. A :exc:`RuntimeWarning`
1359+
is issued when non-zero values are passed on Python < 3.11.
1360+
1361+
.. deprecated:: 3.12.11
1362+
This parameter is deprecated and will be removed in aiohttp 4.0.
1363+
13361364
.. attribute:: family
13371365

13381366
*TCP* socket family e.g. :data:`socket.AF_INET` or

tests/test_client_functional.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,10 @@ async def test_ssl_client_shutdown_timeout(
698698
) -> None:
699699
# Test that ssl_shutdown_timeout is properly used during connection closure
700700

701-
connector = aiohttp.TCPConnector(ssl=client_ssl_ctx, ssl_shutdown_timeout=0.1)
701+
with pytest.warns(
702+
DeprecationWarning, match="ssl_shutdown_timeout parameter is deprecated"
703+
):
704+
connector = aiohttp.TCPConnector(ssl=client_ssl_ctx, ssl_shutdown_timeout=0.1)
702705

703706
async def streaming_handler(request: web.Request) -> NoReturn:
704707
# Create a streaming response that continuously sends data

0 commit comments

Comments
 (0)