Ecosystem Integrations
stygian-proxy can be wired into both stygian-graph HTTP adapters and
stygian-browser page contexts via optional Cargo features.
stygian-graph (graph feature)
Enable the graph feature to get ProxyManagerPort — the trait that decouples
graph HTTP adapters from any specific proxy implementation.
[dependencies]
stygian-proxy = { version = "*", features = ["graph"] }
ProxyManagerPort
#![allow(unused)] fn main() { use stygian_proxy::graph::ProxyManagerPort; // ProxyManager already implements ProxyManagerPort via a blanket impl. // Inside any HTTP adapter: async fn fetch(proxy_src: &dyn ProxyManagerPort, url: &str) -> Result<String, Box<dyn std::error::Error>> { let handle = proxy_src.acquire_proxy().await?; // ... make request through handle.proxy_url ... handle.mark_success(); Ok(String::new()) } }
BoxedProxyManager
BoxedProxyManager is a type alias for Arc<dyn ProxyManagerPort>. Use it to
store a proxy source in HttpAdapter or any other adapter without naming the
concrete type:
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::graph::{BoxedProxyManager, ProxyManagerPort}; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; let storage = Arc::new(MemoryProxyStore::default()); let manager = Arc::new( ProxyManager::with_round_robin(storage, ProxyConfig::default()).unwrap() ); // Coerce to the trait object let boxed: BoxedProxyManager = manager; }
NoopProxyManager
When no proxying is needed, pass NoopProxyManager instead. It returns
ProxyHandle::direct() on every call — a noop handle with an empty URL.
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::graph::{BoxedProxyManager, NoopProxyManager}; let no_proxy: BoxedProxyManager = Arc::new(NoopProxyManager); }
This avoids Option<BoxedProxyManager> branches in adapter code: always pass a
BoxedProxyManager, just swap implementations at construction time.
stygian-browser (browser feature)
Enable the browser feature to bind a specific proxy to each browser page context.
[dependencies]
stygian-proxy = { version = "*", features = ["browser"] }
ProxyManagerBridge
ProxyManagerBridge wraps a ProxyManager and exposes bind_proxy(), which
acquires one proxy and returns (proxy_url, ProxyHandle). The URL can be passed
directly to chromiumoxide when launching a browser context.
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; use stygian_proxy::browser::ProxyManagerBridge; let storage = Arc::new(MemoryProxyStore::default()); let manager = Arc::new( ProxyManager::with_round_robin(storage, ProxyConfig::default()).unwrap() ); let bridge = ProxyManagerBridge::new(Arc::clone(&manager)); // In your browser context setup: let (proxy_url, handle) = bridge.bind_proxy().await?; // Pass proxy_url to chromiumoxide BrowserConfig::builder().proxy(proxy_url) // ... handle.mark_success(); // after the page session completes }
BrowserProxySource
BrowserProxySource is the trait implemented by ProxyManagerBridge. Implement
it directly to plug in any proxy source without depending on ProxyManager:
#![allow(unused)] fn main() { use async_trait::async_trait; use stygian_proxy::browser::BrowserProxySource; use stygian_proxy::manager::ProxyHandle; use stygian_proxy::error::ProxyResult; pub struct MyProxySource; #[async_trait] impl BrowserProxySource for MyProxySource { async fn bind_proxy(&self) -> ProxyResult<(String, ProxyHandle)> { // return (proxy_url, handle) Ok(( "http://my-proxy.example.com:8080".into(), ProxyHandle::direct(), )) } } }
DNS TXT proxy discovery (dns-fetcher feature)
Enable the dns-fetcher feature to resolve proxy lists from DNS TXT records.
This is useful for infrastructure-managed proxy registries that publish endpoints
via DNS rather than an HTTP API.
[dependencies]
stygian-proxy = { version = "*", features = ["dns-fetcher"] }
DnsTxtFetcher implements ProxyFetcher and queries a DNS TXT record where each
string is a proxy URL (http://host:port or socks5://host:port):
use stygian_proxy::DnsTxtFetcher; use stygian_proxy::fetcher::{ProxyFetcher, load_from_fetcher}; use std::sync::Arc; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let storage = Arc::new(MemoryProxyStore::default()); let manager = Arc::new( ProxyManager::with_round_robin(Arc::clone(&storage), ProxyConfig::default())? ); // Load proxies from DNS TXT at "proxies.internal.example.com" let fetcher = DnsTxtFetcher::new("proxies.internal.example.com"); let loaded = load_from_fetcher(&fetcher, &manager).await?; println!("loaded {loaded} proxies from DNS"); Ok(()) }
The TXT record format is one proxy URL per string value:
proxies.internal.example.com. 60 IN TXT "http://10.0.1.5:8080"
proxies.internal.example.com. 60 IN TXT "socks5://10.0.1.6:1080"
DnsTxtFetcher uses hickory-resolver under the hood and currently uses the
system resolver configuration by default.
For hardened deployments, prefer constraining the lookup zone and timeout:
#![allow(unused)] fn main() { use std::time::Duration; use stygian_proxy::DnsTxtFetcher; let fetcher = DnsTxtFetcher::new("proxies.internal.example.com") .with_allowed_zone_suffixes(vec!["internal.example.com".to_string()]) .with_lookup_timeout(Duration::from_secs(3)); }
This helps reduce risk from misconfigured DNS sources by limiting discovery to trusted suffixes and bounding lookup latency.
TLS-profile-aware browser binding
ProxyManagerBridge also exposes bind_proxy_with_tls_profile(), which
combines proxy acquisition with a CapabilityRequirement that matches a
specific TLS fingerprint profile. This ensures the acquired proxy is
capable of presenting the correct TLS fingerprint for the browser session.
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; use stygian_proxy::browser::ProxyManagerBridge; let bridge = ProxyManagerBridge::new(Arc::new( ProxyManager::with_round_robin( Arc::new(MemoryProxyStore::default()), ProxyConfig::default(), ).unwrap() )); // Acquire a proxy that carries the "chrome_131" TLS profile let (proxy_url, handle) = bridge.bind_proxy_with_tls_profile("chrome_131").await?; // Pass proxy_url to your browser context; handle tracks the session outcome. handle.mark_success(); }
If no proxy in the pool satisfies the requested profile,
ProxyError::AllProxiesUnhealthy is returned — the same error returned
when no healthy proxy of any kind is available.
Failure tracking across integrations
ProxyHandle uses RAII to track request outcomes. The same contract applies
regardless of whether it came from a graph or browser integration:
- Acquire a handle from
acquire_proxy()(graph) orbind_proxy()(browser). - Perform the I/O operation.
- Call
handle.mark_success()on success. - Drop the handle (success or failure is recorded in the circuit breaker).
If the handle is dropped without mark_success(), the failure counter
increments. After circuit_open_threshold consecutive failures the circuit
opens and the proxy is skipped until after the circuit_half_open_after cooldown.
When the manager is built with
ProxyManager::with_thompson_sampling,
the same mark_success / drop-failure signals also feed the Bayesian
bandit (see Thompson-sampling Bayesian rotation).
Per-vendor sticky browser integration (vendor-stickiness feature)
When the vendor-stickiness cargo feature is enabled, a browser bridge
can opt into per-vendor sticky bindings by calling
acquire_for_domain_with_vendor:
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::{ MemoryProxyStore, ProxyConfig, ProxyManager, stickiness::VendorStickinessMap, }; use stygian_proxy::types::VendorId; let storage = Arc::new(MemoryProxyStore::default()); let manager = ProxyManager::with_round_robin(storage, ProxyConfig::default())? .builder() .stickiness_map(VendorStickinessMap::with_builtin_defaults()) .build()?; // Akamai's 30-minute sticky policy applies for "store.example.com". let handle = manager .acquire_for_domain_with_vendor("store.example.com", VendorId::Akamai) .await?; }
Pair this with BrowserProxySource in production:
#![allow(unused)] fn main() { use stygian_proxy::browser::ProxyManagerBridge; use stygian_browser::{BrowserConfig, WaitUntil}; use std::time::Duration; let bridge = ProxyManagerBridge::new(Arc::clone(&manager)); let config = BrowserConfig::builder() .proxy_source(bridge) // <- ProxySource trait, not the free function .headless(true) .build(); // Akamai-guarded sites get the 30-minute sticky binding; // DataDome-guarded sites get fresh-per-request. let page = config.acquire().await?; page.navigate( "https://store.example.com/login", WaitUntil::DomContentLoaded, Duration::from_secs(30), ).await?; }
See the Sticky Sessions chapter for the full
policy matrix and VendorStickinessMap builder API.
Network-identity coherence check (coherence-validation feature)
CoherenceValidator is a Box<dyn CoherencePort> that gates acquisition
on the WebRTC + DNS + timezone + locale + Accept-Language five-vector
match. It is composed onto a manager via the builder:
#![allow(unused)] fn main() { use std::sync::Arc; use stygian_proxy::{ CoherenceContext, CoherencePolicy, CoherenceValidator, MemoryProxyStore, MismatchField, ProxyConfig, ProxyManager, }; let storage = Arc::new(MemoryProxyStore::default()); let mgr = ProxyManager::with_round_robin(storage, ProxyConfig::default())? .builder() .coherence_validator(Arc::new(CoherenceValidator::default())) .build()?; }
At acquire time:
#![allow(unused)] fn main() { let ctx = CoherenceContext::from_browser_page(&page).await?; let policy = CoherencePolicy::hard_fail_on(MismatchField::WebRtcPublicIp); let handle = mgr.acquire_proxy_with_coherence(&ctx, &policy).await?; }
policy.advisory() (the default) logs mismatches at tracing::warn! and
still issues the proxy. policy.hard_fail_on(field) upgrades the
chosen field to a hard reject — the call returns
ProxyError::CoherenceMismatch { field, observed, expected } and no
proxy is leased. Zero allocation per call once the CoherenceContext
is built; safe on the hot path.
Thompson-sampling rotation wiring
When the bayesian-rotation feature is enabled, the manager is built
with ThompsonStrategy and observers are fed by the existing
ProxyHandle::mark_success / drop-failure path — no separate observer
call is required at the call site. To seed the bandit from a known-good
feed, use strategy_warmup_observe:
#![allow(unused)] fn main() { use std::sync::Arc; use std::time::Duration; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; let storage = Arc::new(MemoryProxyStore::default()); let mgr = ProxyManager::with_thompson_sampling( storage, ProxyConfig::default(), Duration::from_secs(300), // decay_interval — defaults to 5 min )?; // Seed the bandit from a known-good feed so cold-start traffic // is already informed. mgr.strategy_warmup_observe(proxy_id_a, true).await; mgr.strategy_warmup_observe(proxy_id_b, false).await; }
strategy_warmup_observe is fire-and-forget — the bandit reads from
these observations on its next acquire. In production you typically
seed at startup from a curated trust list and let live traffic update
the posterior.
Ingest with metadata
add_proxy_with_metadata is a convenience constructor that validates
the URL against vendor_quirks::check and accepts (url, asn, city, postal_code) directly without a Proxy struct literal:
#![allow(unused)] fn main() { use stygian_proxy::types::well_known; use stygian_proxy::{MemoryProxyStore, ProxyConfig, ProxyManager}; use std::sync::Arc; let storage = Arc::new(MemoryProxyStore::default()); let mgr = ProxyManager::with_round_robin(storage, ProxyConfig::default())?; mgr.add_proxy_with_metadata( "http://user:pass@edge1.example.com:8080".into(), well_known::KNOWN_ASN_AKAMAI, // 20_940 "Cambridge".into(), "02142".into(), ).await?; }
The metadata is stored on the underlying Proxy's capabilities.{asn, city, postal_code} fields and participates in
CapabilityRequirement::require_asn / require_city /
require_postal_code filters at acquire time.
Vendor-quirk ingest validation
Provider-specific URL traps (Crawlera / Zyte port 8011 plain-HTTP
errors, Bright Data / IPRoyal username-format warnings) surface late —
deep in the TLS handshake or on the first request — without warning.
vendor_quirks::check validates at ingest:
#![allow(unused)] fn main() { use stygian_proxy::vendor_quirks::{check, ProxyUrl, QuirkSeverity}; let url = ProxyUrl::parse("http://proxy.crawlera.com:8011").unwrap(); for m in check(&url) { match m.severity { QuirkSeverity::Error => { return Err(format!("rejected: {}", m.description).into()); } QuirkSeverity::Warning => tracing::warn!(?m, "vendor-quirk"), QuirkSeverity::Info => tracing::info!(?m, "vendor-quirk"), } } }
ProxyManager::add_proxy_with_metadata and the free-list fetchers
internally call this validation; if you build your own ingest
pipeline, call it before add_proxy. Error-severity quirks should
reject the proxy outright; warning-severity quirks should be logged
and accepted.