Expand description
mara is a scraper that fetches over a rotating pool of egress IPs, clearing bot-protection challenges along the way.
Today that means Cloudflare: mara solves the interactive challenge in a real headed browser
once to bank a cf_clearance cookie, then serves every subsequent request to that host
browser-free — a plain HTTP client replaying the cookie and user-agent. The browser is the
fallback; the slim replay is the hot path. Routing is fully explicit: every host you fetch must
be registered (exact match) as either a solve host (Cloudflare) or a raw one (fetched over the
same rotating pool, never touching a browser). An unregistered host fails fast — mara never
silently guesses a route.
§Quick start
use futures::StreamExt;
use mara::{Client, Config, Domain};
let client = Client::new(Config {
domains: vec![Domain::solve("example.com")],
..Default::default()
})
.await?;
// One result per input URL, in completion order. Bare URL strings work directly.
let mut results = client.fetch_all(["https://example.com/a", "https://example.com/b"]);
while let Some(item) = results.next().await {
match item.result {
Ok(page) => println!("{} → {} bytes", item.url, page.value.len()),
Err(err) => eprintln!("{} failed: {err}", item.url),
}
}Client is the entry point; Client::fetch_all is the browser-free bulk path.
See Config and Domain for routing (which hosts take the solve path) and Policy
for timeouts, pacing, and probe tuning. Client::fetch_browser hands you a live headed
chromiumoxide::Page when you need one; pair it with wait_full_load.
The public surface is deliberately small — the entry points above plus the doctor and
store read-models. The orchestration internals (the exit pool, the per-exit workers, the
introspection dashboard, the browser solver) are private: they’re the how, not the contract.
Modules§
- doctor
- Startup diagnostics: environment checks (Xvfb, Chrome, the fingerprint triple, GPU) with a
pass/warn/fail verdict. Backs the
mara doctorcommand. - store
- The persisted, read-only view: per-exit
store::Statsand the clearance/cooldown state surfaced byClient::snapshot. These are report types — the store itself is internal.
Structs§
- Client
- The entry point. Owns the exit pool, the per-exit serving workers, and the background
maintainer that keeps the catalog warm. Cheaply cloneable (an
Arcinside) — clone it to share one pool across tasks. Build one withClient::new; end a run withClient::shutdown. - Config
- How a
Clientis built: the exit catalog, the browser/display knobs for the headed solver, timeouts, persistence, and the routing/pacingdomains. Start fromConfig::defaultand override what you need. There is no client-concurrency dial — serving width is the exit count (one worker per exit). - Domain
- A configured domain, matched by exact host —
example.comdoes not coverwww.example.com; register each host you fetch. Two orthogonal rate ceilings, both in requests-per-minute and both optional (compose freely): - Fetch
All - The stream returned by
Client::fetch_all: an unordered, completion-order stream ofFetchResult, exactly one per input URL, that ends once every URL has settled. - Fetch
Result - One settled item of a
FetchAllstream: the input slot it came from, the URL, the caller’skey(echoed from the inputResource,Noneif none was set), and the fetch result.indexidentifies the slot because URLs can repeat (e.g.--repeat) — it is assigned in strict input order, so anindex → your-datamap is always trivial even without akey.Tis the body type:StringforClient::fetch_all(text-decoded),Vec<u8>forfetch_all_bytes. - Outcome
- A successful fetch: the body plus metadata about how it was obtained.
- Policy
- Tunables for retry/cooldown behaviour, the headed-solve deadlines, pacing, and probing. Read off
Config::policy;Policy::defaultis sensible for most runs. - Resource
- One thing to fetch: a URL plus optional request customization (
method/headers/body, for non-HTML traffic like an Algolia POST) and an optional callerkeyechoed back on the result.From<&str>/From<String>/From<Url>give a bareGETwith no key, sofetch_all(urls)still takes plain strings. Whether a resource is solved or fetched raw is decided by its host (the solve-set), never per-resource.
Enums§
- Fetch
Error - Why a fetch gave up. A resource never fails on a winnable obstacle (rate-limit, block, timeout, a stale-clearance challenge on a registered host) — those retry forever across exits. These variants are the genuinely-unwinnable cases only.
- Method
- The HTTP method a
Resourceuses. A dependency-free mirror of the handful we need, so the public fetch API doesn’t leakwreq/httptypes. Defaults toGet(the HTML bulk path). - Reason
- Why a fetch didn’t reach the origin — the payload of
FetchError::GaveUp.
Functions§
- host_of
- The host component of a URL, or
Noneif it doesn’t parse or has no host. The same host derivation the router uses to match a URL against the configuredDomains. - wait_
full_ load - Wait for a headed
Pageto finish loading — network to settle plus lazy content triggered by scrolling — bounded bybudget, then return its HTML. Pair withClient::fetch_browserwhen you need the fully-rendered page.