can1357/oh-my-pi · error · AIError.OAuthError

Token exchange failed: ${formatOpenAICodexTokenEndpointError

Error message

Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}

What it means

Thrown by exchangeCodeForToken in the OpenAI Codex OAuth flow when the token endpoint responds with a non-ok HTTP status. The message embeds a formatted summary of the status and response body (formatOpenAICodexTokenEndpointError) and the error carries kind='token-exchange' plus the numeric status. It means the authorization code (or refresh token) could not be exchanged for an access token.

Source

Thrown at packages/ai/src/registry/oauth/openai-codex.ts:193

	redirectUri: string,
	fetchImpl: FetchImpl = fetch,
): Promise<OAuthCredentials> {
	const tokenResponse = await fetchImpl(TOKEN_URL, {
		method: "POST",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: new URLSearchParams({
			grant_type: "authorization_code",
			client_id: CLIENT_ID,
			code,
			code_verifier: verifier,
			redirect_uri: redirectUri,
		}),
		signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
	});

	if (!tokenResponse.ok) {
		const bodyText = await tokenResponse.text();
		throw new AIError.OAuthError(
			`Token exchange failed: ${formatOpenAICodexTokenEndpointError(tokenResponse.status, bodyText)}`,
			{ kind: "token-exchange", status: tokenResponse.status },
		);
	}

	const tokenData = (await tokenResponse.json()) as {
		access_token?: string;
		refresh_token?: string;
		id_token?: string;
		expires_in?: number;
	};

	if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
		throw new AIError.OAuthError("Token response missing required fields", { kind: "validation" });
	}

	const { accountId, email, planType } = getTokenProfile(tokenData.access_token, tokenData.id_token);
	if (!accountId) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Redo the login/authorization flow to get a fresh code — used or expired codes cannot be retried
  2. Check the formatted status/body in the message: 400 invalid_grant means the code is spent/expired; 401 means client credentials issue
  3. Update the CLI so the embedded client_id/PKCE handling matches current OpenAI requirements
  4. If status is 5xx, wait and retry; check OpenAI status page for incidents

Example fix

// before: single-shot exchange crashes on hiccup
const tokens = await exchangeCodeForToken(code, verifier);
// after: detect invalid_grant and restart auth
try {
  const tokens = await exchangeCodeForToken(code, verifier);
} catch (e) {
  if (e instanceof AIError.OAuthError && e.kind === 'token-exchange') {
    console.error('Code rejected — restarting login:', e.message);
    return startLoginFlow();
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// exchange codes promptly; codes are single-use and short-lived
if (Date.now() - codeReceivedAtMs > 5 * 60 * 1000) {
  // code likely expired — restart the authorization flow instead of exchanging
}

Try / catch

try {
  const tokens = await exchangeCodeForToken(code, verifier);
} catch (e) {
  if (e instanceof AIError.OAuthError && e.kind === 'token-exchange') {
    // e.status: 400 invalid_grant → get a fresh code; 5xx → retry
    if (e.status === 400) return restartLogin();
  }
  throw e;
}

Prevention

When it happens

Trigger: Exchanging an authorization code that was already used (codes are single-use); the code expired (typically ~ minutes after login); client_id mismatch between the authorize and token requests; PKCE verifier mismatch; OpenAI returning 4xx/5xx from the token endpoint. Called from both exchangeToken (manual paste) and loginOpenAICodexDevice.

Common situations: User pastes an authorization code a second time after a first attempt failed downstream; user takes too long between opening the auth URL and completing exchange; clock skew invalidating PKCE; OpenAI API incident; proxy stripping POST body params.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7a5950182a770898. Report an issue: GitHub.