chatboxai/chatbox · error · Error

Token refresh response missing access_token

Error message

Token refresh response missing access_token

What it means

Thrown by OpenAI's refreshToken() when the refresh POST returned HTTP 2xx but the JSON body has no access_token field. This guards a malformed/unexpected success response: OpenAI's contract requires access_token on refresh, so its absence signals an API contract change, a non-JSON body parsed loosely, or a partial response.

Source

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

        refresh_token: credentials.refreshToken,
        client_id: CLIENT_ID,
      }),
    })

    if (!response.ok) {
      const text = await response.text()
      log.error('[OAuth:OpenAI] Token refresh failed:', text)
      throw new Error(`Token refresh 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 refresh response missing access_token')
    }

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

async function exchangeCodeForTokens(code: string, verifier: string): Promise<OAuthCredentials> {
  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({

View on GitHub (pinned to 81571269ad)

Solutions

  1. Log the full response body when access_token is absent to detect contract drift or captive portals.
  2. Treat as a hard failure — do not return credentials with a missing access_token; trigger an interactive login().
  3. If the cause is a captive portal, detect non-JSON Content-Type before parsing and surface a network error instead.

Example fix

// before
const data = (await response.json()) as { access_token?: string; ... }
if (!data.access_token) {
  throw new Error('Token refresh response missing access_token')
}

// after
const data = (await response.json()) as { access_token?: string; ... }
if (!data.access_token) {
  log.error('[OAuth:OpenAI] refresh 2xx but no access_token', JSON.stringify(data))
  throw new ReauthRequiredError('OpenAI refresh response malformed — re-login required')
}
Defensive patterns

Strategy: type-guard

Type guard

function isOpenAIRefreshResponse(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.refreshToken(credentials)
} catch (e) {
  if (/missing access_token/i.test(String(e))) {
  // 2xx but malformed — likely captive portal or contract drift; re-login
  await clearStoredCredentials()
  throw new ReauthRequiredError('OpenAI refresh response malformed — re-login required')
  }
  throw e
}

Prevention

When it happens

Trigger: OpenAI changed the response shape (field renamed); the body was HTML or empty (e.g. captive portal returning 200); a proxy stripped the body; the response was a partial JSON object missing access_token.

Common situations: Captive portal returning a 200 login page; OpenAI contract drift after an auth endpoint update; middleware corrupting the body; race where response.json() parsed an incomplete stream.

Related errors


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