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
7 changes: 5 additions & 2 deletions pinecone/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cached host skips ssl rewrite

Medium Severity

With ssl_verify=False, the https://http:// rewrite runs only when _resolve_index_host misses the shared _host_cache and calls describe. On a cache hit it returns the stored host unchanged, so entries written by indexes.describe (always normalized to https://) still trigger TLS against plain-HTTP Pinecone Local.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 811ea2a. Configure here.

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.")

Expand Down
2 changes: 0 additions & 2 deletions pinecone/_internal/indexes_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
7 changes: 5 additions & 2 deletions pinecone/async_client/pinecone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")

Expand Down
8 changes: 7 additions & 1 deletion pinecone/preview/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
39 changes: 28 additions & 11 deletions tests/unit/test_indexes_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down