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
- Redo the login/authorization flow to get a fresh code — used or expired codes cannot be retried
- Check the formatted status/body in the message: 400 invalid_grant means the code is spent/expired; 401 means client credentials issue
- Update the CLI so the embedded client_id/PKCE handling matches current OpenAI requirements
- 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
- Never reuse an authorization code — each is single-use
- Complete authorization and exchange within the code lifetime
- Keep client_id and PKCE verifier consistent between authorize and token requests
- Update the CLI when OpenAI rotates OAuth clients
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
- Device authorization initiation failed: ${initResponse.statu
- Failed to initiate device authorization: ${initiateResponse.
- GitLab OAuth token exchange failed: ${response.status} ${awa
- Kimi device flow failed: ${error ?? response.status}${descri
- Kimi token refresh failed: ${response.status}${description}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/7a5950182a770898.
Report an issue: GitHub.