All three bookmark endpoints (GET/POST /api/reader/bookmarks*, DELETE /api/reader/bookmarks/{id}) throw FormatException for any real user, because the handlers ObjectId.Parse the caller identity — which is User.Identity.Name = Keycloak preferred_username, a human string like joey.
CreateBookmarkCommandHandler.cs:17, GetBookBookmarksQueryHandler.cs:16, DeleteBookmarkCommandHandler.cs:23 all call ObjectId.Parse(request.UserId) on the username.
Meanwhile ReadingProgress.UserId and Book.UploadedBy are already plain username strings — bookmarks were the odd one out.
Unit tests hid the bug by passing ObjectId-shaped strings as user ids.
Fix
Re-key UserBookmark.UserId (and UpvotedBy) to username strings, matching ReadingProgress; remove the parses; migration script mongodb-scripts/2026-07-migrate-userbookmarks-userid.js (existing ObjectId-keyed rows were unreachable — read path threw before any real user could create data); tests now use realistic usernames; added {bookId, page, isPublic} index for the upcoming social-bookmarks feature.
Also riding along in the same PR (small, verified):
EpubReaderService now opens EPUBs with the same lenientEpubReaderOptions as the MetadataExtractor, so a malformed EPUB that ingested successfully can't produce a different reading-order count in the live reader (page-number drift between BookPages and /epub-page).
GET /api/book/{id}/file now sets enableRangeProcessing: true — pdf.js range requests were silently unsupported, forcing whole-file downloads.
Discovered during the reader/Kavita-parity planning session (2026-07-16).
## Summary
All three bookmark endpoints (`GET/POST /api/reader/bookmarks*`, `DELETE /api/reader/bookmarks/{id}`) throw `FormatException` for any real user, because the handlers `ObjectId.Parse` the caller identity — which is `User.Identity.Name` = Keycloak `preferred_username`, a human string like `joey`.
## Root cause
- `SpikerSoft.Data/Mongos/UserBookmark.cs`: `UserId` declared `ObjectId` (`UpvotedBy` likewise `List<ObjectId>`).
- `CreateBookmarkCommandHandler.cs:17`, `GetBookBookmarksQueryHandler.cs:16`, `DeleteBookmarkCommandHandler.cs:23` all call `ObjectId.Parse(request.UserId)` on the username.
- Meanwhile `ReadingProgress.UserId` and `Book.UploadedBy` are already plain username strings — bookmarks were the odd one out.
- Unit tests hid the bug by passing ObjectId-shaped strings as user ids.
## Fix
Re-key `UserBookmark.UserId` (and `UpvotedBy`) to username strings, matching `ReadingProgress`; remove the parses; migration script `mongodb-scripts/2026-07-migrate-userbookmarks-userid.js` (existing ObjectId-keyed rows were unreachable — read path threw before any real user could create data); tests now use realistic usernames; added `{bookId, page, isPublic}` index for the upcoming social-bookmarks feature.
Also riding along in the same PR (small, verified):
- `EpubReaderService` now opens EPUBs with the same **lenient** `EpubReaderOptions` as the MetadataExtractor, so a malformed EPUB that ingested successfully can't produce a different reading-order count in the live reader (page-number drift between `BookPages` and `/epub-page`).
- `GET /api/book/{id}/file` now sets `enableRangeProcessing: true` — pdf.js range requests were silently unsupported, forcing whole-file downloads.
Discovered during the reader/Kavita-parity planning session (2026-07-16).
Resolved in spikersoft-backend PR #326 (merged to master, 4d22c0bb — PR-0 of the reader plan): UserBookmark re-keyed to Keycloak username strings, all three bookmark endpoints no longer ObjectId.Parse the caller identity. The API has built + deployed green multiple times since the merge (most recently on the #337 merge commit), so the fix is live. Closing.
Resolved in spikersoft-backend PR #326 (merged to `master`, 4d22c0bb — PR-0 of the reader plan): `UserBookmark` re-keyed to Keycloak username strings, all three bookmark endpoints no longer `ObjectId.Parse` the caller identity. The API has built + deployed green multiple times since the merge (most recently on the #337 merge commit), so the fix is live. Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
All three bookmark endpoints (
GET/POST /api/reader/bookmarks*,DELETE /api/reader/bookmarks/{id}) throwFormatExceptionfor any real user, because the handlersObjectId.Parsethe caller identity — which isUser.Identity.Name= Keycloakpreferred_username, a human string likejoey.Root cause
SpikerSoft.Data/Mongos/UserBookmark.cs:UserIddeclaredObjectId(UpvotedBylikewiseList<ObjectId>).CreateBookmarkCommandHandler.cs:17,GetBookBookmarksQueryHandler.cs:16,DeleteBookmarkCommandHandler.cs:23all callObjectId.Parse(request.UserId)on the username.ReadingProgress.UserIdandBook.UploadedByare already plain username strings — bookmarks were the odd one out.Fix
Re-key
UserBookmark.UserId(andUpvotedBy) to username strings, matchingReadingProgress; remove the parses; migration scriptmongodb-scripts/2026-07-migrate-userbookmarks-userid.js(existing ObjectId-keyed rows were unreachable — read path threw before any real user could create data); tests now use realistic usernames; added{bookId, page, isPublic}index for the upcoming social-bookmarks feature.Also riding along in the same PR (small, verified):
EpubReaderServicenow opens EPUBs with the same lenientEpubReaderOptionsas the MetadataExtractor, so a malformed EPUB that ingested successfully can't produce a different reading-order count in the live reader (page-number drift betweenBookPagesand/epub-page).GET /api/book/{id}/filenow setsenableRangeProcessing: true— pdf.js range requests were silently unsupported, forcing whole-file downloads.Discovered during the reader/Kavita-parity planning session (2026-07-16).
Resolved in spikersoft-backend PR #326 (merged to
master, 4d22c0bb — PR-0 of the reader plan):UserBookmarkre-keyed to Keycloak username strings, all three bookmark endpoints no longerObjectId.Parsethe caller identity. The API has built + deployed green multiple times since the merge (most recently on the #337 merge commit), so the fix is live. Closing.