From acaaf981e25532c47ab955cbe89b3e315ae300bc Mon Sep 17 00:00:00 2001 From: Zawwar Sami Date: Sat, 13 Jun 2026 09:06:40 +0000 Subject: [PATCH 1/2] fix(client): treat empty-string env credentials as unset When ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN are present in the environment but set to an empty string, the SDK treated the empty string as a real credential (guards use `is None`). It then built an `Authorization: Bearer ` header (trailing space) and an empty `X-Api-Key`, which the HTTP layer (h11) rejects at write time, failing every request with a confusing APIConnectionError. Empty-string env vars are common in CI, containers, and wrapper shells (e.g. `export VAR=`, and the Claude Code CLI exports both empty). Coerce an empty env credential to None at read time so it is treated as absent and credential auto-discovery runs as expected. --- src/anthropic/_client.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/anthropic/_client.py b/src/anthropic/_client.py index 96e45eee..2aacd251 100644 --- a/src/anthropic/_client.py +++ b/src/anthropic/_client.py @@ -210,8 +210,13 @@ 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") + # Treat an empty-string env var as unset. Empty values are common in + # CI/containers/wrapper shells (e.g. `export VAR=`), and a "present" + # empty credential would otherwise build a malformed `Bearer ` / + # empty `X-Api-Key` header that the HTTP layer rejects on every + # request, instead of falling through to credential auto-discovery. + 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 --- @@ -627,8 +632,13 @@ 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") + # Treat an empty-string env var as unset. Empty values are common in + # CI/containers/wrapper shells (e.g. `export VAR=`), and a "present" + # empty credential would otherwise build a malformed `Bearer ` / + # empty `X-Api-Key` header that the HTTP layer rejects on every + # request, instead of falling through to credential auto-discovery. + 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 --- From 0b094131dc3aafde0028c24aded25c43f7251ba2 Mon Sep 17 00:00:00 2001 From: Zawwar Sami Date: Sat, 13 Jun 2026 09:06:43 +0000 Subject: [PATCH 2/2] test(client): cover empty-string env credentials for sync and async clients --- tests/test_client.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index d8814b75..e6bf5eca 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -434,6 +434,15 @@ 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_env_credentials_treated_as_unset(self) -> None: + 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"} @@ -1464,6 +1473,15 @@ 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_env_credentials_treated_as_unset(self) -> None: + 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"}