janhq/jan · error · Error
API key rotation exhausted
Error message
API key rotation exhausted
What it means
Thrown by createApiKeyRotatingFetch after the fetch wrapper cycled through every entry in the apiKeys rotation chain. The loop only continues on 401/403/429 (auth/rate-limit) responses while another key remains; reaching this throw means each key was attempted and each was rejected with one of those statuses. It is the terminal guard at the bottom of the rotation for-loop (model-factory.ts:778).
Source
Thrown at web-app/src/lib/model-factory.ts:778
): Promise<Response> => {
for (let i = 0; i < apiKeys.length; i++) {
const key = apiKeys[i]!
const nextHeaders = new Headers(init?.headers as HeadersInit | undefined)
if (headerMode === 'authorization-bearer') {
nextHeaders.set('Authorization', `Bearer ${key}`)
} else if (headerMode === 'x-goog-api-key') {
nextHeaders.set('x-goog-api-key', key)
} else {
nextHeaders.set('x-api-key', key)
}
const res = await inner(input, { ...init, headers: nextHeaders })
if ([401, 403, 429].includes(res.status) && i < apiKeys.length - 1) {
res.body?.cancel().catch(() => {})
continue
}
return res
}
throw new Error('API key rotation exhausted')
}
}
// An empty apiKey still puts an empty auth header on the wire, which upstreams
// answer with misleading 401s (e.g. Anthropic's "x-api-key header is
// required"). Fail here with an actionable message instead.
function requireRemoteApiKey(
provider: ProviderObject,
keyChain: string[]
): string {
const key = keyChain[0] ?? provider.api_key?.trim()
if (!key) {
throw new Error(
`No API key configured for ${provider.provider}. Add one in Settings > Model Providers.`
)
}
return key
}View on GitHub (pinned to fad3f12a14)
Solutions
- Open Settings > Model Providers and replace at least one API key with a freshly issued, verified key.
- If 429s dominate, reduce concurrency / add backoff before retrying — rotation is not a substitute for rate-limit headroom.
- Confirm the keys are valid with a direct curl to the provider's /models or /chat/completions endpoint.
- Check provider.api_key and the keyChain source (keyring) are not all empty strings, which rotate but never authenticate.
Example fix
// before: rotate silently, surface only 'exhausted'
// after: surface the last response body so the user sees the provider's reason
const res = await inner(input, { ...init, headers: nextHeaders })
if ([401, 403, 429].includes(res.status) && i < apiKeys.length - 1) {
res.body?.cancel().catch(() => {})
continue
}
return res
// (loop fallthrough now impossible; keep the throw as a defensive invariant) Defensive patterns
Strategy: validation
Validate before calling
function pickFirstValidKey(apiKeys: string[]): string | undefined {
return apiKeys.find(k => typeof k === 'string' && k.trim().length > 0)
}
// before constructing createApiKeyRotatingFetch:
if (!pickFirstValidKey(apiKeys)) {
throw new Error('No valid API key available for rotation')
} Type guard
function hasUsableKeyChain(keys: unknown): keys is string[] {
return Array.isArray(keys) && keys.some(k => typeof k === 'string' && k.trim().length > 0)
} Try / catch
try {
return await createApiKeyRotatingFetch(fetch, apiKeys, params, headerMode)(url, init)
} catch (e) {
if (e instanceof Error && e.message === 'API key rotation exhausted') {
// surface a settings deep-link; do not retry with the same keys
throw new Error('All API keys were rejected. Update them in Settings > Model Providers.')
}
throw e
} Prevention
- Validate that at least one key is non-empty before entering the rotation path.
- Cache a health flag per key so known-bad keys are skipped on subsequent rotations.
- Surface the last failing status (401 vs 403 vs 429) to the user instead of a generic 'exhausted'.
- Treat 429 with backoff, not key rotation — rotation does not help a rate limit.
When it happens
Trigger: A provider is configured with multiple API keys (keyChain length > 1) and every key returns 401 (invalid), 403 (forbidden), or 429 (rate-limited) on the chat-completion request. The inner fetch runs once per key, cancels the body on those statuses, and continues until i exhausts apiKeys.length.
Common situations: All rotated keys were revoked or expired at once; the user pasted the same dead key into every slot; the provider hit a platform-wide rate limit so even healthy keys 429; a corporate proxy strips auth headers so every key looks invalid.
Related errors
- Failed to fetch models from ${provider.provider}: ${lastStat
- Authentication failed: API key is required or invalid for ${
- Failed to fetch supported backends: ${error instanceof Error
- No API key configured for ${provider.provider}. Add one in S
- Failed to fetch HuggingFace repository: ${response.status} $
AI-assisted analysis of janhq/jan@fad3f12a14 (2026-08-12).
Data as JSON: /api/errors/fcbe7637d042aa4e.
Report an issue: GitHub.