chatboxai/chatbox · error · Error

No refresh token available

Error message

No refresh token available

What it means

Thrown by AnthropicOAuthProvider.refreshToken() when the stored credentials object has no refreshToken. The provider needs a refresh token to POST grant_type=refresh_token to the token endpoint, so an absent one makes refresh impossible and is treated as a hard failure (unlike Qwen/MiniMax which fall back to returning the existing credentials).

Source

Thrown at src/main/oauth/providers/anthropic.ts:138

      throw new Error(`Token exchange failed: ${error}`)
    }

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

    return {
      accessToken: data.access_token,
      refreshToken: data.refresh_token,
      expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
    }
  },

  async refreshToken(credentials) {
    if (!credentials.refreshToken) {
      throw new Error('No refresh token available')
    }

    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        grant_type: 'refresh_token',
        client_id: CLIENT_ID,
        refresh_token: credentials.refreshToken,
      }),
    })

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

View on GitHub (pinned to 81571269ad)

Solutions

  1. Before calling refreshToken, check credentials.refreshToken exists; if absent, trigger a fresh interactive login (startLogin + exchangeCode) instead.
  2. Verify the persisted credential schema includes refreshToken and that exchangeCode's return value was stored wholesale.
  3. If you want graceful degradation, treat a missing refreshToken as 're-auth required' and surface a sign-in prompt rather than throwing.

Example fix

// before
const refreshed = await provider.refreshToken(credentials)

// after
if (!credentials.refreshToken) {
  // Force a fresh interactive login; refresh is impossible without a token.
  return startInteractiveLogin()
}
const refreshed = await provider.refreshToken(credentials)
Defensive patterns

Strategy: validation

Validate before calling

function hasRefreshToken(c: { refreshToken?: string | null }): boolean {
  return typeof c.refreshToken === 'string' && c.refreshToken.length > 0
}

if (!hasRefreshToken(credentials)) {
  // refresh impossible — start a fresh interactive login instead
  return startInteractiveLogin()
}
await provider.refreshToken(credentials)

Type guard

function hasRefreshToken(c: unknown): c is { refreshToken: string } {
  return typeof c === 'object' && c !== null &&
    typeof (c as any).refreshToken === 'string' && (c as any).refreshToken.length > 0
}

Prevention

When it happens

Trigger: Calling refreshToken on credentials produced by a flow that never stored a refresh_token; credentials loaded from storage where refreshToken was never persisted; a previous exchange returned a body without refresh_token but exchangeCode still resolved.

Common situations: Storage migration dropped the refreshToken field; an older build saved credentials before refresh support was added; credentials object was constructed manually for testing without a refreshToken.

Related errors


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