{"record":{"id":"b27309ecd6d6c9cb","repo":"unslothai/unsloth","slug":"authorization-callback-was-invalid-or-already-used","errorCode":null,"errorMessage":"Authorization callback was invalid or already used.","messagePattern":"Authorization callback was invalid or already used\\.","errorType":"exception","errorClass":"CodexAuthError","httpStatus":400,"severity":"warning","filePath":"studio/backend/core/inference/openai_codex_auth.py","lineNumber":337,"sourceCode":"    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:\n        first = await asyncio.wait_for(reader.readline(), timeout = 5)\n        target = first.decode(\"ascii\", \"ignore\").split(\" \")[1]\n        parsed = urlparse(target)\n        query = parse_qs(parsed.query)\n        if parsed.path != OPENAI_CODEX_CALLBACK_PATH or query.get(\"state\", [\"\"])[0] != flow.state:\n            raise CodexAuthError(\"Authorization state did not match.\")\n        code = query.get(\"code\", [\"\"])[0]\n        if not code or flow.consumed:\n            raise CodexAuthError(\"Authorization callback was invalid or already used.\")\n        await _exchange_code(flow, code)\n        message = \"ChatGPT connected. You can close this window.\"\n    except Exception as exc:\n        # Stray requests and state mismatches must not poison the active flow.\n        if flow.consumed and flow.status == \"pending\":\n            flow.status = \"error\"\n            flow.message = str(exc) if isinstance(exc, CodexAuthError) else \"Authorization failed.\"\n        message = \"Authorization failed. Return to Unsloth Studio and try again.\"\n    body = (\n        \"<!doctype html><meta charset=utf-8><title>Unsloth Studio</title>\"\n        + \"<p>\"\n        + message.replace(\"&\", \"&amp;\").replace(\"<\", \"&lt;\")\n        + \"</p>\"\n    ).encode()\n    writer.write(\n        b\"HTTP/1.1 200 OK\\r\\nContent-Type: text/html; charset=utf-8\\r\\nCache-Control: no-store\\r\\nContent-Length: \"\n        + str(len(body)).encode()\n        + b\"\\r\\nConnection: close\\r\\n\\r\\n\"","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/openai_codex_auth.py#L319-L355","documentation":"Raised as CodexAuthError by the loopback handler when the callback query contains no 'code' parameter or the flow was already consumed. It distinguishes a code-less callback (user denied consent, or error=access_denied redirect) from a replayed callback after _exchange_code already ran. Because it may fire on a consumed flow, the except block will then mark a still-pending flow as errored, unlike a pure state mismatch.","triggerScenarios":"OpenAI redirects back with error=access_denied and no code after the user declines consent; a second browser request hits the callback after the code was already exchanged (flow.consumed is True); the code parameter is empty due to URL truncation.","commonSituations":"User clicks 'Cancel/Deny' on the ChatGPT consent screen; browser prefetch or duplicate navigation hitting the callback URL twice; user refreshes the callback success page (refresh re-sends the same URL after consumption).","solutions":["If consent was denied, start a new flow and approve the ChatGPT permission prompt.","Do not refresh the loopback callback page — the code is single-use; check flow.status to confirm the first attempt succeeded.","Guard duplicate completions in the UI (mark the flow complete after the first callback).","If status ended as 'error' from a replay, re-authenticate from scratch; the code cannot be reused."],"exampleFix":"// before\n# loopback page auto-refreshes, re-sending ?code=... to the consumed flow\n<meta http-equiv=\"refresh\" content=\"2\">\n\n// after\n# success page is static; completion is confirmed by polling flow status\nflow = codex_auth.get_flow(provider_id, flow_id)\nassert flow.status == \"connected\"","handlingStrategy":"try-catch","validationCode":"q = parse_qs(urlparse(callback_url).query)\nif not q.get(\"code\", [\"\"])[0]:\n    error = q.get(\"error\", [\"\"])[0]\n    handle_denied(error)  # e.g. access_denied; no code will ever arrive\nif flow.consumed:\n    check_first_attempt_result(flow)","typeGuard":"def callback_has_fresh_code(callback_url: str, flow: codex_auth.OAuthFlow) -> bool:\n    q = parse_qs(urlparse(callback_url).query)\n    return bool(q.get(\"code\", [\"\"])[0]) and not flow.consumed","tryCatchPattern":"try:\n    await _exchange(flow, code)\nexcept codex_auth.CodexAuthError as exc:\n    if \"invalid or already used\" in str(exc) and flow.status == \"connected\":\n        return  # replay of a successful callback; treat as done\n    raise","preventionTips":["Disable auto-refresh on the loopback callback page.","Dedupe callbacks per flow (first code wins, rest are ignored).","Map error=access_denied redirects to friendly 'permission denied' UX before they reach the exchange.","Never re-drive an exchange after the flow shows consumed."],"tags":["oauth","consent-denied","replay","loopback-server","codex"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}