{"record":{"id":"b720aefbe3da3a3e","repo":"SubtitleEdit/subtitleedit","slug":"failed-to-download-file-after-maxretries-attempt","errorCode":null,"errorMessage":"Failed to download file after {maxRetries} attempts. URL: {url}. Downloaded: {bytesDownloaded}/{totalBytes ?? 0} bytes","messagePattern":"Failed to download file after (.+?) attempts\\. URL: (.+?)\\. Downloaded: (.+?)/(.+?) bytes","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/ui/Logic/Download/DownloadHelper.cs","lineNumber":189,"sourceCode":"                var jitter = Random.Shared.Next(0, 1000);\n                var delayMs = baseDelay + jitter;\n\n                await Task.Delay(delayMs, CancellationToken.None).ConfigureAwait(false);\n\n                // Don't reset stream position - we'll resume from where we left off\n                // Only reset if we can't seek (which means we can't resume anyway)\n                if (!destination.CanSeek && destination.Position > 0)\n                {\n                    throw new InvalidOperationException(\n                        \"Cannot retry download on non-seekable stream after partial download\",\n                        lastException);\n                }\n            }\n        }\n\n        // All retries exhausted\n        var bytesDownloaded = destination.CanSeek ? destination.Position : 0;\n        throw new InvalidOperationException(\n            $\"Failed to download file after {maxRetries} attempts. URL: {url}. Downloaded: {bytesDownloaded}/{totalBytes ?? 0} bytes\",\n            lastException);\n    }\n\n    private static async Task<bool> CheckRangeSupport(\n        HttpClient httpClient,\n        string url,\n        CancellationToken cancellationToken)\n    {\n        try\n        {\n            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n            cts.CancelAfter(TimeSpan.FromSeconds(10));\n\n            using var request = new HttpRequestMessage(HttpMethod.Head, url);\n            using var response = await httpClient.SendAsync(request, cts.Token).ConfigureAwait(false);\n\n            return response.Headers.AcceptRanges?.Contains(\"bytes\") == true;","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Logic/Download/DownloadHelper.cs#L171-L207","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the InnerException of the thrown InvalidOperationException — it holds the actual cause of the final attempt.","Verify connectivity to the URL host (curl/HEAD) and that no firewall/proxy is blocking large transfers.","Increase maxRetries or timeoutSeconds if the cause is intermittent, and ensure a seekable destination so each retry resumes rather than restarts.","For a permanent server-side issue, point the service at a mirror or wait for the upstream release to be republished."],"exampleFix":"// before: default 5 retries, 30-min timeout\nawait DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct);\n\n// after: surface the root cause and allow more patience\ntry { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct, maxRetries: 8, timeoutSeconds: 3600); }\ncatch (InvalidOperationException ex)\n{\n    _logger.Error(ex, \"Download failed permanently: {Root}\", ex.InnerException?.Message);\n    throw;\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight connectivity check before the long download\nusing var probe = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, url), ct);\nprobe.EnsureSuccessStatusCode();","typeGuard":null,"tryCatchPattern":"try { await DownloadHelper.DownloadFileAsync(http, url, fs, progress, ct); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Failed to download file after\"))\n{ _logger.Error(ex, \"All retries exhausted. Root: {Root}\", ex.InnerException?.Message); throw; }","preventionTips":["Always log InnerException — it carries the real cause of the final attempt.","Increase maxRetries/timeoutSeconds for known-flaky environments.","Pre-flight HEAD the URL and verify DNS/firewall before kicking off a 30-min download."],"tags":["network","retry-exhausted","download","timeout"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}