chatboxai/chatbox · error · Error

Token exchange response missing access_token

Error message

Token exchange response missing access_token

What it means

Thrown by OpenAI's exchangeCodeForTokens() when the token endpoint returned HTTP 2xx but the JSON body has no access_token. This guards an unexpected success response — the exchange technically succeeded at the transport layer but the payload does not contain the credential the app needs.

Source

Thrown at src/main/oauth/providers/openai.ts:155

      code_verifier: verifier,
      redirect_uri: REDIRECT_URI,
    }),
  })

  if (!response.ok) {
    const text = await response.text()
    log.error('[OAuth:OpenAI] Token exchange failed:', text)
    throw new Error(`Token exchange failed: ${response.status}`)
  }

  const data = (await response.json()) as {
    access_token?: string
    refresh_token?: string
    expires_in?: number
  }

  if (!data.access_token) {
    throw new Error('Token exchange response missing access_token')
  }

  return {
    accessToken: data.access_token,
    refreshToken: data.refresh_token,
    expiresAt: typeof data.expires_in === 'number' ? Date.now() + data.expires_in * 1000 - 5 * 60 * 1000 : undefined,
    extra: {
      accountId: extractAccountId(data.access_token),
    },
  }
}

View on GitHub (pinned to 81571269ad)

Solutions

  1. Log the parsed body to detect contract drift or captive portals.
  2. Do not proceed without an access_token — trigger login() again.
  3. Check Content-Type before calling response.json() and treat non-JSON 200s as network/captive errors.

Example fix

// before
if (!data.access_token) {
  throw new Error('Token exchange response missing access_token')
}

// after
if (!data.access_token) {
  log.error('[OAuth:OpenAI] exchange 2xx but no access_token', JSON.stringify(data))
  throw new ReauthRequiredError('OpenAI token exchange returned no access_token. Restart login.')
}
Defensive patterns

Strategy: type-guard

Type guard

function isOpenAIExchangeResponse(data: unknown): data is { access_token: string; refresh_token?: string; expires_in?: number } {
  return typeof data === 'object' && data !== null && typeof (data as any).access_token === 'string' && (data as any).access_token.length > 0
}

Try / catch

try {
  return await provider.login({ openUrl, signal })
} catch (e) {
  if (/missing access_token/i.test(String(e))) {
  // 2xx but malformed — captive portal or contract drift; restart login
  throw new ReauthRequiredError('OpenAI exchange returned no access_token. Restart login.')
  }
  throw e
}

Prevention

When it happens

Trigger: OpenAI changed the exchange response shape (field renamed); a 200 response body that was HTML/empty due to a captive portal or proxy; partial JSON; the account is in a state where tokens are withheld with a 200 + error field instead of a 4xx.

Common situations: Captive portal returning a 200 page; auth.openai.com contract drift; middleware truncating the response; the consent flow returned a 2xx but did not actually grant (rare OpenAI edge).

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/b894c12b0034255f. Report an issue: GitHub.