From 591ddc9f5b8ccf4579c9d38dc8795e4a25c1001c Mon Sep 17 00:00:00 2001 From: okxint Date: Thu, 2 Jul 2026 10:28:51 +0530 Subject: [PATCH] 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"}