From 811ea2a8f9611bdc6e6b2ae4562e4bc2892bd9a0 Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:14:22 -0700 Subject: [PATCH] fix(local): honor ssl_verify=False for data-plane host scheme and allow dimension for sparse indexes Two interrelated Pinecone Local issues: 1. When ssl_verify=False is set on the Pinecone client, IndexModel.__post_init__ still normalises bare hostnames returned by the control plane to https://. Pinecone Local serves plain HTTP only, so the subsequent TLS handshake fails with SSL_WRONG_VERSION_NUMBER even though the user explicitly opted out of TLS verification. Fix: _resolve_index_host (sync, async, and preview variants) now rewrites https:// to http:// on the resolved host whenever ssl_verify=False, matching the user's expressed intent. Explicit host= arguments are unaffected. 2. validate_create_inputs rejects dimension on sparse indexes with a PineconeValueError, but Pinecone Local requires dimension in the POST /indexes body even for sparse indexes and returns 422 if it is absent. Fix: remove the client-side prohibition so dimension is forwarded to the server when provided; cloud Pinecone continues to ignore the field for sparse indexes, and the server is the appropriate authority for what is valid per environment. Fixes #678 Fixes #679 --- pinecone/_client.py | 7 +++-- pinecone/_internal/indexes_helpers.py | 2 -- pinecone/async_client/pinecone.py | 7 +++-- pinecone/preview/__init__.py | 8 +++++- tests/unit/test_client.py | 41 +++++++++++++++++++++++++++ tests/unit/test_indexes_create.py | 39 ++++++++++++++++++------- 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/pinecone/_client.py b/pinecone/_client.py index 966adbbb..51a18c08 100644 --- a/pinecone/_client.py +++ b/pinecone/_client.py @@ -453,8 +453,11 @@ def _resolve_index_host(self, *, name: str, host: str) -> str: "the index may still be initializing. " "Wait until the index status is 'Ready' before connecting." ) - self._host_cache[name] = desc.host - return desc.host + resolved = desc.host + if not self._config.ssl_verify and resolved.startswith("https://"): + resolved = "http://" + resolved[len("https://"):] + self._host_cache[name] = resolved + return resolved raise ValidationError("Either name or host must be provided to create an Index client.") diff --git a/pinecone/_internal/indexes_helpers.py b/pinecone/_internal/indexes_helpers.py index cf53f68a..7842155b 100644 --- a/pinecone/_internal/indexes_helpers.py +++ b/pinecone/_internal/indexes_helpers.py @@ -116,8 +116,6 @@ def validate_create_inputs( raise PineconeTypeError(f"dimension must be an integer, got {type(dimension).__name__!r}") resolved_vt = resolve_enum_value(vector_type) - if resolved_vt == "sparse" and dimension is not None: - raise ValidationError("dimension must not be provided for sparse indexes") if resolved_vt != "sparse" and dimension is None: raise ValidationError("dimension is required for dense indexes") diff --git a/pinecone/async_client/pinecone.py b/pinecone/async_client/pinecone.py index d5443e69..17b3e173 100644 --- a/pinecone/async_client/pinecone.py +++ b/pinecone/async_client/pinecone.py @@ -818,8 +818,11 @@ async def _resolve_index_host(self, *, name: str, host: str) -> str: "the index may still be initializing. " "Wait until the index status is 'Ready' before connecting." ) - self._host_cache[name] = desc.host - return desc.host + resolved = desc.host + if not self._config.ssl_verify and resolved.startswith("https://"): + resolved = "http://" + resolved[len("https://"):] + self._host_cache[name] = resolved + return resolved raise ValidationError("Either name or host must be provided to create an Index client.") diff --git a/pinecone/preview/__init__.py b/pinecone/preview/__init__.py index b2bb16df..a71adfc4 100644 --- a/pinecone/preview/__init__.py +++ b/pinecone/preview/__init__.py @@ -147,6 +147,8 @@ def index( "the index may still be initializing. " "Wait until the index status is 'Ready' before connecting." ) + if not self._config.ssl_verify and described_host.startswith("https://"): + described_host = "http://" + described_host[len("https://"):] self._host_cache[name] = described_host resolved_host = self._host_cache[name] else: @@ -326,6 +328,7 @@ def index( host_cache = self._host_cache indexes = self.indexes + ssl_verify = self._config.ssl_verify async def _resolve() -> str: if name not in host_cache: @@ -336,7 +339,10 @@ async def _resolve() -> str: "the index may still be initializing. " "Wait until the index status is 'Ready' before connecting." ) - host_cache[name] = desc.host + resolved = desc.host + if not ssl_verify and resolved.startswith("https://"): + resolved = "http://" + resolved[len("https://"):] + host_cache[name] = resolved return host_cache[name] return AsyncPreviewIndex(config=self._config, _host_provider=_resolve) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 3a928f78..8e124467 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -553,3 +553,44 @@ def test_each_client_gets_independent_registry(self) -> None: pc1 = Pinecone(api_key="test-key") pc2 = Pinecone(api_key="test-key") assert pc1._limiter_registry is not pc2._limiter_registry + + +class TestResolveIndexHostSslVerify: + """Test that ssl_verify=False rewrites https:// to http:// when resolving via describe.""" + + def test_ssl_verify_false_rewrites_https_to_http_on_describe(self) -> None: + """When ssl_verify=False the host resolved from describe() uses http://. + + Pinecone Local serves plain HTTP only. A bare hostname returned by the + server is normalised to https:// by IndexModel.__post_init__, which then + causes a TLS handshake against a server that does not speak TLS. + Setting ssl_verify=False signals intent to skip TLS entirely, so the + SDK downgrades the scheme to http:// for the data-plane host. + """ + pc = Pinecone(api_key="pclocal", host="http://localhost:5080", ssl_verify=False) + + mock_desc = MagicMock() + mock_desc.host = "https://localhost:5081" + + with patch.object(pc.indexes, "describe", return_value=mock_desc): + resolved = pc._resolve_index_host(name="my-index", host="") + + assert resolved == "http://localhost:5081" + + def test_ssl_verify_true_preserves_https_on_describe(self) -> None: + """When ssl_verify=True the https:// scheme on the resolved host is kept.""" + pc = Pinecone(api_key="test-key") + + mock_desc = MagicMock() + mock_desc.host = "https://my-index-abc.svc.pinecone.io" + + with patch.object(pc.indexes, "describe", return_value=mock_desc): + resolved = pc._resolve_index_host(name="my-index", host="") + + assert resolved == "https://my-index-abc.svc.pinecone.io" + + def test_explicit_host_not_rewritten(self) -> None: + """An explicit host= argument is returned as-is regardless of ssl_verify.""" + pc = Pinecone(api_key="pclocal", host="http://localhost:5080", ssl_verify=False) + resolved = pc._resolve_index_host(name="", host="http://localhost:5081") + assert resolved == "http://localhost:5081" diff --git a/tests/unit/test_indexes_create.py b/tests/unit/test_indexes_create.py index b3859284..840e3799 100644 --- a/tests/unit/test_indexes_create.py +++ b/tests/unit/test_indexes_create.py @@ -482,18 +482,35 @@ def test_create_name_valid_boundary(indexes: Indexes) -> None: assert isinstance(result, IndexModel) -def test_create_sparse_with_dimension_raises(indexes: Indexes) -> None: - """Sparse index with explicit dimension raises ValidationError.""" - with pytest.raises(ValidationError) as exc_info: - indexes.create( - name="test", - spec=ServerlessSpec(cloud="aws", region="us-east-1"), - vector_type="sparse", - dimension=384, - ) +@respx.mock +def test_create_sparse_with_dimension_allowed(indexes: Indexes) -> None: + """Sparse index accepts an explicit dimension so Pinecone Local can be targeted. + + Pinecone Local requires a ``dimension`` field in the POST body even for sparse + indexes. The SDK no longer rejects this combination at the client-side validation + layer; the field is forwarded to the server and the server decides whether it is + valid for the target environment. + """ + route = respx.post(f"{BASE_URL}/indexes").mock( + return_value=httpx.Response( + 201, json=make_index_response(vector_type="sparse", dimension=1) + ), + ) - assert "dimension" in str(exc_info.value) - assert "sparse" in str(exc_info.value) + result = indexes.create( + name="sparse-local", + spec=ServerlessSpec(cloud="aws", region="us-east-1"), + vector_type="sparse", + dimension=1, + timeout=-1, + ) + + assert result.vector_type == "sparse" + + request = route.calls.last.request + body = json.loads(request.content) + assert body["vector_type"] == "sparse" + assert body["dimension"] == 1 def test_create_with_unrecognized_dict_spec_raises(indexes: Indexes) -> None: