Skip to content

Commit 2061f2a

Browse files
authored
fix: search needs higher timeout (#107)
1 parent 7431164 commit 2061f2a

7 files changed

Lines changed: 135 additions & 16 deletions

File tree

src/deepset_mcp/api/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections.abc import AsyncIterator
33
from contextlib import AbstractAsyncContextManager, asynccontextmanager
44
from types import TracebackType
5-
from typing import Any, Self, TypeVar, overload
5+
from typing import Any, Literal, Self, TypeVar, overload
66

77
from deepset_mcp.api.custom_components.resource import CustomComponentsResource
88
from deepset_mcp.api.haystack_service.resource import HaystackServiceResource
@@ -68,6 +68,7 @@ async def request(
6868
method: str = "GET",
6969
data: dict[str, Any] | None = None,
7070
headers: dict[str, str] | None = None,
71+
timeout: float | None | Literal["config"] = "config",
7172
**kwargs: Any,
7273
) -> TransportResponse[T]: ...
7374

@@ -80,6 +81,7 @@ async def request(
8081
method: str = "GET",
8182
data: dict[str, Any] | None = None,
8283
headers: dict[str, str] | None = None,
84+
timeout: float | None | Literal["config"] = "config",
8385
**kwargs: Any,
8486
) -> TransportResponse[Any]: ...
8587

@@ -91,6 +93,7 @@ async def request(
9193
data: dict[str, Any] | None = None,
9294
headers: dict[str, str] | None = None,
9395
response_type: type[T] | None = None,
96+
timeout: float | None | Literal["config"] = "config",
9497
**kwargs: Any,
9598
) -> TransportResponse[Any]:
9699
"""
@@ -108,6 +111,9 @@ async def request(
108111
Additional headers to include
109112
response_type : type[T], optional
110113
Expected response type for type checking
114+
timeout : float | None | Literal["config"], optional
115+
Request timeout in seconds. If "config", uses transport config timeout.
116+
If None, disables timeout. If float, uses specific timeout.
111117
**kwargs : Any
112118
Additional arguments to pass to transport
113119
@@ -138,6 +144,7 @@ async def request(
138144
json=data,
139145
headers=request_headers,
140146
response_type=response_type,
147+
timeout=timeout,
141148
**kwargs,
142149
)
143150

src/deepset_mcp/api/exceptions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ def __init__(self, message: Any = "Bad request", detail: Any | None = None) -> N
3232
super().__init__(status_code=400, message=message, detail=detail)
3333

3434

35+
class RequestTimeoutError(Exception):
36+
"""Exception raised when a request times out."""
37+
38+
def __init__(
39+
self,
40+
method: str,
41+
url: str,
42+
timeout: float | None | str,
43+
duration: float | None = None,
44+
detail: str | None = None,
45+
):
46+
"""Initialize the timeout exception with request context."""
47+
self.method = method
48+
self.url = url
49+
self.timeout = timeout
50+
self.duration = duration
51+
self.detail = detail
52+
53+
timeout_display = f"{timeout}s" if isinstance(timeout, int | float) else str(timeout)
54+
55+
if duration is not None:
56+
message = f"Request timed out after {duration:.2f}s (limit: {timeout_display}): {method} {url}"
57+
else:
58+
message = f"Request timed out (limit: {timeout_display}): {method} {url}"
59+
60+
super().__init__(message)
61+
62+
3563
class UnexpectedAPIError(DeepsetAPIError):
3664
"""Catch-all exception for unexpected API errors."""
3765

src/deepset_mcp/api/pipeline/resource.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ async def search(
312312
method="POST",
313313
data=data,
314314
response_type=dict[str, Any],
315+
timeout=180.0,
315316
)
316317

317318
raise_for_status(resp)

src/deepset_mcp/api/protocols.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections.abc import AsyncIterator
22
from contextlib import AbstractAsyncContextManager
33
from types import TracebackType
4-
from typing import Any, Protocol, Self, TypeVar, overload
4+
from typing import Any, Literal, Protocol, Self, TypeVar, overload
55

66
from deepset_mcp.api.custom_components.models import CustomComponentInstallationList
77
from deepset_mcp.api.indexes.models import Index, IndexList
@@ -93,6 +93,7 @@ async def request(
9393
method: str = "GET",
9494
data: dict[str, Any] | None = None,
9595
headers: dict[str, str] | None = None,
96+
timeout: float | None | Literal["config"] = "config",
9697
**kwargs: Any,
9798
) -> TransportResponse[T]: ...
9899

@@ -105,6 +106,7 @@ async def request(
105106
method: str = "GET",
106107
data: dict[str, Any] | None = None,
107108
headers: dict[str, str] | None = None,
109+
timeout: float | None | Literal["config"] = "config",
108110
**kwargs: Any,
109111
) -> TransportResponse[Any]: ...
110112

@@ -116,6 +118,7 @@ async def request(
116118
method: str = "GET",
117119
data: dict[str, Any] | None = None,
118120
headers: dict[str, str] | None = None,
121+
timeout: float | None | Literal["config"] = "config",
119122
**kwargs: Any,
120123
) -> TransportResponse[Any]:
121124
"""Make a request to the API."""

src/deepset_mcp/api/transport.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import json
2+
import time
23
from collections.abc import AsyncIterator
34
from contextlib import AbstractAsyncContextManager, asynccontextmanager
45
from dataclasses import dataclass
5-
from typing import Any, Generic, Protocol, TypeVar, cast, overload
6+
from typing import Any, Generic, Literal, Protocol, TypeVar, cast, overload
67

78
import httpx
89

9-
from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
10+
from deepset_mcp.api.exceptions import BadRequestError, RequestTimeoutError, ResourceNotFoundError, UnexpectedAPIError
1011

1112
T = TypeVar("T")
1213

@@ -111,16 +112,34 @@ class TransportProtocol(Protocol):
111112

112113
@overload
113114
async def request(
114-
self, method: str, url: str, *, response_type: type[T], **kwargs: Any
115+
self,
116+
method: str,
117+
url: str,
118+
*,
119+
response_type: type[T],
120+
timeout: float | None | Literal["config"] = "config",
121+
**kwargs: Any,
115122
) -> TransportResponse[T]: ...
116123

117124
@overload
118125
async def request(
119-
self, method: str, url: str, *, response_type: None = None, **kwargs: Any
126+
self,
127+
method: str,
128+
url: str,
129+
*,
130+
response_type: None = None,
131+
timeout: float | None | Literal["config"] = "config",
132+
**kwargs: Any,
120133
) -> TransportResponse[Any]: ...
121134

122135
async def request(
123-
self, method: str, url: str, *, response_type: type[T] | None = None, **kwargs: Any
136+
self,
137+
method: str,
138+
url: str,
139+
*,
140+
response_type: type[T] | None = None,
141+
timeout: float | None | Literal["config"] = "config",
142+
**kwargs: Any,
124143
) -> TransportResponse[Any]:
125144
"""Send a regular HTTP request and return the response."""
126145
...
@@ -189,16 +208,34 @@ def __init__(
189208

190209
@overload
191210
async def request(
192-
self, method: str, url: str, *, response_type: type[T], **kwargs: Any
211+
self,
212+
method: str,
213+
url: str,
214+
*,
215+
response_type: type[T],
216+
timeout: float | None | Literal["config"] = "config",
217+
**kwargs: Any,
193218
) -> TransportResponse[T]: ...
194219

195220
@overload
196221
async def request(
197-
self, method: str, url: str, *, response_type: None = None, **kwargs: Any
222+
self,
223+
method: str,
224+
url: str,
225+
*,
226+
response_type: None = None,
227+
timeout: float | None | Literal["config"] = "config",
228+
**kwargs: Any,
198229
) -> TransportResponse[Any]: ...
199230

200231
async def request(
201-
self, method: str, url: str, *, response_type: type[T] | None = None, **kwargs: Any
232+
self,
233+
method: str,
234+
url: str,
235+
*,
236+
response_type: type[T] | None = None,
237+
timeout: float | None | Literal["config"] = "config",
238+
**kwargs: Any,
202239
) -> TransportResponse[Any]:
203240
"""
204241
Send a regular HTTP request and return the response.
@@ -211,6 +248,9 @@ async def request(
211248
URL endpoint
212249
response_type : type[T], optional
213250
Expected response type for type checking
251+
timeout : float | None | Literal["config"], optional
252+
Request timeout in seconds. If "config", uses transport config timeout.
253+
If None, disables timeout. If float, uses specific timeout.
214254
**kwargs : Any
215255
Additional arguments to pass to httpx
216256
@@ -219,7 +259,26 @@ async def request(
219259
TransportResponse[T]
220260
The response with parsed JSON if available
221261
"""
222-
response = await self._client.request(method, url, **kwargs)
262+
if timeout != "config":
263+
kwargs["timeout"] = timeout
264+
265+
start_time = time.time()
266+
try:
267+
response = await self._client.request(method, url, **kwargs)
268+
except httpx.TimeoutException as e:
269+
duration = time.time() - start_time
270+
timeout_value = kwargs.get("timeout", "config default")
271+
272+
detail = None
273+
if "search" in url and duration > 60:
274+
detail = (
275+
"Search operations can take longer with large document collections or complex pipelines. "
276+
"Consider increasing the timeout for search requests."
277+
)
278+
279+
raise RequestTimeoutError(
280+
method=method, url=url, timeout=timeout_value, duration=duration, detail=detail
281+
) from e
223282

224283
if response_type is not None:
225284
raw = response.json()

test/unit/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections.abc import AsyncGenerator, AsyncIterator
33
from contextlib import AbstractAsyncContextManager, asynccontextmanager
44
from types import TracebackType
5-
from typing import Any, Self, TypeVar, overload
5+
from typing import Any, Literal, Self, TypeVar, overload
66

77
from deepset_mcp.api.protocols import (
88
AsyncClientProtocol,
@@ -65,6 +65,7 @@ async def request(
6565
method: str = "GET",
6666
data: dict[str, Any] | None = None,
6767
headers: dict[str, str] | None = None,
68+
timeout: float | None | Literal["config"] = "config",
6869
**kwargs: Any,
6970
) -> TransportResponse[T]: ...
7071

@@ -77,6 +78,7 @@ async def request(
7778
method: str = "GET",
7879
data: dict[str, Any] | None = None,
7980
headers: dict[str, str] | None = None,
81+
timeout: float | None | Literal["config"] = "config",
8082
**kwargs: Any,
8183
) -> TransportResponse[Any]: ...
8284

@@ -88,6 +90,7 @@ async def request(
8890
method: str = "GET",
8991
data: dict[str, Any] | None = None,
9092
headers: dict[str, str] | None = None,
93+
timeout: float | None | Literal["config"] = "config",
9194
**kwargs: Any,
9295
) -> TransportResponse[Any]:
9396
"""

test/unit/test_async_deepset_client.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
from collections.abc import AsyncIterator
33
from contextlib import asynccontextmanager
4-
from typing import Any, TypeVar, overload
4+
from typing import Any, Literal, TypeVar, overload
55

66
import pytest
77
import pytest_asyncio
@@ -19,16 +19,34 @@ def __init__(self) -> None:
1919

2020
@overload
2121
async def request(
22-
self, method: str, url: str, *, response_type: type[T], **kwargs: Any
22+
self,
23+
method: str,
24+
url: str,
25+
*,
26+
response_type: type[T],
27+
timeout: float | None | Literal["config"] = "config",
28+
**kwargs: Any,
2329
) -> TransportResponse[T]: ...
2430

2531
@overload
2632
async def request(
27-
self, method: str, url: str, *, response_type: None = None, **kwargs: Any
33+
self,
34+
method: str,
35+
url: str,
36+
*,
37+
response_type: None = None,
38+
timeout: float | None | Literal["config"] = "config",
39+
**kwargs: Any,
2840
) -> TransportResponse[Any]: ...
2941

3042
async def request(
31-
self, method: str, url: str, *, response_type: type[T] | None = None, **kwargs: Any
43+
self,
44+
method: str,
45+
url: str,
46+
*,
47+
response_type: type[T] | None = None,
48+
timeout: float | None | Literal["config"] = "config",
49+
**kwargs: Any,
3250
) -> TransportResponse[Any]:
3351
# Record the request and return a dummy response
3452
record: dict[str, Any] = {"method": method, "url": url, **kwargs}

0 commit comments

Comments
 (0)