{"record":{"id":"c3d4065ea4407a61","repo":"SubtitleEdit/subtitleedit","slug":"download-incomplete-expected-totalbytes-bytes","errorCode":null,"errorMessage":"Download incomplete: expected {totalBytes} bytes, received {totalReadBytes} bytes","messagePattern":"Download incomplete: expected (.+?) bytes, received (.+?) bytes","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/ui/Logic/Download/DownloadHelper.cs","lineNumber":139,"sourceCode":"                    totalReadBytes += readBytes;\n\n                    // Report progress at most once per 100ms to avoid overwhelming the UI\n                    if (progress != null && totalBytes > 0)\n                    {\n                        var now = DateTime.UtcNow;\n                        if ((now - lastProgressReport).TotalMilliseconds >= 100)\n                        {\n                            var progressPercentage = (float)totalReadBytes / totalBytes.Value;\n                            progress.Report(Math.Clamp(progressPercentage, 0f, 1f));\n                            lastProgressReport = now;\n                        }\n                    }\n                }\n\n                // Verify download completeness if Content-Length was provided\n                if (totalBytes > 0 && totalReadBytes != totalBytes.Value)\n                {\n                    throw new InvalidOperationException(\n                        $\"Download incomplete: expected {totalBytes} bytes, received {totalReadBytes} bytes\");\n                }\n\n                await destination.FlushAsync(cts.Token).ConfigureAwait(false);\n\n                // Success - report 100%\n                progress?.Report(1f);\n                return;\n            }\n            catch (Exception ex) when (\n                ex is HttpRequestException ||\n                ex is TaskCanceledException ||\n                (ex is IOException && ex is not FileNotFoundException) ||\n                ex is InvalidOperationException)\n            {\n                lastException = ex;\n\n                // If cancellation was requested by user, don't retry","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Logic/Download/DownloadHelper.cs#L121-L157","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: non-seekable MemoryStream-style target defeats resume\nusing var ms = new MemoryStream();\nawait DownloadHelper.DownloadFileAsync(http, url, ms, progress, ct);\n\n// after: seekable FileStream enables range-resume on retry\nawait using var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true);\nawait DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct);","handlingStrategy":"retry","validationCode":"// Ensure server supports range + stream is seekable before relying on retries\nvar supportsRange = await DownloadHelper_CheckRangeSupport(http, url, ct); // mirror of internal helper\nif (!supportsRange || !fs.CanSeek) _logger.Warning(\"Retries may re-download from scratch.\");","typeGuard":"static bool CanResumeDownload(Stream s, bool serverSupportsRange) => s.CanSeek && serverSupportsRange;","tryCatchPattern":"// The helper already retries InvalidOperationException (truncation) internally.\n// At the call site, distinguish permanent truncation from recovered:\ntry { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Download incomplete\"))\n{ _logger.Error(ex, \"Truncated download persisted past retries: {Url}\", url); throw; }","preventionTips":["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."],"tags":["network","truncated-download","content-length","download","retry"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}