coleam00/Archon · error

OpenAI token response missing access_token/expires_in.

Error message

OpenAI token ${operation} response missing access_token/expires_in.

What it means

credentialsFromTokenResponse validates the JSON returned by OpenAI's OAuth token endpoint during an authorization-code exchange or refresh. It throws when the response lacks a usable access_token (non-empty string) or expires_in (finite number), meaning OpenAI's reply did not contain the fields needed to build a credential. Throwing here prevents storing a half-formed credential that would fail later at request time.

Solutions

  1. Log the full response body (redacting secrets) to see what OpenAI actually returned.
  2. Retry the OAuth flow from scratch: the authorization code may be expired or already consumed — start a new PKCE login.
  3. Check for a proxy/interceptor altering the token endpoint response (custom OPENAI_BASE_URL, corporate MITM proxy).
  4. Verify you are POSTing to https://auth.openai.com/oauth/token with grant_type=authorization_code and the correct client_id.
  5. If it persists across attempts, check OpenAI status/Auth0 tenant issues and the library's pinned client config for staleness.

Example fix

// before: passing raw response without checking
const creds = await exchangeOpenAiAuthorizationCode(res.json());
// after: inspect and fail on non-token responses
const json = await res.json();
if (json.error) throw new Error(`Token endpoint error: ${json.error} - ${json.error_description}`);
const creds = await exchangeOpenAiAuthorizationCode(json);
Defensive patterns

Strategy: validation

Validate before calling

const looksLikeTokenResponse = (j) =>
  j && typeof j.access_token === 'string' && j.access_token.length > 0 &&
  Number.isFinite(j.expires_in);
const json = await res.json();
if (json?.error) throw new Error(`token endpoint: ${json.error}: ${json.error_description}`);
if (!looksLikeTokenResponse(json)) throw new Error('unexpected token response shape: ' + JSON.stringify(Object.keys(json ?? {})));

Type guard

function isTokenResponse(j: unknown): j is { access_token: string; expires_in: number } {
  const o = j as Record<string, unknown>;
  return !!o && typeof o.access_token === 'string' && o.access_token !== '' &&
    typeof o.expires_in === 'number' && Number.isFinite(o.expires_in);
}

Try / catch

try {
  const creds = await exchangeOpenAiAuthorizationCode(code, verifier);
} catch (e) {
  if (e.message.includes('missing access_token/expires_in')) {
    logRawTokenResponse(lastResponseBody); // inspect, redact secrets
    throw new Error('OpenAI token endpoint returned a non-token response; restart the OAuth login');
  }
  throw e;
}

Prevention

When it happens

Trigger: An OpenAI token exchange or refresh HTTP response body parses to JSON that is missing access_token or has a non-string access_token, or missing/non-numeric expires_in (e.g. expires_in: null or a string).

Common situations: OpenAI returns a 4xx error body (invalid_grant, expired code) that still parses as JSON; a proxy or captive portal returns HTML/JSON without token fields; OpenAI changes the token response shape; the caller accidentally passes the wrong endpoint's JSON.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/8c52d7d277aec289. Report an issue: GitHub.

Appendix: source

Thrown at packages/core/src/credentials/openai-oauth.ts:232

  }
  return raw as OpenAiTokenResponse;
}

/**
 * Map a token response onto the stored credential blob. Fails loud on a
 * missing `id_token` at exchange time (the whole point of owning this flow);
 * on refresh, a response that omits `id_token`/`refresh_token` PRESERVES the
 * previous values instead of degrading the blob.
 */
function credentialsFromTokenResponse(
  json: OpenAiTokenResponse,
  operation: 'exchange' | 'refresh',
  previous?: OAuthCredentials
): OpenAiOAuthCredentials {
  const access = typeof json.access_token === 'string' ? json.access_token : '';
  const expiresIn = typeof json.expires_in === 'number' ? json.expires_in : NaN;
  if (!access || !Number.isFinite(expiresIn)) {
    throw new Error(`OpenAI token ${operation} response missing access_token/expires_in.`);
  }
  const prevRefresh = typeof previous?.refresh === 'string' ? previous.refresh : '';
  const refresh = typeof json.refresh_token === 'string' ? json.refresh_token : prevRefresh;
  if (!refresh) {
    throw new Error(`OpenAI token ${operation} response missing refresh_token.`);
  }
  const prevIdToken = typeof previous?.id_token === 'string' ? previous.id_token : '';
  const idToken = typeof json.id_token === 'string' && json.id_token ? json.id_token : prevIdToken;
  if (!idToken) {
    // Fail loud: an id_token-less credential reproduces the exact #1924
    // breakage ("invalid ID token format" in the Codex CLI) — never store one.
    throw new Error(
      `OpenAI token ${operation} response did not include an id_token (required by the Codex CLI).`
    );
  }
  const prevAccountId = typeof previous?.accountId === 'string' ? previous.accountId : '';
  const accountId = accountIdFromAccessToken(access) ?? prevAccountId;
  if (!accountId) {

View on GitHub (pinned to 0773b97458)