{"record":{"id":"9a0bb57d8ddaaedd","repo":"headroomlabs-ai/headroom","slug":"env-var-must-be-a-json-object-of-header-name-val","errorCode":null,"errorMessage":"{env_var} must be a JSON object of header name/value strings","messagePattern":"(.+?) must be a JSON object of header name/value strings","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"headroom/providers/registry.py","lineNumber":152,"sourceCode":"def resolve_extra_headers(\n    cli_value: str | None,\n    env_var: str,\n    *,\n    environ: Mapping[str, str] | None = None,\n) -> dict[str, str] | None:\n    \"\"\"Resolve extra headers to merge into (and override) forwarded provider requests.\n\n    Accepts a JSON object string from CLI or env (CLI wins). Returns ``None`` if unset.\n    Raises ``ValueError`` on invalid JSON or a non-string-keyed/valued object.\n    \"\"\"\n    env = environ or os.environ\n    raw = cli_value or env.get(env_var)\n    if not raw:\n        return None\n    try:\n        parsed = json.loads(raw)\n    except (ValueError, TypeError) as exc:\n        raise ValueError(f\"{env_var} must be a JSON object of header name/value strings\") from exc\n    if not isinstance(parsed, dict) or not all(\n        isinstance(k, str) and isinstance(v, str) for k, v in parsed.items()\n    ):\n        raise ValueError(f\"{env_var} must be a JSON object of header name/value strings\")\n    return parsed or None\n\n\ndef resolve_api_targets(overrides: ProviderApiOverrides) -> ProviderApiTargets:\n    \"\"\"Resolve normalized upstream provider targets from configured overrides.\"\"\"\n    return ProviderApiTargets(\n        anthropic=_normalize_api_url(overrides.anthropic, default=DEFAULT_ANTHROPIC_API_URL),\n        openai=_normalize_api_url(overrides.openai, default=DEFAULT_OPENAI_API_URL),\n        gemini=_normalize_api_url(overrides.gemini, default=DEFAULT_GEMINI_API_URL),\n        cloudcode=_normalize_api_url(overrides.cloudcode, default=DEFAULT_CLOUDCODE_API_URL),\n        vertex=_normalize_api_url(overrides.vertex, default=DEFAULT_VERTEX_API_URL),\n    )\n\n","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/providers/registry.py#L134-L170","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Set the value to a valid JSON object of string keys and string values, e.g. ANTHROPIC_TARGET_API_HEADERS='{\"X-Title\":\"my-app\"}'.","Validate before launch: python -c \"import json,sys; json.loads(sys.argv[1])\" \"$VALUE\".","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."],"exampleFix":"# before\nexport ANTHROPIC_TARGET_API_HEADERS=\"{'X-Title':'my-app'}\"  # ValueError\n\n# after\nexport ANTHROPIC_TARGET_API_HEADERS='{\"X-Title\": \"my-app\"}'","handlingStrategy":"validation","validationCode":"import json\n\ndef valid_extra_headers(raw: str | None) -> bool:\n    if not raw:\n        return True\n    try:\n        parsed = json.loads(raw)\n    except (ValueError, TypeError):\n        return False\n    return isinstance(parsed, dict) and all(\n        isinstance(k, str) and isinstance(v, str) for k, v in parsed.items()\n    )","typeGuard":null,"tryCatchPattern":"try:\n    headers = resolve_extra_headers(cli_value, \"ANTHROPIC_TARGET_API_HEADERS\")\nexcept ValueError as exc:\n    sys.exit(f\"bad extra headers config: {exc}\")","preventionTips":["Lint env/config files with a JSON parse check before starting the proxy.","Use single-quoted shell strings around JSON to avoid interpolation mangling.","Keep header injection values in a config file (loaded and json-parsed) instead of inline env strings."],"tags":["configuration","json","headers","environment","cli"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}