Skip to content

Only a missing account may count against an X source's health - #157

Merged
ralyodio merged 1 commit into
mainfrom
x-health
Aug 29, 2026
Merged

Only a missing account may count against an X source's health#157
ralyodio merged 1 commit into
mainfrom
x-health

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

fetchXSource treated every failure except a rate limit as evidence about the account. A provider outage, a dead session, or no provider configured at all incremented error_count — and markCrawlFailure retires a feed at ten consecutive failures.

An X source polls on a five-minute floor. Ten strikes is fifty minutes.

That makes the ordinary order of operations destructive: setting X_ENABLED=true before a provider is reachable is how you find out whether it is reachable, and it would have marked every X source dead within the hour — logging "these accounts are broken" rather than "nothing is configured to collect with".

The comment above the line already claimed the correct behaviour. The code did the opposite. Now only XNoSuchSource reaches markCrawlFailure; everything else returns throttled, which moves next_fetch_at and leaves status, error_count, last_error and last_success_at exactly as they were.

Retry intervals by cause — three different guesses about when the situation changes:

Cause Wait
Rate limit whatever the server named
Provider outage / dead session 20 minutes
No provider configured 1 hour — nothing changes until something is deployed

Five new tests; four fail against the merged code and pass here, including the exact scenario above. All 12 packages green.

Found while checking what switching X on would actually do: there is no RSSHub, Teapot or Nitter service in the Railway project yet, so the "no provider configured" path is the one prod would have taken.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q6QEgpuS4MLamogXtr2ZX6

`fetchXSource` treated every failure except a rate limit as evidence about
the account. So a provider outage, a dead session, or no provider being
configured at all incremented `error_count` — and `markCrawlFailure`
retires a feed at ten consecutive failures.

An X source polls on a five-minute floor. Ten strikes is fifty minutes.

That makes the ordinary order of operations destructive: setting
`X_ENABLED=true` before a provider is reachable is how you find out
whether it is reachable, and it would have marked every X source dead
within the hour, logging "these accounts are broken" rather than "nothing
is configured to collect with".

The comment above the line already claimed the correct behaviour — that a
deleted, suspended or protected account is the one failure genuinely about
the source. The code did the opposite. Now only `XNoSuchSource` reaches
`markCrawlFailure`; everything else returns `throttled`, which moves
`next_fetch_at` and leaves `status`, `error_count`, `last_error` and
`last_success_at` untouched.

Retry intervals by cause, because they are three different guesses about
when the situation changes: a rate limit uses the interval the server
named; an outage waits twenty minutes; "no provider configured" waits an
hour, since nothing changes until somebody deploys something.

Five tests, four of which fail against the merged code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q6QEgpuS4MLamogXtr2ZX6
@ralyodio
ralyodio merged commit 3683486 into main Aug 29, 2026
3 checks passed
ralyodio added a commit that referenced this pull request Aug 29, 2026
…ner (#158)

X needs a collector and there was none. Rather than a second Railway
service, RSSHub runs inside the poller — the only process in this system
that collects anything — reachable on 127.0.0.1:1200.

That is a smaller thing than a service beside it: no second billed
container, no private-networking hop (Railway's outbound IPv6 is opt-in
per service and fails as a fast 504, which is unpleasant to diagnose), and
no public surface to remember to keep unexposed. The web service is
untouched; it never collects, so it never needs any of this.

Supervised, and its death is never the crawler's. RSSHub is a large
third-party app talking to a hostile upstream and it will crash; a crash
costs one restart with backoff and nothing else. If it never starts at
all, X sources simply reschedule — #157 is what makes that safe.

The image copies RSSHub's published artifact rather than resolving it:
the npm package is only ever `1.0.0-master.<sha>` with 83 direct deps
including a Playwright fork that downloads a browser.

Three things were verified against RSSHub's own source rather than
guessed, having been wrong on the first attempt:

- the entry is `dist/index.mjs`, not `lib/index.js` (they publish no bin)
- the base must be `node:24-trixie-slim`, matching theirs. It was
  `node:22-alpine`: musl cannot load glibc-linked artifacts and Node 22
  cannot load a Node 24 ABI addon, and both fail as an image that builds
  cleanly and dies on first require
- `--max-http-header-size=32768`, because X sends headers over Node's
  default and the failure reads like a broken upstream

The bundled Chromium is deleted again — hundreds of megabytes for routes
that scrape pages, and every route this poller asks for is an API call.

Honest limits: the image build cannot be exercised here (no container
runtime on this box), so the Dockerfile is verified by reading RSSHub's
Dockerfile and package.json, not by building. The poller now runs a Node
major CI does not exercise. And it collects nothing until X_SESSIONS
carries a real X login.

7 tests on the supervisor; all 12 packages green.


Claude-Session: https://claude.ai/code/session_01Q6QEgpuS4MLamogXtr2ZX6

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio added a commit that referenced this pull request Aug 29, 2026
…le (#159)

Two more namespaces, and they cost very different amounts — which is the
useful part of this change rather than an aside.

Instagram is X's shape: no feeds, collected through the RSSHub daemon
already running in the poller, mirrored here. It was a canonical.js, a
fetch.js and a route, because the namespace machinery already existed.

Facebook is not. Three doors, measured rather than assumed:

  - facebook.com/feeds/page.php?format=rss20 answers 404 (removed)
  - mbasic.facebook.com/<page> answers 200 with a login wall
  - RSSHub carries a thousand namespaces and no Facebook one

The only remaining route is Meta's Graph API, which returns a Page's
posts only to somebody who ADMINISTERS that Page — reading a stranger's
public Page needs a permission Meta grants rarely and only after review.
So /fb/ takes no open submissions: a Page appears once its operator
connects it via FB_PAGE_TOKENS, and an unconnected Page is told plainly
that it is not collectable rather than shown a button that does nothing.

Nothing here drives a logged-in session against that login wall.

The rule from #157 is now shared rather than per-platform. src/failure.js
owns "only a source that does not exist may count against the source",
and X was refactored onto it rather than Instagram and Facebook being
written against a copy. Its 60 tests passed unchanged, which is the
evidence the refactor was behaviour-identical.

Two bugs the tests caught, both mine:

  - identify.js said in a comment that X is tried before Instagram and
    had Instagram first, so a bare @handle resolved to Instagram
  - the Facebook collector took its display name from the ref, which is
    lowercased because it is an identity — so every item title read
    "somepage:" instead of "SomePage:". Display now comes from feed_url,
    which kept the original casing; identity still comes from the ref

Also: an uncrawled social row now renders its canonical name instead of
whatever the OPML import called it. 50,026 subreddits are imported and a
few hundred crawled, so /r/programming was headed "reddit.com". A render
decision, not a rewrite of stored titles — the crawler owns those.

1,345 tests across 12 packages, all green.


Claude-Session: https://claude.ai/code/session_01Q6QEgpuS4MLamogXtr2ZX6

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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