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
33 changes: 33 additions & 0 deletions docs/user/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,39 @@ library to use SSLv3::
num_pools=connections, maxsize=maxsize,
block=block, ssl_version=ssl.PROTOCOL_SSLv3)

Example: pre-intialized SSL Context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Requests usually parse either default CA bundle or a user-provided one
on every request. This can be slow, so pre-created ``ssl.SSLContext``
may be specified using a custom Transport Adapter.

Here is an example of such implementation:

::

import ssl
import requests.adapters

ssl_context = ssl.create_default_context()

class SSLContextAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
kwargs["ssl_context"] = ssl_context
return super().init_poolmanager(*args, **kwargs)

def cert_verify(self, *_args, **_kwargs) -> None:
# Override HTTPAdapter cert_verify method, it tries to load certs from disk otherwise
pass
Comment on lines +1054 to +1056
Copy link
Author

@Tasssadar Tasssadar Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This override was not necessary between (likely, not tested) 2.32.0 and 2.32.5, see #7040 , because the SSL context caching worked out that way.

Is it intended that it is required now?


with requests.Session() as session:
# Disable environment configuration, it overrides the passed SSLContext
session.trust_env = False

session.mount("https://", SSLContextAdapter())

# HTTPS requests will now use the pre-created `ssl_context`

Example: Automatic Retries
^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
39 changes: 39 additions & 0 deletions tests/test_adapters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import ssl
from unittest.mock import MagicMock

import pytest
import pytest_httpbin.certs

import requests.adapters


Expand All @@ -6,3 +12,36 @@ def test_request_url_trims_leading_path_separators():
a = requests.adapters.HTTPAdapter()
p = requests.Request(method="GET", url="http://127.0.0.1:10000//v:h").prepare()
assert "/v:h" == a.request_url(p, {})


def test_adapter_ssl_context(httpbin_secure):
# We can't verify that SSL actually works on localhost, but we can check
# if the ssl context gets actually used.
ssl_context = ssl.create_default_context(cafile=pytest_httpbin.certs.where())
ssl_mock = MagicMock(spec=ssl.SSLContext, wraps=ssl_context)

class SSLContextAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
kwargs["ssl_context"] = ssl_mock
return super().init_poolmanager(*args, **kwargs)

def cert_verify(self, *_args, **_kwargs) -> None:
# Override HTTPAdapter method, it tries to load certs from disk otherwise
pass

with requests.Session() as session:
# Disable environment configuration, it overrides the passed SSLContext
session.trust_env = False

session.mount("https://", SSLContextAdapter())

res = session.get(httpbin_secure())
res.raise_for_status()

# Check that the SSLContext was actually used
ssl_mock.wrap_socket.assert_called()

# Check it wasn't modified by requests or urllib3
ssl_mock.load_verify_locations.assert_not_called()
ssl_mock.load_default_certs.assert_not_called()
ssl_mock.load_cert_chain.assert_not_called()