decolua/9router · error · Error

`Token exchange failed: ${error}`

Error message

`Token exchange failed: ${error}`

What it means

Codex (OpenAI) OAuth token exchange: the POST to the token endpoint with the authorization code + code_verifier (PKCE) returned a non-2xx status. The response body is read as text and rethrown, so the message contains the server's OAuth error JSON (e.g. {"error":"invalid_grant"}). The login cannot complete and no tokens are returned.

Source

Thrown at src/lib/oauth/providers/codex.js:43

  exchangeToken: async (config, code, redirectUri, codeVerifier) => {
    const response = await fetch(config.tokenUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        Accept: "application/json",
      },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: config.clientId,
        code: code,
        redirect_uri: redirectUri,
        code_verifier: codeVerifier,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    return await response.json();
  },
  mapTokens: (tokens) => {
    const info = extractCodexAccountInfo(tokens.id_token);
    const mapped = {
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      idToken: tokens.id_token,
      expiresIn: tokens.expires_in,
      lastRefreshAt: new Date().toISOString(),
    };
    const email = info.email || extractEmailFromAccessToken(tokens.access_token);
    if (email) mapped.email = email;
    if (info.chatgptAccountId || info.chatgptPlanType) {
      mapped.providerSpecificData = {
        chatgptAccountId: info.chatgptAccountId,

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Parse the body — the 'error' field (invalid_grant, invalid_client, invalid_request) pinpoints the cause.
  2. Restart the full OAuth flow to mint a new code and use the matching code_verifier from the same session.
  3. Verify client_id and redirect_uri in config match the registered Codex OAuth app exactly.
  4. If 429/5xx, wait and retry the exchange with the same (still-valid) code once.

Example fix

// before
if (!response.ok) {
  const error = await response.text();
  throw new Error(`Token exchange failed: ${error}`);
}
// after
if (!response.ok) {
  const error = await response.text();
  let code = "unknown";
  try { code = JSON.parse(error).error; } catch {}
  throw new Error(`Token exchange failed (HTTP ${response.status}, ${code}): ${error}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before exchanging, confirm PKCE state is intact and unexpired
if (!code || !codeVerifier) throw new Error("Missing code or code_verifier for Codex exchange");
if (Date.now() - flowStartedAt > 9 * 60 * 1000) throw new Error("Authorization code likely expired — restart flow");

Type guard

function isOAuthTokenError(body) {
  if (typeof body !== "object" || body === null) return false;
  return typeof body.error === "string"; // RFC 6749 §5.2
}

Try / catch

try {
  const tokens = await codexProvider.exchangeCode(code, codeVerifier);
} catch (err) {
  if (String(err.message).includes("Token exchange failed")) {
    if (/invalid_grant/.test(err.message)) {
      // code expired/replayed/verifier mismatch — restart the whole flow
      await restartCodexOAuthFlow();
    } else if (/invalid_client/.test(err.message)) {
      console.error("Codex client_id/secret misconfigured");
    } else {
      // 429/5xx — one safe retry with the same code
      await retryOnce(() => codexProvider.exchangeCode(code, codeVerifier));
    }
  } else throw err;
}

Prevention

When it happens

Trigger: Calling exchangeCode for codex when the token endpoint replies !response.ok — expired/replayed authorization_code, wrong or mismatched code_verifier, invalid client_id, redirect_uri mismatch, or 429/5xx.

Common situations: PKCE verifier/store mismatch after restarting the flow; taking longer than the code lifetime; replaying a callback URL after a browser refresh; client_id/redirect changed upstream; OpenAI-side outage.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/8930c33f97ad56dc. Report an issue: GitHub.