Skip to content

Anthropic() emits a malformed Authorization: Bearer header when ANTHROPIC_AUTH_TOKEN is an empty string #1640

@hiroki-tamba-research

Description

@hiroki-tamba-research

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:

  1. 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)
  2. "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):
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions