{"record":{"id":"851a7ac438ca28d1","repo":"SubtitleEdit/subtitleedit","slug":"dictionary-download-failed-url","errorCode":null,"errorMessage":"Dictionary download failed: {url}","messagePattern":"Dictionary download failed: (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/ui/Features/SpellCheck/GetDictionaries/GetDictionariesViewModel.cs","lineNumber":206,"sourceCode":"        if (!Directory.Exists(folder))\n        {\n            Directory.CreateDirectory(folder);\n        }\n\n        var dicFiles = new List<string>();\n\n        for (var i = 0; i < files.Count; i++)\n        {\n            var url = files[i];\n            var fileIndex = i;\n            var fileProgress = new Progress<float>(p => progress.Report((fileIndex + p) / files.Count));\n\n            using var stream = new MemoryStream();\n            await _spellCheckDictionaryDownloadService.DownloadDictionary(stream, url, fileProgress, cancellationToken);\n\n            if (stream.Length == 0)\n            {\n                throw new InvalidOperationException($\"Dictionary download failed: {url}\");\n            }\n\n            stream.Position = 0;\n\n            if (IsHunspellFile(url))\n            {\n                var targetFileName = Path.Combine(folder, GetFileNameFromUrl(url));\n                await using (var fileStream = File.Create(targetFileName))\n                {\n                    await stream.CopyToAsync(fileStream, cancellationToken);\n                }\n\n                if (targetFileName.EndsWith(\".dic\", StringComparison.OrdinalIgnoreCase))\n                {\n                    dicFiles.Add(targetFileName);\n                }\n            }\n            else","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/ui/Features/SpellCheck/GetDictionaries/GetDictionariesViewModel.cs#L188-L224","documentation":"Thrown after a spell-check dictionary download via DownloadDictionary completes without throwing, but the resulting MemoryStream has zero bytes. The download service (SpellCheckDictionaryDownloadService) delegates to DownloadHelper.DownloadFileAsync, which can succeed with an empty body if the server returns HTTP 200 with no content or a redirect to an empty page. This guard catches the silent-empty-download case that would otherwise produce a corrupt dictionary file.","triggerScenarios":"The URL in the files list returns HTTP 200 with an empty response body, a 30x redirect chain lands on an empty page, a CDN edge serves a stale/empty object, or a proxy strips the body. The check fires only when stream.Length == 0 after a successful (non-throwing) DownloadDictionary call.","commonSituations":"Dictionary URL list is outdated and the host has removed the file but returns 200 with an HTML error page that DownloadHelper writes as empty; corporate proxy intercepts and returns an empty body; the URL is an HTTPS endpoint whose certificate changed and a middlebox returns an empty response; CDN misconfiguration serves a zero-length object.","solutions":["Verify the failing URL in a browser or with curl -I to check the actual Content-Length and status code.","Check the _spellCheckDictionaryDownloadService's HttpClient configuration for proxy settings or redirect behaviour.","Confirm the dictionary URL list is current — if the host moved files, update the URL source.","If behind a corporate proxy, ensure the proxy is not stripping response bodies for the dictionary host.","Add logging inside DownloadHelper.DownloadFileAsync to capture the HTTP status code and Content-Length header for diagnosis."],"exampleFix":"// before\nusing var stream = new MemoryStream();\nawait _spellCheckDictionaryDownloadService.DownloadDictionary(stream, url, fileProgress, cancellationToken);\nif (stream.Length == 0)\n{\n    throw new InvalidOperationException($\"Dictionary download failed: {url}\");\n}\n\n// after — include the HTTP status / content-length in the message for diagnosis\nusing var stream = new MemoryStream();\nawait _spellCheckDictionaryDownloadService.DownloadDictionary(stream, url, fileProgress, cancellationToken);\nif (stream.Length == 0)\n{\n    throw new InvalidOperationException(\n        $\"Dictionary download produced 0 bytes from {url}. The URL may be stale, blocked by a proxy, or the server returned an empty response.\");\n}","handlingStrategy":"validation","validationCode":"// Validate URL reachability and headers before downloading\nusing var probeReq = new HttpRequestMessage(HttpMethod.Head, url);\nusing var probeResp = await httpClient.SendAsync(probeReq, cancellationToken);\nif (!probeResp.IsSuccessStatusCode || probeResp.Content.Headers.ContentLength is 0)\n{\n    // Skip or flag this dictionary URL before attempting the full download\n    continue;\n}","typeGuard":null,"tryCatchPattern":"try { await downloadService.DownloadDictionary(stream, url, progress, ct); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Dictionary download failed\"))\n{ /* log url, skip this dictionary, continue with remaining files */ }","preventionTips":["Validate each dictionary URL with a HEAD request before downloading to catch empty/stale endpoints.","Maintain a curated, version-pinned URL list for dictionary sources.","Log the HTTP status code and Content-Length inside DownloadHelper for post-mortem diagnosis."],"tags":["network","download","spellcheck","validation","empty-response"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}