Skip to content

Commit 1e111b1

Browse files
fix: honor TLS verification settings in HTTP clients
1 parent ef3a3db commit 1e111b1

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

‎src/typesense/async_/api_call.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def __init__(self, config: Configuration):
160160
self.request_handler = RequestHandler(config)
161161
self._client = httpx.AsyncClient(
162162
timeout=config.connection_timeout_seconds,
163+
verify=config.verify,
163164
)
164165

165166
async def __aenter__(self) -> "AsyncApiCall":

‎src/typesense/sync/api_call.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def __init__(self, config: Configuration):
160160
self.request_handler = RequestHandler(config)
161161
self._client = httpx.Client(
162162
timeout=config.connection_timeout_seconds,
163+
verify=config.verify,
163164
)
164165

165166
def __enter__(self) -> "ApiCall":

‎tests/api_call_ssl_test.py‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Regression tests for TLS configuration in both HTTP clients."""
2+
3+
import ssl
4+
import warnings
5+
from pathlib import Path
6+
7+
import certifi
8+
import pytest
9+
from pytest_mock import MockerFixture
10+
11+
from typesense.async_.api_call import AsyncApiCall
12+
from typesense.configuration import Configuration
13+
from typesense.sync.api_call import ApiCall
14+
15+
16+
@pytest.fixture(params=[ApiCall, AsyncApiCall])
17+
def api_call_class(request):
18+
"""Exercise both the async source and the generated sync client."""
19+
return request.param
20+
21+
22+
async def close_api_call(api_call):
23+
if isinstance(api_call, AsyncApiCall):
24+
await api_call.aclose()
25+
else:
26+
api_call.close()
27+
28+
29+
@pytest.mark.parametrize("verify", [True, False])
30+
async def test_verification_mode(fake_config, api_call_class, verify):
31+
"""The effective TLS context must honor explicit verification settings."""
32+
fake_config.verify = verify
33+
api_call = api_call_class(fake_config)
34+
try:
35+
ssl_context = api_call._client._transport._pool._ssl_context
36+
assert ssl_context.verify_mode == (
37+
ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
38+
)
39+
assert ssl_context.check_hostname is verify
40+
finally:
41+
await close_api_call(api_call)
42+
43+
44+
async def test_custom_ca_bundle(
45+
fake_config: Configuration,
46+
api_call_class,
47+
tmp_path: Path,
48+
mocker: MockerFixture,
49+
):
50+
"""A configured CA bundle must reach the real SSL context builder."""
51+
ca_bundle = tmp_path / "custom-ca.pem"
52+
ca_bundle.write_bytes(Path(certifi.where()).read_bytes())
53+
fake_config.verify = str(ca_bundle)
54+
create_context = mocker.spy(ssl, "create_default_context")
55+
56+
with warnings.catch_warnings():
57+
warnings.filterwarnings(
58+
"ignore", message="`verify=<str>`", category=DeprecationWarning
59+
)
60+
api_call = api_call_class(fake_config)
61+
try:
62+
create_context.assert_any_call(cafile=str(ca_bundle))
63+
finally:
64+
await close_api_call(api_call)
65+
66+
67+
async def test_missing_ca_bundle(fake_config, api_call_class, tmp_path):
68+
"""An invalid CA path must fail instead of silently using default trust roots."""
69+
fake_config.verify = str(tmp_path / "missing-ca.pem")
70+
with warnings.catch_warnings():
71+
warnings.filterwarnings(
72+
"ignore", message="`verify=<str>`", category=DeprecationWarning
73+
)
74+
with pytest.raises(FileNotFoundError):
75+
api_call_class(fake_config)

0 commit comments

Comments
 (0)