Kareadita/Kavita · error · UnauthorizedAccessException

series-restricted-age-restriction

series-restricted-age-restriction

Error message

series-restricted-age-restriction

What it means

Thrown as UnauthorizedAccessException in GetSeriesDetail when the user has an AgeRestriction (not NotApplicable) and the series' metadata AgeRating exceeds it. Kavita enforces per-user age gates so restricted users cannot open series above their allowed rating.

Source

Thrown at Kavita.Services/SeriesService.cs:539

    /// <param name="seriesId"></param>
    /// <param name="userId"></param>
    /// <param name="ct"></param>
    /// <returns></returns>
    public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId, CancellationToken ct = default)
    {
        var series = await unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId, ct);
        if (series == null) throw new KavitaException(await localizationService.TranslateAsync(userId, "series-doesnt-exist"));

        var libraryIds = await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, ct: ct);
        if (!libraryIds.Contains(series.LibraryId))
            throw new UnauthorizedAccessException("user-no-access-library-from-series");

        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct);
        if (user!.AgeRestriction != AgeRating.NotApplicable)
        {
            var seriesMetadata = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);
            if (seriesMetadata!.AgeRating > user.AgeRestriction)
                throw new UnauthorizedAccessException("series-restricted-age-restriction");
        }


        var libraryType = await unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId, ct);
        var volumes = await unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId, ct: ct);
        var namingContext = await LocalizedNamingContext.CreateAsync(namingService, localizationService, userId, libraryType);
        var bookTreatment = libraryType is LibraryType.Book or LibraryType.LightNovel;

        // For books, the Name of the Volume is remapped to the actual name of the book, rather than Volume number.
        var processedVolumes = new List<VolumeDto>();
        foreach (var volume in volumes)
        {
            if (volume.IsLooseLeaf() || volume.IsSpecial())
            {
                continue;
            }

            var formattedName = namingContext.FormatVolumeName(volume);

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Raise the user's AgeRestriction (admin -> users) to a level that includes the series' rating.
  2. Return 403 and hide the series from restricted users via the age-aware query.
  3. Correct the series' AgeRating metadata if it was set too high during import.
  4. Pre-filter browse/search results by AgeRating so restricted users never get a link to open it.

Example fix

// before
var detail = await seriesService.GetSeriesDetail(seriesId, userId);

// after
var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct);
var meta = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);
if (user!.AgeRestriction != AgeRating.NotApplicable && meta!.AgeRating > user.AgeRestriction)
    return Forbid("series-restricted-age-restriction");
var detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);
Defensive patterns

Strategy: validation

Validate before calling

var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct);
var meta = await unitOfWork.SeriesRepository.GetSeriesMetadataAsync(seriesId, ct);
if (user!.AgeRestriction != AgeRating.NotApplicable && meta!.AgeRating > user.AgeRestriction)
    return Forbid("series-restricted-age-restriction");
var detail = await seriesService.GetSeriesDetail(seriesId, userId, ct);

Type guard

static bool AgeRatingAllowed(AgeRating restriction, AgeRating content) => restriction == AgeRating.NotApplicable || content <= restriction;

Try / catch

try { return await seriesService.GetSeriesDetail(seriesId, userId, ct); }
catch (UnauthorizedAccessException) { return Forbid("series-restricted-age-restriction"); }

Prevention

When it happens

Trigger: GetSeriesDetail for a series whose SeriesMetadata.AgeRating is greater than the requesting user's AgeRestriction (both are AgeRating enum values). The metadata is fetched and compared only when user.AgeRestriction != NotApplicable.

Common situations: A restricted/child account opens a Mature-rated series; the user's age restriction was lowered by an admin; series metadata AgeRating was upgraded after import; or a default age restriction policy applies to a new user.

Related errors


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