Kareadita/Kavita · error · OpdsException

volume-doesnt-exist

Error message

volume-doesnt-exist

What it means

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.

Source

Thrown at Kavita.Services/OpdsService.cs:649

    }

    public async Task<Feed> GetItemsFromVolume(OpdsItemsFromCompoundEntityIdsRequest request,
        CancellationToken ct = default)
    {
        var userId = UnpackRequest(request, out var apiKey, out var prefix, out _);
        var seriesId = request.SeriesId;
        var volumeId = request.VolumeId;

        var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);
        if (series == null)
        {
            throw new OpdsException(await localizationService.TranslateAsync(userId, "series-doesnt-exist"));
        }

        var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, request.UserId, ct);
        if (volume == null)
        {
            throw new OpdsException(await localizationService.TranslateAsync(userId, "volume-doesnt-exist"));
        }

        var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);
        var namingContext = await LocalizedNamingContext.CreateAsync( namingService, localizationService, userId, libraryType);

        var feed = CreateFeed($"{series.Name} - Volume {volume.Name}",
            $"{apiKey}/series/{seriesId}/volume/{volumeId}", apiKey, prefix);
        SetFeedId(feed, $"series-{series.Id}-volume-{volume.Id}");

        // Check if there is reading progress or not, if so, inject a "continue-reading" item
        if (request.Preferences.IncludeContinueFrom && request.PageNumber == FirstPageNumber)
        {
            var firstChapterWithProgress = volume.Chapters.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.Pages)
                                           ?? volume.Chapters.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.Pages);

            if (firstChapterWithProgress is { Files.Count: 1 })
            {
                feed.Entries.Add(await CreateContinueReadingEntryAsync(series, volume, firstChapterWithProgress, namingContext, request));

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Navigate back to the series detail feed in the OPDS client to get current volume IDs.
  2. Rescan the library in Kavita to ensure volumes are up to date.
  3. Verify the volumeId in the URL belongs to the specified series.
  4. If volumes were restructured (specials re-grouped), refresh the OPDS client's cache.
  5. Check Kavita's UI to confirm the volume exists under the expected series.

Example fix

// No code fix — stale or mismatched volume reference.
// OPDS client: go back to the series detail feed and
// re-select the volume to get a current URL.

// Verify volumeId matches seriesId before requesting:
// GET {apiKey}/series/{seriesId} -> list of valid volume IDs
Defensive patterns

Strategy: validation

Validate before calling

// Before requesting a volume feed, verify both series and volume:
// var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);
// if (series == null) { ShowUser("Series not found."); return; }
// var volume = await unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId, ct);
// if (volume == null) { ShowUser("Volume not found."); return; }
// var feed = await opdsService.GetItemsFromVolume(request, ct);

Try / catch

// try { var feed = await opdsService.GetItemsFromVolume(request, ct); }
// catch (OpdsException ex) when (ex.Message.Contains("volume-doesnt-exist"))
// {
//     // Volume was removed or restructured during a library rescan.
//     await RefreshSeriesDetailFeed(seriesId);
//     return BadRequest(ex.Message);
// }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/408cc535002abb72. Report an issue: GitHub.