From d8c4b30cb8f87e5286e2b49d8167ef69585c3b12 Mon Sep 17 00:00:00 2001 From: Sam Collinson Date: Fri, 5 Dec 2025 12:12:07 +1300 Subject: [PATCH 1/6] create_ssl_context: Don't skip ctx.load_cert_chain call when verify is a str --- src/httpx2/httpx2/_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/httpx2/httpx2/_config.py b/src/httpx2/httpx2/_config.py index 46a6e6ec..03efd1a1 100644 --- a/src/httpx2/httpx2/_config.py +++ b/src/httpx2/httpx2/_config.py @@ -50,8 +50,9 @@ def create_ssl_context( ) warnings.warn(message, DeprecationWarning) if os.path.isdir(verify): - return ssl.create_default_context(capath=verify) - return ssl.create_default_context(cafile=verify) + ctx = ssl.create_default_context(capath=verify) + else: + ctx = ssl.create_default_context(cafile=verify) else: ctx = verify From e8a93b327681c9c31d55e261fecfa283e6b8923d Mon Sep 17 00:00:00 2001 From: "Michiel W. Beijen" Date: Mon, 25 May 2026 11:25:53 +0200 Subject: [PATCH 2/6] Add regression test for create_ssl_context verify=str with cert Covers the case where `verify` is a string path and `cert` is a tuple: the cert chain must still be loaded. Before the previous commit, the function returned early and silently dropped the client cert. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/httpx2/test_config.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/httpx2/test_config.py b/tests/httpx2/test_config.py index ef47844e..053d2921 100644 --- a/tests/httpx2/test_config.py +++ b/tests/httpx2/test_config.py @@ -83,6 +83,15 @@ def test_load_ssl_config_no_verify() -> None: assert context.check_hostname is False +def test_create_ssl_context_verify_str_with_cert( + cert_pem_file: str, cert_private_key_file: str, tmp_path: Path +) -> None: + """Cert chain is applied even when `verify` is given as a string path.""" + missing = str(tmp_path / "missing.pem") + with pytest.warns(DeprecationWarning), pytest.raises(OSError): + httpx2.create_ssl_context(verify=cert_pem_file, cert=(missing, missing)) + + def test_SSLContext_with_get_request(server: TestServer, cert_pem_file: str) -> None: context = httpx2.create_ssl_context() context.load_verify_locations(cert_pem_file) From f5aa1c10fd2575ecb3446c563724be58f72cd2e4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 25 May 2026 13:16:20 +0200 Subject: [PATCH 3/6] Place pragma on the uncovered ssl context branches --- src/httpx2/httpx2/_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/httpx2/httpx2/_config.py b/src/httpx2/httpx2/_config.py index 03efd1a1..0a545cc9 100644 --- a/src/httpx2/httpx2/_config.py +++ b/src/httpx2/httpx2/_config.py @@ -42,7 +42,7 @@ def create_ssl_context( ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE - elif isinstance(verify, str): # pragma: nocover + elif isinstance(verify, str): message = ( "`verify=` is deprecated. " "Use `verify=ssl.create_default_context(cafile=...)` " @@ -50,20 +50,20 @@ def create_ssl_context( ) warnings.warn(message, DeprecationWarning) if os.path.isdir(verify): - ctx = ssl.create_default_context(capath=verify) + ctx = ssl.create_default_context(capath=verify) # pragma: nocover else: ctx = ssl.create_default_context(cafile=verify) else: ctx = verify - if cert: # pragma: nocover + if cert: message = ( "`cert=...` is deprecated. Use `verify=` instead," "with `.load_cert_chain()` to configure the certificate chain." ) warnings.warn(message, DeprecationWarning) if isinstance(cert, str): - ctx.load_cert_chain(cert) + ctx.load_cert_chain(cert) # pragma: nocover else: ctx.load_cert_chain(*cert) From ce761fc9406e39b9d21f63f4eb6d8122fe79efcf Mon Sep 17 00:00:00 2001 From: "Michiel W. Beijen" Date: Mon, 25 May 2026 14:30:58 +0200 Subject: [PATCH 4/6] create_ssl_context: raise on `verify=` combined with `cert=...` The combination of two deprecated parameters silently dropped the client cert chain. Rather than fixing the silent skip, refuse the combination and direct users to build an `ssl.SSLContext` themselves, matching the path the existing deprecation warnings already suggest. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/httpx2/httpx2/_config.py | 10 ++++++++-- tests/httpx2/test_config.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/httpx2/httpx2/_config.py b/src/httpx2/httpx2/_config.py index 46a6e6ec..0e9bb7ff 100644 --- a/src/httpx2/httpx2/_config.py +++ b/src/httpx2/httpx2/_config.py @@ -42,14 +42,20 @@ def create_ssl_context( ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE - elif isinstance(verify, str): # pragma: nocover + elif isinstance(verify, str): + if cert: + raise TypeError( + "`verify=` cannot be combined with `cert=...`. " + "Build an `ssl.SSLContext` and pass it as `verify=`, " + "using `.load_cert_chain()` to configure the certificate chain." + ) message = ( "`verify=` is deprecated. " "Use `verify=ssl.create_default_context(cafile=...)` " "or `verify=ssl.create_default_context(capath=...)` instead." ) warnings.warn(message, DeprecationWarning) - if os.path.isdir(verify): + if os.path.isdir(verify): # pragma: nocover return ssl.create_default_context(capath=verify) return ssl.create_default_context(cafile=verify) else: diff --git a/tests/httpx2/test_config.py b/tests/httpx2/test_config.py index ef47844e..abf2f501 100644 --- a/tests/httpx2/test_config.py +++ b/tests/httpx2/test_config.py @@ -83,6 +83,17 @@ def test_load_ssl_config_no_verify() -> None: assert context.check_hostname is False +def test_create_ssl_context_verify_str(cert_pem_file: str) -> None: + with pytest.warns(DeprecationWarning, match="`verify=` is deprecated"): + context = httpx2.create_ssl_context(verify=cert_pem_file) + assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED + + +def test_create_ssl_context_verify_str_with_cert_raises(cert_pem_file: str, cert_private_key_file: str) -> None: + with pytest.raises(TypeError, match="cannot be combined with `cert=...`"): + httpx2.create_ssl_context(verify=cert_pem_file, cert=(cert_pem_file, cert_private_key_file)) + + def test_SSLContext_with_get_request(server: TestServer, cert_pem_file: str) -> None: context = httpx2.create_ssl_context() context.load_verify_locations(cert_pem_file) From 25de33e108f70473283b7209a16fccb2a7df3f68 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 1 Jun 2026 14:39:43 +0200 Subject: [PATCH 5/6] Return directly in verify=str branch instead of reassigning ctx --- src/httpx2/httpx2/_config.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/httpx2/httpx2/_config.py b/src/httpx2/httpx2/_config.py index b210f944..e51ae0b1 100644 --- a/src/httpx2/httpx2/_config.py +++ b/src/httpx2/httpx2/_config.py @@ -55,10 +55,9 @@ def create_ssl_context( "or `verify=ssl.create_default_context(capath=...)` instead." ) warnings.warn(message, DeprecationWarning) - if os.path.isdir(verify): - ctx = ssl.create_default_context(capath=verify) # pragma: no cover - else: - ctx = ssl.create_default_context(cafile=verify) + if os.path.isdir(verify): # pragma: no cover + return ssl.create_default_context(capath=verify) + return ssl.create_default_context(cafile=verify) else: ctx = verify From a75b1cd96f954195a69495c4b8a121586440f3dd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 1 Jun 2026 14:44:16 +0200 Subject: [PATCH 6/6] Exclude deprecated cert branch from coverage --- src/httpx2/httpx2/_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/httpx2/httpx2/_config.py b/src/httpx2/httpx2/_config.py index e51ae0b1..069441e7 100644 --- a/src/httpx2/httpx2/_config.py +++ b/src/httpx2/httpx2/_config.py @@ -61,14 +61,14 @@ def create_ssl_context( else: ctx = verify - if cert: + if cert: # pragma: no cover message = ( "`cert=...` is deprecated. Use `verify=` instead," "with `.load_cert_chain()` to configure the certificate chain." ) warnings.warn(message, DeprecationWarning) if isinstance(cert, str): - ctx.load_cert_chain(cert) # pragma: no cover + ctx.load_cert_chain(cert) else: ctx.load_cert_chain(*cert)