Summary
When ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) is present in the environment but set to an empty string, the SDK treats the empty string as a real credential instead of as "absent". It then builds an Authorization: Bearer header (literally Bearer + a trailing space, from the empty token). The underlying HTTP layer (h11, via httpx) validates header values at write time and rejects the trailing space, so every request fails with:
h11._util.LocalProtocolError: Illegal header value b'Bearer '
surfaced by the SDK as anthropic.APIConnectionError. Empty-string env vars are common (CI, containers, export VAR=, wrapper shells), so this turns a benign/empty credential into a hard, confusing failure. The conventional behavior is to treat an empty-string credential as unset.
This is reliably triggered when running inside the Claude Code CLI shell, which exports ANTHROPIC_AUTH_TOKEN= and ANTHROPIC_API_KEY= (both empty) into the environment — so any script there that constructs Anthropic() without an explicit non-empty key fails on every request.
Environment
anthropic 0.103.1 (observed). The same code path is present on main (→ latest 0.105.2), so upgrading does not fix it.
httpx 0.28.1, h11 0.16.0, Python 3.14.2, Windows 11.
Reproduction (self-contained, no network — full script in the collapsible section below)
import os
os.environ["ANTHROPIC_AUTH_TOKEN"] = "" # present but empty
os.environ["ANTHROPIC_API_KEY"] = "" # present but empty
from anthropic import Anthropic
client = Anthropic() # no explicit credentials
print(repr(client.auth_token)) # ''
print(dict(client.auth_headers)) # {'X-Api-Key': '', 'Authorization': 'Bearer '}
client.auth_headers["Authorization"] is 'Bearer ' (length 7, trailing space). Validating that exact value reproduces the error with no network call:
from h11._headers import normalize_and_validate
normalize_and_validate([("Authorization", "Bearer ")])
# h11._util.LocalProtocolError: Illegal header value b'Bearer '
# ('Bearer x' validates fine; only the trailing space from the empty token is rejected.)
On a real call the failure surfaces at write time (after TCP connect), wrapped as anthropic.APIConnectionError.
Root cause
The SDK guards credential presence with is None, so an empty string passes every guard:
- Env read — no empty-string coercion (
_client.py#L211-L212):
api_key = os.environ.get("ANTHROPIC_API_KEY") # -> "" (not None)
auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") # -> "" (not None)
- "Missing auth" fallback uses
is None, so it never triggers (_client.py#L258):
if credentials is None and api_key is None and auth_token is None and _is_base_client(self):
- Header builders emit on
is not None (_client.py#L334-L351):
@property
def _api_key_auth(self) -> dict[str, str]:
api_key = self.api_key
if api_key is None:
return {}
return {"X-Api-Key": api_key} # -> {"X-Api-Key": ""}
@property
def _bearer_auth(self) -> dict[str, str]:
auth_token = self.auth_token
if auth_token is None:
return {}
return {"Authorization": f"Bearer {auth_token}"} # -> {"Authorization": "Bearer "}
(The _bearer_auth comment says "always emit if self.auth_token is set" — but "set" is implemented as is not None, which also matches "".)
Suggested fix
Treat an empty-string credential as absent. Most robust at the env read (covers all downstream guards):
api_key = os.environ.get("ANTHROPIC_API_KEY") or None
auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") or None
and/or change the guards at L258 / L336 / L349 to truthiness (if not api_key: / if not auth_token:). With either change, an empty-string env credential falls through to the normal "no credential" path (or default_credentials), instead of producing a malformed header.
Workaround (callers, today)
- Pass a non-empty credential explicitly:
Anthropic(api_key=MY_REAL_KEY) — this sets has_explicit_credential, the empty env vars are not read, and only X-Api-Key is sent (confirmed in the repro). Note: passing api_key="" explicitly does not help (still empty); pass a real key, or
- Clear the empty vars before constructing the client:
for k in ("ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"):
if os.environ.get(k) == "":
os.environ.pop(k)
Full repro script (repro_sdk_empty_bearer.py, no network)
"""
Minimal, self-contained, NO-NETWORK repro for:
anthropic-sdk-python emits a malformed `Authorization: Bearer ` header when
ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) is set to an EMPTY STRING.
Sets the env vars to "" itself, so it reproduces on any machine (you do NOT need
to run it inside a Claude Code shell). Makes no API calls.
pip install anthropic
python repro_sdk_empty_bearer.py
Observed: anthropic 0.103.1 (bug also present on main -> 0.105.2),
httpx 0.28.1, h11 0.16.0, Python 3.14.
"""
import os
# Simulate the environment that triggers the bug: credentials present-but-empty.
# (e.g. a parent process exported `ANTHROPIC_AUTH_TOKEN=` / `ANTHROPIC_API_KEY=`.)
os.environ["ANTHROPIC_AUTH_TOKEN"] = ""
os.environ["ANTHROPIC_API_KEY"] = ""
from anthropic import Anthropic # noqa: E402
client = Anthropic() # no explicit credentials -> reads the empty env vars
print("self.api_key :", repr(client.api_key)) # -> ''
print("self.auth_token:", repr(client.auth_token)) # -> ''
print("auth_headers :", dict(client.auth_headers))
# -> {'X-Api-Key': '', 'Authorization': 'Bearer '} <-- note the trailing space
auth = client.auth_headers.get("Authorization")
print("Authorization repr:", repr(auth), "(len", len(auth or ""), ")")
# Why every request then fails (shown WITHOUT a network call): the underlying
# HTTP layer (h11, via httpx) validates header values at write time and rejects
# the trailing space in 'Bearer '.
print("\n-- h11 header validation of the value the SDK produced --")
try:
from h11._headers import normalize_and_validate
normalize_and_validate([("Authorization", auth)])
print(" unexpectedly OK")
except Exception as e:
print(f" {type(e).__module__}.{type(e).__name__}: {e}")
# -> h11._util.LocalProtocolError: Illegal header value b'Bearer '
print("\nExpected real-call failure: anthropic.APIConnectionError wrapping the above"
"\n('Illegal header value b\\'Bearer \\'') - raised on the first request.")
# The fix on the caller side (no bug if a non-empty credential is passed explicitly):
print("\n-- workaround: explicit non-empty api_key --")
fixed = Anthropic(api_key="sk-ant-DUMMY-not-real")
print("auth_headers :", {k: ("<set>" if v else repr(v)) for k, v in fixed.auth_headers.items()})
# -> {'X-Api-Key': '<set>'} (no Authorization header, no malformed Bearer)
Summary
When
ANTHROPIC_AUTH_TOKEN(orANTHROPIC_API_KEY) is present in the environment but set to an empty string, the SDK treats the empty string as a real credential instead of as "absent". It then builds anAuthorization: Bearerheader (literallyBearer+ a trailing space, from the empty token). The underlying HTTP layer (h11, via httpx) validates header values at write time and rejects the trailing space, so every request fails with:surfaced by the SDK as
anthropic.APIConnectionError. Empty-string env vars are common (CI, containers,export VAR=, wrapper shells), so this turns a benign/empty credential into a hard, confusing failure. The conventional behavior is to treat an empty-string credential as unset.This is reliably triggered when running inside the Claude Code CLI shell, which exports
ANTHROPIC_AUTH_TOKEN=andANTHROPIC_API_KEY=(both empty) into the environment — so any script there that constructsAnthropic()without an explicit non-empty key fails on every request.Environment
anthropic0.103.1 (observed). The same code path is present on main (→ latest 0.105.2), so upgrading does not fix it.httpx0.28.1,h110.16.0, Python 3.14.2, Windows 11.Reproduction (self-contained, no network — full script in the collapsible section below)
client.auth_headers["Authorization"]is'Bearer '(length 7, trailing space). Validating that exact value reproduces the error with no network call:On a real call the failure surfaces at write time (after TCP connect), wrapped as
anthropic.APIConnectionError.Root cause
The SDK guards credential presence with
is None, so an empty string passes every guard:_client.py#L211-L212):is None, so it never triggers (_client.py#L258):is not None(_client.py#L334-L351):_bearer_authcomment says "always emit ifself.auth_tokenis set" — but "set" is implemented asis not None, which also matches"".)Suggested fix
Treat an empty-string credential as absent. Most robust at the env read (covers all downstream guards):
and/or change the guards at L258 / L336 / L349 to truthiness (
if not api_key:/if not auth_token:). With either change, an empty-string env credential falls through to the normal "no credential" path (ordefault_credentials), instead of producing a malformed header.Workaround (callers, today)
Anthropic(api_key=MY_REAL_KEY)— this setshas_explicit_credential, the empty env vars are not read, and onlyX-Api-Keyis sent (confirmed in the repro). Note: passingapi_key=""explicitly does not help (still empty); pass a real key, orFull repro script (
repro_sdk_empty_bearer.py, no network)