{"record":{"id":"c32ca37eea059e54","repo":"Kareadita/Kavita","slug":"no-download-url-available-for-filepath","errorCode":null,"errorMessage":"No download URL available for {filePath}","messagePattern":"No download URL available for (.+?)","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"error","filePath":"Kavita.Services/ReadingLists/CblGithubService.cs","lineNumber":145,"sourceCode":"\n        var item = await BuildApiUrl(normalizedPath)\n            .WithGithubHeaders()\n            .GetJsonAsync<GithubContentItem>();\n\n        return item.Sha;\n    }\n\n    public async Task<string> GetFileContent(string filePath)\n    {\n        var normalizedPath = NormalizePath(filePath);\n\n        var item = await BuildApiUrl(normalizedPath)\n            .WithGithubHeaders()\n            .GetJsonAsync<GithubContentItem>();\n\n        if (string.IsNullOrEmpty(item.DownloadUrl))\n        {\n            throw new KavitaException($\"No download URL available for {filePath}\");\n        }\n\n        return await DownloadByUrl(item.DownloadUrl);\n    }\n\n    /// <summary>\n    /// Downloads the content of a file\n    /// </summary>\n    /// <param name=\"url\"></param>\n    /// <returns></returns>\n    private static async Task<string> DownloadByUrl(string url)\n    {\n        return await url\n            .WithGithubHeaders()\n            .GetStringAsync();\n    }\n\n    private async Task<(List<CblRepoItemDto> Items, GithubRateLimitDto RateLimit)> FetchDirectoryFromGithub(string path)","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/ReadingLists/CblGithubService.cs#L127-L163","documentation":"Thrown by CblGithubService.GetFileContent after fetching a GitHub content item whose download_url is null or empty. GitHub's contents API omits download_url for some object types (e.g. directories, symlinks, or submodule entries) or returns null when the repo moved/renamed the file. Kavita needs a raw download URL to fetch the file body, so an empty one is treated as an unrecoverable fetch failure.","triggerScenarios":"Calling GetFileContent(filePath) where filePath resolves to a directory entry, a submodule, a file >100MB (where GitHub requires the blob API), or a path whose content item's download_url field came back null from the REST API.","commonSituations":"User clicks a folder row in the CBL browser that was mislabeled as a file; the CBL repo added a submodule; a file was force-pushed and the API temporarily returns null download_url; or a path with special characters was normalized incorrectly so it points at the wrong object.","solutions":["Verify filePath points to an actual .cbl/.json file and not a directory using BrowseRepo's IsDirectory flag before calling GetFileContent.","If the file is large, fetch it via the Git blobs API with the sha instead of relying on download_url.","Retry once after a short delay in case the null download_url is a transient GitHub inconsistency.","Check the CBL-ReadingLists repo in a browser to confirm the path still resolves to a file with a raw link."],"exampleFix":"// before\nvar content = await cblGithubService.GetFileContent(filePath);\n\n// after\nvar item = await repo.BrowseRepo(parentDir);\nvar target = item.Items.SingleOrDefault(i => i.Path == filePath);\nif (target is null || target.IsDirectory || string.IsNullOrEmpty(target.DownloadUrl))\n    return BadRequest($\"{filePath} has no downloadable raw content\");\nvar content = await cblGithubService.GetFileContent(filePath);","handlingStrategy":"validation","validationCode":"var browse = await cblGithubService.BrowseRepo(Path.GetDirectoryName(filePath));\nvar item = browse.Items.FirstOrDefault(i => i.Path == filePath);\nif (item is null || item.IsDirectory || string.IsNullOrEmpty(item.DownloadUrl))\n    return BadRequest($\"{filePath} is not downloadable\");\nvar content = await cblGithubService.GetFileContent(filePath);","typeGuard":"static bool IsDownloadable(CblRepoItemDto item) => !item.IsDirectory && !string.IsNullOrEmpty(item.DownloadUrl);","tryCatchPattern":"try { return await cblGithubService.GetFileContent(filePath); }\ncatch (KavitaException ex) when (ex.Message.Contains(\"No download URL\"))\n{ return NotFound($\"{filePath} has no raw download URL\"); }","preventionTips":["Gate GetFileContent behind a BrowseRepo lookup that confirms a downloadable file.","Avoid calling GetFileContent on directory entries.","Retry once for transient null download_url from GitHub."],"tags":["github-api","cbl","network","external-data"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}