-
Notifications
You must be signed in to change notification settings - Fork 841
Fix connect() crash on malformed CLAUDE_CODE_STREAM_CLOSE_TIMEOUT #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Comment on lines
1206
to
+1233
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Nit: The assertion Extended reasoning...What the issue isThe test Why the assertion is vacuous for fallback verificationThe production code path is: initialize_timeout_ms = _parse_timeout_ms_from_env("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT") # default: 60000.0
initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0)Walk through the concrete scenarios:
All of these pass the assertion. The What the test does verify correctlyThe test does successfully verify its primary stated goal: "Malformed timeout env values should not break connect()." The fact that ImpactThis is a test quality issue, not a production bug. If someone accidentally changed the default in Suggested improvementAdd a test with a valid high value to confirm parsing works end-to-end:
Alternatively, test |
||
|
|
||
| def test_disconnect_without_connect(self): | ||
| """Test disconnecting without connecting first.""" | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 The
_parse_timeout_ms_from_env()helper is only applied inClaudeSDKClient.connect()but the identical vulnerableint()pattern remains insrc/claude_agent_sdk/_internal/client.py:103-105(thequery()one-shot path). SettingCLAUDE_CODE_STREAM_CLOSE_TIMEOUT=60swill still crashquery()with aValueError. The fix should use_parse_timeout_ms_from_envin both locations.Extended reasoning...
What the bug is
This PR introduces a
_parse_timeout_ms_from_env()helper insrc/claude_agent_sdk/client.pythat safely parses theCLAUDE_CODE_STREAM_CLOSE_TIMEOUTenvironment variable, falling back to a default when the value is malformed (e.g.,"60s","inf", or other non-numeric strings). The helper is correctly applied inClaudeSDKClient.connect()at line 178. However, the identical vulnerable pattern was not updated in the separatequery()one-shot code path atsrc/claude_agent_sdk/_internal/client.py:103-105.The specific code path
In
src/claude_agent_sdk/_internal/client.py, lines 103-105:This is the code path used when calling the top-level
query()function (the one-shot, non-streaming API). The comment on line 102 even says# Match ClaudeSDKClient.connect(), confirming the intent is for these two locations to stay in sync.Step-by-step proof
CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=60sin their environment (a plausible mistake, mixing up seconds vs milliseconds notation).query(prompt="Hello")which enters the one-shot code path in_internal/client.py.os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000")returns"60s".int("60s")raisesValueError: invalid literal for int() with base 10: 60s.Meanwhile, the same environment variable is now handled gracefully in
ClaudeSDKClient.connect()thanks to this PR — creating an inconsistency where the streaming path works but the one-shot path crashes.Impact
This is the exact same class of bug the PR aims to fix. Users who use the
query()function (which is the simpler, recommended API for one-off prompts) remain vulnerable to the crash, while only users of the streamingClaudeSDKClientare protected. The PR description states the goal is to avoid "startup failures in real automation workflows where environment variables can be inherited, templated, or accidentally misformatted" — but that goal is only half-achieved.How to fix
Import and use the
_parse_timeout_ms_from_envhelper fromclient.pyin_internal/client.py, replacing theint(os.environ.get(...))call on lines 103-105 with:Alternatively, move the helper to a shared utility module to avoid circular imports, or duplicate it in
_internal/client.py.