github new-usemame/Calibre-Web-NextGen v4.0.44
v4.0.44 — Kobo subsystem hardening: popup loop, reverse-proxy sync, six robustness bugs

latest releases: v4.0.172, v4.0.171, v4.0.170...
one month ago

What changed

Comprehensive Kobo subsystem audit triggered by a household report of the "Sync to last page read?" popup loop. The audit found six independent bugs spanning the reading-state PUT/GET cycle, sync-token parsing, magic-shelf truncation, and reverse-proxy handling. All six are fixed in this release. Reading on a Kobo synced to a Calibre-Web-NextGen instance should be noticeably more reliable.

The popup loop is fixed

The "Sync to last page read? It looks like you've been reading on another device" popup that fired after auto-sleep / wake on every Kobo device was caused by three independent bugs in the reading-state response, each of which made the device think the server's view diverged from its own:

  1. Float / int mismatch on ProgressPercent. Kobo sends 33 (int); the SQLite Float column round-tripped it as 33.0. The device read 33 ≠ 33.0 as a divergent position.
  2. ProgressPercent: 0 was silently dropped from responses. A if value: truthy check omitted the field whenever progress was exactly 0. Particularly bad for comics and manga, where ProgressPercent stays 0 while ContentSourceProgressPercent carries page-by-page progress.
  3. priority_timestamp advanced server-side on every PUT. SQLAlchemy's onupdate=datetime.now() rewrote the timestamp on every flush, even when the data didn't change. The device caches PriorityTimestamp from each GET; if the next GET returns a newer one, it shows the popup. After auto-sleep/wake, this fired reliably.

The fix mirrors the pattern that official Kobo cloud uses: the server reads LastModified from the device's PUT body and uses that timestamp as both LastModified and PriorityTimestamp for the affected reading-state row. The next GET returns the device's own timestamp back, eliminating the divergence trigger. Float values that are whole numbers are now coerced to int before they hit the wire, matching what the device sent.

Backports: janeczku/calibre-web PRs #3585 (@leahjessie) and #3601 (@jarynclouatre), plus the still-open janeczku PR #3607 (@leahjessie) which the upstream author has been running locally for weeks without seeing the popup.

Reverse-proxy users can now sync

HandleInitRequest rewrites a handful of Kobo resource URLs to point at the local Calibre-Web instance (image host, cover templates, OAuth, etc.) but the library_sync URL was left at its default https://storeapi.kobo.com/v1/library/sync. Kobo devices behind nginx, Caddy, Traefik, or any other reverse proxy dutifully called Kobo's actual store API for sync — which has no idea what's in your local library — so no books arrived on the device.

The fix adds the missing rewrite in both proxy / unproxied branches. Backport of janeczku commit a9713bd4 (@Rakjlou).

PUT /state robustness — three additional bugs

The same audit that surfaced the popup loop found three more independent failure modes in the same handler:

  • 500 on Statistics-only / StatusInfo-only PUTs. Kobo legitimately PUTs without CurrentBookmark (e.g. when reporting just time-spent). The hardcover-progress-push call accessed request_bookmark['ProgressPercent'] outside the if request_bookmark: guard, raising TypeError and returning 500 to the device. Device retries forever.
  • Partial Statistics rolled back the whole PUT. When Kobo sent only SpentReadingMinutes (typical when a book is just opened), the unconditional request_statistics["RemainingTimeMinutes"] raised KeyError, the request returned 400, and the bookmark + status updates in the same PUT were lost.
  • Falsy check dropped 0-valued reading minutes from responses. Same class of bug as the ProgressPercent: 0 issue above; the missing field reads as a divergent state to the device.

All three guarded with is not None / .get(...) patterns so partial Kobo payloads no longer break the round-trip.

SyncToken validation is now actually validation

The from_headers parser had four overlapping bugs that turned validation into a no-op and let malformed tokens crash the request:

  • data_schema_v1 was applied to the wrapper object instead of the inner data payload.
  • token_schema declared version and data properties but didn't require them — a token missing either crashed the next line.
  • raw_kobo_store_token was read outside the try/except, so a token that passed the (no-op) validation but omitted that key still 500'd.
  • The schema declared timestamp fields as string but the wire format writes them as numbers (epoch seconds), guaranteeing a real validation would always fail.

Fixed end-to-end. A malformed sync token now degrades gracefully to a fresh SyncToken (which triggers a full resync) instead of returning 500 to the device.

Magic-shelf 1000-book cap removed

HandleSyncRequest's magic-shelf branch passed page_size=1000 to get_books_for_magic_shelf, silently truncating shelves over 1000 books. Anything past the first 1000 never reached the device — no error, no log, just missing books on the Kobo. The sibling helper get_magic_shelf_book_ids_for_kobo already passes page_size=None for full traversal; the sync path now matches.

sync_shelves only-Kobo-sync filter actually filters

The only_kobo_shelves branch of sync_shelves used not ub.Shelf.kobo_sync inside a SQLAlchemy filter chain. Python's not operator against a Column object doesn't produce a SQL NOT — depending on the SQLAlchemy version it either raises or coerces the column to a Python bool, leaving the filter unconstrained. Switched to the idiomatic ub.Shelf.kobo_sync == False.

To get the update

docker pull ghcr.io/new-usemame/calibre-web-nextgen:v4.0.44

Or :latest. No data migration; existing instances upgrade in place. The reading-state row's priority_timestamp no longer auto-bumps on flush, so existing rows are healed implicitly on the next Kobo PUT.

Credit

This release is almost entirely backports from upstream janeczku/calibre-web PRs that haven't yet shipped in a stock Calibre-Web release, plus internal-audit fixes:

The PUT /state robustness, SyncToken validation, and shelf bugs were all found in our audit and don't have upstream counterparts yet.

E2E verification

  • cps/kobo.py + cps/ub.py + cps/services/SyncToken.py regression suite: 19 new tests covering each of the six fixes; 16 of 19 fail on main and pass on this build (the 3 negative cases are correct on both sides).
  • Full Kobo + KOSync test suite: 77 passed, no regressions in adjacent paths (sync, metadata, cover, magic shelf, kosync, hardcover).
  • Maggie's daily Safari + Kobo Libra Color flow is the production verification once this image is deployed.

Don't miss a new Calibre-Web-NextGen release

NewReleases is sending notifications on new releases.