nexu-io/open-design · error · Error
elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}
Error message
elevenlabs voices ${resp.status}: ${errText.slice(0, 240)} What it means
Thrown by listElevenLabsVoiceOptions() when the GET to {baseUrl}/v2/voices?page_size=N returns a non-ok response. The first 240 chars of the response body are embedded so the operator can see ElevenLabs' error detail (rate limit, invalid key, wrong base URL).
Source
Thrown at apps/daemon/src/integrations/elevenlabs-voices.ts:137
pageSize,
});
const cached = voiceOptionsCache.get(cacheKey);
const now = Date.now();
if (cached && cached.expiresAt > now) {
return cloneVoiceOptions(cached.voices);
}
const resp = await fetch(`${baseUrl}/v2/voices?page_size=${pageSize}`, {
...options.requestInit,
method: 'GET',
headers: {
'xi-api-key': credentials.apiKey,
accept: 'application/json',
},
});
if (!resp.ok) {
const errText = await resp.text();
throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);
}
const payload = await resp.json() as unknown;
const rawVoices = isRecord(payload) && Array.isArray(payload.voices)
? payload.voices
: [];
const voices = rawVoices
.map((voice) => normalizeVoice(voice))
.filter((voice): voice is ElevenLabsVoiceOption => voice !== null);
voiceOptionsCache.set(cacheKey, {
expiresAt: now + ELEVENLABS_VOICE_CACHE_TTL_MS,
voices: cloneVoiceOptions(voices),
});
return voices;
}
View on GitHub (pinned to 5be4028344)
Solutions
- Inspect the embedded status/body: 401 → rotate the key, 429 → back off and cache longer, 404 → fix baseUrl.
- Confirm baseUrl (credentials.baseUrl or https://api.elevenlabs.io) actually serves /v2/voices.
- Rely on the in-memory voice cache (TTL 10 min) to ride through short outages instead of re-fetching on every call.
Example fix
// before
const resp = await fetch(`${baseUrl}/v2/voices?page_size=${pageSize}`, ...);
if (!resp.ok) throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);
// after
if (!resp.ok) {
const cached = voiceOptionsCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) return cached.voices;
throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);
} Defensive patterns
Strategy: retry
Type guard
function isElevenLabsHttpError(err: unknown): boolean {
return err instanceof Error && /^elevenlabs voices \d{3}:/.test(err.message);
}
function extractStatus(err: unknown): number | null {
const m = err instanceof Error ? err.message.match(/^elevenlabs voices (\d{3}):/) : null;
return m ? Number(m[1]) : null;
} Try / catch
import { voiceOptionsCache } from '../integrations/elevenlabs-voices.js';
try {
return await listElevenLabsVoiceOptions(projectRoot);
} catch (err) {
const status = extractStatus(err);
if (status && status >= 500) {
// transient — caller may retry; cache serves stale if present
}
throw err;
} Prevention
- Rely on the 10-minute in-memory voice cache to absorb short outages.
- Rotate keys before they expire to avoid 401s in production.
- Confirm baseUrl actually serves /v2/voices before pointing ElevenLabs at a proxy.
When it happens
Trigger: ElevenLabs returns 401 (invalid xi-api-key), 429 (rate limit / quota), 404 (custom baseUrl that does not host /v2/voices), or 5xx. Also when a proxy baseUrl (e.g. an OpenAI-compatible gateway) is set for elevenlabs but does not implement the /v2/voices route.
Common situations: Expired or revoked API key; free-tier rate limit during catalogue refresh; baseUrl pointing at a ElevenLabs-compatible proxy that lacks the voices route; transient 5xx during an ElevenLabs incident.
Related errors
- collab cloud error ${status} (${code})
- no ElevenLabs API key - configure it in Settings or set OD_E
- daemon ${resp.status} on ${url}: ${body || resp.statusText}
- anthropic ${resp.status}: ${await resp.text().catch(() => ''
- openai ${resp.status}: ${await resp.text().catch(() => '')}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/51c8be96f1f8b454.
Report an issue: GitHub.