SubtitleEdit/subtitleedit · error · HttpRequestException

ElevenLabs voice list request failed: HTTP {(int)result.Stat

Error message

ElevenLabs voice list request failed: HTTP {(int)result.StatusCode} {result.StatusCode}

What it means

HttpRequestException from DownloadElevenLabsVoiceList when the voice-list GET returns non-2xx. Per the comment, this throw-on-failure is intentional: copying an error body into the cached voice-list JSON used to permanently empty the list because the bundled fallback only restores when the file is missing.

Source

Thrown at src/ui/Logic/Download/TtsDownloadService.cs:230

        requestMessage.Headers.TryAddWithoutValidation("Content-Type", "application/json");
        requestMessage.Headers.TryAddWithoutValidation("Accept", "application/json");

        if (!string.IsNullOrEmpty(Se.Settings.Video.TextToSpeech.ElevenLabsApiKey))
        {
            requestMessage.Headers.TryAddWithoutValidation("xi-api-key", Se.Settings.Video.TextToSpeech.ElevenLabsApiKey);
        }

        var result = await _httpClient.SendAsync(requestMessage, cancellationToken);

        // Throw on failure instead of copying the error body into the stream: the caller writes
        // the stream over its cached voice-list JSON, and since the bundled fallback only kicks
        // in when the file is *missing*, one failed refresh (expired key, network error) used to
        // permanently empty the voice list.
        if (!result.IsSuccessStatusCode)
        {
            var error = (await result.Content.ReadAsStringAsync(cancellationToken)).Trim();
            SeLogger.Error($"ElevenLabs TTS failed calling API address {url} : Status code={result.StatusCode} {TruncateForLog(error)}");
            throw new HttpRequestException($"ElevenLabs voice list request failed: HTTP {(int)result.StatusCode} {result.StatusCode}");
        }

        await result.Content.CopyToAsync(ms, cancellationToken);
    }

    public async Task DownloadMurfVoiceList(MemoryStream ms, IProgress<float>? progress, CancellationToken cancellationToken)
    {
        var url = "https://api.murf.ai/v1/speech/voices";

        using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
        requestMessage.Headers.TryAddWithoutValidation("Content-Type", "application/json");
        requestMessage.Headers.TryAddWithoutValidation("Accept", "application/json");
        requestMessage.Headers.TryAddWithoutValidation("api-key", Se.Settings.Video.TextToSpeech.MurfApiKey);

        var result = await _httpClient.SendAsync(requestMessage, cancellationToken);

        // See DownloadElevenLabsVoiceList: an error body must not reach the cached voice list.
        if (!result.IsSuccessStatusCode)

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Verify the ElevenLabs API key is valid and not expired.
  2. Check the account quota / billing status (429 means throttle).
  3. Inspect the SeLogger line for the trimmed server error body.
  4. Retry after a short backoff for transient 5xx responses.
Defensive patterns

Strategy: try-catch

Validate before calling

if (string.IsNullOrWhiteSpace(Se.Settings.Video.TextToSpeech.ElevenLabsApiKey))
{
    throw new InvalidOperationException("ElevenLabs API key is not set.");
}

Try / catch

try { await service.DownloadElevenLabsVoiceList(ms, progress, ct); }
catch (HttpRequestException ex) when (ex.Message.Contains("ElevenLabs voice list"))
{
    // keep the existing cached voice list; surface the status code to the user
}

Prevention

When it happens

Trigger: _httpClient.SendAsync for the ElevenLabs voice list returns !IsSuccessStatusCode. The trimmed error body is logged, and the status code is surfaced in the exception message.

Common situations: Expired or invalid ElevenLabs API key (401), quota exhausted (429), wrong endpoint, or transient server error (5xx).

Related errors


AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13). Data as JSON: /api/errors/502f1c36ca9b5434. Report an issue: GitHub.