{"record":{"id":"dd3e916072b1acda","repo":"unslothai/unsloth","slug":"authorization-flow-was-cancelled-before-credential","errorCode":null,"errorMessage":"Authorization flow was cancelled before credentials were saved.","messagePattern":"Authorization flow was cancelled before credentials were saved\\.","errorType":"exception","errorClass":"CodexAuthError","httpStatus":400,"severity":"error","filePath":"studio/backend/core/inference/openai_codex_auth.py","lineNumber":310,"sourceCode":"    verifier: str | None = None,\n    redirect_uri: str | None = None,\n) -> None:\n    if flow.consumed:\n        raise CodexAuthError(\"Authorization callback was already used.\")\n    flow.consumed = True\n    try:\n        body = await _token_request(\n            {\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": OPENAI_CODEX_CLIENT_ID,\n                \"code\": code,\n                \"redirect_uri\": redirect_uri or flow.redirect_uri,\n                \"code_verifier\": verifier or flow.verifier,\n            }\n        )\n        bundle = _validate_token_payload(body)\n        if flow.persist_bundle is None or flow.status != \"pending\":\n            raise CodexAuthError(\"Authorization flow was cancelled before credentials were saved.\")\n        persisted = flow.persist_bundle(flow.provider_id, bundle)\n        if persisted is not None:\n            await persisted\n    except Exception:\n        flow.status = \"error\"\n        flow.message = \"ChatGPT authorization failed. Please reconnect.\"\n        await _persist_terminal_flow(flow)\n        raise\n    flow.status = \"connected\"\n    if flow.server:\n        flow.server.close()\n        flow.server = None\n\n\nasync def _loopback_handler(\n    flow: OAuthFlow, reader: asyncio.StreamReader, writer: asyncio.StreamWriter\n) -> None:\n    try:","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/openai_codex_auth.py#L292-L328","documentation":"Raised as CodexAuthError inside _exchange_code after a successful token exchange when flow.persist_bundle is None or flow.status is no longer 'pending'. The tokens were obtained from OpenAI but there is no way to save them because the flow was cancelled (or never wired with a persistence callback) at that moment. The except block then marks the flow as errored and persists the terminal state before re-raising.","triggerScenarios":"User clicks 'Cancel' in Studio while the browser is completing the OAuth redirect — cancel_flow sets status='cancelled' and the race is lost when _exchange_code reaches the persist step; a flow object constructed without a persist_bundle callable; the flow expired and get_flow set status='error' just before the callback arrived.","commonSituations":"Race between cancellation and the authorization redirect landing; tests or custom integrations building OAuthFlow objects directly without the persistence callback; a long user delay on the ChatGPT consent page until after the flow was cancelled.","solutions":["Simply start a new connection flow — the obtained credentials are intentionally discarded, nothing was saved.","If building custom flows, always pass persist_bundle when constructing the flow so a successful exchange can be saved.","Avoid cancelling flows while the user's browser is still on the OpenAI consent page; wait for the flow TTL to expire instead.","Check flow.status == 'pending' before issuing cancel to reduce the race window."],"exampleFix":"// before\nflow = OAuthFlow(id=..., provider_id=..., method=\"browser\", ...)  # no persist_bundle\n\n// after\nflow = OAuthFlow(\n    id=...,\n    provider_id=...,\n    method=\"browser\",\n    persist_bundle=persist_oauth_bundle,  # successful exchange can now be saved\n    ...,\n)","handlingStrategy":"try-catch","validationCode":"flow = codex_auth.get_flow(provider_id, flow_id)\nassert flow.status == \"pending\", f\"flow is {flow.status}; completing now risks a cancel race\"\nassert flow.persist_bundle is not None, \"flow lacks persistence; exchange cannot be saved\"","typeGuard":"def flow_is_persistable(flow: codex_auth.OAuthFlow) -> bool:\n    return flow.persist_bundle is not None and flow.status == \"pending\"","tryCatchPattern":"try:\n    flow = await complete(flow_id, callback_url)\nexcept codex_auth.CodexAuthError as exc:\n    if \"cancelled\" in str(exc):\n        flow = await start_new_flow(provider_id)  # tokens were discarded; restart\n    else:\n        raise","preventionTips":["Always construct flows with a persist_bundle callback.","Do not cancel flows while the user's browser is still on the consent page.","After cancelling, always mint a new flow rather than resuming.","Test the cancel-then-callback race explicitly (see test_openai_codex_subscription.py 'cancelled' case)."],"tags":["oauth","race-condition","cancellation","persistence","codex"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}