Skip to content

Commit 86a9a38

Browse files
authored
Abort ssl connections on close when ssl_shutdown_timeout is 0 (#11148)
1 parent 6d85efc commit 86a9a38

11 files changed

+672
-94
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
@@ -297,7 +297,7 @@ def __init__(
297297
max_field_size: int = 8190,
298298
fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8",
299299
middlewares: Sequence[ClientMiddlewareType] = (),
300-
ssl_shutdown_timeout: Optional[float] = 0.1,
300+
ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
301301
) -> None:
302302
# We initialise _connector to None immediately, as it's referenced in __del__()
303303
# and could cause issues if an exception occurs during initialisation.
@@ -323,6 +323,13 @@ def __init__(
323323
)
324324
self._timeout = timeout
325325

326+
if ssl_shutdown_timeout is not sentinel:
327+
warnings.warn(
328+
"The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
329+
DeprecationWarning,
330+
stacklevel=2,
331+
)
332+
326333
if connector is None:
327334
connector = TCPConnector(ssl_shutdown_timeout=ssl_shutdown_timeout)
328335
# Initialize these three attrs before raising any exception,

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: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,19 @@ def closed(self) -> bool:
205205
class _TransportPlaceholder:
206206
"""placeholder for BaseConnector.connect function"""
207207

208-
__slots__ = ("closed",)
208+
__slots__ = ("closed", "transport")
209209

210210
def __init__(self, closed_future: asyncio.Future[Optional[Exception]]) -> None:
211211
"""Initialize a placeholder for a transport."""
212212
self.closed = closed_future
213+
self.transport = None
213214

214215
def close(self) -> None:
215216
"""Close the placeholder."""
216217

218+
def abort(self) -> None:
219+
"""Abort the placeholder (does nothing)."""
220+
217221

218222
class BaseConnector:
219223
"""Base connector class.
@@ -431,17 +435,22 @@ def _cleanup_closed(self) -> None:
431435
timeout_ceil_threshold=self._timeout_ceil_threshold,
432436
)
433437

434-
async def close(self) -> None:
435-
"""Close all opened transports."""
436-
waiters = self._close_immediately()
438+
async def close(self, *, abort_ssl: bool = False) -> None:
439+
"""Close all opened transports.
440+
441+
:param abort_ssl: If True, SSL connections will be aborted immediately
442+
without performing the shutdown handshake. This provides
443+
faster cleanup at the cost of less graceful disconnection.
444+
"""
445+
waiters = self._close_immediately(abort_ssl=abort_ssl)
437446
if waiters:
438447
results = await asyncio.gather(*waiters, return_exceptions=True)
439448
for res in results:
440449
if isinstance(res, Exception):
441450
err_msg = "Error while closing connector: " + repr(res)
442451
client_logger.debug(err_msg)
443452

444-
def _close_immediately(self) -> List[Awaitable[object]]:
453+
def _close_immediately(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
445454
waiters: List[Awaitable[object]] = []
446455

447456
if self._closed:
@@ -463,12 +472,26 @@ def _close_immediately(self) -> List[Awaitable[object]]:
463472

464473
for data in self._conns.values():
465474
for proto, _ in data:
466-
proto.close()
475+
if (
476+
abort_ssl
477+
and proto.transport
478+
and proto.transport.get_extra_info("sslcontext") is not None
479+
):
480+
proto.abort()
481+
else:
482+
proto.close()
467483
if closed := proto.closed:
468484
waiters.append(closed)
469485

470486
for proto in self._acquired:
471-
proto.close()
487+
if (
488+
abort_ssl
489+
and proto.transport
490+
and proto.transport.get_extra_info("sslcontext") is not None
491+
):
492+
proto.abort()
493+
else:
494+
proto.close()
472495
if closed := proto.closed:
473496
waiters.append(closed)
474497

@@ -838,11 +861,12 @@ class TCPConnector(BaseConnector):
838861
socket_factory - A SocketFactoryType function that, if supplied,
839862
will be used to create sockets given an
840863
AddrInfoType.
841-
ssl_shutdown_timeout - Grace period for SSL shutdown handshake on TLS
842-
connections. Default is 0.1 seconds. This usually
843-
allows for a clean SSL shutdown by notifying the
844-
remote peer of connection closure, while avoiding
845-
excessive delays during connector cleanup.
864+
ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0.
865+
Grace period for SSL shutdown handshake on TLS
866+
connections. Default is 0 seconds (immediate abort).
867+
This parameter allowed for a clean SSL shutdown by
868+
notifying the remote peer of connection closure,
869+
while avoiding excessive delays during connector cleanup.
846870
Note: Only takes effect on Python 3.11+.
847871
"""
848872

@@ -866,7 +890,7 @@ def __init__(
866890
happy_eyeballs_delay: Optional[float] = 0.25,
867891
interleave: Optional[int] = None,
868892
socket_factory: Optional[SocketFactoryType] = None,
869-
ssl_shutdown_timeout: Optional[float] = 0.1,
893+
ssl_shutdown_timeout: Union[_SENTINEL, None, float] = sentinel,
870894
):
871895
super().__init__(
872896
keepalive_timeout=keepalive_timeout,
@@ -903,26 +927,57 @@ def __init__(
903927
self._interleave = interleave
904928
self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
905929
self._socket_factory = socket_factory
906-
self._ssl_shutdown_timeout = ssl_shutdown_timeout
930+
self._ssl_shutdown_timeout: Optional[float]
907931

908-
def _close_immediately(self) -> List[Awaitable[object]]:
932+
# Handle ssl_shutdown_timeout with warning for Python < 3.11
933+
if ssl_shutdown_timeout is sentinel:
934+
self._ssl_shutdown_timeout = 0
935+
else:
936+
# Deprecation warning for ssl_shutdown_timeout parameter
937+
warnings.warn(
938+
"The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0",
939+
DeprecationWarning,
940+
stacklevel=2,
941+
)
942+
if (
943+
sys.version_info < (3, 11)
944+
and ssl_shutdown_timeout is not None
945+
and ssl_shutdown_timeout != 0
946+
):
947+
warnings.warn(
948+
f"ssl_shutdown_timeout={ssl_shutdown_timeout} is ignored on Python < 3.11; "
949+
"only ssl_shutdown_timeout=0 is supported. The timeout will be ignored.",
950+
RuntimeWarning,
951+
stacklevel=2,
952+
)
953+
self._ssl_shutdown_timeout = ssl_shutdown_timeout
954+
955+
async def close(self, *, abort_ssl: bool = False) -> None:
956+
"""Close all opened transports.
957+
958+
:param abort_ssl: If True, SSL connections will be aborted immediately
959+
without performing the shutdown handshake. If False (default),
960+
the behavior is determined by ssl_shutdown_timeout:
961+
- If ssl_shutdown_timeout=0: connections are aborted
962+
- If ssl_shutdown_timeout>0: graceful shutdown is performed
963+
"""
964+
if self._resolver_owner:
965+
await self._resolver.close()
966+
# Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default
967+
await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0)
968+
969+
def _close_immediately(self, *, abort_ssl: bool = False) -> List[Awaitable[object]]:
909970
for fut in chain.from_iterable(self._throttle_dns_futures.values()):
910971
fut.cancel()
911972

912-
waiters = super()._close_immediately()
973+
waiters = super()._close_immediately(abort_ssl=abort_ssl)
913974

914975
for t in self._resolve_host_tasks:
915976
t.cancel()
916977
waiters.append(t)
917978

918979
return waiters
919980

920-
async def close(self) -> None:
921-
"""Close all opened transports."""
922-
if self._resolver_owner:
923-
await self._resolver.close()
924-
await super().close()
925-
926981
@property
927982
def family(self) -> int:
928983
"""Socket family like AF_INET."""
@@ -1155,7 +1210,7 @@ async def _wrap_create_connection(
11551210
# Add ssl_shutdown_timeout for Python 3.11+ when SSL is used
11561211
if (
11571212
kwargs.get("ssl")
1158-
and self._ssl_shutdown_timeout is not None
1213+
and self._ssl_shutdown_timeout
11591214
and sys.version_info >= (3, 11)
11601215
):
11611216
kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
@@ -1233,10 +1288,7 @@ async def _start_tls_connection(
12331288
):
12341289
try:
12351290
# ssl_shutdown_timeout is only available in Python 3.11+
1236-
if (
1237-
sys.version_info >= (3, 11)
1238-
and self._ssl_shutdown_timeout is not None
1239-
):
1291+
if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
12401292
tls_transport = await self._loop.start_tls(
12411293
underlying_transport,
12421294
tls_proto,
@@ -1257,7 +1309,10 @@ async def _start_tls_connection(
12571309
# We need to close the underlying transport since
12581310
# `start_tls()` probably failed before it had a
12591311
# chance to do this:
1260-
underlying_transport.close()
1312+
if self._ssl_shutdown_timeout == 0:
1313+
underlying_transport.abort()
1314+
else:
1315+
underlying_transport.close()
12611316
raise
12621317
if isinstance(tls_transport, asyncio.Transport):
12631318
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

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

242242
.. versionadded:: 3.8.6
243243

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

252258
.. versionadded:: 3.12.5
253259

260+
.. versionchanged:: 3.12.11
261+
Changed default from ``0.1`` to ``0`` to abort SSL connections
262+
immediately when the connector is closed. Added support for
263+
``ssl_shutdown_timeout=0`` on all Python versions. A :exc:`RuntimeWarning`
264+
is issued when non-zero values are passed on Python < 3.11.
265+
266+
.. deprecated:: 3.12.11
267+
This parameter is deprecated and will be removed in aiohttp 4.0.
268+
254269
.. attribute:: closed
255270

256271
``True`` if the session has been closed, ``False`` otherwise.
@@ -1180,7 +1195,7 @@ is controlled by *force_close* constructor's parameter).
11801195
force_close=False, limit=100, limit_per_host=0, \
11811196
enable_cleanup_closed=False, timeout_ceil_threshold=5, \
11821197
happy_eyeballs_delay=0.25, interleave=None, loop=None, \
1183-
socket_factory=None, ssl_shutdown_timeout=0.1)
1198+
socket_factory=None, ssl_shutdown_timeout=0)
11841199

11851200
Connector for working with *HTTP* and *HTTPS* via *TCP* sockets.
11861201

@@ -1307,16 +1322,29 @@ is controlled by *force_close* constructor's parameter).
13071322

13081323
.. versionadded:: 3.12
13091324

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

13181337
.. versionadded:: 3.12.5
13191338

1339+
.. versionchanged:: 3.12.11
1340+
Changed default from ``0.1`` to ``0`` to abort SSL connections
1341+
immediately when the connector is closed. Added support for
1342+
``ssl_shutdown_timeout=0`` on all Python versions. A :exc:`RuntimeWarning`
1343+
is issued when non-zero values are passed on Python < 3.11.
1344+
1345+
.. deprecated:: 3.12.11
1346+
This parameter is deprecated and will be removed in aiohttp 4.0.
1347+
13201348
.. attribute:: family
13211349

13221350
*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
@@ -717,7 +717,10 @@ async def test_ssl_client_shutdown_timeout(
717717
) -> None:
718718
# Test that ssl_shutdown_timeout is properly used during connection closure
719719

720-
connector = aiohttp.TCPConnector(ssl=client_ssl_ctx, ssl_shutdown_timeout=0.1)
720+
with pytest.warns(
721+
DeprecationWarning, match="ssl_shutdown_timeout parameter is deprecated"
722+
):
723+
connector = aiohttp.TCPConnector(ssl=client_ssl_ctx, ssl_shutdown_timeout=0.1)
721724

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

0 commit comments

Comments
 (0)