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
- Raise the user's AgeRestriction (admin -> users) to a level that includes the series' rating.
- Return 403 and hide the series from restricted users via the age-aware query.
- Correct the series' AgeRating metadata if it was set too high during import.
- 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
- Pre-filter browse/search by the user's AgeRestriction.
- Adjust user AgeRestriction in admin when appropriate.
- Correct inflated series AgeRating metadata.
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
- series-restricted-age-restriction
- user-no-access-library-from-series
- no-library-access
- series-doesnt-exist
- denied
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/b4136253576a9fb5.
Report an issue: GitHub.