{"record":{"id":"8e6436d7c51a95a3","repo":"headroomlabs-ai/headroom","slug":"headroom-oauth2-misconfigured-e","errorCode":null,"errorMessage":"headroom-oauth2 misconfigured: {e}","messagePattern":"headroom-oauth2 misconfigured: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"critical","filePath":"plugins/headroom-oauth2/src/headroom_oauth2/__init__.py","lineNumber":100,"sourceCode":"        token_url=token_url,\n        client_id=env.get(\"HEADROOM_OAUTH2_CLIENT_ID\", \"\"),\n        client_secret=env.get(\"HEADROOM_OAUTH2_CLIENT_SECRET\", \"\"),\n        scopes=_split(env.get(\"HEADROOM_OAUTH2_SCOPES\")),\n        audience=env.get(\"HEADROOM_OAUTH2_AUDIENCE\") or None,\n        grant_type=env.get(\"HEADROOM_OAUTH2_GRANT_TYPE\", \"client_credentials\"),\n        auth_style=env.get(\"HEADROOM_OAUTH2_AUTH_STYLE\", \"post\"),\n        extra_params={\"resource\": resource} if resource else None,\n        allow_insecure=allow_insecure,\n        **kwargs,\n    )\n\n\ndef install(app: Any, config: Any) -> None:\n    \"\"\"Headroom proxy-extension entry point: install(app, config) -> None.\"\"\"\n    try:\n        provider = provider_from_env()\n    except ValueError as e:\n        raise RuntimeError(f\"headroom-oauth2 misconfigured: {e}\") from None  # fail-closed\n    if provider is None:\n        log.info(\"headroom-oauth2 loaded but HEADROOM_OAUTH2_TOKEN_URL unset; no-op\")\n        return\n    static = parse_headers(os.environ.get(\"HEADROOM_OAUTH2_HEADERS\"))\n    if static:\n        try:\n            # litellm's import runs load_dotenv and can inject .env values into os.environ;\n            # snapshot and restore so we never leak unrelated keys into the process env.\n            _before = dict(os.environ)\n            import litellm\n\n            # drop keys litellm/load_dotenv added, restore any it changed (no empty-env window)\n            for k in list(os.environ):\n                if k not in _before:\n                    del os.environ[k]\n            os.environ.update(_before)\n            litellm.headers = {**(getattr(litellm, \"headers\", None) or {}), **static}\n            log.info(\"headroom-oauth2: static upstream headers: %s\", list(static))","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/plugins/headroom-oauth2/src/headroom_oauth2/__init__.py#L82-L118","documentation":"The plugin's `install(app, config)` entry point calls `provider_from_env()` and wraps any ValueError from env parsing (e.g. a non-integer numeric setting, a bad auth_style, or a non-https token URL) in a RuntimeError marked fail-closed. The plugin deliberately refuses to half-load: a malformed OAuth2 config must crash proxy startup instead of running without authentication.","triggerScenarios":"Enabling the plugin (HEADROOM_OAUTH2_TOKEN_URL set) with any malformed sibling variable: non-integer `HEADROOM_OAUTH2_TIMEOUT_SECONDS`, `HEADROOM_OAUTH2_AUTH_STYLE=digest`, or an http token URL without the insecure override — startup then aborts with `RuntimeError: headroom-oauth2 misconfigured: <cause>`.","commonSituations":"Deploying the proxy with templated env files where one variable got mangled; enabling OAuth2 for the first time and missing a required setting; CI secrets injected with wrong types (quoted numbers); the underlying cause message points at the specific bad variable.","solutions":["Read the `<e>` suffix — it names the actual bad setting (e.g. `KEY=value is not an integer`, `token_url must be https`); fix that variable per its message","Dry-run the config before startup: `python -c \"from headroom_oauth2 import provider_from_env; provider_from_env()\"` to validate env in isolation","If OAuth2 is not intended in this deployment, unset HEADROOM_OAUTH2_TOKEN_URL — with no token URL the plugin logs 'no-op' and loads cleanly"],"exampleFix":"# before\n$ HEADROOM_OAUTH2_TOKEN_URL=https://idp/oauth2/token HEADROOM_OAUTH2_TIMEOUT_SECONDS=30s headroom-proxy\nRuntimeError: headroom-oauth2 misconfigured: HEADROOM_OAUTH2_TIMEOUT_SECONDS='30s' is not an integer\n\n# after\n$ HEADROOM_OAUTH2_TOKEN_URL=https://idp/oauth2/token HEADROOM_OAUTH2_TIMEOUT_SECONDS=30 headroom-proxy","handlingStrategy":"validation","validationCode":"from headroom_oauth2 import provider_from_env\n\ntry:\n    provider_from_env()  # dry-run: validates all HEADROOM_OAUTH2_* settings\nexcept ValueError as e:\n    raise SystemExit(f\"oauth2 config invalid: {e}\") from e\n# only then start the proxy / install the plugin","typeGuard":"def oauth2_env_valid() -> bool:\n    try:\n        provider_from_env()\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    install(app, config)\nexcept RuntimeError as e:\n    if \"headroom-oauth2 misconfigured\" in str(e):\n        raise SystemExit(f\"fix HEADROOM_OAUTH2_* env: {e}\") from e  # fail closed, do not boot\n    raise","preventionTips":["Validate env before boot; never catch-and-continue this RuntimeError — it is fail-closed by design","Keep a single source of truth for oauth2 env (one .env or secret) and lint it in CI","Unset HEADROOM_OAUTH2_TOKEN_URL in environments where OAuth2 is intentionally off"],"tags":["oauth2","configuration","plugin","fail-closed","startup"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}