SubtitleEdit/subtitleedit · error · InvalidOperationException

Download incomplete: expected {totalBytes} bytes, received {

Error message

Download incomplete: expected {totalBytes} bytes, received {totalReadBytes} bytes

What it means

Thrown by DownloadHelper after the response stream is fully read but the number of bytes received does not equal Content-Length (or Content-Range total). This catches truncated downloads where the server closed the connection cleanly (HTTP 200) before delivering all bytes — a case EnsureSuccessStatusCode would miss. It is an InvalidOperationException, which the retry filter catches, so the download is retried with range-resume if the server supports it.

Source

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

                    totalReadBytes += readBytes;

                    // Report progress at most once per 100ms to avoid overwhelming the UI
                    if (progress != null && totalBytes > 0)
                    {
                        var now = DateTime.UtcNow;
                        if ((now - lastProgressReport).TotalMilliseconds >= 100)
                        {
                            var progressPercentage = (float)totalReadBytes / totalBytes.Value;
                            progress.Report(Math.Clamp(progressPercentage, 0f, 1f));
                            lastProgressReport = now;
                        }
                    }
                }

                // Verify download completeness if Content-Length was provided
                if (totalBytes > 0 && totalReadBytes != totalBytes.Value)
                {
                    throw new InvalidOperationException(
                        $"Download incomplete: expected {totalBytes} bytes, received {totalReadBytes} bytes");
                }

                await destination.FlushAsync(cts.Token).ConfigureAwait(false);

                // Success - report 100%
                progress?.Report(1f);
                return;
            }
            catch (Exception ex) when (
                ex is HttpRequestException ||
                ex is TaskCanceledException ||
                (ex is IOException && ex is not FileNotFoundException) ||
                ex is InvalidOperationException)
            {
                lastException = ex;

                // If cancellation was requested by user, don't retry

View on GitHub (pinned to 17a9f07487)

Solutions

  1. Let the built-in retry loop run (it catches InvalidOperationException and resumes via Range) — do not wrap the call to swallow and rethrow without retry.
  2. If it persists, switch to a more stable mirror/source for the artifact.
  3. For a non-seekable destination, replace it with a seekable FileStream so range-resume can actually work across retries.
  4. Verify network stability / MTU / proxy settings if truncation repeats for every file.

Example fix

// before: non-seekable MemoryStream-style target defeats resume
using var ms = new MemoryStream();
await DownloadHelper.DownloadFileAsync(http, url, ms, progress, ct);

// after: seekable FileStream enables range-resume on retry
await using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true);
await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct);
Defensive patterns

Strategy: retry

Validate before calling

// Ensure server supports range + stream is seekable before relying on retries
var supportsRange = await DownloadHelper_CheckRangeSupport(http, url, ct); // mirror of internal helper
if (!supportsRange || !fs.CanSeek) _logger.Warning("Retries may re-download from scratch.");

Type guard

static bool CanResumeDownload(Stream s, bool serverSupportsRange) => s.CanSeek && serverSupportsRange;

Try / catch

// The helper already retries InvalidOperationException (truncation) internally.
// At the call site, distinguish permanent truncation from recovered:
try { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("Download incomplete"))
{ _logger.Error(ex, "Truncated download persisted past retries: {Url}", url); throw; }

Prevention

When it happens

Trigger: Server sends Content-Length: N but the body ends at M<N without an HTTP error; connection RST after partial read that ReadAsync reports as clean EOF; proxy/CDN truncating large files; flaky mobile/satellite links; Content-Range total differs from what was actually streamed.

Common situations: Large model files over unreliable networks; corporate proxy with a body-size cap that silently truncates; server-side timeout mid-stream; antivirus injecting into the stream. The built-in retry (maxRetries=5, exponential backoff + jitter, range resume when supported) usually recovers transient cases.

Related errors


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