headroomlabs-ai/headroom · error · ValueError

{env_var} must be a JSON object of header name/value strings

Error message

{env_var} must be a JSON object of header name/value strings

What it means

resolve_extra_headers() parses the extra-headers override (CLI flag or the ANTHROPIC_TARGET_API_HEADERS / OPENAI_TARGET_API_HEADERS env vars) as JSON. This raise site fires when json.loads() itself fails — the value is not parseable JSON at all (trailing commas, single quotes, missing quotes around keys, shell-mangled escaping). The result is a ValueError surfaced by the proxy CLI, which prints 'error: ...' and exits 1.

Source

Thrown at headroom/providers/registry.py:152

def resolve_extra_headers(
    cli_value: str | None,
    env_var: str,
    *,
    environ: Mapping[str, str] | None = None,
) -> dict[str, str] | None:
    """Resolve extra headers to merge into (and override) forwarded provider requests.

    Accepts a JSON object string from CLI or env (CLI wins). Returns ``None`` if unset.
    Raises ``ValueError`` on invalid JSON or a non-string-keyed/valued object.
    """
    env = environ or os.environ
    raw = cli_value or env.get(env_var)
    if not raw:
        return None
    try:
        parsed = json.loads(raw)
    except (ValueError, TypeError) as exc:
        raise ValueError(f"{env_var} must be a JSON object of header name/value strings") from exc
    if not isinstance(parsed, dict) or not all(
        isinstance(k, str) and isinstance(v, str) for k, v in parsed.items()
    ):
        raise ValueError(f"{env_var} must be a JSON object of header name/value strings")
    return parsed or None


def resolve_api_targets(overrides: ProviderApiOverrides) -> ProviderApiTargets:
    """Resolve normalized upstream provider targets from configured overrides."""
    return ProviderApiTargets(
        anthropic=_normalize_api_url(overrides.anthropic, default=DEFAULT_ANTHROPIC_API_URL),
        openai=_normalize_api_url(overrides.openai, default=DEFAULT_OPENAI_API_URL),
        gemini=_normalize_api_url(overrides.gemini, default=DEFAULT_GEMINI_API_URL),
        cloudcode=_normalize_api_url(overrides.cloudcode, default=DEFAULT_CLOUDCODE_API_URL),
        vertex=_normalize_api_url(overrides.vertex, default=DEFAULT_VERTEX_API_URL),
    )

View on GitHub (pinned to 322425c43b)

Solutions

  1. Set the value to a valid JSON object of string keys and string values, e.g. ANTHROPIC_TARGET_API_HEADERS='{"X-Title":"my-app"}'.
  2. Validate before launch: python -c "import json,sys; json.loads(sys.argv[1])" "$VALUE".
  3. Prefer the CLI flag over the env var so shell history shows the exact string, and use single quotes around the JSON to prevent shell interpolation.

Example fix

# before
export ANTHROPIC_TARGET_API_HEADERS="{'X-Title':'my-app'}"  # ValueError

# after
export ANTHROPIC_TARGET_API_HEADERS='{"X-Title": "my-app"}'
Defensive patterns

Strategy: validation

Validate before calling

import json

def valid_extra_headers(raw: str | None) -> bool:
    if not raw:
        return True
    try:
        parsed = json.loads(raw)
    except (ValueError, TypeError):
        return False
    return isinstance(parsed, dict) and all(
        isinstance(k, str) and isinstance(v, str) for k, v in parsed.items()
    )

Try / catch

try:
    headers = resolve_extra_headers(cli_value, "ANTHROPIC_TARGET_API_HEADERS")
except ValueError as exc:
    sys.exit(f"bad extra headers config: {exc}")

Prevention

When it happens

Trigger: Launching the headroom proxy with --anthropic-extra-headers / --openai-extra-headers (or the corresponding TARGET_API_HEADERS env var) set to a string that is not valid JSON, e.g. "{'X-Foo':'bar'}" or 'X-Foo: bar'.

Common situations: Writing JSON in a shell with incorrect quoting so the value reaching Python is truncated or single-quoted; using YAML-style or header-line syntax instead of a JSON object; secrets injected with newline or quote characters that break the parse.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/9a0bb57d8ddaaedd. Report an issue: GitHub.