{"record":{"id":"7b90893e923c1249","repo":"headroomlabs-ai/headroom","slug":"token-endpoint-returned-http-e-code","errorCode":null,"errorMessage":"token endpoint returned HTTP {e.code}","messagePattern":"token endpoint returned HTTP (.+?)","errorType":"exception","errorClass":"OAuth2Error","httpStatus":null,"severity":"error","filePath":"plugins/headroom-oauth2/src/headroom_oauth2/provider.py","lineNumber":134,"sourceCode":"        if self.auth_style == \"basic\":\n            creds = base64.b64encode(f\"{self.client_id}:{self.client_secret}\".encode()).decode()\n            headers[\"Authorization\"] = \"Basic \" + creds\n        else:\n            form[\"client_id\"] = self.client_id\n            form[\"client_secret\"] = self.client_secret\n        req = urllib.request.Request(\n            self.token_url,\n            data=urllib.parse.urlencode(form).encode(),\n            headers=headers,\n            method=\"POST\",\n        )\n        try:\n            with urllib.request.urlopen(req, timeout=self.timeout) as resp:\n                payload = json.load(resp)\n        except HTTPError as e:\n            with suppress(Exception):\n                e.read()  # drain; do NOT surface the IdP body (may echo sensitive context)\n            raise OAuth2Error(f\"token endpoint returned HTTP {e.code}\") from None\n        except (URLError, OSError) as e:\n            raise OAuth2Error(f\"token endpoint unreachable: {e}\") from None\n        except json.JSONDecodeError:\n            raise OAuth2Error(\"token endpoint returned non-JSON\") from None\n        token = payload.get(\"access_token\")\n        if not token:\n            raise OAuth2Error(\"token endpoint response had no access_token\")\n        raw = payload.get(\"expires_in\")\n        try:\n            ttl = int(float(raw))  # tolerate \"3600\", \"3600.0\", 3600, or a JSON float\n        except (TypeError, ValueError):\n            ttl = 300\n        ttl = max(1, ttl)  # 0/negative would cause a stale token or per-request minting\n        log.info(\"oauth2: minted token (ttl=%ss, scopes=%s)\", ttl, self.scopes or \"-\")\n        return token, ttl\n","sourceCodeStart":116,"sourceCodeEnd":150,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/plugins/headroom-oauth2/src/headroom_oauth2/provider.py#L116-L150","documentation":"The OAuth2 provider's token request (stdlib urllib POST) received an HTTP error status from the IdP; it is re-raised as OAuth2Error with just the status code. The response body is deliberately drained and NOT surfaced, because IdP error bodies may echo sensitive context (client_id, redirect hints). Common codes: 400 invalid_client/scope, 401 bad credentials, 404 wrong URL path.","triggerScenarios":"Minting a token with wrong client_id/client_secret (401), requesting an unsupported scope or audience (400), a token URL with a wrong path (404), or client credentials that were rotated and revoked (400 invalid_client).","commonSituations":"Secret rotation where the deployed credential was revoked; typo'd scope names like `api://my-app/.default` mangled by templating; copying a token URL from docs but with the wrong tenant or missing `/token` suffix; clock/env drift between environments sharing one app registration.","solutions":["Check the status: 401/400 → verify client_id/client_secret and scopes against the IdP app registration; 404 → re-check the token_url path and tenant","Re-issue and redeploy the client secret if credentials were rotated or expired","Reproduce outside the app with curl (safely, in a dev tenant) to see the IdP's error body, which the library intentionally hides: `curl -d grant_type=client_credentials -d client_id=... -d client_secret=... -d scope=... $TOKEN_URL`"],"exampleFix":"# before\nHEADROOM_OAUTH2_CLIENT_SECRET=old-rotated-secret\n\n# after\nHEADROOM_OAUTH2_CLIENT_SECRET=newly-issued-secret\n# verify shape first:\n# curl -s -o /dev/null -w '%{http_code}' -d 'grant_type=client_credentials' \\\n#   -d 'client_id=$ID' -d 'client_secret=$SECRET' -d 'scope=$SCOPE' $TOKEN_URL  -> want 200","handlingStrategy":"retry","validationCode":"# cheap pre-flight: credentials non-empty and scope names confirmed against app registration\nassert client_id and client_secret, \"empty oauth2 credentials will yield HTTP 400/401\"\n# optional dev-tenant probe (never log the secret):\n# curl -o /dev/null -w '%{http_code}' -d grant_type=client_credentials -d client_id=$ID -d client_secret=$SEC -d scope=$SCOPE $TOKEN_URL == 200","typeGuard":null,"tryCatchPattern":"from headroom_oauth2.provider import OAuth2Error\n\nfor attempt in range(3):\n    try:\n        token = provider.get_token()\n        break\n    except OAuth2Error as e:\n        if \"HTTP 401\" in str(e) or \"HTTP 400\" in str(e):\n            raise RuntimeError(f\"credentials/scopes rejected by IdP: {e}\") from e  # not retryable\n        if attempt == 2:\n            raise\n        time.sleep(2 ** attempt)  # 5xx / transient only","preventionTips":["Treat 4xx as config bugs (fail fast) and 5xx as transient (retry with backoff)","Automate secret rotation so deployed credentials are never the revoked ones","Keep a curl repro command in runbooks to see the IdP error body the library hides"],"tags":["oauth2","network","http","identity-provider"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}