use std::sync::Arc;
use http::HeaderMap;
use crate::queue::Job;
use crate::runner::{
ChallengeDetector, ChallengeSignal, Fetcher, RetryDecision, RetryReason, SessionContext,
SpoofFetcher,
};
use crate::Result;
#[cfg(feature = "cdp-backend")]
use crate::runner::RenderFetcher;
#[deprecated(
since = "1.0.5",
note = "ADR-0004: AutoFetcher is dead code; policy::engine remains the auto-escalation arbiter. Removed in B15."
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AutoOutcome {
UseSpoofResponse,
EscalateToRender { signal: ChallengeSignal },
}
impl AutoOutcome {
pub fn into_retry_decision(self) -> RetryDecision {
match self {
AutoOutcome::UseSpoofResponse => RetryDecision::None,
AutoOutcome::EscalateToRender { .. } => RetryDecision::Suggest {
reason: RetryReason::EscalateToRender,
backoff_hint: None,
},
}
}
}
#[deprecated(
since = "1.0.5",
note = "ADR-0004: AutoFetcher is dead code; policy::engine remains the auto-escalation arbiter. Removed in B15."
)]
pub struct AutoFetcher {
spoof: Arc<SpoofFetcher>,
#[cfg(feature = "cdp-backend")]
render: Arc<RenderFetcher>,
detector: ChallengeDetector,
}
impl AutoFetcher {
#[cfg(feature = "cdp-backend")]
pub fn new(spoof: Arc<SpoofFetcher>, render: Arc<RenderFetcher>) -> Self {
Self {
spoof,
render,
detector: ChallengeDetector::new(),
}
}
#[cfg(not(feature = "cdp-backend"))]
pub fn new(spoof: Arc<SpoofFetcher>) -> Self {
Self {
spoof,
detector: ChallengeDetector::new(),
}
}
pub fn spoof(&self) -> &Arc<SpoofFetcher> {
&self.spoof
}
#[cfg(feature = "cdp-backend")]
pub fn render(&self) -> &Arc<RenderFetcher> {
&self.render
}
pub fn decide_after_spoof(&self, status: u16, headers: &HeaderMap, body: &[u8]) -> AutoOutcome {
match self.detector.detect(status, headers, body) {
Some(signal) => AutoOutcome::EscalateToRender { signal },
None => AutoOutcome::UseSpoofResponse,
}
}
}
#[async_trait::async_trait]
impl Fetcher for AutoFetcher {
async fn fetch(&self, job: &Job, ctx: &SessionContext) -> Result<crate::runner::FetchOutput> {
self.spoof.fetch(job, ctx).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::AntibotVendor;
fn html_headers() -> HeaderMap {
let mut h = HeaderMap::new();
h.insert("content-type", "text/html; charset=utf-8".parse().unwrap());
h
}
fn build_auto() -> AutoFetcher {
let client = Arc::new(
crate::impersonate::ImpersonateClient::new(
crate::impersonate::Profile::Chrome131Stable,
)
.expect("client"),
);
let spoof = Arc::new(SpoofFetcher::new(client));
#[cfg(feature = "cdp-backend")]
{
let cfg = Arc::new(crate::config::Config::default());
let storage: Arc<dyn crate::storage::Storage> =
Arc::new(crate::storage::memory::MemoryStorage::default());
let pool = Arc::new(crate::render::pool::RenderPool::new(cfg, storage));
let render = Arc::new(RenderFetcher::new(pool));
AutoFetcher::new(spoof, render)
}
#[cfg(not(feature = "cdp-backend"))]
{
AutoFetcher::new(spoof)
}
}
#[test]
fn challenge_response_triggers_escalate_to_render() {
let auto = build_auto();
let outcome = auto.decide_after_spoof(403, &html_headers(), b"cf-chl-bypass");
match outcome {
AutoOutcome::EscalateToRender { signal } => {
assert_eq!(signal.vendor, AntibotVendor::Cloudflare);
}
other => panic!("expected EscalateToRender, got {other:?}"),
}
}
#[test]
fn healthy_response_uses_spoof() {
let auto = build_auto();
let body = b"<html><body><h1>real content</h1><p>plenty of text here</p></body></html>";
assert_eq!(
auto.decide_after_spoof(200, &html_headers(), body),
AutoOutcome::UseSpoofResponse
);
}
#[test]
fn escalation_projects_to_retry_decision_with_escalate_reason() {
let signal = ChallengeSignal {
vendor: AntibotVendor::Cloudflare,
};
let decision = AutoOutcome::EscalateToRender { signal }.into_retry_decision();
match decision {
RetryDecision::Suggest {
reason,
backoff_hint,
} => {
assert_eq!(reason, RetryReason::EscalateToRender);
assert_eq!(backoff_hint, None);
}
_ => panic!("expected Suggest"),
}
}
#[test]
fn use_spoof_projects_to_retry_none() {
assert!(matches!(
AutoOutcome::UseSpoofResponse.into_retry_decision(),
RetryDecision::None
));
}
#[test]
fn auto_fetcher_does_not_chain_render_inline() {
let auto = build_auto();
let _ = auto.decide_after_spoof(403, &html_headers(), b"cf-chl-bypass");
#[cfg(feature = "cdp-backend")]
{
let _ = auto.render(); }
}
#[test]
fn auto_fetcher_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AutoFetcher>();
}
}