{"record":{"id":"8c73d0ade27b4af7","repo":"Kareadita/Kavita","slug":"volume-doesnt-exist","errorCode":null,"errorMessage":"volume-doesnt-exist","messagePattern":"volume-doesnt-exist","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/KoreaderService.cs","lineNumber":48,"sourceCode":"    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\n        var reportedProgress = koreaderBookDto.progress;\n        KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress);\n","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/KoreaderService.cs#L30-L66","documentation":"Thrown in the same rebuild branch of KoreaderService.SaveProgress: the chapter was found and has a VolumeId, but GetVolumeByIdAsync returns null. The chapter references a volume that doesn't exist — a deeper orphan in the chapter → volume chain. Only reachable when user progress is missing for the chapter.","triggerScenarios":"KOReader progress sync where Chapter.VolumeId points at a deleted volume row; happens after a volume restructure (e.g. special/normal volume recategorization) that orphaned chapters.","commonSituations":"Library scan reorganized volumes and left chapters pointing at removed volume IDs; partial DB restore or interrupted migration.","solutions":["Force-scan the series to rebuild the volume → chapter → file graph.","Inspect the DB for chapters whose VolumeId has no matching volume row and repair or remove them.","If the volume was intentionally deleted, the chapter should have been re-homed; re-add the source files and scan.","Catch KavitaException upstream and advise the user to rescan rather than silently dropping progress."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var volume = await unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId, ct: ct);\nif (volume == null)\n    return Conflict(new { error = \"volume-doesnt-exist\", hint = \"Rescan the series.\" });","typeGuard":null,"tryCatchPattern":"try { await koreaderService.SaveProgress(dto, userId, ct); }\ncatch (KavitaException ex) when (ex.Message.Contains(\"volume-doesnt-exist\"))\n{ /* request a rescan; degrade gracefully rather than retry in a loop */ }","preventionTips":["Rescan after volume/special reorganization.","Audit chapters whose VolumeId has no matching volume row.","Treat hierarchy-integrity errors as 'rescan required', not transient."],"tags":["koreader","data-integrity","volume","scan"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}