Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/13021.feature.rst
Original file line number Diff line number Diff line change
@@ -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`.
21 changes: 17 additions & 4 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -283,6 +283,7 @@ class ClientSession:
"_max_headers",
"_resolve_charset",
"_default_proxy",
"_default_ssl",
"_retry_connection",
"_middlewares",
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 = sentinel,
server_hostname: str | None = None,
proxy_headers: LooseHeaders | None = None,
trace_request_ctx: object = None,
Expand All @@ -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, "
Expand Down Expand Up @@ -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 = sentinel,
server_hostname: str | None = None,
proxy_headers: LooseHeaders | None = None,
compress: int = 0,
Expand Down Expand Up @@ -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 = sentinel,
server_hostname: str | None = None,
proxy_headers: LooseHeaders | None = None,
compress: int = 0,
Expand Down Expand Up @@ -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, "
Expand Down
58 changes: 58 additions & 0 deletions tests/test_client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,64 @@ 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_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})
Expand Down
Loading