unslothai/unsloth · warning · CodexAuthError

Authorization callback was invalid or already used.

Error message

Authorization callback was invalid or already used.

What it means

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.

Source

Thrown at studio/backend/core/inference/openai_codex_auth.py:337

    flow.status = "connected"
    if flow.server:
        flow.server.close()
        flow.server = None


async def _loopback_handler(
    flow: OAuthFlow, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
    try:
        first = await asyncio.wait_for(reader.readline(), timeout = 5)
        target = first.decode("ascii", "ignore").split(" ")[1]
        parsed = urlparse(target)
        query = parse_qs(parsed.query)
        if parsed.path != OPENAI_CODEX_CALLBACK_PATH or query.get("state", [""])[0] != flow.state:
            raise CodexAuthError("Authorization state did not match.")
        code = query.get("code", [""])[0]
        if not code or flow.consumed:
            raise CodexAuthError("Authorization callback was invalid or already used.")
        await _exchange_code(flow, code)
        message = "ChatGPT connected. You can close this window."
    except Exception as exc:
        # Stray requests and state mismatches must not poison the active flow.
        if flow.consumed and flow.status == "pending":
            flow.status = "error"
            flow.message = str(exc) if isinstance(exc, CodexAuthError) else "Authorization failed."
        message = "Authorization failed. Return to Unsloth Studio and try again."
    body = (
        "<!doctype html><meta charset=utf-8><title>Unsloth Studio</title>"
        + "<p>"
        + message.replace("&", "&amp;").replace("<", "&lt;")
        + "</p>"
    ).encode()
    writer.write(
        b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\nContent-Length: "
        + str(len(body)).encode()
        + b"\r\nConnection: close\r\n\r\n"

View on GitHub (pinned to 203007d190)

Solutions

  1. If consent was denied, start a new flow and approve the ChatGPT permission prompt.
  2. Do not refresh the loopback callback page — the code is single-use; check flow.status to confirm the first attempt succeeded.
  3. Guard duplicate completions in the UI (mark the flow complete after the first callback).
  4. If status ended as 'error' from a replay, re-authenticate from scratch; the code cannot be reused.

Example fix

// before
# loopback page auto-refreshes, re-sending ?code=... to the consumed flow
<meta http-equiv="refresh" content="2">

// after
# success page is static; completion is confirmed by polling flow status
flow = codex_auth.get_flow(provider_id, flow_id)
assert flow.status == "connected"
Defensive patterns

Strategy: try-catch

Validate before calling

q = parse_qs(urlparse(callback_url).query)
if not q.get("code", [""])[0]:
    error = q.get("error", [""])[0]
    handle_denied(error)  # e.g. access_denied; no code will ever arrive
if flow.consumed:
    check_first_attempt_result(flow)

Type guard

def callback_has_fresh_code(callback_url: str, flow: codex_auth.OAuthFlow) -> bool:
    q = parse_qs(urlparse(callback_url).query)
    return bool(q.get("code", [""])[0]) and not flow.consumed

Try / catch

try:
    await _exchange(flow, code)
except codex_auth.CodexAuthError as exc:
    if "invalid or already used" in str(exc) and flow.status == "connected":
        return  # replay of a successful callback; treat as done
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/b27309ecd6d6c9cb. Report an issue: GitHub.