SubtitleEdit/subtitleedit · error · InvalidOperationException

Failed to download file after

Error message

Failed to download file after {maxRetries} attempts. URL: {url}. Downloaded: {bytesDownloaded}/{totalBytes ?? 0} bytes

What it means

The terminal failure of DownloadHelper.DownloadFileAsync: every retry attempt failed and the loop exited. The message reports the URL, the configured maxRetries, bytes actually on disk, and the total expected (or 0 if unknown). The lastException is attached as InnerException so the root cause (timeout, socket error, truncation, etc.) is preserved. InvalidOperationException is the type to catch at call sites.

Solutions

  1. Inspect the InnerException of the thrown InvalidOperationException — it holds the actual cause of the final attempt.
  2. Verify connectivity to the URL host (curl/HEAD) and that no firewall/proxy is blocking large transfers.
  3. Increase maxRetries or timeoutSeconds if the cause is intermittent, and ensure a seekable destination so each retry resumes rather than restarts.
  4. For a permanent server-side issue, point the service at a mirror or wait for the upstream release to be republished.

Example fix

// before: default 5 retries, 30-min timeout
await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct);

// after: surface the root cause and allow more patience
try { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct, maxRetries: 8, timeoutSeconds: 3600); }
catch (InvalidOperationException ex)
{
    _logger.Error(ex, "Download failed permanently: {Root}", ex.InnerException?.Message);
    throw;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight connectivity check before the long download
using var probe = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, url), ct);
probe.EnsureSuccessStatusCode();

Try / catch

try { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("Failed to download file after"))
{ _logger.Error(ex, "All retries exhausted. Root: {Root}", ex.InnerException?.Message); throw; }

Prevention

When it happens

Trigger: Persistent network failure across all attempts: repeated timeouts (timeoutSeconds=1800 default, 30 min), DNS errors, connection refused, server returning 5xx that EnsureSuccessStatusCode rejects, or repeated truncation on a non-resumable connection. Each attempt waited baseDelay = min(2000*2^(n-1), 30000) + up to 1000ms jitter.

Common situations: Offline / captive-portal network at download time; corporate firewall blocking the CDN host; server-side outage for the artifact; throttled connection where each attempt times out; clock/MTU misconfiguration causing consistent resets. Inspect lastException (InnerException chain) to pinpoint.

Related errors


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

Appendix: source

Thrown at src/ui/Logic/Download/DownloadHelper.cs:189

                var jitter = Random.Shared.Next(0, 1000);
                var delayMs = baseDelay + jitter;

                await Task.Delay(delayMs, CancellationToken.None).ConfigureAwait(false);

                // Don't reset stream position - we'll resume from where we left off
                // Only reset if we can't seek (which means we can't resume anyway)
                if (!destination.CanSeek && destination.Position > 0)
                {
                    throw new InvalidOperationException(
                        "Cannot retry download on non-seekable stream after partial download",
                        lastException);
                }
            }
        }

        // All retries exhausted
        var bytesDownloaded = destination.CanSeek ? destination.Position : 0;
        throw new InvalidOperationException(
            $"Failed to download file after {maxRetries} attempts. URL: {url}. Downloaded: {bytesDownloaded}/{totalBytes ?? 0} bytes",
            lastException);
    }

    private static async Task<bool> CheckRangeSupport(
        HttpClient httpClient,
        string url,
        CancellationToken cancellationToken)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromSeconds(10));

            using var request = new HttpRequestMessage(HttpMethod.Head, url);
            using var response = await httpClient.SendAsync(request, cts.Token).ConfigureAwait(false);

            return response.Headers.AcceptRanges?.Contains("bytes") == true;

View on GitHub (pinned to 17a9f07487)