From c575da0037deb4b2ae1f075189530179e979281b Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Tue, 30 Jun 2026 23:36:53 +0000 Subject: [PATCH 1/2] feat: add ssl parameter to ClientSession Signed-off-by: NIK-TIGER-BILL --- aiohttp/client.py | 19 ++++++++++++++++--- tests/test_client_session.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index 809fa63ed24..1cb56c100dc 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -283,6 +283,7 @@ class ClientSession: "_max_headers", "_resolve_charset", "_default_proxy", + "_default_ssl", "_retry_connection", "_middlewares", ) @@ -317,6 +318,7 @@ def __init__( fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8", middlewares: Sequence[ClientMiddlewareType] = (), ssl_shutdown_timeout: _SENTINEL | None | float = sentinel, + ssl: SSLContext | bool | Fingerprint = True, ) -> None: # We initialise _connector to None immediately, as it's referenced in __del__() # and could cause issues if an exception occurs during initialisation. @@ -349,6 +351,12 @@ def __init__( stacklevel=2, ) + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, Fingerprint, or bool, " + f"got {ssl!r} instead." + ) + if connector is None: connector = TCPConnector(ssl_shutdown_timeout=ssl_shutdown_timeout) # Initialize these three attrs before raising any exception, @@ -407,6 +415,7 @@ def __init__( self._resolve_charset = fallback_charset_resolver self._default_proxy = proxy + self._default_ssl = ssl self._retry_connection: bool = True self._middlewares = tuple(middlewares) @@ -472,7 +481,7 @@ async def _request( read_until_eof: bool = True, proxy: StrOrURL | None = None, timeout: ClientTimeout | _SENTINEL | None = sentinel, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, trace_request_ctx: object = None, @@ -492,6 +501,8 @@ async def _request( method = method.upper() + if ssl is sentinel: + ssl = self._default_ssl if not isinstance(ssl, SSL_ALLOWED_TYPES): raise TypeError( "ssl should be SSLContext, Fingerprint, or bool, " @@ -919,7 +930,7 @@ def ws_connect( params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, compress: int = 0, @@ -994,7 +1005,7 @@ async def _ws_connect( params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, compress: int = 0, @@ -1050,6 +1061,8 @@ async def _ws_connect( extstr = ws_ext_gen(compress=compress) real_headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = extstr + if ssl is sentinel: + ssl = self._default_ssl if not isinstance(ssl, SSL_ALLOWED_TYPES): raise TypeError( "ssl should be SSLContext, Fingerprint, or bool, " diff --git a/tests/test_client_session.py b/tests/test_client_session.py index bee0a49263c..7361ac1d313 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -946,6 +946,35 @@ class OnCall(Exception): await session.close() +async def test_default_ssl() -> None: + class OnCall(Exception): + pass + + request_class_mock = mock.Mock(side_effect=OnCall()) + session = ClientSession(ssl=False, request_class=request_class_mock) + + assert session._default_ssl is False, "`ClientSession._default_ssl` not set" + + with pytest.raises(OnCall): + await session.get("http://example.com") + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is False + ), "`ClientSession._request` uses default ssl not one used in ClientSession.get" + + request_class_mock.reset_mock() + with pytest.raises(OnCall): + await session.get("http://example.com", ssl=True) + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is True + ), "`ClientSession._request` does not use per-request ssl" + + await session.close() + + async def test_request_tracing(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: return web.json_response({"ok": True}) From 1654c96e6f38cf7c73ef2f83ce180d60f47d1bbc Mon Sep 17 00:00:00 2001 From: NIK-TIGER-BILL Date: Wed, 1 Jul 2026 03:27:23 +0000 Subject: [PATCH 2/2] fix: resolve mypy errors and add ws_connect coverage for ssl parameter - Include _SENTINEL in the type annotation for the ssl parameter of _request, ws_connect, and _ws_connect so mypy accepts the sentinel default value. - Add test_default_ssl_ws to cover the default ssl propagation in websocket connections. - Add CHANGES fragment for #13021. Signed-off-by: NIK-TIGER-BILL --- CHANGES/13021.feature.rst | 1 + aiohttp/client.py | 8 ++++---- tests/test_client_session.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 CHANGES/13021.feature.rst diff --git a/CHANGES/13021.feature.rst b/CHANGES/13021.feature.rst new file mode 100644 index 00000000000..8710a886134 --- /dev/null +++ b/CHANGES/13021.feature.rst @@ -0,0 +1 @@ +Added an ``ssl`` parameter to :py:class:`~aiohttp.ClientSession` allowing a default SSL configuration to be applied to all requests -- by :user:`NIK-TIGER-BILL`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 1cb56c100dc..c4ab30cbfe5 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -208,7 +208,7 @@ class _WSConnectOptions(TypedDict, total=False): params: Query headers: LooseHeaders | None proxy: StrOrURL | None - ssl: SSLContext | bool | Fingerprint + ssl: SSLContext | bool | Fingerprint | _SENTINEL server_hostname: str | None proxy_headers: LooseHeaders | None compress: int @@ -481,7 +481,7 @@ async def _request( read_until_eof: bool = True, proxy: StrOrURL | None = None, timeout: ClientTimeout | _SENTINEL | None = sentinel, - ssl: SSLContext | bool | Fingerprint = sentinel, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, trace_request_ctx: object = None, @@ -930,7 +930,7 @@ def ws_connect( params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - ssl: SSLContext | bool | Fingerprint = sentinel, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, compress: int = 0, @@ -1005,7 +1005,7 @@ async def _ws_connect( params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - ssl: SSLContext | bool | Fingerprint = sentinel, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, compress: int = 0, diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 7361ac1d313..cedd8d95b11 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -975,6 +975,35 @@ class OnCall(Exception): await session.close() +async def test_default_ssl_ws() -> None: + class OnCall(Exception): + pass + + request_class_mock = mock.Mock(side_effect=OnCall()) + session = ClientSession(ssl=False, request_class=request_class_mock) + + assert session._default_ssl is False, "`ClientSession._default_ssl` not set" + + with pytest.raises(OnCall): + await session.ws_connect("http://example.com") + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is False + ), "`ClientSession._ws_connect` uses default ssl not one used in ClientSession.ws_connect" + + request_class_mock.reset_mock() + with pytest.raises(OnCall): + await session.ws_connect("http://example.com", ssl=True) + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is True + ), "`ClientSession._ws_connect` does not use per-request ssl" + + await session.close() + + async def test_request_tracing(aiohttp_client: AiohttpClient) -> None: async def handler(request: web.Request) -> web.Response: return web.json_response({"ok": True})