From 591ddc9f5b8ccf4579c9d38dc8795e4a25c1001c Mon Sep 17 00:00:00 2001 From: okxint Date: Thu, 2 Jul 2026 10:28:51 +0530 Subject: [PATCH 1/2] fix: treat empty-string ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN as absent os.environ.get() returns "" for a present-but-empty var, not None. When the SDK stored an empty string as auth_token, _bearer_auth emitted an "Authorization: Bearer " header (trailing space, no token). h11 rejects that value at write time with LocalProtocolError, surfaced to callers as APIConnectionError. Apply `or None` to both env reads so an empty string is treated the same as the variable being unset. Adds regression tests for both Anthropic and AsyncAnthropic confirming that api_key, auth_token, and auth_headers are all None / empty when the env vars are set to "". --- src/anthropic/_client.py | 8 ++++---- tests/test_client.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/anthropic/_client.py b/src/anthropic/_client.py index 76b840608..3f527902c 100644 --- a/src/anthropic/_client.py +++ b/src/anthropic/_client.py @@ -211,8 +211,8 @@ def __init__( or profile is not None ) if not has_explicit_credential: - api_key = os.environ.get("ANTHROPIC_API_KEY") - auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") + api_key = os.environ.get("ANTHROPIC_API_KEY") or None + auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") or None self.api_key = api_key self.auth_token = auth_token # --- end credentials support --- @@ -628,8 +628,8 @@ def __init__( or profile is not None ) if not has_explicit_credential: - api_key = os.environ.get("ANTHROPIC_API_KEY") - auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") + api_key = os.environ.get("ANTHROPIC_API_KEY") or None + auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") or None self.api_key = api_key self.auth_token = auth_token # --- end credentials support --- diff --git a/tests/test_client.py b/tests/test_client.py index b6ebf9e4d..61fd0994b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -434,6 +434,17 @@ def test_validate_headers(self) -> None: request2 = client2._build_request(FinalRequestOptions(method="get", url="/foo", headers={"X-Api-Key": Omit()})) assert request2.headers.get("X-Api-Key") is None + def test_empty_string_env_credentials_treated_as_absent(self) -> None: + # An empty-string env var must not be used as a credential — it would + # produce a malformed "Authorization: Bearer " header rejected by h11. + with mock.patch("anthropic._client.default_credentials", return_value=None): + with update_env(ANTHROPIC_API_KEY="", ANTHROPIC_AUTH_TOKEN=""): + client = Anthropic(base_url=base_url, _strict_response_validation=True) + + assert client.api_key is None + assert client.auth_token is None + assert client.auth_headers == {} + def test_default_query_option(self) -> None: client = Anthropic( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} @@ -1546,6 +1557,17 @@ def test_validate_headers(self) -> None: request2 = client2._build_request(FinalRequestOptions(method="get", url="/foo", headers={"X-Api-Key": Omit()})) assert request2.headers.get("X-Api-Key") is None + def test_empty_string_env_credentials_treated_as_absent(self) -> None: + # An empty-string env var must not be used as a credential — it would + # produce a malformed "Authorization: Bearer " header rejected by h11. + with mock.patch("anthropic._client.default_credentials", return_value=None): + with update_env(ANTHROPIC_API_KEY="", ANTHROPIC_AUTH_TOKEN=""): + client = AsyncAnthropic(base_url=base_url, _strict_response_validation=True) + + assert client.api_key is None + assert client.auth_token is None + assert client.auth_headers == {} + async def test_default_query_option(self) -> None: client = AsyncAnthropic( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} From 63165f308b633c69eefa73d1d2ff16bc9c3ccbfe Mon Sep 17 00:00:00 2001 From: okxint Date: Mon, 7 Sep 2026 06:12:40 +0530 Subject: [PATCH 2/2] test(tools): add needs_symlinks marker to three symlink tests Three tests in test_agent_toolset.py create symlinks without a platform guard, so they fail on Windows when the user is not elevated and Developer Mode is off: test_read_through_symlink_escape_is_rejected test_glob_post_filters_symlink_escape test_grep_skips_symlinked_files All three raise OSError: [WinError 1314] because os.symlink / Path.symlink_to need the SeCreateSymbolicLinkPrivilege on Windows. Add a local needs_symlinks marker (matching the needs_pydantic_v2 pattern already in the file) and apply it to all three tests so they skip on Windows instead of failing. Fixes #1915 Co-Authored-By: Claude Sonnet 4.6 --- tests/lib/tools/test_agent_toolset.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/lib/tools/test_agent_toolset.py b/tests/lib/tools/test_agent_toolset.py index a4ebfd435..e2b3d0e2f 100644 --- a/tests/lib/tools/test_agent_toolset.py +++ b/tests/lib/tools/test_agent_toolset.py @@ -22,6 +22,7 @@ ) needs_pydantic_v2 = pytest.mark.skipif(PYDANTIC_V1, reason="tool functions are only supported with pydantic v2") +needs_symlinks = pytest.mark.skipif(sys.platform == "win32", reason="symlink fixtures need a POSIX filesystem") @pytest.mark.parametrize( @@ -373,6 +374,7 @@ async def test_bash_outer_cancel_closes_subprocess_no_stale_state(tmp_path: Path await s.close() +@needs_symlinks @needs_pydantic_v2 async def test_read_through_symlink_escape_is_rejected(tmp_path: Path) -> None: """resolve_path realpaths, so a symlink that escapes the workdir is caught.""" @@ -401,6 +403,7 @@ async def test_glob_rejects_dotdot_pattern(tmp_path: Path) -> None: await beta_glob_tool(env).call({"pattern": "../outside/*.txt"}) +@needs_symlinks @needs_pydantic_v2 async def test_glob_post_filters_symlink_escape(tmp_path: Path) -> None: """A symlink traversed mid-pattern must not let a glob result escape the workdir.""" @@ -415,6 +418,7 @@ async def test_glob_post_filters_symlink_escape(tmp_path: Path) -> None: assert res == "no matches" +@needs_symlinks @needs_pydantic_v2 async def test_grep_skips_symlinked_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The fallback walker must not read through a symlink that escapes the workdir."""