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("&", "&").replace("<", "<")
+ "</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
- 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.
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
- 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.
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
- Authorization state did not match.
- The callback URL did not contain an authorization code.
- ChatGPT credential update is busy. Please retry.
- ChatGPT returned an invalid access token.
- The ChatGPT account identifier was missing.
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/b27309ecd6d6c9cb.
Report an issue: GitHub.