{"record":{"id":"9dcc75a9f1fafbb3","repo":"Kareadita/Kavita","slug":"github-api-rate-limit-exhausted-resetsin-cached","errorCode":null,"errorMessage":"GitHub API rate limit exhausted.{resetsIn} Cached data may still be available.","messagePattern":"GitHub API rate limit exhausted\\.(.+?) Cached data may still be available\\.","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"warning","filePath":"Kavita.Services/ReadingLists/CblGithubService.cs","lineNumber":215,"sourceCode":"                .ThenBy(i => i.Name, StringComparer.OrdinalIgnoreCase)\n                .ToList();\n\n            return (result, rateLimit);\n        }\n        catch (FlurlHttpException ex) when (ex.StatusCode == 403)\n        {\n            var rateLimit = ex.Call?.Response != null\n                ? ex.Call.Response.GetRateLimit()\n                : new GithubRateLimitDto();\n\n            if (rateLimit.IsExhausted)\n            {\n                var resetsIn = rateLimit.ResetsAtUtc.HasValue\n                    ? $\" Resets at {rateLimit.ResetsAtUtc.Value:HH:mm} UTC.\"\n                    : string.Empty;\n\n                _logger.LogWarning(\"GitHub API rate limit exhausted.{ResetsIn}\", resetsIn);\n                throw new KavitaException(\n                    $\"GitHub API rate limit exhausted.{resetsIn} Cached data may still be available.\");\n            }\n\n            _logger.LogWarning(ex, \"GitHub API returned 403 for CBL repo\");\n            throw new KavitaException(\"GitHub API access denied. Please try again later.\");\n        }\n        catch (FlurlHttpException ex) when (ex.StatusCode == 404)\n        {\n            _logger.LogWarning(\"CBL repo path not found: {Path}\", path.Sanitize());\n            throw new KavitaException($\"Path not found in CBL repository: {path}\");\n        }\n    }\n\n    private CblRepoCache LoadCache()\n    {\n        var cachePath = GetCacheFilePath();\n        if (!File.Exists(cachePath)) return new CblRepoCache();\n","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/ReadingLists/CblGithubService.cs#L197-L233","documentation":"Thrown inside CblGithubService's 403 handler when the parsed GitHub rate limit reports IsExhausted == true. Unauthenticated GitHub calls are capped at 60 requests/hour per IP; authenticated calls at 5000/hour. Once exhausted, every further contents API call returns 403, so Kavita surfaces a user-facing message with the reset time and points to the 4-hour file cache as a fallback data source.","triggerScenarios":"Any BrowseRepo(forceRefresh: true) or GetFileContent/GetFileSha call that triggers FetchDirectoryFromGithub after the hourly quota is depleted, producing a FlurlHttpException with StatusCode 403 and an X-RateLimit-Remaining of 0.","commonSituations":"Running Kavita behind a NAT/proxy where many users share one egress IP (anonymous quota burns fast); rapid cache invalidation or many forceRefresh requests; an automated scanner loop hitting the CBL browser; or no GitHub token configured so the 60/hour anonymous cap applies.","solutions":["Serve from cache: call BrowseRepo without forceRefresh so the 4-hour CblRepoCache is used instead of the API.","Configure a GitHub personal access token in the app's GithubHeaders so requests authenticate and get the 5000/hour quota.","Wait until the reported ResetsAtUtc time before issuing new CBL requests.","Throttle/invalidate less: stop calling InvalidateCache or forceRefresh in loops."],"exampleFix":"// before\nvar result = await cblGithubService.BrowseRepo(path, forceRefresh: true);\n\n// after - prefer cache, only force refresh when user explicitly requests it\nvar result = await cblGithubService.BrowseRepo(path, forceRefresh: false);\n// surface result.FromCache to the UI so users know stale data is being shown","handlingStrategy":"fallback","validationCode":"// Prefer cache to avoid burning the quota\nvar result = await cblGithubService.BrowseRepo(path, forceRefresh: false);\nif (result.FromCache) return Ok(result); // cached, no API call\n// only force refresh when cache is empty/expired AND user requested it","typeGuard":null,"tryCatchPattern":"try { return await cblGithubService.BrowseRepo(path, forceRefresh); }\ncatch (KavitaException ex) when (ex.Message.Contains(\"rate limit\"))\n{ return await cblGithubService.BrowseRepo(path, forceRefresh: false); /* serve cache */ }","preventionTips":["Default to forceRefresh: false so the 4-hour cache absorbs traffic.","Configure a GitHub PAT in GithubHeaders to lift the 60/hour anonymous cap.","Avoid InvalidateCache / forceRefresh in loops.","Surface reset time to users so they know when to retry."],"tags":["github-api","rate-limit","network","cbl","transient"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}