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 retryView on GitHub (pinned to 17a9f07487)
Solutions
- 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.
- If it persists, switch to a more stable mirror/source for the artifact.
- For a non-seekable destination, replace it with a seekable FileStream so range-resume can actually work across retries.
- 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
- Use a seekable FileStream destination so range-resume works on retry.
- Keep the default maxRetries=5 unless you have a reason to change it.
- Monitor truncation rate per mirror; switch mirrors if a CDN edge keeps truncating.
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
- Dictionary download failed: {url}
- The requested URL was not found: {url}
- Cannot retry download on non-seekable stream after partial d
- Failed to download file after {maxRetries} attempts. URL: {u
- Downloaded yt-dlp ({assetName}) failed SHA-256 verification
AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13).
Data as JSON: /api/errors/c3d4065ea4407a61.
Report an issue: GitHub.