Skip to content

Back off retrying cover URLs that a CDN just rejected with 403 - #153

Open
jordanfelle wants to merge 1 commit into
Chaptarr:developfrom
jordanfelle:fix-cover-download-403-retry-storm
Open

jordanfelle wants to merge 1 commit into
Chaptarr:developfrom
jordanfelle:fix-cover-download-403-retry-storm

Conversation

@jordanfelle

Copy link
Copy Markdown

Summary

  • MediaCoverService had no negative-caching/backoff for cover URLs a CDN has already rejected with 403 Forbidden. Every rename, refresh, import, or RSS sync that touches a book/author re-attempts the exact same known-bad URL from scratch.
  • On a library where multiple books share an overlapping/duplicate cover URL that Amazon/Goodreads' CDN is currently blocking, this becomes an unthrottled retry storm against that single host. ManagedHttpDispatcher caps outbound connections at maxConnectionsPerServer: 12, so the retry rate can exceed the pool and starve it.
  • Observed impact: a RenameAuthorCommand covering ~1600 authors hung with zero progress for 15+ minutes (message field frozen on one author, no new log lines) while the app itself stayed fully responsive. Diagnosing via /proc/<pid>/net/tcp on the host showed several leaked CLOSE_WAIT sockets to another local service at the moment of the hang. Cancelling the stuck command via DELETE /api/v1/command/{id} cleared the CLOSE_WAIT sockets instantly and let every other queued command resume.
  • Because each 403 is caught and logged rather than treated as fatal, the command never surfaces an error — it just silently stops making progress.

Fix

Adds a 6-hour in-memory negative cache (ConcurrentDictionary<string, DateTime>) keyed by URL inside GetImageResponse, the single chokepoint both the author-cover (DownloadCover) and book-cover (DownloadBookCover) paths already funnel through. A URL that just 403'd is skipped with a synthetic HttpException (fast-fail, no network call) for the backoff window instead of being retried by every subsequent book/author referencing it — existing catch (HttpException e) when (... Forbidden) call sites are untouched and keep working exactly as before.

This doesn't fix the CDN-side blocking itself (out of Chaptarr's control), but it stops one blocked URL from being able to stall unrelated bulk operations across the whole library.

Test plan

  • dotnet build succeeds with no new warnings/errors.
  • Reproduced the original hang on a live instance (~1600 authors), confirmed root cause via /proc/<pid>/net/tcp (leaked CLOSE_WAIT to a same-host service) and log correlation (repeated Book/Author cover download blocked (403 Forbidden) for the same handful of URLs across different book IDs), and confirmed cancelling the stuck command immediately cleared the connection pool and resumed the queue — consistent with the fix's target.

Fixes #152

🤖 Generated with Claude Code

https://claude.ai/code/session_01ED3rga2EPdAvo3ChtnJCtH

Every book/author cover download that fails with 403 Forbidden was
retried from scratch on the next rename, refresh, import, or RSS sync
that touched the same book, with no backoff. When several books share
a cover URL a CDN is currently blocking, this turns into an unthrottled
retry storm against that host, which starves ManagedHttpDispatcher's
12-connections-per-host pool and can stall unrelated bulk commands
(observed: RenameAuthorCommand hanging with zero progress for 15+
minutes on a ~1600-author library, cleared instantly by cancelling it).

Adds a 6-hour negative cache keyed by URL in GetImageResponse, the
single chokepoint both the author-cover and book-cover paths already
share, so a known-403 URL is skipped without a network call until the
backoff expires.

Fixes Chaptarr#152

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ED3rga2EPdAvo3ChtnJCtH
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.

Rename/refresh commands can hang indefinitely: unthrottled retries of 403-blocked cover URLs exhaust the shared HTTP connection pool

1 participant