{"record":{"id":"fd06d43e724cba5e","repo":"unslothai/unsloth","slug":"authorization-callback-was-already-used","errorCode":null,"errorMessage":"Authorization callback was already used.","messagePattern":"Authorization callback was already used\\.","errorType":"exception","errorClass":"CodexAuthError","httpStatus":400,"severity":"error","filePath":"studio/backend/core/inference/openai_codex_auth.py","lineNumber":296,"sourceCode":"            raise CodexReauthorizationRequired(\n                \"ChatGPT authorization is no longer valid. Please reconnect.\"\n            )\n        raise CodexAuthError(\"ChatGPT authorization failed. Please reconnect.\")\n    try:\n        return response.json()\n    except Exception as exc:\n        raise CodexAuthError(\"ChatGPT returned an invalid authorization response.\") from exc\n\n\nasync def _exchange_code(\n    flow: OAuthFlow,\n    code: str,\n    *,\n    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:","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/openai_codex_auth.py#L278-L314","documentation":"Raised as CodexAuthError from _exchange_code when the OAuthFlow object already has consumed=True at entry. A flow is single-use: the first call to _exchange_code sets flow.consumed = True immediately, so any second attempt to exchange a code against the same flow (double-click, duplicate callback, retry after a partial failure) fails with this message before any network call is made.","triggerScenarios":"The loopback HTTP handler receives two callback requests with valid codes; complete_browser_flow is invoked twice for the same flow_id; a client retries the completion request after a timeout while the first attempt already consumed the flow; the browser fires both an automatic redirect and a manual paste of the callback URL.","commonSituations":"Frontend double-submits the 'complete connection' form; user pastes the callback URL after the loopback server already handled the redirect; an HTTP client auto-retries a POST on connection reuse; a stale flow object was rehydrated from persistence while an in-flight exchange had already run.","solutions":["Treat this error as benign in the success path — if the first exchange succeeded, the flow is connected; poll flow.status instead of re-completing.","Fix the caller to submit the callback exactly once (disable the button after first click, dedupe on flow_id).","Start a new flow if the first exchange genuinely failed; a consumed flow can never be reused.","Check get_flow() status before calling complete_browser_flow and skip if status is already 'connected'."],"exampleFix":"// before\n@app.post(\"/complete\")\nasync def complete(flow_id: str, callback_url: str):\n    return await codex_auth.complete_browser_flow(provider_id, flow_id, callback_url)  # double-submit reuses flow\n\n// after\n@app.post(\"/complete\")\nasync def complete(flow_id: str, callback_url: str):\n    flow = codex_auth.get_flow(provider_id, flow_id)\n    if flow.status == \"connected\":\n        return flow  # idempotent: already exchanged\n    return await codex_auth.complete_browser_flow(provider_id, flow_id, callback_url)","handlingStrategy":"validation","validationCode":"flow = codex_auth.get_flow(provider_id, flow_id)\nif flow.consumed:\n    if flow.status == \"connected\":\n        handle_already_connected(flow)  # benign\n    else:\n        start_new_flow()  # consumed and failed: cannot reuse\n","typeGuard":"def flow_is_reusable(flow: codex_auth.OAuthFlow) -> bool:\n    return not flow.consumed and flow.status == \"pending\"","tryCatchPattern":"try:\n    await complete(flow_id, callback_url)\nexcept codex_auth.CodexAuthError as exc:\n    if \"already used\" in str(exc):\n        flow = codex_auth.get_flow(provider_id, flow_id)\n        if flow.status == \"connected\":\n            return flow  # idempotent success\n        raise","preventionTips":["Make completion requests idempotent client-side: submit once per flow_id.","Poll flow.status instead of re-driving the exchange.","Treat 'already used' + status 'connected' as success, not failure.","Never cache and replay callback URLs against restarted flows."],"tags":["oauth","idempotency","state-machine","double-submit","codex"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}