Summary #
Ships the full RSS pipeline (subscribe → fetch → process → render) and hardens it to the shape of FreshRSS / Miniflux. The digest read path no longer triggers fetches — SPEC's three refresh triggers (30-min cron, manual, on-add) are now the only paths into the queue.
What's new #
- Outbound HTTP —
OutboundFeedClientblocks SSRF (loopback, RFC1918, link-local, IPv4-mapped IPv6);ConditionalFeedClientplumbsIf-None-Match/If-Modified-Sinceand readsETag/Last-Modified/Retry-After. - Fetch result variants —
FeedFetcherreturnsModified | NotModified | Gone | RateLimited | Failed; the job branches on case kind, not exception messages. - Processor seam —
ContentTypeClassifier(blogpost / microblog / video / podcast) +HtmlSanitizer(HTMLPurifier viastevebauman/purify) run between fetch and upsert. Sanitized HTML is persisted; the reader renders verbatim. - Per-feed scheduling —
next_check_atfilter, per-feedlast_dispatched_atstamped inside the dispatch loop (no bulk pre-stamp race), 30-min cron. - Failure handling — exponential backoff (5m → 15m → 1h → 6h → 24h), silent auto-mute at 20 consecutive failures, daily retry command silently re-enables on success.
- Batch upsert —
FeedEntry::upsert()instead of per-entry SELECT+INSERT/UPDATE;first_seen_atpreserved. - Digest read path —
ConsumeControllerreduced toFeedEntryRepository::digestEntries(...); stale-on-visit refresh removed. - PDS reconcile —
SubscriptionService::reconcileLocalMirrorwrapped inDB::transaction(); new feeds since last login are sync-fetched at login. - Model hygiene —
$fillableallow-lists replace$guarded = []; state transitions move to methods (markFetched,markFailed,markGone,mute, …).
SPEC #
<feed-sources> and <content-types> updated inline during planning: three-trigger refresh model, fetch-time classification, and the "30-min default is a product choice, not an architectural ceiling" posture.
Tests #
- Per-variant job tests (Modified / NotModified / Gone / RateLimited / Failed).
- Backoff ladder + auto-mute + daily retry.
- Classifier + sanitizer table tests.
- Scheduler eligibility (
next_check_athonored, per-feed stamp, dispatch failure leaves row unstamped). - SSRF dataset over loopback / RFC1918 / link-local / IPv4-mapped IPv6.
ReconcileLocalMirrorTestcovers transactional update + cross-user isolation.
bun run lint, bun run types, pint --dirty, php artisan test --compact — all green.
Out of scope (P2) #
Dedup hash fallback for guid-less + link-less entries, body-size streaming cap, outbound port allow-list, identifying User-Agent, redirect TOCTOU pinning via CURLOPT_RESOLVE, reading-time / YouTube-ID / podcast-enclosure processors (no-op stubs in the seam today), and full-article fetching ("Readability") — tracked as a separate PRD.