Kareadita/Kavita · error · KavitaException

Bookmarks cannot be null!

Error message

Bookmarks cannot be null!

What it means

Thrown by BookmarkPage when the passed AppUser object is null or its Bookmarks navigation collection has not been loaded. The null-coalescing check 'userWithBookmarks?.Bookmarks == null' catches both cases. This is a programming-contract violation — the caller is expected to pass a user with the Bookmarks relationship eagerly loaded.

Source

Thrown at Kavita.Services/BookmarkService.cs:110

        await unitOfWork.CommitAsync();
    }


    /// <summary>
    /// Creates a new entry in the AppUserBookmarks and copies an image to BookmarkDirectory.
    /// </summary>
    /// <param name="userWithBookmarks">An AppUser object with Bookmarks populated</param>
    /// <param name="bookmarkDto"></param>
    /// <param name="imageToBookmark">Full path to the cached image that is going to be copied</param>
    /// <param name="ct"></param>
    /// <returns>If the save to DB and copy was successful</returns>
    public async Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark,
        CancellationToken ct = default)
    {
        if (userWithBookmarks?.Bookmarks == null)
        {
            throw new KavitaException("Bookmarks cannot be null!");
        }

        try
        {
            var userBookmark = userWithBookmarks.Bookmarks
                .SingleOrDefault(b => b.Page == bookmarkDto.Page && b.ChapterId == bookmarkDto.ChapterId && b.ImageOffset == bookmarkDto.ImageOffset);
            if (userBookmark != null)
            {
                logger.LogError("Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page);
                return true;
            }

            var fileInfo = directoryService.FileSystem.FileInfo.New(imageToBookmark);
            var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();
            var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId);
            var targetFilepath = Path.Join(settings.BookmarksDirectory, targetFolderStem);

            var bookmark = new AppUserBookmark()

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Ensure the repository query that fetches the user includes the Bookmarks collection: userRepository.GetUserByUsernameAsync(username, includeBookmarks: true) or equivalent Include
  2. Add a null check on the user before calling BookmarkPage and return a not-found error to the caller instead
  3. Review the calling controller to confirm it uses the overload that eagerly loads Bookmarks

Example fix

// Before (Bookmarks not loaded):
// var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
// await bookmarkService.BookmarkPage(user, dto, imgPath);

// After:
// var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId)
//     ?? throw new KavitaException("user-doesnt-exist");
// user.Bookmarks ??= new List<AppUserBookmark>();
// await bookmarkService.BookmarkPage(user, dto, imgPath);
Defensive patterns

Strategy: validation

Validate before calling

// Validate user and bookmarks before calling BookmarkPage:
// if (userWithBookmarks == null) throw new KavitaException("user-doesnt-exist");
// userWithBookmarks.Bookmarks ??= new List<AppUserBookmark>();
// await bookmarkService.BookmarkPage(userWithBookmarks, bookmarkDto, imageToBookmark, ct);

Type guard

// Type guard to ensure Bookmarks is loaded:
// static bool HasBookmarksLoaded(AppUser user) => user?.Bookmarks != null;

Try / catch

// try {
//     await bookmarkService.BookmarkPage(user, dto, img, ct);
// } catch (KavitaException ex) when (ex.Message.Contains("Bookmarks cannot be null")) {
//     logger.LogError("Bookmarks collection not loaded for user {UserId}", userId);
//     // Re-fetch user with bookmarks included and retry
//     user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);
//     await bookmarkService.BookmarkPage(user, dto, img, ct);
// }

Prevention

When it happens

Trigger: A controller or service calls BookmarkPage with a user fetched via a query that did not include .Include(u => u.Bookmarks); or the caller passes a null AppUser reference (e.g., a user lookup returned null and was not checked before calling).

Common situations: Refactoring a calling method to use a different repository query that omits the Bookmarks include; passing a freshly constructed AppUser without loading navigation properties; a race condition where the user was deleted between lookup and the bookmark call.

Related errors


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