Skip to content

v1.9.0

Choose a tag to compare

@KeyCode17 KeyCode17 released this 07 Aug 17:30
· 9 commits to main since this release
f5b3ee8

v1.9.0 — Egress proxies actually reach the browser

Four proxy surfaces shipped through v1.8.0 and only one of them reached a browser. This release makes the per-request proxy real end to end, and corrects the docs that described behaviour the code never had.

The bug, precisely

Surface Before v1.9.0
PX_PROXIES → ProxyPool → SessionPool worked, /v1/fetch sessions only
POST /v1/solve body "proxy" deserialized into SolveRequestDto, never read
px-cli solve --proxy sent in the request body, dropped server-side
pxsolver-core::SolveRequest::with_proxy() published builder with zero consumers
HarvestRequest.proxy honoured by CamoufoxPool, ignored by ChromiumoxidePool

The cause was structural rather than a missed call site: SolveDispatcher::solve took &str and ChallengeHandler::solve took &PageHtml, so a proxy had no parameter to travel in, and both browser handlers built HarvestRequest::new(url) — which defaults proxy to None. ChromiumoxidePool::launch_browser had no --proxy-server argument at all.

What's new

  • Per-request egress on /v1/solve. ChallengeHandler::solve now takes a SolveAction { page, proxy } constructed at the HTTP edge; SolveDispatcher::solve takes pxsolver_core::SolveRequest, so the published builder is the type the edge maps into. Chromium, Camoufox and the native sensor path all honour it.
  • Native path proxying. SolveContext carries the proxy and SensorNativeSolver posts through a per-proxy reqwest client, cached in ProxyClients so the pooled TLS connection survives repeat solves.
  • The egress is part of the cache key. A bundle earned through proxy A is no longer replayed to a caller who asked for proxy B. A direct solve keeps fp_key = 0, so existing entries still resolve. (This is the fix for the reported "different proxy values return the same cached bundle" behaviour.)
  • Credentials are handled honestly. No browser engine can answer a proxy 407 — geckodriver's W3C proxy capability has no credential field and Chromium ignores userinfo without a CDP Fetch.authRequired handler. user:pass@ is now stripped with a warning naming the sanitized URL. reqwest does support proxy auth, so the native sensor path is exempt.
  • socks5h:// is normalized for Chromium, which has no such scheme and silently ignores specs it cannot parse.

Deliberately unchanged

/v1/solve never falls back to the PX_PROXIES rotation. A _px3 bundle is bound to the IP that earned it, so a caller who named no egress could not use a bundle harvested through a rotating one. Rotation stays where it pays: long-lived /v1/fetch Camoufox sessions, which consume the bundle themselves.

Docs corrected

  • docs/deployment.md claimed N × len(proxies) parallel egress paths. A session takes its proxy at spawn and holds it for the 300s TTL, so distinct egress IPs per domain is min(PX_FETCH_MAX_PER_DOMAIN, len(PX_PROXIES)) — ten proxies at the default N=2 gives a domain two IPs, not twenty.
  • The PX_PROXIES example showed user:pass@ for paths that cannot authenticate.
  • docs/runbook-native-bypass.md told operators PX_PROXIES covered the solve path, and its throughput soak built a direct reqwest client while claiming to run through their proxy. The soak now reads NATIVE_SOAK_PROXY (or the first PX_PROXIES entry) and prints the egress it used.
  • README gains a Proxies section. Rationale: ADR-0025.

⚠️ Source-breaking despite the minor bump

ChallengeHandler::solve and SolveDispatcher::solve changed signature. If you implement ChallengeHandler outside this workspace:

// before
async fn solve(&self, page: &PageHtml) -> Result<HandlerOutcome, AppError>

// after
async fn solve(&self, action: &SolveAction) -> Result<HandlerOutcome, AppError>
// action.page is the old argument; action.proxy is the requested egress

Everything else in the published surface is unchanged. A pxsolver-* = "1" pin will fail to compile on cargo update — this was shipped as a minor knowingly, and the trade is recorded in ADR-0025.

Release mechanics

xtask bump only ever rewrote [workspace.package] version, leaving the internal pxsolver-* dependency pins at 1.4.0 since that release. Minor bumps hid it (1.8.0 satisfies ^1.4.0). bump now re-pins them with the workspace version, and all pins track 1.9.0.

Also clears RUSTSEC-2026-0185 (quinn-proto) and RUSTSEC-2026-0190 (anyhow) — both transitive, lock-only.

Verification

cargo fmt --check · cargo clippy --workspace --all-targets --all-features under the unwrap/expect/panic ban, zero warnings · full workspace test suite, zero failures · 200-LOC gate green · cargo audit --deny warnings clean. New tests cover the proxy reaching the routing dispatcher, the Cloudflare harvester and the native solver, the --proxy-server argument shape and socks5h normalization, egress-scoped cache entries, and absent optionals staying off the wire.

PRs merged

  • #20 — feat(proxy): honour the per-request egress proxy end to end

Published to crates.io

All 16 pxsolver-* library crates are published at 1.9.0. The pxsolver-server and
pxsolver-cli binaries are publish = false — build them from source.

Commits since v1.8.0:

  • Merge pull request #20 from KeyCode17/feat/proxy-end-to-end (f5b3ee8)
  • chore(deps): clear RUSTSEC-2026-0185 and RUSTSEC-2026-0190 (2094431)
  • chore: bump to 1.9.0 (d6e263a)
  • fix(proxy): stop two silent drops on the wired-up egress path (24be22e)
  • style(errors): capitalize error messages in the proxy-touched harvester files (f534b63)
  • fix(xtask): re-pin internal deps when bumping the workspace version (ed4bf67)
  • feat(proxy): honour the per-request egress proxy end to end (9d3cdba)