Skip to main content

Module network

Module network 

Source
Expand description

Read-only queries against the public atproto network.

Where crate::atproto reads and writes one user’s own PDS repo, this module asks the public relays a single question: how many repos on the network hold a given collection? Today that collection is community.lexicon.rss.subscription, and the answer is FeatherReader’s adoption metric — the one number that says whether the portability claim on every page is being exercised by anybody but us.

Three properties are load-bearing, and each is a rule rather than an intention (design/NETWORK-SPEC.md §4):

  1. It is never a source of truth. The result is a projection: drop the network_stat table and the next probe rebuilds it. Nothing in the reader path reads it, and no reader surface may depend on it.
  2. It counts, it does not collect. The relay answers with a list of DIDs; we read repos.len() and drop the page. Persisting the DID list would build a durable register of “accounts that use an RSS reader” on our disk, for a feature whose entire output is an integer.
  3. The number is a LOWER BOUND, not a census. Since sync v1.1 relays are non-archival: a relay’s index only covers hosts it actually crawls, so a PDS no relay crawls is invisible to it. Two relays can therefore disagree; we query every configured host, record each observation separately, surface the max, and log the disagreement. Any copy derived from this number must say “at least”, never “exactly”.

Why the SSRF guard, when the relay host is operator-configured? Not because the operator is the threat — they can already edit the code. It earns its place for three other reasons. The shared reqwest::Client has no timeouts, so a hung relay would pin a background task forever, while crate::net’s per-hop pinned client bounds both the total request and the idle read. The shared client also follows up to ten redirects with no re-validation, whereas crate::net::guarded_get_no_privacy follows at most five and re-checks scheme + resolved IP on each — a relay that 302s (DNS takeover, a misconfigured proxy, a captive portal on a self-hoster’s network) is the realistic rebinding path. And it is the coherent choice: the same milestone that routes PdsClient::list_records through the guard should not open a fresh unguarded http.get() next door. It is free besides — crate::USER_AGENT and the body cap come along with it.

Structs§

AdoptionObservation
One relay’s answer to “how many repos hold this collection?”.
AdoptionReport
The outcome of one probe run across every configured relay.
RelayClient
A thin, read-only client over the public relays.
RelayFailure
One relay that did not answer, and why.

Enums§

RelayError
Why a relay query failed. Every variant carries host, so a fan-out failure is attributable without the caller threading context back in.

Constants§

DEFAULT_PAGE_LIMIT
Repos requested per page. The relay’s documented ceiling is 1000; 500 keeps a page body small (~30 KB) while making the whole current network one page.
DEFAULT_RELAY_HOSTS
The public Bluesky relays queried by default, in order. Lives here (mirroring crate::atproto::DEFAULT_PLC_DIRECTORY) so crate::config imports the network fact rather than re-typing the hostnames.
LIST_REPOS_BY_COLLECTION
The relay XRPC method that answers “which repos hold this collection?”.
MAX_PAGES
Hard cap on pages walked per host: 25 000 repos at DEFAULT_PAGE_LIMIT, i.e. ~25 000× today’s network. Hitting it means something went wrong (a cursor loop, a wrong collection) — the run is recorded as truncated, so the number reads as “at least N”.

Functions§

normalize_relay_host
Normalize one configured relay host into an origin URL.