headroomlabs-ai/headroom · error · ValueError

client_id and client_secret are required

Error message

client_id and client_secret are required

What it means

OAuth2ClientCredentials requires both `client_id` and `client_secret` to be non-empty at construction; missing either raises ValueError. Client-credentials flow authenticates the application itself with this pair, so there is nothing sensible to default.

Source

Thrown at plugins/headroom-oauth2/src/headroom_oauth2/provider.py:60

    def __init__(
        self,
        *,
        token_url: str,
        client_id: str,
        client_secret: str,
        scopes=None,
        audience: str | None = None,
        grant_type: str = "client_credentials",
        auth_style: str = "post",
        extra_params=None,
        skew_seconds: int = 60,
        timeout_seconds: float = 30.0,
        allow_insecure: bool = False,
    ):
        if not token_url:
            raise ValueError("token_url is required")
        if not client_id or not client_secret:
            raise ValueError("client_id and client_secret are required")
        if auth_style not in ("post", "basic"):
            raise ValueError("auth_style must be 'post' or 'basic'")
        if not allow_insecure and not _https_or_local(token_url):
            raise ValueError(
                "token_url must be https (loopback http allowed for tests; set "
                "allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override)"
            )
        self.token_url = token_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.scopes = list(scopes or [])
        self.audience = audience
        self.grant_type = grant_type
        self.auth_style = auth_style
        self.extra_params = dict(extra_params or {})
        self.skew = max(0, int(skew_seconds))
        self.timeout = timeout_seconds
        self._lock = threading.Lock()

View on GitHub (pinned to 322425c43b)

Solutions

  1. Set both credentials in the environment/config the provider reads from, and verify they are non-empty before constructing (see validation code)
  2. Check secret injection in the deployment: `kubectl describe pod`, CI masked-variable exposure, or print `bool(os.environ.get(...))` (never the value) in a startup probe
  3. If OAuth2 shouldn't be active in this environment, unset HEADROOM_OAUTH2_TOKEN_URL so the provider is never built

Example fix

# before
provider = OAuth2ClientCredentials(
    token_url=os.environ["HEADROOM_OAUTH2_TOKEN_URL"],
    client_id=os.environ.get("HEADROOM_OAUTH2_CLIENT_ID", ""),
    client_secret=os.environ.get("HEADROOM_OAUTH2_CLIENT_SECRET", ""),
)

# after
missing = [k for k in ("HEADROOM_OAUTH2_CLIENT_ID", "HEADROOM_OAUTH2_CLIENT_SECRET")
           if not os.environ.get(k, "").strip()]
if missing:
    raise RuntimeError(f"missing oauth2 config: {missing}")
provider = OAuth2ClientCredentials(
    token_url=os.environ["HEADROOM_OAUTH2_TOKEN_URL"],
    client_id=os.environ["HEADROOM_OAUTH2_CLIENT_ID"],
    client_secret=os.environ["HEADROOM_OAUTH2_CLIENT_SECRET"],
)
Defensive patterns

Strategy: validation

Validate before calling

REQUIRED = ("HEADROOM_OAUTH2_TOKEN_URL", "HEADROOM_OAUTH2_CLIENT_ID", "HEADROOM_OAUTH2_CLIENT_SECRET")

def oauth2_env_complete(env) -> None:
    missing = [k for k in REQUIRED if not str(env.get(k) or "").strip()]
    if missing:
        raise RuntimeError(f"oauth2 env incomplete, missing: {missing}")

oauth2_env_complete(os.environ)
provider = provider_from_env()

Type guard

def credentials_present(client_id, client_secret) -> bool:
    return bool(str(client_id or "").strip()) and bool(str(client_secret or "").strip())

Try / catch

try:
    provider = OAuth2ClientCredentials(token_url=url, client_id=cid, client_secret=sec)
except ValueError as e:
    if "client_id and client_secret are required" in str(e):
        raise RuntimeError("deployment error: oauth2 secrets not injected") from e
    raise

Prevention

When it happens

Trigger: Constructing the provider with an empty/None `client_id` or `client_secret` — typically values read from env vars (`HEADROOM_OAUTH2_CLIENT_ID`/`..._CLIENT_SECRET`) that are unset in the current shell, pod, or CI runner while TOKEN_URL is set.

Common situations: Secrets not injected into the deployment (Kubernetes secret missing, CI secret not exposed, .env not loaded); secret name typos; running locally with prod config that expects a vault; empty-string values from templating (`CLIENT_ID=${OAUTH_CLIENT_ID}` when the inner var is unset).

Related errors


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