Skip to content

Commit bbf8cff

Browse files
Fix api_timeout not being used in APIRemoteWorkspace (#1206)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 7aded6f commit bbf8cff

File tree

4 files changed

+163
-3
lines changed

4 files changed

+163
-3
lines changed

openhands-sdk/openhands/sdk/workspace/remote/async_remote_workspace.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,32 @@ class AsyncRemoteWorkspace(RemoteWorkspaceMixin):
1515

1616
_client: httpx.AsyncClient | None = PrivateAttr(default=None)
1717

18+
async def reset_client(self) -> None:
19+
"""Reset the HTTP client to force re-initialization.
20+
21+
This is useful when connection parameters (host, api_key) have changed
22+
and the client needs to be recreated with new values.
23+
"""
24+
if self._client is not None:
25+
try:
26+
await self._client.aclose()
27+
except Exception:
28+
pass
29+
self._client = None
30+
1831
@property
1932
def client(self) -> httpx.AsyncClient:
2033
client = self._client
2134
if client is None:
22-
client = httpx.AsyncClient(base_url=self.host)
35+
# Configure reasonable timeouts for HTTP requests
36+
# - connect: 10 seconds to establish connection
37+
# - read: 60 seconds to read response (for LLM operations)
38+
# - write: 10 seconds to send request
39+
# - pool: 10 seconds to get connection from pool
40+
timeout = httpx.Timeout(connect=10.0, read=60.0, write=10.0, pool=10.0)
41+
client = httpx.AsyncClient(
42+
base_url=self.host, timeout=timeout, headers=self._headers
43+
)
2344
self._client = client
2445
return client
2546

openhands-sdk/openhands/sdk/workspace/remote/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ class RemoteWorkspace(RemoteWorkspaceMixin, BaseWorkspace):
3030

3131
_client: httpx.Client | None = PrivateAttr(default=None)
3232

33+
def reset_client(self) -> None:
34+
"""Reset the HTTP client to force re-initialization.
35+
36+
This is useful when connection parameters (host, api_key) have changed
37+
and the client needs to be recreated with new values.
38+
"""
39+
if self._client is not None:
40+
try:
41+
self._client.close()
42+
except Exception:
43+
pass
44+
self._client = None
45+
3346
@property
3447
def client(self) -> httpx.Client:
3548
client = self._client

openhands-workspace/openhands/workspace/remote_api/workspace.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ class APIRemoteWorkspace(RemoteWorkspace):
7979
_runtime_url: str | None = PrivateAttr(default=None)
8080
_session_api_key: str | None = PrivateAttr(default=None)
8181

82+
@property
83+
def client(self) -> httpx.Client:
84+
"""Override client property to use api_timeout for HTTP requests."""
85+
client = self._client
86+
if client is None:
87+
# Use api_timeout for the read timeout to allow longer operations
88+
timeout = httpx.Timeout(
89+
connect=10.0,
90+
read=self.api_timeout,
91+
write=10.0,
92+
pool=10.0,
93+
)
94+
client = httpx.Client(
95+
base_url=self.host, timeout=timeout, headers=self._headers
96+
)
97+
self._client = client
98+
return client
99+
82100
@property
83101
def _api_headers(self):
84102
"""Headers for runtime API requests."
@@ -120,8 +138,9 @@ def _start_or_attach_to_runtime(self) -> None:
120138
logger.info(f"Runtime ready at {self._runtime_url}")
121139
self.host = self._runtime_url.rstrip("/")
122140
self.api_key = self._session_api_key
123-
self._client = None # Reset HTTP client with new host and API key
124-
_ = self.client # Initialize client by accessing the property
141+
# Reset HTTP client with new host and API key
142+
self.reset_client()
143+
# Verify client is properly initialized
125144
assert self.client is not None
126145
assert self.client.base_url == self.host
127146

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Test APIRemoteWorkspace timeout configuration."""
2+
3+
from unittest.mock import patch
4+
5+
import httpx
6+
7+
8+
def test_api_timeout_is_used_in_client():
9+
"""Test that api_timeout parameter is used for the HTTP client timeout."""
10+
from openhands.workspace import APIRemoteWorkspace
11+
12+
# Mock the entire initialization process
13+
with patch.object(APIRemoteWorkspace, "_start_or_attach_to_runtime") as mock_init:
14+
mock_init.return_value = None
15+
16+
# Create a workspace with custom api_timeout
17+
custom_timeout = 300.0
18+
workspace = APIRemoteWorkspace(
19+
runtime_api_url="https://example.com",
20+
runtime_api_key="test-key",
21+
server_image="test-image",
22+
api_timeout=custom_timeout,
23+
)
24+
25+
# The runtime properties need to be set for client initialization
26+
workspace._runtime_id = "test-runtime-id"
27+
workspace._runtime_url = "https://test-runtime.com"
28+
workspace._session_api_key = "test-session-key"
29+
workspace.host = workspace._runtime_url
30+
31+
# Access the client property to trigger initialization
32+
client = workspace.client
33+
34+
# Verify that the client's timeout uses the custom api_timeout
35+
assert isinstance(client, httpx.Client)
36+
assert client.timeout.read == custom_timeout
37+
assert client.timeout.connect == 10.0
38+
assert client.timeout.write == 10.0
39+
assert client.timeout.pool == 10.0
40+
41+
# Clean up
42+
workspace._runtime_id = None # Prevent cleanup from trying to stop runtime
43+
workspace.cleanup()
44+
45+
46+
def test_api_timeout_default_value():
47+
"""Test that the default api_timeout is 60 seconds."""
48+
from openhands.workspace import APIRemoteWorkspace
49+
50+
with patch.object(APIRemoteWorkspace, "_start_or_attach_to_runtime") as mock_init:
51+
mock_init.return_value = None
52+
53+
workspace = APIRemoteWorkspace(
54+
runtime_api_url="https://example.com",
55+
runtime_api_key="test-key",
56+
server_image="test-image",
57+
)
58+
59+
# The runtime properties need to be set for client initialization
60+
workspace._runtime_id = "test-runtime-id"
61+
workspace._runtime_url = "https://test-runtime.com"
62+
workspace._session_api_key = "test-session-key"
63+
workspace.host = workspace._runtime_url
64+
65+
# Access the client property to trigger initialization
66+
client = workspace.client
67+
68+
# Verify default timeout is 60 seconds
69+
assert client.timeout.read == 60.0
70+
71+
# Clean up
72+
workspace._runtime_id = None
73+
workspace.cleanup()
74+
75+
76+
def test_different_timeout_values():
77+
"""Test that different api_timeout values are correctly applied."""
78+
from openhands.workspace import APIRemoteWorkspace
79+
80+
test_timeouts = [30.0, 120.0, 600.0]
81+
82+
for timeout_value in test_timeouts:
83+
with patch.object(
84+
APIRemoteWorkspace, "_start_or_attach_to_runtime"
85+
) as mock_init:
86+
mock_init.return_value = None
87+
88+
workspace = APIRemoteWorkspace(
89+
runtime_api_url="https://example.com",
90+
runtime_api_key="test-key",
91+
server_image="test-image",
92+
api_timeout=timeout_value,
93+
)
94+
95+
workspace._runtime_id = "test-runtime-id"
96+
workspace._runtime_url = "https://test-runtime.com"
97+
workspace._session_api_key = "test-session-key"
98+
workspace.host = workspace._runtime_url
99+
100+
client = workspace.client
101+
102+
assert client.timeout.read == timeout_value, (
103+
f"Expected timeout {timeout_value}, got {client.timeout.read}"
104+
)
105+
106+
workspace._runtime_id = None
107+
workspace.cleanup()

0 commit comments

Comments
 (0)