{"record":{"id":"408cc535002abb72","repo":"Kareadita/Kavita","slug":"volume-doesnt-exist-408cc5","errorCode":null,"errorMessage":"volume-doesnt-exist","messagePattern":"volume-doesnt-exist","errorType":"exception","errorClass":"OpdsException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/OpdsService.cs","lineNumber":649,"sourceCode":"    }\n\n    public async Task<Feed> GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request,\n        CancellationToken ct = default)\n    {\n        var userId = UnpackRequest(request, out var apiKey, out var prefix, out _);\n        var seriesId = request.SeriesId;\n        var volumeId = request.VolumeId;\n\n        var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\n        if (series == null)\n        {\n            throw new OpdsException(await localizationService.TranslateAsync(userId, \"series-doesnt-exist\"));\n        }\n\n        var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId, ct);\n        if (volume == null)\n        {\n            throw new OpdsException(await localizationService.TranslateAsync(userId, \"volume-doesnt-exist\"));\n        }\n\n        var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);\n        var namingContext = await LocalizedNamingContext.CreateAsync( namingService, localizationService, userId, libraryType);\n\n        var feed = CreateFeed($\"{series.Name} - Volume {volume.Name}\",\n            $\"{apiKey}/series/{seriesId}/volume/{volumeId}\", apiKey, prefix);\n        SetFeedId(feed, $\"series-{series.Id}-volume-{volume.Id}\");\n\n        // Check if there is reading progress or not, if so, inject a \"continue-reading\" item\n        if (request.Preferences.IncludeContinueFrom && request.PageNumber == FirstPageNumber)\n        {\n            var firstChapterWithProgress = volume.Chapters.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.Pages)\n                                           ?? volume.Chapters.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.Pages);\n\n            if (firstChapterWithProgress is { Files.Count: 1 })\n            {\n                feed.Entries.Add(await CreateContinueReadingEntryAsync(series, volume, firstChapterWithProgress, namingContext, request));","sourceCodeStart":631,"sourceCodeEnd":667,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/OpdsService.cs#L631-L667","documentation":"Thrown by OpdsService.GetItemsFromVolume when GetVolumeDtoAsync(volumeId, userId) returns null after the series check passed. This means the volume ID does not exist within the context of the requesting user. The volume lookup is user-scoped, so a volume that exists but is in a library the user can't access also returns null. Since the series check already passed, this typically means the volumeId doesn't belong to the validated series or was deleted.","triggerScenarios":"An OPDS client requests {apiKey}/series/{seriesId}/volume/{volumeId}. The series exists and the user has access, but the volumeId is invalid, belongs to a different series, or was removed during a library rescan. This is the second guard in the two-step (series then volume) validation chain.","commonSituations":"The OPDS reader cached a volume URL from before a library rescan that restructured volumes. The volumeId is from a different series than seriesId (URL manipulation or client bug). The volume was a special that got merged or split during scan.","solutions":["Navigate back to the series detail feed in the OPDS client to get current volume IDs.","Rescan the library in Kavita to ensure volumes are up to date.","Verify the volumeId in the URL belongs to the specified series.","If volumes were restructured (specials re-grouped), refresh the OPDS client's cache.","Check Kavita's UI to confirm the volume exists under the expected series."],"exampleFix":"// No code fix — stale or mismatched volume reference.\n// OPDS client: go back to the series detail feed and\n// re-select the volume to get a current URL.\n\n// Verify volumeId matches seriesId before requesting:\n// GET {apiKey}/series/{seriesId} -> list of valid volume IDs","handlingStrategy":"validation","validationCode":"// Before requesting a volume feed, verify both series and volume:\n// var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);\n// if (series == null) { ShowUser(\"Series not found.\"); return; }\n// var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId, ct);\n// if (volume == null) { ShowUser(\"Volume not found.\"); return; }\n// var feed = await opdsService.GetItemsFromVolume(request, ct);","typeGuard":null,"tryCatchPattern":"// try { var feed = await opdsService.GetItemsFromVolume(request, ct); }\n// catch (OpdsException ex) when (ex.Message.Contains(\"volume-doesnt-exist\"))\n// {\n//     // Volume was removed or restructured during a library rescan.\n//     await RefreshSeriesDetailFeed(seriesId);\n//     return BadRequest(ex.Message);\n// }","preventionTips":["Navigate from series detail -> volume to ensure the volumeId is current.","Refresh the OPDS feed after library rescans that restructure volumes.","Verify the volumeId belongs to the specified seriesId.","Handle 'volume-doesnt-exist' by refreshing the series detail feed.","Avoid caching deep OPDS URLs (series/volume/chapter) across library rescans."],"tags":["opds","volume","not-found","feed","stale-reference","library-scan"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}