You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(camoufox): per-domain persistent session for /v1/fetch (#7)
Replaces the spawn-per-fetch model with a per-domain warm Camoufox
session that survives across `/v1/fetch` calls. Three concrete wins:
1. The ~13s geckodriver + Camoufox cold start now happens once per
domain, not on every fetch.
2. PerimeterX sees a single coherent browser session instead of a
stream of identical short-lived browsers; this is what trips its
"tráfico inusual" rate limit and was making /v1/fetch unusable
after roughly the 5th call against the same target.
3. `cf_clearance` issued during the first navigation persists for
the life of the session, so subsequent fetches don't re-run the
Cloudflare managed challenge.
What's in this commit
- New `infrastructure/session.rs` — `PersistentSession` holding
fantoccini::Client + tokio::process::Child (kill_on_drop) plus an
`Inner` mutex (last_used, fetch_count, warmed flag). One handle ==
one warm browser.
- New `infrastructure/session_pool.rs` — `SessionPool` keyed by
domain, lazy spawn, 5-minute TTL, race-safe double-check on insert.
- New `infrastructure/fetch_script.rs` — extracted the in-page
`fetch` JS builder so `camoufox_fetcher.rs` stays under the 200-LOC
axum-best-practice rule.
- New `infrastructure/fetch_strategies.rs` — `navigate_and_read`
(GET, lets the browser handle CF interactive challenges) and
`in_page_fetch` (POST/PUT/etc., since webdriver navigation can't
carry a body). The GET branch sniffs status from body shape since
webdriver doesn't surface response codes.
- `camoufox_pool.rs` — `CamoufoxPool` now owns `Arc<SessionPool>`.
Harvest path is unchanged (still spawn-per-call); only the Fetcher
impl uses the pool.
- `camoufox_fetcher.rs` rewritten to acquire a session, warm it
once (homepage navigation → cf_clearance), optionally navigate to
the caller's `Referer` so PX's sensor runs against the right path,
then dispatch GET vs POST.
Operational notes
- Session TTL is hardcoded at 5 minutes for v1; lifecycle/eviction
ADR follow-up if this needs to be configurable.
- Concurrent fetches for the same domain serialize on the session
mutex — intentional, since one warm browser can't handle parallel
navigations safely.
- No background reaper task yet; aged-out sessions are recycled
lazily on the next acquire().
- Process exit cleans up via geckodriver's `kill_on_drop` on the
held Child, which fires when `SessionPool`'s HashMap drops.
Live validation deferred
- The implementation has been smoke-tested on the local box, but the
test IP is currently sitting under a pedidosya PerimeterX
rate-limit ("tráfico inusual" body) from the many ad-hoc /v1/solve
calls made while debugging upstream JA3 / cookie-replay issues.
The rate limit is not specific to the session-pool change — it
also blocks the v1.4.0 binary. Re-running this once the rate limit
lifts (or from a fresh IP) is the verification path before
shipping this in a release.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
0 commit comments