From 133ea002e9cb9ccefb2005e5a6c078a81f0939b7 Mon Sep 17 00:00:00 2001 From: Harsh23Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:17:06 +0530 Subject: [PATCH 1/2] fix(http): propagate per-call timeout to httpx in AsyncApiClient The sync ApiClient.request promotes a per-call timeout= from query params up to httpx's own client-side timeout (added in #534), but the async counterpart did not. Async REST callers that asked for more time via timeout= were silently bound by httpx's default 5s timeout and could fail with a spurious ReadTimeout on slow operations. Mirror the same two-line block in AsyncApiClient.request, and add a unit regression that mocks httpx.AsyncClient.build_request and asserts the timeout kwarg reaches it. The test fails on master and passes with this change. Fixes #1325 --- qdrant_client/http/api_client.py | 2 ++ tests/test_qdrant_client.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/qdrant_client/http/api_client.py b/qdrant_client/http/api_client.py index 4c907c3cf..fda3596c8 100644 --- a/qdrant_client/http/api_client.py +++ b/qdrant_client/http/api_client.py @@ -183,6 +183,8 @@ async def request( # noqa F811 # in order to do a correct join, url join requires base_url to end with /, and url to not start with /, # since url is treated as an absolute path and might truncate prefix in base_url url = urljoin(host, url.format(**path_params)) + if "params" in kwargs and "timeout" in kwargs["params"]: + kwargs["timeout"] = int(kwargs["params"]["timeout"]) request = self._async_client.build_request(method, url, **kwargs) return await self.send(request, type_) diff --git a/tests/test_qdrant_client.py b/tests/test_qdrant_client.py index db7651203..a02a4540e 100644 --- a/tests/test_qdrant_client.py +++ b/tests/test_qdrant_client.py @@ -8,6 +8,7 @@ from pprint import pprint from tempfile import mkdtemp from time import sleep +from unittest.mock import patch import numpy as np import pytest @@ -1757,6 +1758,35 @@ def test_timeout_propagation(): ) +def test_async_rest_timeout_propagation(): + # Regression for #1325: AsyncApiClient.request dropped the per-call + # `timeout=` from kwargs before build_request, so async callers were + # bound by httpx's default 5s timeout regardless of what they asked + # for. The sync ApiClient.request already promotes that value. + from qdrant_client.http.api_client import AsyncApiClient + + captured: dict = {} + + def _capture(method, url, **kwargs): + captured["kwargs"] = kwargs + return type("DummyRequest", (), {"headers": {}})() + + async_client = AsyncApiClient(host="http://localhost:6333") + with patch.object(async_client._async_client, "build_request", _capture): + with pytest.raises(Exception): + asyncio.run( + async_client.request( + type_=None, + method="GET", + url="/collections/{c}", + path_params={"c": "x"}, + params={"timeout": "50"}, + ) + ) + + assert captured["kwargs"].get("timeout") == 50 + + def test_grpc_options(): client_version = importlib.metadata.version("qdrant-client") user_agent = f"python-client/{client_version}" From c5eeef0dd3cda4b9b933b9c0e03e4ba57d3a7f04 Mon Sep 17 00:00:00 2001 From: Harsh23Kashyap <55448981+Harsh23Kashyap@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:19:43 +0530 Subject: [PATCH 2/2] test(http): replace broad pytest.raises with send mock in #1325 regression CodeRabbit flagged `pytest.raises(Exception)` (Ruff B017) on the test added in #1326. The broad catch also hid any real failure after build_request. Replacing it with an AsyncMock for AsyncApiClient.send makes the test fail loudly on any unexpected error and removes the blanket-exception smell. Trims the regression comment to 3 lines (pointing at the bug number and the operation, not re-diagnosing). --- tests/test_qdrant_client.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_qdrant_client.py b/tests/test_qdrant_client.py index a02a4540e..41f313e9e 100644 --- a/tests/test_qdrant_client.py +++ b/tests/test_qdrant_client.py @@ -1759,11 +1759,11 @@ def test_timeout_propagation(): def test_async_rest_timeout_propagation(): - # Regression for #1325: AsyncApiClient.request dropped the per-call - # `timeout=` from kwargs before build_request, so async callers were - # bound by httpx's default 5s timeout regardless of what they asked - # for. The sync ApiClient.request already promotes that value. + # Regression for #1325: async client dropped the per-call `timeout=` + # from kwargs before build_request, leaving callers bound by httpx's + # default 5s timeout. Sync ApiClient.request already promotes it. from qdrant_client.http.api_client import AsyncApiClient + from unittest.mock import AsyncMock captured: dict = {} @@ -1772,17 +1772,17 @@ def _capture(method, url, **kwargs): return type("DummyRequest", (), {"headers": {}})() async_client = AsyncApiClient(host="http://localhost:6333") - with patch.object(async_client._async_client, "build_request", _capture): - with pytest.raises(Exception): - asyncio.run( - async_client.request( - type_=None, - method="GET", - url="/collections/{c}", - path_params={"c": "x"}, - params={"timeout": "50"}, - ) + with patch.object(async_client._async_client, "build_request", _capture), \ + patch.object(async_client, "send", new=AsyncMock()): + asyncio.run( + async_client.request( + type_=None, + method="GET", + url="/collections/{c}", + path_params={"c": "x"}, + params={"timeout": "50"}, ) + ) assert captured["kwargs"].get("timeout") == 50