{"record":{"id":"2e47b193d277b4cf","repo":"Kareadita/Kavita","slug":"bookmarks-cannot-be-null","errorCode":null,"errorMessage":"Bookmarks cannot be null!","messagePattern":"Bookmarks cannot be null!","errorType":"exception","errorClass":"KavitaException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/BookmarkService.cs","lineNumber":110,"sourceCode":"\n        await unitOfWork.CommitAsync();\n    }\n\n\n    /// <summary>\n    /// Creates a new entry in the AppUserBookmarks and copies an image to BookmarkDirectory.\n    /// </summary>\n    /// <param name=\"userWithBookmarks\">An AppUser object with Bookmarks populated</param>\n    /// <param name=\"bookmarkDto\"></param>\n    /// <param name=\"imageToBookmark\">Full path to the cached image that is going to be copied</param>\n    /// <param name=\"ct\"></param>\n    /// <returns>If the save to DB and copy was successful</returns>\n    public async Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark,\n        CancellationToken ct = default)\n    {\n        if (userWithBookmarks?.Bookmarks == null)\n        {\n            throw new KavitaException(\"Bookmarks cannot be null!\");\n        }\n\n        try\n        {\n            var userBookmark = userWithBookmarks.Bookmarks\n                .SingleOrDefault(b => b.Page == bookmarkDto.Page && b.ChapterId == bookmarkDto.ChapterId && b.ImageOffset == bookmarkDto.ImageOffset);\n            if (userBookmark != null)\n            {\n                logger.LogError(\"Bookmark already exists for Series {SeriesId}, Volume {VolumeId}, Chapter {ChapterId}, Page {PageNum}\", bookmarkDto.SeriesId, bookmarkDto.VolumeId, bookmarkDto.ChapterId, bookmarkDto.Page);\n                return true;\n            }\n\n            var fileInfo = directoryService.FileSystem.FileInfo.New(imageToBookmark);\n            var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync();\n            var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId);\n            var targetFilepath = Path.Join(settings.BookmarksDirectory, targetFolderStem);\n\n            var bookmark = new AppUserBookmark()","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/BookmarkService.cs#L92-L128","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// Before (Bookmarks not loaded):\n// var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);\n// await bookmarkService.BookmarkPage(user, dto, imgPath);\n\n// After:\n// var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId)\n//     ?? throw new KavitaException(\"user-doesnt-exist\");\n// user.Bookmarks ??= new List<AppUserBookmark>();\n// await bookmarkService.BookmarkPage(user, dto, imgPath);","handlingStrategy":"validation","validationCode":"// Validate user and bookmarks before calling BookmarkPage:\n// if (userWithBookmarks == null) throw new KavitaException(\"user-doesnt-exist\");\n// userWithBookmarks.Bookmarks ??= new List<AppUserBookmark>();\n// await bookmarkService.BookmarkPage(userWithBookmarks, bookmarkDto, imageToBookmark, ct);","typeGuard":"// Type guard to ensure Bookmarks is loaded:\n// static bool HasBookmarksLoaded(AppUser user) => user?.Bookmarks != null;","tryCatchPattern":"// try {\n//     await bookmarkService.BookmarkPage(user, dto, img, ct);\n// } catch (KavitaException ex) when (ex.Message.Contains(\"Bookmarks cannot be null\")) {\n//     logger.LogError(\"Bookmarks collection not loaded for user {UserId}\", userId);\n//     // Re-fetch user with bookmarks included and retry\n//     user = await unitOfWork.UserRepository.GetUserByIdAsync(userId);\n//     await bookmarkService.BookmarkPage(user, dto, img, ct);\n// }","preventionTips":["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"],"tags":["bookmarks","null-check","eager-loading","programming-error"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}