{"record":{"id":"2b80041580203923","repo":"Kareadita/Kavita","slug":"chapter-doesnt-exist-2b8004","errorCode":null,"errorMessage":"chapter-doesnt-exist","messagePattern":"chapter-doesnt-exist","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/KoreaderService.cs","lineNumber":45,"sourceCode":"    /// <param name=\"koreaderBookDto\"></param>\n    /// <param name=\"userId\"></param>\n    /// <param name=\"ct\"></param>\n    public async Task SaveProgress(KoreaderBookDto koreaderBookDto, int userId, CancellationToken ct = default)\n    {\n        logger.LogDebug(\"Saving KOReader progress for User ({UserId}): {KoreaderProgress} - {KoreaderHash}\",\n            userId, koreaderBookDto.progress.Sanitize(), koreaderBookDto.document.Sanitize());\n        var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(koreaderBookDto.document, ct);\n        if (file == null)\n        {\n            logger.LogWarning(\"KOReader progress for unknown book: {BookHash}. Run a force scan on the series to generate KOReader hashes\", koreaderBookDto.document.Sanitize());\n            throw new KavitaException(await localizationService.TranslateAsync(userId, \"file-missing\"));\n        }\n\n        var userProgressDto = await unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId, ct);\n        if (userProgressDto == null)\n        {\n            var chapterDto = await unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId, ct);\n            if (chapterDto == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"chapter-doesnt-exist\"));\n\n            var volumeDto = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapterDto.VolumeId, ct: ct);\n            if (volumeDto == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"volume-doesnt-exist\"));\n\n            var seriesDto = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(volumeDto.SeriesId, userId, ct);\n            if (seriesDto == null) throw new KavitaException(await localizationService.TranslateAsync(userId, \"series-doesnt-exist\"));\n\n            userProgressDto = new ProgressDto()\n            {\n                PageNum = 0, // This is updated in KoreaderHelper.UpdateProgressDto\n                ChapterId = file.ChapterId,\n                VolumeId = chapterDto.VolumeId,\n                SeriesId = seriesDto.Id,\n                LibraryId = seriesDto.LibraryId\n            };\n        }\n\n        // Update the bookScrollId if possible","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/KoreaderService.cs#L27-L63","documentation":"Thrown inside the no-progress branch of KoreaderService.SaveProgress: the MangaFile exists and points at a ChapterId, but GetChapterDtoAsync for that chapter returns null. This means the file row references a chapter that no longer exists (orphaned FK). It surfaces only when a user has no prior progress on the chapter, since the progress-less branch rebuilds the entity graph (chapter → volume → series).","triggerScenarios":"KOReader SaveProgress for a file whose MangaFile.ChapterId points at a deleted chapter — e.g. after a re-scan merged/split chapters and rewrote the chapter table while the manga_file row still carries the old ChapterId.","commonSituations":"Post-scan inconsistency: chapter was re-keyed but manga_file.ChapterId not updated; a manual DB edit removed a chapter; a volume/chapter recount left dangling MangaFile references.","solutions":["Re-scan the affected series so MangaFile rows are re-linked to current chapters.","If inconsistent, run a library scan with 'force' to rebuild chapter-to-file mappings.","Audit the DB: select MangaFile rows whose ChapterId has no matching chapter row, then either fix or delete the orphan.","As a caller, catch KavitaException and surface 'rescan the series' to the user rather than retrying."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var chapter = await unitOfWork.ChapterRepository.GetChapterDtoAsync(file.ChapterId, userId, ct);\nif (chapter == null)\n    return Conflict(new { error = \"chapter-doesnt-exist\", hint = \"Rescan the series.\" });","typeGuard":null,"tryCatchPattern":"try { await koreaderService.SaveProgress(dto, userId, ct); }\ncatch (KavitaException ex) when (ex.Message.Contains(\"chapter-doesnt-exist\"))\n{ /* log + trigger a rescan; tell the device to retry after scan completes */ }","preventionTips":["Trigger a scan after any structural metadata change so chapter->file links stay consistent.","Periodically check for orphaned manga_files (ChapterId with no chapter row).","Avoid direct DB edits that delete chapters without cascading to manga_files."],"tags":["koreader","data-integrity","chapter","scan"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}