Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/strands_tools/http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1022,3 +1029,4 @@ def http_request(tool: ToolUse, **kwargs: Any) -> ToolResult:
"status": "error",
"content": [{"text": error_text}],
}

73 changes: 73 additions & 0 deletions tests/test_http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading