{"record":{"id":"9d607472b14fe18f","repo":"Kareadita/Kavita","slug":"file-missing","errorCode":null,"errorMessage":"file-missing","messagePattern":"file-missing","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/KoreaderService.cs","lineNumber":38,"sourceCode":"    ILocalizationService localizationService,\n    ILogger<KoreaderService> logger)\n    : IKoreaderService\n{\n    /// <summary>\n    /// Given a Koreader hash, locate the underlying file and generate/update a progress event.\n    /// </summary>\n    /// <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,","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/KoreaderService.cs#L20-L56","documentation":"Thrown inside KoreaderService.SaveProgress when GetByKoreaderHash returns null — no MangaFile row matches the document hash KOReader sent. KOReader identifies documents by a hash; if Kavita has never computed/that hash is stale, the progress event cannot be attached to a chapter. The localization key 'file-missing' is translated per user before throwing.","triggerScenarios":"A KOReader sync POST (SaveProgress) carries a document hash that is absent from the manga_files koreader_hash column. Happens when the file was added but the KOReader hash generation hasn't run, the file was renamed/moved so its hash differs, or the book was deleted from Kavita but the user still has it on the device.","commonSituations":"Library scanned normally but the KOReader-hash job (force scan) was never invoked; user points KOReader at a copy of the epub that differs in bytes from the one Kavita indexed; hash column migration did not backfill on upgrade.","solutions":["Run a Force Scan on the series/library so Kavita regenerates KOReader hashes for each MangaFile.","Verify the file KOReader has open is byte-identical (same path/edition) to the one in Kavita's library.","If the book was removed intentionally, ignore the error; otherwise re-add the missing file and rescan.","Check logs: the preceding LogWarning includes the BookHash that failed — search the manga_files table for that hash to confirm absence."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"var file = await unitOfWork.MangaFileRepository.GetByKoreaderHash(dto.document, ct);\nif (file == null)\n    return NotFound(new { error = \"file-missing\", hint = \"Run a force scan on the series.\" });","typeGuard":null,"tryCatchPattern":"try { await koreaderService.SaveProgress(dto, userId, ct); }\ncatch (KavitaException ex) when (ex.Message.Contains(\"file-missing\"))\n{ /* return 404 + 'force-scan required' to the device; do not retry */ }","preventionTips":["Run a Force Scan after adding new books so KOReader hashes are generated.","Keep the file KOReader reads byte-identical to the one in Kavita's library.","Surface the failed BookHash (from logs) so users can locate the stale file."],"tags":["koreader","progress-sync","file-hash","scan"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}