{"record":{"id":"acb7753540468465","repo":"unslothai/unsloth","slug":"chatgpt-did-not-return-a-refresh-token","errorCode":null,"errorMessage":"ChatGPT did not return a refresh token.","messagePattern":"ChatGPT did not return a refresh token\\.","errorType":"exception","errorClass":"CodexAuthError","httpStatus":400,"severity":"error","filePath":"studio/backend/core/inference/openai_codex_auth.py","lineNumber":216,"sourceCode":"        raise CodexAuthError(\"ChatGPT returned an invalid access token.\") from exc\n    account_id = payload.get(\"https://api.openai.com/auth\", {}).get(\"chatgpt_account_id\")\n    if not isinstance(account_id, str) or not account_id or len(account_id) > 512:\n        account_id = payload.get(\"https://api.openai.com/auth.chatgpt_account_id\")\n    if not isinstance(account_id, str) or not account_id or len(account_id) > 512:\n        raise CodexAuthError(\"The ChatGPT account identifier was missing.\")\n    return account_id\n\n\ndef _validate_token_payload(body: Any, previous_refresh_token: str = \"\") -> dict[str, Any]:\n    if not isinstance(body, dict):\n        raise CodexAuthError(\"ChatGPT returned an invalid token response.\")\n    access_token = body.get(\"access_token\")\n    refresh_token = body.get(\"refresh_token\") or previous_refresh_token\n    expires_in = body.get(\"expires_in\", 3600)\n    if not isinstance(access_token, str) or not access_token:\n        raise CodexAuthError(\"ChatGPT returned an invalid token response.\")\n    if not isinstance(refresh_token, str) or not refresh_token:\n        raise CodexAuthError(\"ChatGPT did not return a refresh token.\")\n    try:\n        expires_in = max(60, min(int(expires_in), 30 * 24 * 3600))\n    except (TypeError, ValueError) as exc:\n        raise CodexAuthError(\"ChatGPT returned an invalid token lifetime.\") from exc\n    return {\n        \"access_token\": access_token,\n        \"refresh_token\": refresh_token,\n        \"expires_at\": int(time.time()) + expires_in,\n        \"account_id\": extract_chatgpt_account_id(access_token),\n    }\n\n\ndef save_oauth_bundle(provider_id: str, bundle: dict[str, Any]) -> None:\n    credential_secrets.upsert_secret(\n        credential_secrets.OPENAI_CODEX_OAUTH_KIND,\n        provider_id,\n        json.dumps(bundle, separators = (\",\", \":\")),\n    )","sourceCodeStart":198,"sourceCodeEnd":234,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/openai_codex_auth.py#L198-L234","documentation":"Validation in _validate_token_payload for the refresh token: after falling back (body['refresh_token'] or previous_refresh_token), the result must be a non-empty string. ChatGPT tokens are rotated on every refresh, so the endpoint must hand back a usable refresh token; if it omits one and no previous token was supplied to reuse, the credential cannot be persisted for future refreshes and the flow fails.","triggerScenarios":"A refresh_token grant response that omits refresh_token while _validate_token_payload was called without previous_refresh_token; or the field present but empty/non-string.","commonSituations":"Upstream policy change stopping refresh-token rotation; a first-time code-exchange response missing the refresh token (scope/PKCE misconfiguration); mocks that only return access_token.","solutions":["Restart the OAuth authorization flow (full reconnect) to obtain a fresh refresh token instead of refreshing a half-broken credential.","Check the authorization request included the offline-access scope/parameters ChatGPT OAuth requires for refresh tokens.","Persist the latest refresh_token after every exchange so the previous_refresh_token fallback is available on rotation hiccups."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"refresh_token = body.get('refresh_token') or previous_refresh_token\nif not isinstance(refresh_token, str) or not refresh_token:\n    raise ValueError('no refresh token available; full re-authorization required')","typeGuard":null,"tryCatchPattern":"try:\n    bundle = _validate_token_payload(body, previous_refresh_token=stored_refresh)\nexcept CodexAuthError as e:\n    if 'refresh token' in str(e):\n        bundle = await full_reauthorization(provider_id)  # fresh code exchange\n    else:\n        raise","preventionTips":["Always pass the last stored refresh token as previous_refresh_token so rotation hiccups are survivable.","Request offline access in the authorization call so refresh tokens are issued.","Persist the new refresh token atomically after every exchange."],"tags":["oauth","refresh-token","token-rotation","codex"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}