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
- Ensure the repository query that fetches the user includes the Bookmarks collection: userRepository.GetUserByUsernameAsync(username, includeBookmarks: true) or equivalent Include
- Add a null check on the user before calling BookmarkPage and return a not-found error to the caller instead
- 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
- Always use the repository overload that includes Bookmarks when fetching a user for bookmark operations
- Centralize user-fetching logic in the controller to ensure consistent Include patterns
- Add integration tests that verify the user is fetched with Bookmarks before calling BookmarkPage
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.