From a871e746299b63b3e11a24f7b012fee585a3f479 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Thu, 10 Sep 2026 18:07:31 -0500 Subject: [PATCH] fix(storage): send camelCase pagination keys in list_indexes VectorBucketScope.list_indexes sent next_token and max_results as snake_case body keys. The /vector/ListIndexes endpoint only reads nextToken and maxResults and ignores unknown properties, so both values were silently dropped: the page size was always the server default and a next_token request returned the first page again, making the usual pagination loop never terminate. Use the wire names, as every other vector request in the module already does. Applies to the async client and its generated sync twin. Fixes #1632 --- src/storage/src/storage3/_async/vectors.py | 2 +- src/storage/src/storage3/_sync/vectors.py | 2 +- src/storage/tests/test_client.py | 67 ++++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/storage/src/storage3/_async/vectors.py b/src/storage/src/storage3/_async/vectors.py index 8da9b83f5..9d647ddfc 100644 --- a/src/storage/src/storage3/_async/vectors.py +++ b/src/storage/src/storage3/_async/vectors.py @@ -74,7 +74,7 @@ async def list_indexes( prefix: Optional[str] = None, ) -> ListVectorIndexesResponse: body = self.with_metadata( - next_token=next_token, max_results=max_results, prefix=prefix + nextToken=next_token, maxResults=max_results, prefix=prefix ) data = await self._request.send( http_method="POST", path=["ListIndexes"], body=body diff --git a/src/storage/src/storage3/_sync/vectors.py b/src/storage/src/storage3/_sync/vectors.py index 0673bd23b..cb41947f6 100644 --- a/src/storage/src/storage3/_sync/vectors.py +++ b/src/storage/src/storage3/_sync/vectors.py @@ -72,7 +72,7 @@ def list_indexes( prefix: Optional[str] = None, ) -> ListVectorIndexesResponse: body = self.with_metadata( - next_token=next_token, max_results=max_results, prefix=prefix + nextToken=next_token, maxResults=max_results, prefix=prefix ) data = self._request.send(http_method="POST", path=["ListIndexes"], body=body) return ListVectorIndexesResponse.model_validate_json(data.content) diff --git a/src/storage/tests/test_client.py b/src/storage/tests/test_client.py index 92f0ea012..da06bbcc6 100644 --- a/src/storage/tests/test_client.py +++ b/src/storage/tests/test_client.py @@ -409,3 +409,70 @@ async def test_async_bucket_proxy_exists_false_on_headless_error() -> None: proxy._client, "request", new_callable=AsyncMock, return_value=mock_response ): assert await proxy.exists("missing.txt") is False + + +def _list_indexes_transport(captured: Dict[str, Any]) -> Any: + """A transport that records the ListIndexes request body and replies with one page.""" + import json as _json + + def handler(request: Request) -> Response: + captured["url"] = str(request.url) + captured["body"] = _json.loads(request.content) + return Response(200, json={"indexes": [], "nextToken": None}) + + return handler + + +@pytest.mark.asyncio +async def test_async_list_indexes_sends_camel_case_pagination( + valid_url, valid_headers +) -> None: + from httpx import MockTransport + + captured: Dict[str, Any] = {} + client = AsyncStorageClient( + url=valid_url + "/", + headers=valid_headers, + http_client=AsyncClient( + transport=MockTransport(_list_indexes_transport(captured)) + ), + ) + + await ( + client.vectors() + .from_("my-bucket") + .list_indexes(next_token="cursor-1", max_results=10, prefix="idx-") + ) + + assert captured["url"].endswith("/vector/ListIndexes") + assert captured["body"] == { + "vectorBucketName": "my-bucket", + "nextToken": "cursor-1", + "maxResults": 10, + "prefix": "idx-", + } + + +def test_sync_list_indexes_sends_camel_case_pagination( + valid_url, valid_headers +) -> None: + from httpx import MockTransport + + captured: Dict[str, Any] = {} + client = SyncStorageClient( + url=valid_url + "/", + headers=valid_headers, + http_client=Client(transport=MockTransport(_list_indexes_transport(captured))), + ) + + client.vectors().from_("my-bucket").list_indexes( + next_token="cursor-1", max_results=10, prefix="idx-" + ) + + assert captured["url"].endswith("/vector/ListIndexes") + assert captured["body"] == { + "vectorBucketName": "my-bucket", + "nextToken": "cursor-1", + "maxResults": 10, + "prefix": "idx-", + }