Skip to content

fix(ui): stop the book page refetching forever for a book that does not exist - #272

Open
jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:fix-book-page-refetch-loop
Open

jordanfelle wants to merge 2 commits into
Chaptarr:developfrom
jordanfelle:fix-book-page-refetch-loop

Conversation

@jordanfelle

@jordanfelle jordanfelle commented Sep 29, 2026 •

Copy link
Copy Markdown

Problem

Opening /book/<id> for a book that no longer exists (deleted, or pruned by a metadata profile) shows a spinner indefinitely instead of the existing "Sorry, that book cannot be found" screen.

Cause

BookDetailsPageConnector fetches the book when it is not in the store (needsBookFetch = !isFetching && !hasBook). For a missing book the server answers 404 (GET /book?bookId=<id>; checked against a live instance: 404 in ~1 ms). The FETCH_BOOKS failure branch sets isFetching: false, so needsBookFetch flips false -> true, and componentDidUpdate treats that transition as a reason to populate() again. The result is an endless loop of failing requests with the spinner up most of the time, so the NotFound render at the bottom of render() is never reached.

Change

  • A definitive 404 is final: the book is fetched once and the existing NotFound screen renders.
  • Anything else (a transient 500, a network error, a request aborted by another fetchBooks, an empty 200) is retried, up to 3 fetches per route (MAX_BOOK_FETCH_ATTEMPTS), so a persistent failure still ends on NotFound instead of looping, and a transient one still recovers.
  • The counter resets when the route key changes. The connector now also reads books.error to tell a 404 from other failures.
  • The author fetch gets a once-per-author guard, since it has the same shape if an author is missing.

Testing

  • Syntax-checked with esbuild; the full webpack production build compiles.
  • I modelled the render/componentDidUpdate cycle in plain JavaScript (this checks the reasoning, not the React component): book deleted (404 every time) = 1 fetch then NotFound; transient 500 then success = 2 fetches then the book; aborted request then success = 2 fetches then the book; persistent 500 = 3 fetches then NotFound; empty 200 forever = 3 fetches then NotFound; book exists = 1 fetch. Without the guard the 404 case fetches on every cycle (stopped by my cap at 40).
  • No frontend tests exist in this repo, and I have not yet checked it in a browser.

…ot exist

Opening /book/<id> for a book that no longer exists showed a spinner indefinitely instead of the
existing "Sorry, that book cannot be found" screen.

BookDetailsPageConnector fetches the book when it is not in the store (needsBookFetch = not
fetching && no book). For a missing book the server answers 404 (GET /book?bookId=<id>), the
FETCH_BOOKS handler's failure branch sets isFetching=false, and needsBookFetch flips false -> true
again; componentDidUpdate treats that transition as a reason to populate() and fetches again. That
is an endless loop of failing requests with the spinner up most of the time, so the NotFound
render is never reached.

Remember the route key that was fetched and only fetch once per route (same for the author
fetch, which has the same shape if an author is missing). Once that one fetch fails, isFetching is
false, there is no book, and the existing NotFound screen renders.

Any bookmark or list entry pointing at a pruned or deleted book hit this.
Adversarial review of Chaptarr#272: a once-per-route guard would leave 'cannot be found' up after a transient 500, a network error, or a request aborted by another fetchBooks, and also if the book is dropped from the store while the page is open. A definitive 404 is now final (that is the loop being fixed); any other outcome is retried up to 3 fetches per route (MAX_BOOK_FETCH_ATTEMPTS), so a persistent failure still ends on the NotFound screen. Adds booksError to the connector props and resets the counter when the route key changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant