chatboxai/chatbox · error · Error

Device flow failed: ${raw.error}

Error message

Device flow failed: ${raw.error}

What it means

Thrown inside the Copilot device-flow polling loop when GitHub's access_token endpoint returns HTTP 200 with an error field that is neither 'authorization_pending' nor 'slow_down'. These are application-level OAuth errors delivered in a 200 response, so fetchJson does not catch them; the loop's own error switch does.

Source

Thrown at src/main/oauth/providers/github-copilot.ts:125

        if (raw.access_token) {
          log.info('[OAuth:Copilot] GitHub access token obtained')
          // Use the GitHub access token directly as the Copilot API credential
          // (same approach as openllmprovider)
          return {
            accessToken: raw.access_token,
            // No refresh token — the GitHub access token doesn't expire
            // but the Copilot API session may need re-auth periodically
          }
        }

        if (raw.error === 'authorization_pending') continue
        if (raw.error === 'slow_down') {
          intervalMs += 5000
          continue
        }

        throw new Error(`Device flow failed: ${raw.error}`)
      }

      throw new Error('Device flow timed out')
    } finally {
      pendingDeviceCode = null
    }
  },

  async refreshToken(credentials) {
    // GitHub access tokens from device flow don't expire in the traditional sense.
    // Just return the existing credentials.
    return credentials
  },
}

View on GitHub (pinned to 81571269ad)

Solutions

  1. Inspect raw.error: 'expired_token' and 'access_denied' should restart the flow (startDeviceFlow), not retry the same device_code.
  2. For 'incorrect_client_credentials' / 'unsupported_grant_type' / 'device_flow_disabled', surface a config error — no amount of retrying will help.
  3. Surface a user-facing message: 'Authorization expired, please try again' for expired_token, 'Access was denied' for access_denied.

Example fix

// before
throw new Error(`Device flow failed: ${raw.error}`)

// after
if (raw.error === 'expired_token' || raw.error === 'access_denied') {
  throw new Error(raw.error === 'expired_token' ? 'Device code expired. Restart login.' : 'User denied the login request.')
}
throw new Error(`Device flow failed: ${raw.error}`)
Defensive patterns

Strategy: try-catch

Type guard

type DeviceFlowError = 'expired_token' | 'access_denied' | 'incorrect_client_credentials' | 'unsupported_grant_type' | 'device_flow_disabled'
function isTerminalDeviceError(error: unknown): error is DeviceFlowError {
  return typeof error === 'string' && ['expired_token','access_denied','incorrect_client_credentials','unsupported_grant_type','device_flow_disabled'].includes(error as string)
}

Try / catch

try {
  return await provider.waitForToken(signal)
} catch (e) {
  const msg = String(e)
  if (/expired_token/i.test(msg) || /access_denied/i.test(msg)) {
  // restartable: user-driven causes
  const fresh = await provider.startDeviceFlow()
  throw new ReauthRequiredError(fresh)
  }
  if (/incorrect_client|unsupported_grant|device_flow_disabled/i.test(msg)) {
  throw new Error('GitHub device flow misconfigured for this OAuth App.')
  }
  throw e
}

Prevention

When it happens

Trigger: error='expired_token' (user didn't authorize within the device-code lifetime), 'access_denied' (user explicitly denied the request at github.com/login/device), 'incorrect_client_credentials', 'unsupported_grant_type', or 'device_flow_disabled' for the client.

Common situations: User walked away past the ~15-minute device-code expiry; user clicked 'Cancel' on GitHub's device confirmation page; the OAuth App has device flow disabled in its settings; CLIENT_ID mismatch produces incorrect_client_credentials.

Related errors


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