Skip to content

OPDS feed reports numberOfItems: 0 while emitting rel=next when Open Library is down #208

Description

@openlibrary-bot

Found live during the Open Library outage on 2026-09-04.

Symptom

While Open Library was unreachable, GET /v1/api/opds returned this:

{
  "metadata": { "title": "Lenny Catalog", "numberOfItems": 0 },
  "links": [ ..., { "rel": "next", "href": ".../opds?offset=50&limit=50" } ]
}

Zero publications, numberOfItems: 0and a rel=next link. The library held 96 items the entire time.

The two fields contradict each other: numberOfItems says the collection is empty, while rel=next says there is another page to fetch. Both are emitted from the same response.

Cause

opds_feed falls back to LennyDataProvider.empty_catalog(...) when Open Library is unreachable (lenny/core/api.py, the RequestException/HTTPError handler). That fallback is handed the real paging context — page["total"] from Item.count() — but _lenny_empty_catalog (lenny/core/api.py:138-144) hardcodes:

"numberOfItems": 0

while _lenny_catalog_links computes rel=next from the total it was given (offset + limit < page["total"]). So during a fallback the count is a literal 0 and the paging links describe a 96-item collection.

Incidentally this is what let me measure the DB count from outside during the outage: rel=next was present for limit<=95 and absent for limit>=96, pinning Item.count() at exactly 96 while the feed insisted it was empty.

Why this matters more than it looks

modified_since + rel=next + metadata.modified were added in #200 specifically so Open Library's BookWorm harvester can harvest this feed incrementally (internetarchive/openlibrary#13241). That harvester is the primary consumer.

A harvester that reads numberOfItems: 0 during any Open Library blip concludes the library has no books. Depending on how it reconciles, that is a plausible path to it pruning all 96 records from the registry — triggered not by a Lenny bug but by a transient upstream outage, and self-inflicted by a hardcoded zero.

This is the same defect class as #203numberOfItems not reconciled against what is actually emitted — but far more severe: not one book missing, but the entire catalogue, and reachable any time Open Library has a bad minute.

Suggested fix

empty_catalog should report the real total when it has one:

  • When page is supplied, take numberOfItems from page["total"] rather than hardcoding 0. An empty page of a non-empty collection is a legitimate state and should be representable.
  • Decide deliberately what an OL-unreachable feed should say, and make the two fields agree. If the honest answer is "I cannot enumerate the collection right now", a 503 is a better contract than a 200 describing an empty library — a harvester retries a 503 and does not prune on it.

The second point is the important one. A 200 that says "empty" is indistinguishable, to a consumer, from a library that genuinely has nothing — which is precisely why the outage was able to look like a data-loss event from the outside.

Reproducing

Block egress to openlibrary.org from the API container (or point OL at an unroutable host) and request /v1/api/opds with any local items present. Observed on 0.2.15; the fallback path is unchanged in 0.2.16.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions