diff --git a/src/strands_tools/http_request.py b/src/strands_tools/http_request.py index 95176184..f54e834b 100644 --- a/src/strands_tools/http_request.py +++ b/src/strands_tools/http_request.py @@ -148,6 +148,10 @@ "type": "integer", "description": "Maximum number of redirects to follow (default: 30)", }, + "timeout": { + "type": "number", + "description": "Request timeout in seconds (default: 30). Use a very large value (e.g. 9999) for effectively no timeout.", + }, "convert_to_markdown": { "type": "boolean", "description": "Convert HTML responses to markdown format (default: False).", @@ -879,6 +883,9 @@ def http_request(tool: ToolUse, **kwargs: Any) -> ToolResult: if body: request_kwargs["data"] = body + # Set default timeout (30s) to prevent indefinite blocking + request_kwargs.setdefault("timeout", tool_input.get("timeout", 30)) + # Execute request with metrics start_time = time.time() response = session.request(**request_kwargs) @@ -1022,3 +1029,4 @@ def http_request(tool: ToolUse, **kwargs: Any) -> ToolResult: "status": "error", "content": [{"text": error_text}], } + diff --git a/tests/test_http_request.py b/tests/test_http_request.py index d3a084cc..a0e8db66 100644 --- a/tests/test_http_request.py +++ b/tests/test_http_request.py @@ -1326,3 +1326,76 @@ def test_payment_required_header_missing(): assert "Status Code: 200" in result_text # The headers dict should still be present but without Payment-Required assert "Headers:" in result_text + + +@responses.activate +def test_timeout_default_value_passed_to_request(): + """Test that default timeout=30 is actually passed to session.request.""" + responses.add( + responses.GET, + "https://example.com/api/verify-timeout", + json={"status": "ok"}, + status=200, + ) + + tool_use = { + "toolUseId": "test-verify-timeout-id", + "input": { + "method": "GET", + "url": "https://example.com/api/verify-timeout", + }, + } + + http_request.SESSION_CACHE.clear() + with patch("strands_tools.http_request.get_user_input") as mock_input: + mock_input.return_value = "y" + # Patch the session's request method to capture the timeout kwarg + with patch("strands_tools.http_request.get_cached_session") as mock_get_session: + mock_session = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.content = b'{"status": "ok"}' + mock_response.cookies = {} + mock_session.request.return_value = mock_response + mock_session.cookies = {} + mock_get_session.return_value = mock_session + + http_request.http_request(tool=tool_use) + + # Verify timeout=30 was passed to session.request + call_kwargs = mock_session.request.call_args[1] + assert "timeout" in call_kwargs + assert call_kwargs["timeout"] == 30 + + +@responses.activate +def test_custom_timeout_value_passed_to_request(): + """Test that custom timeout value is passed to session.request.""" + tool_use = { + "toolUseId": "test-custom-timeout-verify-id", + "input": { + "method": "GET", + "url": "https://example.com/api/custom-timeout-verify", + "timeout": 120, + }, + } + + http_request.SESSION_CACHE.clear() + with patch("strands_tools.http_request.get_user_input") as mock_input: + mock_input.return_value = "y" + with patch("strands_tools.http_request.get_cached_session") as mock_get_session: + mock_session = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.headers = {} + mock_response.content = b'{"status": "ok"}' + mock_response.cookies = {} + mock_session.request.return_value = mock_response + mock_session.cookies = {} + mock_get_session.return_value = mock_session + + http_request.http_request(tool=tool_use) + + call_kwargs = mock_session.request.call_args[1] + assert call_kwargs["timeout"] == 120