diff --git a/src/claude_agent_sdk/client.py b/src/claude_agent_sdk/client.py index c6ad1171..93cbb77b 100644 --- a/src/claude_agent_sdk/client.py +++ b/src/claude_agent_sdk/client.py @@ -1,6 +1,7 @@ """Claude SDK Client for interacting with Claude Code.""" import json +import math import os from collections.abc import AsyncIterable, AsyncIterator from dataclasses import asdict, replace @@ -20,6 +21,29 @@ ) +def _parse_timeout_ms_from_env( + env_var: str, default_timeout_ms: float = 60000.0 +) -> float: + """Parse timeout milliseconds from environment, falling back safely. + + Accepts integer or float-like strings. Invalid or non-finite values return + the provided default. + """ + raw = os.environ.get(env_var) + if raw is None: + return default_timeout_ms + + try: + parsed = float(raw) + except (TypeError, ValueError): + return default_timeout_ms + + if not math.isfinite(parsed): + return default_timeout_ms + + return parsed + + class ClaudeSDKClient: """ Client for bidirectional, interactive conversations with Claude Code. @@ -152,8 +176,8 @@ async def _empty_stream() -> AsyncIterator[dict[str, Any]]: # Calculate initialize timeout from CLAUDE_CODE_STREAM_CLOSE_TIMEOUT env var if set # CLAUDE_CODE_STREAM_CLOSE_TIMEOUT is in milliseconds, convert to seconds - initialize_timeout_ms = int( - os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000") + initialize_timeout_ms = _parse_timeout_ms_from_env( + "CLAUDE_CODE_STREAM_CLOSE_TIMEOUT" ) initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0) diff --git a/tests/test_streaming_client.py b/tests/test_streaming_client.py index 1c2b6980..a786247e 100644 --- a/tests/test_streaming_client.py +++ b/tests/test_streaming_client.py @@ -2,6 +2,7 @@ import asyncio import json +import os import sys import tempfile from pathlib import Path @@ -1203,6 +1204,34 @@ async def _test(): anyio.run(_test) + def test_connect_with_invalid_timeout_env_falls_back_to_default(self): + """Malformed timeout env values should not break connect().""" + + async def _test(): + with ( + patch.dict( + os.environ, + {"CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "60s"}, + clear=False, + ), + patch( + "claude_agent_sdk._internal.transport.subprocess_cli.SubprocessCLITransport" + ) as mock_transport_class, + ): + mock_transport = create_mock_transport() + mock_transport_class.return_value = mock_transport + + client = ClaudeSDKClient() + await client.connect() + + assert client._query is not None + # Default is 60000ms, and initialize timeout enforces a 60s minimum. + assert client._query._initialize_timeout == 60.0 + + await client.disconnect() + + anyio.run(_test) + def test_disconnect_without_connect(self): """Test disconnecting without connecting first."""