HeyPuter/puter · error · HttpError

ElevenLabs returned ${response.status}

Error message

ElevenLabs returned ${response.status}

What it means

After the upstream fetch, if response.ok is false the driver wraps the ElevenLabs status into a tagged legacyCode: upstream_provider_unavailable (ElevenLabs 5xx, exposed as 400), upstream_auth_failed (ElevenLabs 401/403, exposed as 500), upstream_rate_limited (429, exposed as 429), otherwise upstream_bad_request (exposed as the raw status). The message is ElevenLabs' detail field when present, else 'ElevenLabs returned <status>'. provider and upstreamStatus are attached as fields.

Source

Thrown at src/backend/drivers/ai-speech2speech/VoiceChangerDriver.ts:289

            // as 400 like the TTS provider does — user can't act on
            // them, but it's not our bug either).
            const legacyCode =
                response.status >= 500
                    ? 'upstream_provider_unavailable'
                    : response.status === 401 || response.status === 403
                      ? 'upstream_auth_failed'
                      : response.status === 429
                        ? 'upstream_rate_limited'
                        : 'upstream_bad_request';
            const exposedStatus =
                legacyCode === 'upstream_rate_limited'
                    ? 429
                    : legacyCode === 'upstream_auth_failed'
                      ? 500
                      : legacyCode === 'upstream_provider_unavailable'
                        ? 400
                        : response.status;
            throw new HttpError(exposedStatus, message, {
                legacyCode,
                fields: {
                    provider: 'elevenlabs',
                    upstreamStatus: response.status,
                },
            });
        }

        const arrayBuffer = await response.arrayBuffer();
        const stream = Readable.from(Buffer.from(arrayBuffer));
        this.services.metering.incrementUsage(
            actor,
            usageKey,
            estimatedSeconds,
            ucentsPerSecond * estimatedSeconds,
        );

        return {

View on GitHub (pinned to 908ec23eda)

Solutions

  1. For 429 (upstream_rate_limited): backoff and retry with jitter; reduce concurrency.
  2. For 401/403 (upstream_auth_failed): rotate the ElevenLabs API key in config.
  3. For 5xx (upstream_provider_unavailable): surface a 'provider unavailable' message and check the ElevenLabs status page.
  4. For 400 (upstream_bad_request): inspect fields.upstreamStatus and the message detail; fix the offending voice/model/params.
Defensive patterns

Strategy: retry

Try / catch

async function convertWithRetry(args, tries = 3) {
  for (let i = 0; i < tries; i++) {
    try {
      return await driver.convert(args);
    } catch (e) {
      const retriable = e?.legacyCode === 'upstream_rate_limited'
        || e?.legacyCode === 'upstream_provider_unavailable';
      if (retriable && i < tries - 1) {
        await new Promise(r => setTimeout(r, 2 ** i * 500 + Math.random() * 250));
        continue;
      }
      throw e;
    }
  }
}

Prevention

When it happens

Trigger: ElevenLabs returns 429 (quota/rate), 401/403 (our key revoked or invalid), 400 (a voice/model it rejects), or 5xx (ElevenLabs outage). The exposed HTTP status depends on the mapped legacyCode, not always the upstream status.

Common situations: Burst traffic hitting ElevenLabs rate limits; an expired or rotated API key; an ElevenLabs incident; a voice id that exists but is disabled on the account.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/797dec36c4091cde. Report an issue: GitHub.