Skip to content

Commit 19afa7a

Browse files
committed
transport: cache default ssl context
1 parent c7c13f1 commit 19afa7a

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

httpx/_transports/default.py

+45-8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import contextlib
3030
import typing
31+
from functools import lru_cache
3132
from types import TracebackType
3233

3334
if typing.TYPE_CHECKING:
@@ -70,6 +71,34 @@
7071

7172
HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {}
7273

74+
_DEFAULT_VERIFY = True
75+
_DEFAULT_CERT = None
76+
_DEFAULT_TRUST_ENV = True
77+
78+
79+
@lru_cache(maxsize=1)
80+
def _create_default_ssl_context() -> ssl.SSLContext:
81+
return create_ssl_context(
82+
verify=_DEFAULT_VERIFY,
83+
cert=_DEFAULT_CERT,
84+
trust_env=_DEFAULT_TRUST_ENV,
85+
)
86+
87+
88+
def _create_or_reuse_ssl_context(
89+
verify: ssl.SSLContext | str | bool = _DEFAULT_VERIFY,
90+
cert: CertTypes | None = _DEFAULT_CERT,
91+
trust_env: bool = _DEFAULT_TRUST_ENV,
92+
) -> ssl.SSLContext:
93+
if (verify, cert, trust_env) == (
94+
_DEFAULT_VERIFY,
95+
_DEFAULT_CERT,
96+
_DEFAULT_TRUST_ENV,
97+
):
98+
return _create_default_ssl_context()
99+
else:
100+
return create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
101+
73102

74103
def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
75104
import httpcore
@@ -135,9 +164,9 @@ def close(self) -> None:
135164
class HTTPTransport(BaseTransport):
136165
def __init__(
137166
self,
138-
verify: ssl.SSLContext | str | bool = True,
139-
cert: CertTypes | None = None,
140-
trust_env: bool = True,
167+
verify: ssl.SSLContext | str | bool = _DEFAULT_VERIFY,
168+
cert: CertTypes | None = _DEFAULT_CERT,
169+
trust_env: bool = _DEFAULT_TRUST_ENV,
141170
http1: bool = True,
142171
http2: bool = False,
143172
limits: Limits = DEFAULT_LIMITS,
@@ -150,7 +179,11 @@ def __init__(
150179
import httpcore
151180

152181
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
153-
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
182+
ssl_context = _create_or_reuse_ssl_context(
183+
verify=verify,
184+
cert=cert,
185+
trust_env=trust_env,
186+
)
154187

155188
if proxy is None:
156189
self._pool = httpcore.ConnectionPool(
@@ -279,9 +312,9 @@ async def aclose(self) -> None:
279312
class AsyncHTTPTransport(AsyncBaseTransport):
280313
def __init__(
281314
self,
282-
verify: ssl.SSLContext | str | bool = True,
283-
cert: CertTypes | None = None,
284-
trust_env: bool = True,
315+
verify: ssl.SSLContext | str | bool = _DEFAULT_VERIFY,
316+
cert: CertTypes | None = _DEFAULT_CERT,
317+
trust_env: bool = _DEFAULT_TRUST_ENV,
285318
http1: bool = True,
286319
http2: bool = False,
287320
limits: Limits = DEFAULT_LIMITS,
@@ -294,7 +327,11 @@ def __init__(
294327
import httpcore
295328

296329
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
297-
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
330+
ssl_context = _create_or_reuse_ssl_context(
331+
verify=verify,
332+
cert=cert,
333+
trust_env=trust_env,
334+
)
298335

299336
if proxy is None:
300337
self._pool = httpcore.AsyncConnectionPool(

tests/client/test_transports.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from httpx import AsyncHTTPTransport, HTTPTransport
4+
from httpx._transports.default import _DEFAULT_TRUST_ENV, _DEFAULT_VERIFY
5+
6+
DIFFERENT_VERIFY = {"verify": not _DEFAULT_VERIFY}
7+
DIFFERENT_CERT_ENV = {"cert": ()}
8+
DIFFERENT_TRUST_ENV = {"trust_env": not _DEFAULT_TRUST_ENV}
9+
10+
11+
@pytest.mark.parametrize("transport", [HTTPTransport, AsyncHTTPTransport])
12+
def test_default_ssl_config_cached(transport):
13+
transport_1 = transport()
14+
transport_2 = transport()
15+
assert transport_1._pool._ssl_context is not None
16+
assert transport_2._pool._ssl_context is not None
17+
18+
assert transport_1._pool._ssl_context is transport_2._pool._ssl_context
19+
20+
21+
@pytest.mark.parametrize("transport", [HTTPTransport, AsyncHTTPTransport])
22+
@pytest.mark.parametrize(
23+
("kwargs_1", "kwargs_2"),
24+
[
25+
({}, DIFFERENT_VERIFY),
26+
(DIFFERENT_VERIFY, {}),
27+
(DIFFERENT_VERIFY, DIFFERENT_VERIFY),
28+
({}, DIFFERENT_CERT_ENV),
29+
(DIFFERENT_CERT_ENV, {}),
30+
(DIFFERENT_CERT_ENV, DIFFERENT_CERT_ENV),
31+
({}, DIFFERENT_TRUST_ENV),
32+
(DIFFERENT_TRUST_ENV, {}),
33+
(DIFFERENT_TRUST_ENV, DIFFERENT_TRUST_ENV),
34+
],
35+
)
36+
def test_non_default_ssl_config_not_cached(transport, kwargs_1, kwargs_2):
37+
transport_1 = transport(**kwargs_1)
38+
transport_2 = transport(**kwargs_2)
39+
assert transport_1._pool._ssl_context is not None
40+
assert transport_2._pool._ssl_context is not None
41+
42+
assert transport_1._pool._ssl_context is not transport_2._pool._ssl_context

0 commit comments

Comments
 (0)