Skip to content

fix: handle empty response body in deserializer (#564)#623

Merged
jhamon merged 7 commits intomainfrom
fix/asyncio-delete-empty-response
Apr 1, 2026
Merged

fix: handle empty response body in deserializer (#564)#623
jhamon merged 7 commits intomainfrom
fix/asyncio-delete-empty-response

Conversation

@jhamon
Copy link
Copy Markdown
Collaborator

@jhamon jhamon commented Apr 1, 2026

Problem

Deserializer crash (fixes #564): When using IndexAsyncio.delete() against pinecone-local, the server returns an empty response body (b""). The deserializer tries orjson.loads(""), which fails, and the except ValueError fallback assigns the raw empty string as return_data. Downstream code then crashes attempting setattr("", "_response_info", ...).

Release process clobbers ruff config: The version bump regex in publish-to-pypi.yaml matches all version = "..." occurrences, including ruff's target-version = "py310". Every release overwrites it with the SDK version, breaking the lint CI check. This was previously fixed in 0f60567 but never merged to main.

Docs link test uses wrong version: test_docs_links.py calls the versioned URL template with __version__ (SDK version, e.g. "8.1.0") instead of API_VERSION ("2025-10"), producing URLs that 404.

Solution

Deserializer: Return None early from Deserializer.deserialize() when response.data is empty or whitespace-only. This is the single shared deserialization codepath for both sync and async clients, so the fix covers all endpoints. Both api_client.py and asyncio_api_client.py already guard _response_info attachment with if return_data is not None.

Release process: Anchor the version bump regex with ^ and re.MULTILINE so only the project version at the start of a line is matched. Restore target-version to "py310".

Docs link test: Use API_VERSION from pinecone.core.openapi.db_control instead of __version__, matching what the production code does.

Key decisions

  • Deserializer fix location: Single fix point in Deserializer.deserialize() rather than patching both api_client.py and asyncio_api_client.py separately.
  • Whitespace handling: The check covers whitespace-only bodies (e.g. "\n", " ") in addition to truly empty strings, since these are equally unparseable and could come from non-standard server implementations.
  • Return type annotations: Updated delete() signatures on both Index and _IndexAsyncio to -> dict[str, Any] | None to reflect that an empty server response now returns None instead of crashing.

Test plan

  • Parametrized tests for empty and whitespace-only bodies (b"", b" ", b"\n", b" \n ") — both async and sync clients
  • Existing behavior preserved: b"{}" still deserializes to dict
  • Full unit test suite passes with no regressions
  • Ruff linting should now pass with restored target-version = "py310"
  • Docs link test should now pass with API_VERSION instead of __version__

Note

Medium Risk
Touches the shared OpenAPI deserialization path, changing empty/whitespace HTTP bodies to deserialize as None, which could affect any endpoint that previously surfaced an empty string. Other changes are limited to type hints/tests and CI/release tooling.

Overview
Fixes crashes when delete endpoints return an empty/whitespace body by making Deserializer.deserialize() return None early, and updates Index.delete()/delete_namespace() (sync + asyncio) return types to dict | None to reflect this.

Adds unit coverage for sync/async call_api behavior with empty bodies, fixes the docs-link test to use API_VERSION (not the SDK package version), and hardens the PyPI publish workflow’s version-bump regex so it only updates the project version line (preventing clobbering Ruff’s target-version, which is also restored to py310).

Written by Cursor Bugbot for commit 84e0dc8. This will update automatically on new commits. Configure here.

jhamon added 2 commits April 1, 2026 14:59
When the server returns an empty response body (e.g. from pinecone-local),
the deserializer falls back to a raw empty string which then crashes
downstream. Return None early from the deserializer when response data
is empty, which is already handled correctly by both sync and async clients.
…tests

- Handle whitespace-only response bodies (e.g. "\n", " ") in deserializer
- Update delete() return type to dict[str, Any] | None in both clients
- Add sync client tests and parametrize over whitespace variants
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale cast contradicts new nullable return type
    • Updated cast statements in both delete methods from cast(dict[str, Any], result) to cast(dict[str, Any] | None, result) to match the nullable return type annotation.

Create PR

Or push these changes by commenting:

@cursor push abb4041c83
Preview (abb4041c83)
diff --git a/pinecone/db_data/index.py b/pinecone/db_data/index.py
--- a/pinecone/db_data/index.py
+++ b/pinecone/db_data/index.py
@@ -821,7 +821,7 @@
             ),
             **self._openapi_kwargs(kwargs),
         )
-        return cast(dict[str, Any], result)
+        return cast(dict[str, Any] | None, result)
 
     @validate_and_convert_errors
     def fetch(self, ids: list[str], namespace: str | None = None, **kwargs) -> FetchResponse:

diff --git a/pinecone/db_data/index_asyncio.py b/pinecone/db_data/index_asyncio.py
--- a/pinecone/db_data/index_asyncio.py
+++ b/pinecone/db_data/index_asyncio.py
@@ -595,7 +595,7 @@
             ),
             **{k: v for k, v in kwargs.items() if k in _OPENAPI_ENDPOINT_PARAMS},
         )
-        return cast(dict[str, Any], result)
+        return cast(dict[str, Any] | None, result)
 
     @validate_and_convert_errors
     async def fetch(self, ids: list[str], namespace: str | None = None, **kwargs) -> FetchResponse:

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread pinecone/db_data/index.py
jhamon added 4 commits April 1, 2026 15:55
The version bump regex in publish-to-pypi.yaml matches all
`version = "..."` occurrences, including ruff's `target-version`.
Anchor the pattern to start-of-line with `^` and `re.MULTILINE`
so only the project version on line 3 is replaced.

Also restores target-version to "py310" after it was overwritten
by the v8.1.0 release.

This is the same fix as 0f60567 which was never merged to main.
The test_docs_links test was calling the versioned URL template with
__version__ (SDK version, e.g. "8.1.0") instead of API_VERSION
("2025-10"), producing URLs like .../api/8.1.0/... that 404.

The production code already uses API_VERSION correctly — only the
test was wrong.
The cast() statements still asserted dict[str, Any] while the return
type was widened to dict[str, Any] | None. This misleads the type
checker and suppresses None-safety warnings.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread pinecone/db_data/index.py
Same issue as delete() — delete_namespace() goes through the same
deserializer path and can return None for empty response bodies.
@jhamon jhamon merged commit f8086ff into main Apr 1, 2026
76 checks passed
@jhamon jhamon deleted the fix/asyncio-delete-empty-response branch April 1, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] asyncio sdk error when deleting vectors

1 participant