1use crate::compact_str::CompactString;
2use crate::features::chrome_common::RequestInterceptConfiguration;
3pub use crate::features::chrome_common::{
4 AuthChallengeResponse, AuthChallengeResponseResponse, AutomationScripts, AutomationScriptsMap,
5 CaptureScreenshotFormat, CaptureScreenshotParams, ClipViewport, ExecutionScripts,
6 ExecutionScriptsMap, ScreenShotConfig, ScreenshotParams, Viewport, WaitFor, WaitForDelay,
7 WaitForIdleNetwork, WaitForSelector, WebAutomation,
8};
9pub use crate::features::gemini_common::GeminiConfigs;
10pub use crate::features::openai_common::GPTConfigs;
11#[cfg(feature = "search")]
12pub use crate::features::search::{
13 SearchError, SearchOptions, SearchResult, SearchResults, TimeRange,
14};
15pub use crate::features::webdriver_common::{WebDriverBrowser, WebDriverConfig};
16use crate::utils::get_domain_from_url;
17use crate::utils::BasicCachePolicy;
18use crate::website::CronType;
19use reqwest::header::{AsHeaderName, HeaderMap, HeaderName, HeaderValue, IntoHeaderName};
20use std::net::IpAddr;
21use std::sync::Arc;
22use std::time::Duration;
23
24#[cfg(feature = "chrome")]
25pub use spider_fingerprint::Fingerprint;
26
27pub fn is_placeholder_api_key(key: &str) -> bool {
29 let trimmed = key.trim();
30 trimmed.is_empty()
31 || trimmed.eq_ignore_ascii_case("YOUR_API_KEY")
32 || trimmed.eq_ignore_ascii_case("YOUR-API-KEY")
33 || trimmed.eq_ignore_ascii_case("API_KEY")
34 || trimmed.eq_ignore_ascii_case("API-KEY")
35}
36
37#[derive(Debug, Default, Clone, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum RedirectPolicy {
41 #[default]
42 #[cfg_attr(
43 feature = "serde",
44 serde(alias = "Loose", alias = "loose", alias = "LOOSE",)
45 )]
46 Loose,
48 #[cfg_attr(
49 feature = "serde",
50 serde(alias = "Strict", alias = "strict", alias = "STRICT",)
51 )]
52 Strict,
54 #[cfg_attr(
55 feature = "serde",
56 serde(alias = "None", alias = "none", alias = "NONE",)
57 )]
58 None,
60}
61
62#[cfg(not(feature = "regex"))]
63pub type AllowList = Vec<CompactString>;
65
66#[cfg(feature = "regex")]
67pub type AllowList = Box<regex::RegexSet>;
69
70#[derive(Debug, Default, Clone)]
72#[cfg_attr(not(feature = "regex"), derive(PartialEq, Eq))]
73pub struct AllowListSet(pub AllowList);
74
75#[cfg(feature = "chrome")]
76#[derive(Debug, PartialEq, Eq, Clone, Default)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct ChromeEventTracker {
80 pub responses: bool,
82 pub requests: bool,
84 pub automation: bool,
86}
87
88#[cfg(feature = "chrome")]
89impl ChromeEventTracker {
90 pub fn new(requests: bool, responses: bool) -> Self {
92 ChromeEventTracker {
93 requests,
94 responses,
95 automation: true,
96 }
97 }
98}
99
100#[cfg(feature = "sitemap")]
101#[derive(Debug, Default)]
102pub struct SitemapWhitelistChanges {
104 pub added_default: bool,
106 pub added_custom: bool,
108}
109
110#[cfg(feature = "sitemap")]
111impl SitemapWhitelistChanges {
112 pub(crate) fn modified(&self) -> bool {
114 self.added_default || self.added_custom
115 }
116}
117
118#[derive(Debug, Default, Clone, PartialEq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub enum ProxyIgnore {
122 Chrome,
124 Http,
126 #[default]
127 No,
129}
130
131#[derive(Debug, Default, Clone, PartialEq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct RequestProxy {
135 pub addr: String,
137 pub ignore: ProxyIgnore,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Hash)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160pub enum ProxyKind {
161 Default,
164 MediaAsset,
169 Custom(CompactString),
171}
172
173impl Default for ProxyKind {
174 #[inline]
175 fn default() -> Self {
176 ProxyKind::Default
177 }
178}
179
180#[cfg(feature = "parallel_backends")]
182#[derive(Debug, Clone, PartialEq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184pub enum BackendProtocol {
185 Cdp,
187 WebDriver,
189}
190
191#[cfg(feature = "parallel_backends")]
193#[derive(Debug, Default, Clone, PartialEq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195pub enum BackendEngine {
196 #[default]
197 Cdp,
199 Servo,
201 Custom,
204}
205
206#[cfg(feature = "parallel_backends")]
212#[derive(Debug, Default, Clone, PartialEq)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214#[cfg_attr(feature = "serde", serde(default))]
215pub struct BackendEndpoint {
216 pub engine: BackendEngine,
218 pub endpoint: Option<String>,
223 pub binary_path: Option<String>,
227 pub protocol: Option<BackendProtocol>,
231 pub proxy: Option<String>,
237}
238
239#[cfg(feature = "parallel_backends")]
244#[derive(Debug, Clone, PartialEq)]
245#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
246#[cfg_attr(feature = "serde", serde(default))]
247pub struct ParallelBackendsConfig {
248 pub backends: Vec<BackendEndpoint>,
250 pub grace_period_ms: u64,
254 pub enabled: bool,
256 pub fast_accept_threshold: u16,
260 pub max_consecutive_errors: u16,
263 pub connect_timeout_ms: u64,
267 pub skip_binary_content_types: bool,
272 pub max_concurrent_sessions: usize,
275 pub skip_extensions: Vec<CompactString>,
279 pub max_backend_bytes_in_flight: usize,
285 pub backend_timeout_ms: u64,
291}
292
293#[cfg(feature = "parallel_backends")]
294impl Default for ParallelBackendsConfig {
295 fn default() -> Self {
296 Self {
297 backends: Vec::new(),
298 grace_period_ms: 500,
299 enabled: true,
300 fast_accept_threshold: 80,
301 max_consecutive_errors: 10,
302 connect_timeout_ms: 5000,
303 skip_binary_content_types: true,
304 max_concurrent_sessions: 8,
305 skip_extensions: Vec::new(),
306 max_backend_bytes_in_flight: 256 * 1024 * 1024, backend_timeout_ms: 30_000,
308 }
309 }
310}
311
312#[derive(Debug, Default, Clone, PartialEq, Eq)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315#[cfg_attr(feature = "serde", serde(default))]
316pub struct CustomAntibotPatterns {
317 pub body: Vec<CompactString>,
319 pub url: Vec<CompactString>,
321 pub header_keys: Vec<CompactString>,
323}
324
325#[derive(Debug, Default, Clone)]
335#[cfg_attr(
336 all(
337 not(feature = "regex"),
338 not(feature = "openai"),
339 not(feature = "cache_openai"),
340 not(feature = "gemini"),
341 not(feature = "cache_gemini")
342 ),
343 derive(PartialEq)
344)]
345#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
346#[cfg_attr(feature = "serde", serde(default))]
347pub struct Configuration {
348 pub respect_robots_txt: bool,
350 pub subdomains: bool,
352 pub tld: bool,
354 pub crawl_timeout: Option<Duration>,
356 pub preserve_host_header: bool,
358 pub blacklist_url: Option<Vec<CompactString>>,
360 pub whitelist_url: Option<Vec<CompactString>>,
362 pub user_agent: Option<Box<CompactString>>,
364 pub delay: u64,
366 pub request_timeout: Option<Duration>,
368 pub http2_prior_knowledge: bool,
370 pub proxies: Option<Vec<RequestProxy>>,
372 pub proxies_by_kind: Option<hashbrown::HashMap<ProxyKind, Vec<RequestProxy>>>,
387 pub headers: Option<Box<SerializableHeaderMap>>,
389 #[cfg(feature = "sitemap")]
390 pub sitemap_url: Option<Box<CompactString>>,
392 #[cfg(feature = "sitemap")]
393 pub ignore_sitemap: bool,
395 pub redirect_limit: usize,
397 pub redirect_policy: RedirectPolicy,
399 #[cfg_attr(feature = "serde", serde(skip))]
406 pub redirect_limit_set: bool,
407 pub max_main_frame_navigations: Option<u32>,
417 #[cfg(feature = "cookies")]
418 pub cookie_str: String,
420 #[cfg(feature = "wreq")]
421 pub emulation: Option<wreq_util::Emulation>,
423 #[cfg(feature = "cron")]
424 pub cron_str: String,
426 #[cfg(feature = "cron")]
427 pub cron_type: CronType,
429 pub depth: usize,
431 pub depth_distance: usize,
433 pub stealth_mode: spider_fingerprint::configs::Tier,
435 pub viewport: Option<Viewport>,
437 pub budget: Option<hashbrown::HashMap<case_insensitive_string::CaseInsensitiveString, u32>>,
439 pub wild_card_budgeting: bool,
441 pub external_domains_caseless:
443 Arc<hashbrown::HashSet<case_insensitive_string::CaseInsensitiveString>>,
444 pub full_resources: bool,
446 pub accept_invalid_certs: bool,
448 pub auth_challenge_response: Option<AuthChallengeResponse>,
450 pub openai_config: Option<Box<GPTConfigs>>,
452 pub gemini_config: Option<Box<GeminiConfigs>>,
454 pub remote_multimodal: Option<Box<crate::features::automation::RemoteMultimodalConfigs>>,
457 pub shared_queue: bool,
459 pub return_page_links: bool,
461 pub retry: u8,
463 pub custom_antibot: Option<CustomAntibotPatterns>,
466 pub no_control_thread: bool,
468 blacklist: AllowListSet,
470 whitelist: AllowListSet,
472 pub(crate) inner_budget:
474 Option<hashbrown::HashMap<case_insensitive_string::CaseInsensitiveString, u32>>,
475 pub only_html: bool,
477 pub concurrency_limit: Option<usize>,
479 pub normalize: bool,
481 pub shared: bool,
483 pub modify_headers: bool,
485 pub modify_http_client_headers: bool,
487 #[cfg(any(
489 feature = "cache_request",
490 feature = "chrome",
491 feature = "chrome_remote_cache"
492 ))]
493 pub cache: bool,
494 #[cfg(any(
497 feature = "cache_request",
498 feature = "chrome",
499 feature = "chrome_remote_cache"
500 ))]
501 pub cache_skip_browser: bool,
502 pub cache_namespace: Option<Box<String>>,
509 #[cfg(feature = "chrome_remote_cache")]
517 pub chrome_remote_cache_read_only: bool,
518 #[cfg(feature = "chrome_remote_cache")]
526 pub remote_cache_skip_browser: bool,
527 #[cfg(feature = "chrome_remote_cache")]
539 pub chrome_remote_cache_main_doc_only: bool,
540 #[cfg(feature = "chrome")]
541 pub service_worker_enabled: bool,
543 #[cfg(feature = "chrome")]
544 #[cfg(feature = "chrome")]
546 pub timezone_id: Option<Box<String>>,
547 #[cfg(feature = "chrome")]
549 pub locale: Option<Box<String>>,
550 #[cfg(feature = "chrome")]
552 pub evaluate_on_new_document: Option<Box<String>>,
553 #[cfg(feature = "chrome")]
554 pub dismiss_dialogs: Option<bool>,
556 #[cfg(feature = "chrome")]
557 pub prefer_native_markdown: bool,
567 #[cfg(feature = "chrome")]
568 pub wait_for: Option<WaitFor>,
570 #[cfg(feature = "chrome")]
571 pub screenshot: Option<ScreenShotConfig>,
573 #[cfg(feature = "chrome")]
574 pub track_events: Option<ChromeEventTracker>,
576 #[cfg(feature = "chrome")]
577 pub fingerprint: Fingerprint,
579 #[cfg(feature = "chrome")]
580 pub chrome_connection_url: Option<String>,
582 #[cfg(feature = "chrome")]
583 pub chrome_connection_urls: Option<Vec<String>>,
587 #[cfg(feature = "chrome")]
588 #[cfg_attr(feature = "serde", serde(skip))]
589 pub(crate) chrome_failover: crate::features::chrome::LazyChromeFailover,
593 #[cfg(feature = "chrome")]
594 pub chrome_first_byte_timeout: Option<Duration>,
603 #[cfg(feature = "chrome")]
604 pub chrome_first_byte_timeout_jitter: Option<Duration>,
612 pub http_first_byte_timeout: Option<Duration>,
624 pub http_first_byte_timeout_jitter: Option<Duration>,
629 #[cfg(feature = "chrome")]
631 pub execution_scripts: Option<ExecutionScripts>,
632 #[cfg(feature = "chrome")]
634 pub automation_scripts: Option<AutomationScripts>,
635 #[cfg(feature = "chrome")]
637 pub chrome_intercept: RequestInterceptConfiguration,
638 pub referer: Option<String>,
640 pub max_page_bytes: Option<f64>,
642 pub max_bytes_allowed: Option<u64>,
644 #[cfg(feature = "chrome")]
645 pub disable_log: bool,
647 #[cfg(feature = "chrome")]
648 pub auto_geolocation: bool,
650 pub cache_policy: Option<BasicCachePolicy>,
652 #[cfg(feature = "chrome")]
653 pub bypass_csp: bool,
655 #[cfg(feature = "chrome")]
656 pub disable_javascript: bool,
658 pub network_interface: Option<String>,
660 pub local_address: Option<IpAddr>,
662 pub default_http_connect_timeout: Option<Duration>,
664 pub default_http_read_timeout: Option<Duration>,
666 #[cfg(feature = "webdriver")]
667 pub webdriver_config: Option<Box<WebDriverConfig>>,
669 #[cfg(feature = "search")]
670 pub search_config: Option<Box<SearchConfig>>,
672 #[cfg(feature = "spider_cloud")]
673 pub spider_cloud: Option<Box<SpiderCloudConfig>>,
675 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
676 pub spider_browser: Option<Box<SpiderBrowserConfig>>,
678 #[cfg(feature = "hedge")]
679 pub hedge: Option<crate::utils::hedge::HedgeConfig>,
682 #[cfg(feature = "auto_throttle")]
683 pub auto_throttle: Option<crate::utils::auto_throttle::AutoThrottleConfig>,
686 #[cfg(feature = "etag_cache")]
687 pub etag_cache: bool,
692 #[cfg(feature = "warc")]
693 pub warc: Option<crate::utils::warc::WarcConfig>,
696 #[cfg(feature = "parallel_backends")]
697 pub parallel_backends: Option<ParallelBackendsConfig>,
700 pub enhancements: EnhancementSettings,
707 #[cfg(feature = "decentralized")]
708 pub worker_connection_urls: Option<Vec<String>>,
713 #[cfg(feature = "decentralized")]
714 pub scraper_worker_connection_urls: Option<Vec<String>>,
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq)]
730#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
731pub enum CrawlEnhancement {
732 RenderUpgrade = 0,
736 PointerAssist = 1,
739 DnsGuard = 2,
742 GatewayReacquire = 3,
745 DnsHedge = 4,
748}
749
750impl CrawlEnhancement {
751 pub const COUNT: usize = 5;
753
754 pub const ALL: [CrawlEnhancement; Self::COUNT] = [
756 CrawlEnhancement::RenderUpgrade,
757 CrawlEnhancement::PointerAssist,
758 CrawlEnhancement::DnsGuard,
759 CrawlEnhancement::GatewayReacquire,
760 CrawlEnhancement::DnsHedge,
761 ];
762
763 #[cfg(feature = "chrome")]
770 #[inline]
771 fn env_defaults() -> [bool; Self::COUNT] {
772 static CELL: std::sync::OnceLock<[bool; CrawlEnhancement::COUNT]> =
773 std::sync::OnceLock::new();
774 *CELL.get_or_init(|| {
775 let env = |k: &str| crate::utils::opt_out_flag(std::env::var(k).ok().as_deref());
776 [
777 env("SPIDER_CHROME_RENDER_UPGRADE"),
778 env("SPIDER_CHROME_POINTER_ASSIST"),
779 env("SPIDER_CHROME_DNS_GUARD"),
780 env("SPIDER_CHROME_REACQUIRE"),
781 true,
782 ]
783 })
784 }
785
786 #[cfg(feature = "chrome")]
788 #[inline]
789 pub(crate) fn env_default(self) -> bool {
790 Self::env_defaults()[self as usize]
791 }
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Eq)]
805#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
806pub struct EnhancementSettings {
807 overrides: [Option<bool>; CrawlEnhancement::COUNT],
808}
809
810impl Default for EnhancementSettings {
811 #[inline]
812 fn default() -> Self {
813 Self::new()
814 }
815}
816
817impl EnhancementSettings {
818 #[inline]
820 pub const fn new() -> Self {
821 Self {
822 overrides: [None; CrawlEnhancement::COUNT],
823 }
824 }
825
826 #[inline]
829 pub const fn all_off() -> Self {
830 Self {
831 overrides: [Some(false); CrawlEnhancement::COUNT],
832 }
833 }
834
835 #[inline]
837 pub fn set(&mut self, section: CrawlEnhancement, enabled: bool) -> &mut Self {
838 self.overrides[section as usize] = Some(enabled);
839 self
840 }
841
842 #[inline]
844 pub fn set_all(&mut self, enabled: bool) -> &mut Self {
845 self.overrides = [Some(enabled); CrawlEnhancement::COUNT];
846 self
847 }
848
849 #[inline]
851 pub fn clear(&mut self, section: CrawlEnhancement) -> &mut Self {
852 self.overrides[section as usize] = None;
853 self
854 }
855
856 #[inline]
858 pub fn get(&self, section: CrawlEnhancement) -> Option<bool> {
859 self.overrides[section as usize]
860 }
861
862 #[inline]
864 pub fn is_customized(&self) -> bool {
865 self.overrides.iter().any(Option::is_some)
866 }
867
868 #[cfg(feature = "chrome")]
872 #[inline]
873 pub(crate) fn enabled(&self, section: CrawlEnhancement) -> bool {
874 match self.overrides[section as usize] {
875 Some(v) => v,
876 None => section.env_default(),
877 }
878 }
879}
880
881#[derive(Default, Debug, Clone, PartialEq, Eq)]
882pub struct SerializableHeaderMap(pub HeaderMap);
884
885impl SerializableHeaderMap {
886 pub fn inner(&self) -> &HeaderMap {
888 &self.0
889 }
890 pub fn contains_key<K>(&self, key: K) -> bool
892 where
893 K: AsHeaderName,
894 {
895 self.0.contains_key(key)
896 }
897 pub fn insert<K>(
899 &mut self,
900 key: K,
901 val: reqwest::header::HeaderValue,
902 ) -> Option<reqwest::header::HeaderValue>
903 where
904 K: IntoHeaderName,
905 {
906 self.0.insert(key, val)
907 }
908 pub fn extend<I>(&mut self, iter: I)
910 where
911 I: IntoIterator<Item = (Option<HeaderName>, HeaderValue)>,
912 {
913 self.0.extend(iter);
914 }
915}
916
917pub fn get_referer(header_map: &Option<Box<SerializableHeaderMap>>) -> Option<String> {
919 match header_map {
920 Some(header_map) => {
921 header_map
922 .0
923 .get(crate::client::header::REFERER) .and_then(|value| value.to_str().ok()) .map(String::from) }
927 _ => None,
928 }
929}
930
931impl From<HeaderMap> for SerializableHeaderMap {
932 fn from(header_map: HeaderMap) -> Self {
933 SerializableHeaderMap(header_map)
934 }
935}
936
937#[cfg(feature = "serde")]
938impl serde::Serialize for SerializableHeaderMap {
939 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
940 where
941 S: serde::Serializer,
942 {
943 let map: std::collections::BTreeMap<String, String> = self
944 .0
945 .iter()
946 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
947 .collect();
948 map.serialize(serializer)
949 }
950}
951
952#[cfg(feature = "serde")]
953impl<'de> serde::Deserialize<'de> for SerializableHeaderMap {
954 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
955 where
956 D: serde::Deserializer<'de>,
957 {
958 use reqwest::header::{HeaderName, HeaderValue};
959 use std::collections::BTreeMap;
960 let map: BTreeMap<String, String> = BTreeMap::deserialize(deserializer)?;
961 let mut headers = HeaderMap::with_capacity(map.len());
962 for (k, v) in map {
963 let key = HeaderName::from_bytes(k.as_bytes()).map_err(serde::de::Error::custom)?;
964 let value = HeaderValue::from_str(&v).map_err(serde::de::Error::custom)?;
965 headers.insert(key, value);
966 }
967 Ok(SerializableHeaderMap(headers))
968 }
969}
970
971#[cfg(feature = "serde")]
972impl serde::Serialize for AllowListSet {
973 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
974 where
975 S: serde::Serializer,
976 {
977 #[cfg(not(feature = "regex"))]
978 {
979 self.0.serialize(serializer)
980 }
981
982 #[cfg(feature = "regex")]
983 {
984 self.0
985 .patterns()
986 .iter()
987 .collect::<Vec<&String>>()
988 .serialize(serializer)
989 }
990 }
991}
992
993#[cfg(feature = "serde")]
994impl<'de> serde::Deserialize<'de> for AllowListSet {
995 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
996 where
997 D: serde::Deserializer<'de>,
998 {
999 #[cfg(not(feature = "regex"))]
1000 {
1001 let vec = Vec::<CompactString>::deserialize(deserializer)?;
1002 Ok(AllowListSet(vec))
1003 }
1004
1005 #[cfg(feature = "regex")]
1006 {
1007 let patterns = Vec::<String>::deserialize(deserializer)?;
1008 let regex_set = regex::RegexSet::new(&patterns).map_err(serde::de::Error::custom)?;
1009 Ok(AllowListSet(regex_set.into()))
1010 }
1011 }
1012}
1013
1014#[cfg(feature = "ua_generator")]
1016pub fn get_ua(chrome: bool) -> &'static str {
1017 if chrome {
1018 ua_generator::ua::spoof_chrome_ua()
1019 } else {
1020 ua_generator::ua::spoof_ua()
1021 }
1022}
1023
1024#[cfg(not(feature = "ua_generator"))]
1026pub fn get_ua(_chrome: bool) -> &'static str {
1027 use std::env;
1028
1029 lazy_static! {
1030 static ref AGENT: &'static str =
1031 concat!(env!("CARGO_PKG_NAME"), '/', env!("CARGO_PKG_VERSION"));
1032 };
1033
1034 AGENT.as_ref()
1035}
1036
1037impl Configuration {
1038 #[cfg(not(feature = "chrome"))]
1040 pub fn new() -> Self {
1041 Self {
1042 delay: 0,
1043 depth: 25,
1044 redirect_limit: 7,
1045 request_timeout: Some(Duration::from_secs(120)),
1046 only_html: true,
1047 modify_headers: true,
1048 ..Default::default()
1049 }
1050 }
1051
1052 #[cfg(feature = "chrome")]
1054 pub fn new() -> Self {
1055 Self {
1056 delay: 0,
1057 depth: 25,
1058 redirect_limit: 7,
1059 request_timeout: Some(Duration::from_secs(120)),
1060 chrome_intercept: RequestInterceptConfiguration::new(cfg!(
1061 feature = "chrome_intercept"
1062 )),
1063 user_agent: Some(Box::new(get_ua(true).into())),
1064 only_html: true,
1065 cache: true,
1066 modify_headers: true,
1067 service_worker_enabled: true,
1068 fingerprint: Fingerprint::Basic,
1069 auto_geolocation: false,
1070 ..Default::default()
1071 }
1072 }
1073
1074 #[cfg(feature = "agent")]
1077 pub fn build_remote_multimodal_engine(
1078 &self,
1079 ) -> Option<crate::features::automation::RemoteMultimodalEngine> {
1080 let cfgs = self.remote_multimodal.as_ref()?;
1081 let sem = cfgs
1082 .concurrency_limit
1083 .filter(|&n| n > 0)
1084 .map(|n| std::sync::Arc::new(tokio::sync::Semaphore::new(n)));
1085
1086 #[allow(unused_mut)]
1087 let mut engine = crate::features::automation::RemoteMultimodalEngine::new(
1088 cfgs.api_url.clone(),
1089 cfgs.model_name.clone(),
1090 cfgs.system_prompt.clone(),
1091 )
1092 .with_api_key(cfgs.api_key.as_deref())
1093 .with_system_prompt_extra(cfgs.system_prompt_extra.as_deref())
1094 .with_user_message_extra(cfgs.user_message_extra.as_deref())
1095 .with_remote_multimodal_config(cfgs.cfg.clone())
1096 .with_prompt_url_gate(cfgs.prompt_url_gate.clone())
1097 .with_vision_model(cfgs.vision_model.clone())
1098 .with_text_model(cfgs.text_model.clone())
1099 .with_vision_route_mode(cfgs.vision_route_mode)
1100 .with_chrome_ai(cfgs.use_chrome_ai)
1101 .with_semaphore(sem)
1102 .to_owned();
1103
1104 #[cfg(feature = "agent_skills")]
1105 if let Some(ref registry) = cfgs.skill_registry {
1106 engine.with_skill_registry(Some(registry.clone()));
1107 }
1108
1109 let model_pool = cfgs.model_pool.clone();
1111 if model_pool.len() >= 3 {
1112 let model_names: Vec<&str> =
1113 model_pool.iter().map(|ep| ep.model_name.as_str()).collect();
1114 let policy = crate::features::automation::auto_policy(&model_names);
1115 engine.model_router = Some(crate::features::automation::ModelRouter::with_policy(
1116 policy,
1117 ));
1118 }
1119 engine.model_pool = model_pool;
1120
1121 Some(engine)
1122 }
1123
1124 #[cfg(not(feature = "chrome"))]
1126 pub(crate) fn only_chrome_agent(&self) -> bool {
1127 false
1128 }
1129
1130 #[cfg(feature = "chrome")]
1132 pub(crate) fn only_chrome_agent(&self) -> bool {
1133 self.chrome_connection_url.is_some()
1134 || self.wait_for.is_some()
1135 || self.chrome_intercept.enabled
1136 || self.stealth_mode.stealth()
1137 || self.fingerprint.valid()
1138 }
1139
1140 #[cfg(feature = "regex")]
1141 pub fn get_blacklist(&self) -> Box<regex::RegexSet> {
1143 match &self.blacklist_url {
1144 Some(blacklist) => match regex::RegexSet::new(&**blacklist) {
1145 Ok(s) => Box::new(s),
1146 _ => Default::default(),
1147 },
1148 _ => Default::default(),
1149 }
1150 }
1151
1152 #[cfg(not(feature = "regex"))]
1153 pub fn get_blacklist(&self) -> AllowList {
1155 match &self.blacklist_url {
1156 Some(blacklist) => blacklist.to_owned(),
1157 _ => Default::default(),
1158 }
1159 }
1160
1161 pub(crate) fn set_blacklist(&mut self) {
1163 self.blacklist = AllowListSet(self.get_blacklist());
1164 }
1165
1166 pub fn set_whitelist(&mut self) {
1168 self.whitelist = AllowListSet(self.get_whitelist());
1169 }
1170
1171 pub fn configure_allowlist(&mut self) {
1173 self.set_whitelist();
1174 self.set_blacklist();
1175 }
1176
1177 pub fn get_blacklist_compiled(&self) -> &AllowList {
1179 &self.blacklist.0
1180 }
1181
1182 pub fn configure_budget(&mut self) {
1184 self.inner_budget.clone_from(&self.budget);
1185 }
1186
1187 pub fn get_whitelist_compiled(&self) -> &AllowList {
1189 &self.whitelist.0
1190 }
1191
1192 #[cfg(feature = "regex")]
1193 pub fn get_whitelist(&self) -> Box<regex::RegexSet> {
1195 match &self.whitelist_url {
1196 Some(whitelist) => match regex::RegexSet::new(&**whitelist) {
1197 Ok(s) => Box::new(s),
1198 _ => Default::default(),
1199 },
1200 _ => Default::default(),
1201 }
1202 }
1203
1204 #[cfg(not(feature = "regex"))]
1205 pub fn get_whitelist(&self) -> AllowList {
1207 match &self.whitelist_url {
1208 Some(whitelist) => whitelist.to_owned(),
1209 _ => Default::default(),
1210 }
1211 }
1212
1213 #[cfg(feature = "sitemap")]
1214 pub fn add_sitemap_to_whitelist(&mut self) -> SitemapWhitelistChanges {
1216 let mut changes = SitemapWhitelistChanges::default();
1217
1218 if self.ignore_sitemap && self.whitelist_url.is_none() {
1219 return changes;
1220 }
1221
1222 if let Some(list) = self.whitelist_url.as_mut() {
1223 if list.is_empty() {
1224 return changes;
1225 }
1226
1227 let default = CompactString::from("sitemap.xml");
1228
1229 if !list.contains(&default) {
1230 list.push(default);
1231 changes.added_default = true;
1232 }
1233
1234 if let Some(custom) = &self.sitemap_url {
1235 if !list.contains(custom) {
1236 list.push((**custom).clone());
1239 changes.added_custom = true;
1240 }
1241 }
1242 }
1243
1244 changes
1245 }
1246
1247 #[cfg(feature = "sitemap")]
1248 pub fn remove_sitemap_from_whitelist(&mut self, changes: SitemapWhitelistChanges) {
1250 if let Some(list) = self.whitelist_url.as_mut() {
1251 if changes.added_default {
1252 let default = CompactString::from("sitemap.xml");
1253 if let Some(pos) = list.iter().position(|s| s == default) {
1254 list.remove(pos);
1255 }
1256 }
1257 if changes.added_custom {
1258 if let Some(custom) = &self.sitemap_url {
1259 if let Some(pos) = list.iter().position(|s| *s == **custom) {
1260 list.remove(pos);
1261 }
1262 }
1263 }
1264 if list.is_empty() {
1265 self.whitelist_url = None;
1266 }
1267 }
1268 }
1269
1270 pub fn with_respect_robots_txt(&mut self, respect_robots_txt: bool) -> &mut Self {
1272 self.respect_robots_txt = respect_robots_txt;
1273 self
1274 }
1275
1276 pub fn with_subdomains(&mut self, subdomains: bool) -> &mut Self {
1278 self.subdomains = subdomains;
1279 self
1280 }
1281
1282 pub fn with_enhancement(&mut self, section: CrawlEnhancement, enabled: bool) -> &mut Self {
1285 self.enhancements.set(section, enabled);
1286 self
1287 }
1288
1289 pub fn with_all_enhancements(&mut self, enabled: bool) -> &mut Self {
1292 self.enhancements.set_all(enabled);
1293 self
1294 }
1295
1296 pub fn with_enhancements(&mut self, enhancements: EnhancementSettings) -> &mut Self {
1298 self.enhancements = enhancements;
1299 self
1300 }
1301
1302 pub fn for_builtin_browser(&mut self) -> &mut Self {
1306 self.enhancements.set_all(false);
1307 self
1308 }
1309
1310 #[cfg(feature = "chrome")]
1312 pub fn with_csp_bypass(&mut self, enabled: bool) -> &mut Self {
1313 self.bypass_csp = enabled;
1314 self
1315 }
1316
1317 #[cfg(not(feature = "chrome"))]
1319 pub fn with_csp_bypass(&mut self, _enabled: bool) -> &mut Self {
1320 self
1321 }
1322
1323 #[cfg(feature = "chrome")]
1325 pub fn with_disable_javascript(&mut self, disabled: bool) -> &mut Self {
1326 self.disable_javascript = disabled;
1327 self
1328 }
1329
1330 #[cfg(not(feature = "chrome"))]
1332 pub fn with_disable_javascript(&mut self, _disabled: bool) -> &mut Self {
1333 self
1334 }
1335
1336 pub fn with_network_interface(&mut self, network_interface: Option<String>) -> &mut Self {
1338 self.network_interface = network_interface;
1339 self
1340 }
1341
1342 pub fn with_local_address(&mut self, local_address: Option<IpAddr>) -> &mut Self {
1344 self.local_address = local_address;
1345 self
1346 }
1347
1348 pub fn with_tld(&mut self, tld: bool) -> &mut Self {
1350 self.tld = tld;
1351 self
1352 }
1353
1354 pub fn with_crawl_timeout(&mut self, crawl_timeout: Option<Duration>) -> &mut Self {
1356 self.crawl_timeout = crawl_timeout;
1357 self
1358 }
1359
1360 pub fn with_delay(&mut self, delay: u64) -> &mut Self {
1362 self.delay = delay;
1363 self
1364 }
1365
1366 pub fn with_http2_prior_knowledge(&mut self, http2_prior_knowledge: bool) -> &mut Self {
1368 self.http2_prior_knowledge = http2_prior_knowledge;
1369 self
1370 }
1371
1372 pub fn with_request_timeout(&mut self, request_timeout: Option<Duration>) -> &mut Self {
1374 match request_timeout {
1375 Some(timeout) => self.request_timeout = Some(timeout),
1376 _ => self.request_timeout = None,
1377 };
1378
1379 self
1380 }
1381
1382 #[cfg(feature = "sitemap")]
1383 pub fn with_sitemap(&mut self, sitemap_url: Option<&str>) -> &mut Self {
1385 match sitemap_url {
1386 Some(sitemap_url) => {
1387 self.sitemap_url = Some(CompactString::new(sitemap_url.to_string()).into())
1388 }
1389 _ => self.sitemap_url = None,
1390 };
1391 self
1392 }
1393
1394 #[cfg(not(feature = "sitemap"))]
1395 pub fn with_sitemap(&mut self, _sitemap_url: Option<&str>) -> &mut Self {
1397 self
1398 }
1399
1400 #[cfg(feature = "sitemap")]
1401 pub fn with_ignore_sitemap(&mut self, ignore_sitemap: bool) -> &mut Self {
1403 self.ignore_sitemap = ignore_sitemap;
1404 self
1405 }
1406
1407 #[cfg(not(feature = "sitemap"))]
1408 pub fn with_ignore_sitemap(&mut self, _ignore_sitemap: bool) -> &mut Self {
1410 self
1411 }
1412
1413 pub fn with_user_agent(&mut self, user_agent: Option<&str>) -> &mut Self {
1415 match user_agent {
1416 Some(agent) => self.user_agent = Some(CompactString::new(agent).into()),
1417 _ => self.user_agent = None,
1418 };
1419 self
1420 }
1421
1422 pub fn with_preserve_host_header(&mut self, preserve: bool) -> &mut Self {
1424 self.preserve_host_header = preserve;
1425 self
1426 }
1427
1428 #[cfg(feature = "agent")]
1431 pub fn with_remote_multimodal(
1432 &mut self,
1433 remote_multimodal: Option<crate::features::automation::RemoteMultimodalConfigs>,
1434 ) -> &mut Self {
1435 self.remote_multimodal = remote_multimodal.map(Box::new);
1436 self
1437 }
1438
1439 #[cfg(not(feature = "agent"))]
1442 pub fn with_remote_multimodal(
1443 &mut self,
1444 remote_multimodal: Option<crate::features::automation::RemoteMultimodalConfigs>,
1445 ) -> &mut Self {
1446 self.remote_multimodal = remote_multimodal.map(Box::new);
1447 self
1448 }
1449
1450 #[cfg(not(feature = "openai"))]
1451 pub fn with_openai(&mut self, _openai_config: Option<GPTConfigs>) -> &mut Self {
1453 self
1454 }
1455
1456 #[cfg(feature = "openai")]
1458 pub fn with_openai(&mut self, openai_config: Option<GPTConfigs>) -> &mut Self {
1459 match openai_config {
1460 Some(openai_config) => self.openai_config = Some(Box::new(openai_config)),
1461 _ => self.openai_config = None,
1462 };
1463 self
1464 }
1465
1466 #[cfg(not(feature = "gemini"))]
1467 pub fn with_gemini(&mut self, _gemini_config: Option<GeminiConfigs>) -> &mut Self {
1469 self
1470 }
1471
1472 #[cfg(feature = "gemini")]
1474 pub fn with_gemini(&mut self, gemini_config: Option<GeminiConfigs>) -> &mut Self {
1475 match gemini_config {
1476 Some(gemini_config) => self.gemini_config = Some(Box::new(gemini_config)),
1477 _ => self.gemini_config = None,
1478 };
1479 self
1480 }
1481
1482 #[cfg(feature = "cookies")]
1483 pub fn with_cookies(&mut self, cookie_str: &str) -> &mut Self {
1485 self.cookie_str = cookie_str.into();
1486 self
1487 }
1488
1489 #[cfg(not(feature = "cookies"))]
1490 pub fn with_cookies(&mut self, _cookie_str: &str) -> &mut Self {
1492 self
1493 }
1494
1495 #[cfg(feature = "chrome")]
1496 pub fn with_fingerprint(&mut self, fingerprint: bool) -> &mut Self {
1498 if fingerprint {
1499 self.fingerprint = Fingerprint::Basic;
1500 } else {
1501 self.fingerprint = Fingerprint::None;
1502 }
1503 self
1504 }
1505
1506 #[cfg(feature = "chrome")]
1507 pub fn with_fingerprint_advanced(&mut self, fingerprint: Fingerprint) -> &mut Self {
1509 self.fingerprint = fingerprint;
1510 self
1511 }
1512
1513 #[cfg(not(feature = "chrome"))]
1514 pub fn with_fingerprint(&mut self, _fingerprint: bool) -> &mut Self {
1516 self
1517 }
1518
1519 pub fn with_proxies(&mut self, proxies: Option<Vec<String>>) -> &mut Self {
1521 self.proxies = proxies.map(|p| {
1522 p.iter()
1523 .map(|addr| RequestProxy {
1524 addr: addr.to_owned(),
1525 ..Default::default()
1526 })
1527 .collect::<Vec<RequestProxy>>()
1528 });
1529 self
1530 }
1531
1532 pub fn with_proxies_direct(&mut self, proxies: Option<Vec<RequestProxy>>) -> &mut Self {
1534 self.proxies = proxies;
1535 self
1536 }
1537
1538 pub fn with_proxies_for_kind(
1550 &mut self,
1551 kind: ProxyKind,
1552 proxies: Option<Vec<RequestProxy>>,
1553 ) -> &mut Self {
1554 match (proxies, self.proxies_by_kind.as_mut()) {
1555 (Some(p), Some(map)) => {
1556 map.insert(kind, p);
1557 }
1558 (Some(p), None) => {
1559 let mut map = hashbrown::HashMap::new();
1560 map.insert(kind, p);
1561 self.proxies_by_kind = Some(map);
1562 }
1563 (None, Some(map)) => {
1564 map.remove(&kind);
1565 if map.is_empty() {
1566 self.proxies_by_kind = None;
1567 }
1568 }
1569 (None, None) => {}
1570 }
1571 self
1572 }
1573
1574 pub fn with_shared_queue(&mut self, shared_queue: bool) -> &mut Self {
1576 self.shared_queue = shared_queue;
1577 self
1578 }
1579
1580 pub fn with_blacklist_url<T>(&mut self, blacklist_url: Option<Vec<T>>) -> &mut Self
1582 where
1583 Vec<CompactString>: From<Vec<T>>,
1584 {
1585 match blacklist_url {
1586 Some(p) => self.blacklist_url = Some(p.into()),
1587 _ => self.blacklist_url = None,
1588 };
1589 self
1590 }
1591
1592 pub fn with_whitelist_url<T>(&mut self, whitelist_url: Option<Vec<T>>) -> &mut Self
1594 where
1595 Vec<CompactString>: From<Vec<T>>,
1596 {
1597 match whitelist_url {
1598 Some(p) => self.whitelist_url = Some(p.into()),
1599 _ => self.whitelist_url = None,
1600 };
1601 self
1602 }
1603
1604 pub fn with_return_page_links(&mut self, return_page_links: bool) -> &mut Self {
1606 self.return_page_links = return_page_links;
1607 self
1608 }
1609
1610 pub fn with_headers(&mut self, headers: Option<reqwest::header::HeaderMap>) -> &mut Self {
1612 match headers {
1613 Some(m) => self.headers = Some(SerializableHeaderMap::from(m).into()),
1614 _ => self.headers = None,
1615 };
1616 self
1617 }
1618
1619 pub fn with_redirect_limit(&mut self, redirect_limit: usize) -> &mut Self {
1625 self.redirect_limit = redirect_limit;
1626 self.redirect_limit_set = true;
1627 self
1628 }
1629
1630 pub fn with_max_main_frame_navigations(&mut self, cap: Option<u32>) -> &mut Self {
1638 self.max_main_frame_navigations = cap;
1639 self
1640 }
1641
1642 pub fn with_redirect_policy(&mut self, policy: RedirectPolicy) -> &mut Self {
1644 self.redirect_policy = policy;
1645 self
1646 }
1647
1648 pub fn with_referer(&mut self, referer: Option<String>) -> &mut Self {
1650 self.referer = referer;
1651 self
1652 }
1653
1654 pub fn with_referrer(&mut self, referer: Option<String>) -> &mut Self {
1656 self.referer = referer;
1657 self
1658 }
1659
1660 pub fn with_full_resources(&mut self, full_resources: bool) -> &mut Self {
1662 self.full_resources = full_resources;
1663 self
1664 }
1665
1666 #[cfg(feature = "chrome")]
1668 pub fn with_dismiss_dialogs(&mut self, dismiss_dialogs: bool) -> &mut Self {
1669 self.dismiss_dialogs = Some(dismiss_dialogs);
1670 self
1671 }
1672
1673 #[cfg(not(feature = "chrome"))]
1675 pub fn with_dismiss_dialogs(&mut self, _dismiss_dialogs: bool) -> &mut Self {
1676 self
1677 }
1678
1679 #[cfg(feature = "chrome")]
1684 pub fn with_prefer_native_markdown(&mut self, prefer_native_markdown: bool) -> &mut Self {
1685 self.prefer_native_markdown = prefer_native_markdown;
1686 self
1687 }
1688
1689 #[cfg(not(feature = "chrome"))]
1694 pub fn with_prefer_native_markdown(&mut self, _prefer_native_markdown: bool) -> &mut Self {
1695 self
1696 }
1697
1698 #[cfg(feature = "wreq")]
1700 pub fn with_emulation(&mut self, emulation: Option<wreq_util::Emulation>) -> &mut Self {
1701 self.emulation = emulation;
1702 self
1703 }
1704
1705 #[cfg(feature = "cron")]
1706 pub fn with_cron(&mut self, cron_str: &str, cron_type: CronType) -> &mut Self {
1708 self.cron_str = cron_str.into();
1709 self.cron_type = cron_type;
1710 self
1711 }
1712
1713 #[cfg(not(feature = "cron"))]
1714 pub fn with_cron(&mut self, _cron_str: &str, _cron_type: CronType) -> &mut Self {
1716 self
1717 }
1718
1719 pub fn with_limit(&mut self, limit: u32) -> &mut Self {
1721 self.with_budget(Some(hashbrown::HashMap::from([("*", limit)])));
1722 self
1723 }
1724
1725 pub fn with_concurrency_limit(&mut self, limit: Option<usize>) -> &mut Self {
1727 self.concurrency_limit = limit;
1728 self
1729 }
1730
1731 #[cfg(feature = "chrome")]
1732 pub fn with_auth_challenge_response(
1734 &mut self,
1735 auth_challenge_response: Option<AuthChallengeResponse>,
1736 ) -> &mut Self {
1737 self.auth_challenge_response = auth_challenge_response;
1738 self
1739 }
1740
1741 #[cfg(feature = "chrome")]
1742 pub fn with_evaluate_on_new_document(
1744 &mut self,
1745 evaluate_on_new_document: Option<Box<String>>,
1746 ) -> &mut Self {
1747 self.evaluate_on_new_document = evaluate_on_new_document;
1748 self
1749 }
1750
1751 #[cfg(not(feature = "chrome"))]
1752 pub fn with_evaluate_on_new_document(
1754 &mut self,
1755 _evaluate_on_new_document: Option<Box<String>>,
1756 ) -> &mut Self {
1757 self
1758 }
1759
1760 #[cfg(not(feature = "chrome"))]
1761 pub fn with_auth_challenge_response(
1763 &mut self,
1764 _auth_challenge_response: Option<AuthChallengeResponse>,
1765 ) -> &mut Self {
1766 self
1767 }
1768
1769 pub fn with_depth(&mut self, depth: usize) -> &mut Self {
1771 self.depth = depth;
1772 self
1773 }
1774
1775 #[cfg(any(feature = "cache_request", feature = "chrome_remote_cache"))]
1776 pub fn with_caching(&mut self, cache: bool) -> &mut Self {
1778 self.cache = cache;
1779 self
1780 }
1781
1782 #[cfg(not(any(feature = "cache_request", feature = "chrome_remote_cache")))]
1783 pub fn with_caching(&mut self, _cache: bool) -> &mut Self {
1785 self
1786 }
1787
1788 #[cfg(any(feature = "cache_request", feature = "chrome_remote_cache"))]
1789 pub fn with_cache_skip_browser(&mut self, skip: bool) -> &mut Self {
1793 self.cache_skip_browser = skip;
1794 self
1795 }
1796
1797 #[cfg(not(any(feature = "cache_request", feature = "chrome_remote_cache")))]
1798 pub fn with_cache_skip_browser(&mut self, _skip: bool) -> &mut Self {
1801 self
1802 }
1803
1804 pub fn with_cache_namespace<S: Into<String>>(&mut self, namespace: Option<S>) -> &mut Self {
1811 self.cache_namespace = namespace.map(|s| Box::new(s.into()));
1812 self
1813 }
1814
1815 #[inline]
1819 #[allow(dead_code)]
1820 pub(crate) fn cache_namespace_str(&self) -> Option<&str> {
1821 self.cache_namespace.as_ref().map(|s| s.as_str())
1822 }
1823
1824 #[cfg(feature = "chrome_remote_cache")]
1829 pub fn with_chrome_remote_cache_read_only(&mut self, read_only: bool) -> &mut Self {
1830 self.chrome_remote_cache_read_only = read_only;
1831 self
1832 }
1833
1834 #[cfg(not(feature = "chrome_remote_cache"))]
1837 pub fn with_chrome_remote_cache_read_only(&mut self, _read_only: bool) -> &mut Self {
1838 self
1839 }
1840
1841 #[inline]
1846 #[allow(dead_code)]
1847 pub(crate) fn chrome_remote_cache_read_only_enabled(&self) -> bool {
1848 #[cfg(feature = "chrome_remote_cache")]
1849 {
1850 self.chrome_remote_cache_read_only
1851 }
1852 #[cfg(not(feature = "chrome_remote_cache"))]
1853 {
1854 false
1855 }
1856 }
1857
1858 #[cfg(feature = "chrome_remote_cache")]
1866 pub fn with_remote_cache_skip_browser(&mut self, enabled: bool) -> &mut Self {
1867 self.remote_cache_skip_browser = enabled;
1868 spider_remote_cache::set_skip_browser_dumps_enabled(enabled);
1869 spider_remote_cache::set_spool_enabled(enabled);
1870 self
1871 }
1872
1873 #[cfg(not(feature = "chrome_remote_cache"))]
1877 pub fn with_remote_cache_skip_browser(&mut self, _enabled: bool) -> &mut Self {
1878 self
1879 }
1880
1881 #[inline]
1888 #[allow(dead_code)]
1889 pub(crate) fn remote_cache_skip_browser_enabled(&self) -> bool {
1890 #[cfg(feature = "chrome_remote_cache")]
1891 {
1892 self.remote_cache_skip_browser
1893 }
1894 #[cfg(not(feature = "chrome_remote_cache"))]
1895 {
1896 false
1897 }
1898 }
1899
1900 #[cfg(feature = "chrome_remote_cache")]
1909 pub fn with_chrome_remote_cache_main_doc_only(&mut self, enabled: bool) -> &mut Self {
1910 self.chrome_remote_cache_main_doc_only = enabled;
1911 self
1912 }
1913
1914 #[cfg(not(feature = "chrome_remote_cache"))]
1918 pub fn with_chrome_remote_cache_main_doc_only(&mut self, _enabled: bool) -> &mut Self {
1919 self
1920 }
1921
1922 #[inline]
1927 #[allow(dead_code)]
1928 pub(crate) fn chrome_remote_cache_main_doc_only_enabled(&self) -> bool {
1929 #[cfg(feature = "chrome_remote_cache")]
1930 {
1931 self.chrome_remote_cache_main_doc_only
1932 }
1933 #[cfg(not(feature = "chrome_remote_cache"))]
1934 {
1935 false
1936 }
1937 }
1938
1939 #[cfg(feature = "chrome")]
1940 pub fn with_service_worker_enabled(&mut self, enabled: bool) -> &mut Self {
1942 self.service_worker_enabled = enabled;
1943 self
1944 }
1945
1946 #[cfg(not(feature = "chrome"))]
1947 pub fn with_service_worker_enabled(&mut self, _enabled: bool) -> &mut Self {
1949 self
1950 }
1951
1952 #[cfg(not(feature = "chrome"))]
1954 pub fn with_auto_geolocation(&mut self, _enabled: bool) -> &mut Self {
1955 self
1956 }
1957
1958 #[cfg(feature = "chrome")]
1960 pub fn with_auto_geolocation(&mut self, enabled: bool) -> &mut Self {
1961 self.auto_geolocation = enabled;
1962 self
1963 }
1964
1965 pub fn with_retry(&mut self, retry: u8) -> &mut Self {
1967 self.retry = retry;
1968 self
1969 }
1970
1971 pub fn with_default_http_connect_timeout(
1973 &mut self,
1974 default_http_connect_timeout: Option<Duration>,
1975 ) -> &mut Self {
1976 self.default_http_connect_timeout = default_http_connect_timeout;
1977 self
1978 }
1979
1980 pub fn with_default_http_read_timeout(
1982 &mut self,
1983 default_http_read_timeout: Option<Duration>,
1984 ) -> &mut Self {
1985 self.default_http_read_timeout = default_http_read_timeout;
1986 self
1987 }
1988
1989 pub fn with_no_control_thread(&mut self, no_control_thread: bool) -> &mut Self {
1991 self.no_control_thread = no_control_thread;
1992 self
1993 }
1994
1995 pub fn with_viewport(&mut self, viewport: Option<crate::configuration::Viewport>) -> &mut Self {
1997 self.viewport = viewport.map(|vp| vp);
1998 self
1999 }
2000
2001 #[cfg(feature = "chrome")]
2002 pub fn with_stealth(&mut self, stealth_mode: bool) -> &mut Self {
2004 if stealth_mode {
2005 self.stealth_mode = spider_fingerprint::configs::Tier::Basic;
2006 } else {
2007 self.stealth_mode = spider_fingerprint::configs::Tier::None;
2008 }
2009 self
2010 }
2011
2012 #[cfg(feature = "chrome")]
2013 pub fn with_stealth_advanced(
2015 &mut self,
2016 stealth_mode: spider_fingerprint::configs::Tier,
2017 ) -> &mut Self {
2018 self.stealth_mode = stealth_mode;
2019 self
2020 }
2021
2022 #[cfg(not(feature = "chrome"))]
2023 pub fn with_stealth(&mut self, _stealth_mode: bool) -> &mut Self {
2025 self
2026 }
2027
2028 #[cfg(feature = "chrome")]
2029 pub fn with_wait_for_idle_network(
2031 &mut self,
2032 wait_for_idle_network: Option<WaitForIdleNetwork>,
2033 ) -> &mut Self {
2034 match self.wait_for.as_mut() {
2035 Some(wait_for) => wait_for.idle_network = wait_for_idle_network,
2036 _ => {
2037 let mut wait_for = WaitFor::default();
2038 wait_for.idle_network = wait_for_idle_network;
2039 self.wait_for = Some(wait_for);
2040 }
2041 }
2042 self
2043 }
2044
2045 #[cfg(feature = "chrome")]
2046 pub fn with_wait_for_idle_network0(
2048 &mut self,
2049 wait_for_idle_network0: Option<WaitForIdleNetwork>,
2050 ) -> &mut Self {
2051 match self.wait_for.as_mut() {
2052 Some(wait_for) => wait_for.idle_network0 = wait_for_idle_network0,
2053 _ => {
2054 let mut wait_for = WaitFor::default();
2055 wait_for.idle_network0 = wait_for_idle_network0;
2056 self.wait_for = Some(wait_for);
2057 }
2058 }
2059 self
2060 }
2061
2062 #[cfg(feature = "chrome")]
2063 pub fn with_wait_for_almost_idle_network0(
2065 &mut self,
2066 wait_for_almost_idle_network0: Option<WaitForIdleNetwork>,
2067 ) -> &mut Self {
2068 match self.wait_for.as_mut() {
2069 Some(wait_for) => wait_for.almost_idle_network0 = wait_for_almost_idle_network0,
2070 _ => {
2071 let mut wait_for = WaitFor::default();
2072 wait_for.almost_idle_network0 = wait_for_almost_idle_network0;
2073 self.wait_for = Some(wait_for);
2074 }
2075 }
2076 self
2077 }
2078
2079 #[cfg(not(feature = "chrome"))]
2080 pub fn with_wait_for_almost_idle_network0(
2082 &mut self,
2083 _wait_for_almost_idle_network0: Option<WaitForIdleNetwork>,
2084 ) -> &mut Self {
2085 self
2086 }
2087
2088 #[cfg(not(feature = "chrome"))]
2089 pub fn with_wait_for_idle_network0(
2091 &mut self,
2092 _wait_for_idle_network0: Option<WaitForIdleNetwork>,
2093 ) -> &mut Self {
2094 self
2095 }
2096
2097 #[cfg(not(feature = "chrome"))]
2098 pub fn with_wait_for_idle_network(
2100 &mut self,
2101 _wait_for_idle_network: Option<WaitForIdleNetwork>,
2102 ) -> &mut Self {
2103 self
2104 }
2105
2106 #[cfg(feature = "chrome")]
2107 pub fn with_wait_for_idle_dom(
2109 &mut self,
2110 wait_for_idle_dom: Option<WaitForSelector>,
2111 ) -> &mut Self {
2112 match self.wait_for.as_mut() {
2113 Some(wait_for) => wait_for.dom = wait_for_idle_dom,
2114 _ => {
2115 let mut wait_for = WaitFor::default();
2116 wait_for.dom = wait_for_idle_dom;
2117 self.wait_for = Some(wait_for);
2118 }
2119 }
2120 self
2121 }
2122
2123 #[cfg(not(feature = "chrome"))]
2124 pub fn with_wait_for_idle_dom(
2126 &mut self,
2127 _wait_for_idle_dom: Option<WaitForSelector>,
2128 ) -> &mut Self {
2129 self
2130 }
2131
2132 #[cfg(feature = "chrome")]
2133 pub fn with_wait_for_selector(
2135 &mut self,
2136 wait_for_selector: Option<WaitForSelector>,
2137 ) -> &mut Self {
2138 match self.wait_for.as_mut() {
2139 Some(wait_for) => wait_for.selector = wait_for_selector,
2140 _ => {
2141 let mut wait_for = WaitFor::default();
2142 wait_for.selector = wait_for_selector;
2143 self.wait_for = Some(wait_for);
2144 }
2145 }
2146 self
2147 }
2148
2149 #[cfg(not(feature = "chrome"))]
2150 pub fn with_wait_for_selector(
2152 &mut self,
2153 _wait_for_selector: Option<WaitForSelector>,
2154 ) -> &mut Self {
2155 self
2156 }
2157
2158 #[cfg(feature = "chrome")]
2159 pub fn with_wait_for_delay(&mut self, wait_for_delay: Option<WaitForDelay>) -> &mut Self {
2161 match self.wait_for.as_mut() {
2162 Some(wait_for) => wait_for.delay = wait_for_delay,
2163 _ => {
2164 let mut wait_for = WaitFor::default();
2165 wait_for.delay = wait_for_delay;
2166 self.wait_for = Some(wait_for);
2167 }
2168 }
2169 self
2170 }
2171
2172 #[cfg(not(feature = "chrome"))]
2173 pub fn with_wait_for_delay(&mut self, _wait_for_delay: Option<WaitForDelay>) -> &mut Self {
2175 self
2176 }
2177
2178 #[cfg(feature = "chrome_intercept")]
2179 pub fn with_chrome_intercept(
2181 &mut self,
2182 chrome_intercept: RequestInterceptConfiguration,
2183 url: &Option<Box<url::Url>>,
2184 ) -> &mut Self {
2185 self.chrome_intercept = chrome_intercept;
2186 self.chrome_intercept.setup_intercept_manager(url);
2187 self
2188 }
2189
2190 #[cfg(not(feature = "chrome_intercept"))]
2191 pub fn with_chrome_intercept(
2193 &mut self,
2194 _chrome_intercept: RequestInterceptConfiguration,
2195 _url: &Option<Box<url::Url>>,
2196 ) -> &mut Self {
2197 self
2198 }
2199
2200 #[cfg(feature = "chrome_intercept")]
2201 pub fn with_remote_local_policy(&mut self, enabled: bool) -> &mut Self {
2208 if enabled {
2209 self.chrome_intercept.enabled = true;
2210 }
2211 self.chrome_intercept.set_remote_local_policy(enabled);
2212 self
2213 }
2214
2215 #[cfg(not(feature = "chrome_intercept"))]
2216 pub fn with_remote_local_policy(&mut self, _enabled: bool) -> &mut Self {
2219 self
2220 }
2221
2222 #[cfg(feature = "chrome")]
2223 pub fn with_chrome_connection(&mut self, chrome_connection_url: Option<String>) -> &mut Self {
2225 self.chrome_connection_url = chrome_connection_url;
2226 self
2227 }
2228
2229 #[cfg(not(feature = "chrome"))]
2230 pub fn with_chrome_connection(&mut self, _chrome_connection_url: Option<String>) -> &mut Self {
2232 self
2233 }
2234
2235 #[cfg(feature = "chrome")]
2236 pub fn with_chrome_connections(&mut self, urls: Vec<String>) -> &mut Self {
2244 match urls.len() {
2245 0 => {
2246 self.chrome_connection_urls = None;
2247 }
2248 1 => {
2249 self.chrome_connection_url = urls.into_iter().next();
2250 self.chrome_connection_urls = None;
2251 }
2252 _ => {
2253 self.chrome_connection_urls = Some(urls);
2254 }
2255 }
2256 self.chrome_failover = crate::features::chrome::LazyChromeFailover::default();
2260 self
2261 }
2262
2263 #[cfg(not(feature = "chrome"))]
2264 pub fn with_chrome_connections(&mut self, _urls: Vec<String>) -> &mut Self {
2266 self
2267 }
2268
2269 #[cfg(feature = "decentralized")]
2270 pub fn with_worker_connection(&mut self, worker_connection_url: Option<String>) -> &mut Self {
2277 self.worker_connection_urls = worker_connection_url.map(|url| {
2278 let url = url.trim();
2279 if url.is_empty() {
2280 Vec::new()
2281 } else {
2282 vec![url.to_string()]
2283 }
2284 });
2285 self
2286 }
2287
2288 #[cfg(not(feature = "decentralized"))]
2289 pub fn with_worker_connection(&mut self, _worker_connection_url: Option<String>) -> &mut Self {
2292 self
2293 }
2294
2295 #[cfg(feature = "decentralized")]
2296 pub fn with_worker_connections(&mut self, urls: Vec<String>) -> &mut Self {
2300 self.worker_connection_urls = Some(
2301 urls.into_iter()
2302 .map(|url| url.trim().to_string())
2303 .filter(|url| !url.is_empty())
2304 .collect(),
2305 );
2306 self
2307 }
2308
2309 #[cfg(not(feature = "decentralized"))]
2310 pub fn with_worker_connections(&mut self, _urls: Vec<String>) -> &mut Self {
2313 self
2314 }
2315
2316 #[cfg(feature = "decentralized")]
2317 pub fn with_scraper_worker_connection(
2323 &mut self,
2324 scraper_worker_connection_url: Option<String>,
2325 ) -> &mut Self {
2326 self.scraper_worker_connection_urls = scraper_worker_connection_url.map(|url| {
2327 let url = url.trim();
2328 if url.is_empty() {
2329 Vec::new()
2330 } else {
2331 vec![url.to_string()]
2332 }
2333 });
2334 self
2335 }
2336
2337 #[cfg(not(feature = "decentralized"))]
2338 pub fn with_scraper_worker_connection(
2341 &mut self,
2342 _scraper_worker_connection_url: Option<String>,
2343 ) -> &mut Self {
2344 self
2345 }
2346
2347 #[cfg(feature = "decentralized")]
2348 pub fn with_scraper_worker_connections(&mut self, urls: Vec<String>) -> &mut Self {
2352 self.scraper_worker_connection_urls = Some(
2353 urls.into_iter()
2354 .map(|url| url.trim().to_string())
2355 .filter(|url| !url.is_empty())
2356 .collect(),
2357 );
2358 self
2359 }
2360
2361 #[cfg(not(feature = "decentralized"))]
2362 pub fn with_scraper_worker_connections(&mut self, _urls: Vec<String>) -> &mut Self {
2365 self
2366 }
2367
2368 #[cfg(feature = "chrome")]
2369 pub fn with_chrome_first_byte_timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
2374 self.chrome_first_byte_timeout = timeout;
2375 self
2376 }
2377
2378 #[cfg(not(feature = "chrome"))]
2379 pub fn with_chrome_first_byte_timeout(&mut self, _timeout: Option<Duration>) -> &mut Self {
2381 self
2382 }
2383
2384 #[cfg(feature = "chrome")]
2385 pub fn with_chrome_first_byte_timeout_jitter(&mut self, jitter: Option<Duration>) -> &mut Self {
2389 self.chrome_first_byte_timeout_jitter = jitter;
2390 self
2391 }
2392
2393 #[cfg(not(feature = "chrome"))]
2394 pub fn with_chrome_first_byte_timeout_jitter(
2396 &mut self,
2397 _jitter: Option<Duration>,
2398 ) -> &mut Self {
2399 self
2400 }
2401
2402 pub fn with_http_first_byte_timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
2411 self.http_first_byte_timeout = timeout;
2412 self
2413 }
2414
2415 pub fn with_http_first_byte_timeout_jitter(&mut self, jitter: Option<Duration>) -> &mut Self {
2419 self.http_first_byte_timeout_jitter = jitter;
2420 self
2421 }
2422
2423 #[cfg(not(feature = "chrome"))]
2424 pub fn with_execution_scripts(
2426 &mut self,
2427 _execution_scripts: Option<ExecutionScriptsMap>,
2428 ) -> &mut Self {
2429 self
2430 }
2431
2432 #[cfg(feature = "chrome")]
2433 pub fn with_execution_scripts(
2435 &mut self,
2436 execution_scripts: Option<ExecutionScriptsMap>,
2437 ) -> &mut Self {
2438 self.execution_scripts =
2439 crate::features::chrome_common::convert_to_trie_execution_scripts(&execution_scripts);
2440 self
2441 }
2442
2443 #[cfg(not(feature = "chrome"))]
2444 pub fn with_automation_scripts(
2446 &mut self,
2447 _automation_scripts: Option<AutomationScriptsMap>,
2448 ) -> &mut Self {
2449 self
2450 }
2451
2452 #[cfg(feature = "chrome")]
2453 pub fn with_automation_scripts(
2455 &mut self,
2456 automation_scripts: Option<AutomationScriptsMap>,
2457 ) -> &mut Self {
2458 self.automation_scripts =
2459 crate::features::chrome_common::convert_to_trie_automation_scripts(&automation_scripts);
2460 self
2461 }
2462
2463 pub fn with_budget(&mut self, budget: Option<hashbrown::HashMap<&str, u32>>) -> &mut Self {
2465 self.budget = match budget {
2466 Some(budget) => {
2467 let mut crawl_budget: hashbrown::HashMap<
2468 case_insensitive_string::CaseInsensitiveString,
2469 u32,
2470 > = hashbrown::HashMap::new();
2471
2472 for b in budget.into_iter() {
2473 crawl_budget.insert(
2474 case_insensitive_string::CaseInsensitiveString::from(b.0),
2475 b.1,
2476 );
2477 }
2478
2479 Some(crawl_budget)
2480 }
2481 _ => None,
2482 };
2483 self
2484 }
2485
2486 pub fn with_external_domains<'a, 'b>(
2488 &mut self,
2489 external_domains: Option<impl Iterator<Item = String> + 'a>,
2490 ) -> &mut Self {
2491 match external_domains {
2492 Some(external_domains) => {
2493 self.external_domains_caseless = external_domains
2494 .into_iter()
2495 .filter_map(|d| {
2496 if d == "*" {
2497 Some("*".into())
2498 } else {
2499 let host = get_domain_from_url(&d);
2500
2501 if !host.is_empty() {
2502 Some(host.into())
2503 } else {
2504 None
2505 }
2506 }
2507 })
2508 .collect::<hashbrown::HashSet<case_insensitive_string::CaseInsensitiveString>>()
2509 .into();
2510 }
2511 _ => self.external_domains_caseless = Default::default(),
2512 }
2513
2514 self
2515 }
2516
2517 pub fn with_danger_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self {
2519 self.accept_invalid_certs = accept_invalid_certs;
2520 self
2521 }
2522
2523 pub fn with_normalize(&mut self, normalize: bool) -> &mut Self {
2525 self.normalize = normalize;
2526 self
2527 }
2528
2529 #[cfg(not(feature = "disk"))]
2530 pub fn with_shared_state(&mut self, _shared: bool) -> &mut Self {
2532 self
2533 }
2534
2535 #[cfg(feature = "disk")]
2537 pub fn with_shared_state(&mut self, shared: bool) -> &mut Self {
2538 self.shared = shared;
2539 self
2540 }
2541
2542 #[cfg(not(feature = "chrome"))]
2543 pub fn with_timezone_id(&mut self, _timezone_id: Option<String>) -> &mut Self {
2545 self
2546 }
2547
2548 #[cfg(feature = "chrome")]
2549 pub fn with_timezone_id(&mut self, timezone_id: Option<String>) -> &mut Self {
2551 self.timezone_id = timezone_id.map(|timezone_id| timezone_id.into());
2552 self
2553 }
2554
2555 #[cfg(not(feature = "chrome"))]
2556 pub fn with_locale(&mut self, _locale: Option<String>) -> &mut Self {
2558 self
2559 }
2560
2561 #[cfg(feature = "chrome")]
2562 pub fn with_locale(&mut self, locale: Option<String>) -> &mut Self {
2564 self.locale = locale.map(|locale| locale.into());
2565 self
2566 }
2567
2568 #[cfg(feature = "chrome")]
2569 pub fn with_event_tracker(&mut self, track_events: Option<ChromeEventTracker>) -> &mut Self {
2571 self.track_events = track_events;
2572 self
2573 }
2574
2575 #[cfg(not(feature = "chrome"))]
2577 pub fn with_screenshot(&mut self, _screenshot_config: Option<ScreenShotConfig>) -> &mut Self {
2578 self
2579 }
2580
2581 #[cfg(feature = "chrome")]
2583 pub fn with_screenshot(&mut self, screenshot_config: Option<ScreenShotConfig>) -> &mut Self {
2584 self.screenshot = screenshot_config;
2585 self
2586 }
2587
2588 pub fn with_max_page_bytes(&mut self, max_page_bytes: Option<f64>) -> &mut Self {
2590 self.max_page_bytes = max_page_bytes;
2591 self
2592 }
2593
2594 pub fn with_max_bytes_allowed(&mut self, max_bytes_allowed: Option<u64>) -> &mut Self {
2596 self.max_bytes_allowed = max_bytes_allowed;
2597 self
2598 }
2599
2600 pub fn with_block_assets(&mut self, only_html: bool) -> &mut Self {
2602 self.only_html = only_html;
2603 self
2604 }
2605
2606 pub fn with_modify_headers(&mut self, modify_headers: bool) -> &mut Self {
2608 self.modify_headers = modify_headers;
2609 self
2610 }
2611
2612 pub fn with_modify_http_client_headers(
2614 &mut self,
2615 modify_http_client_headers: bool,
2616 ) -> &mut Self {
2617 self.modify_http_client_headers = modify_http_client_headers;
2618 self
2619 }
2620
2621 pub fn with_cache_policy(&mut self, cache_policy: Option<BasicCachePolicy>) -> &mut Self {
2623 self.cache_policy = cache_policy;
2624 self
2625 }
2626
2627 #[cfg(feature = "webdriver")]
2628 pub fn with_webdriver_config(
2630 &mut self,
2631 webdriver_config: Option<WebDriverConfig>,
2632 ) -> &mut Self {
2633 self.webdriver_config = webdriver_config.map(Box::new);
2634 self
2635 }
2636
2637 #[cfg(not(feature = "webdriver"))]
2638 pub fn with_webdriver_config(
2640 &mut self,
2641 _webdriver_config: Option<WebDriverConfig>,
2642 ) -> &mut Self {
2643 self
2644 }
2645
2646 #[inline]
2667 pub fn auto_http_first_byte_args(&self) -> (Option<Duration>, Option<Duration>) {
2668 match self.http_first_byte_timeout {
2669 Some(_) => (
2670 self.http_first_byte_timeout,
2671 self.http_first_byte_timeout_jitter,
2672 ),
2673 None => (None, None),
2674 }
2675 }
2676
2677 #[cfg(feature = "chrome")]
2683 #[inline]
2684 fn native_markdown_safe(&self) -> bool {
2685 !self.return_page_links
2686 && !self.full_resources
2687 && self
2688 .inner_budget
2689 .as_ref()
2690 .and_then(|b| b.get(&case_insensitive_string::CaseInsensitiveString::from("*")))
2691 .is_some_and(|v| *v == 1)
2692 }
2693
2694 #[cfg(feature = "chrome")]
2700 #[inline]
2701 pub fn chrome_fetch_params(&self) -> crate::utils::ChromeFetchParams<'_> {
2702 crate::utils::ChromeFetchParams {
2703 wait_for: &self.wait_for,
2704 screenshot: &self.screenshot,
2705 openai_config: &self.openai_config,
2706 execution_scripts: &self.execution_scripts,
2707 automation_scripts: &self.automation_scripts,
2708 viewport: &self.viewport,
2709 request_timeout: &self.request_timeout,
2710 track_events: &self.track_events,
2711 cache_policy: &self.cache_policy,
2712 remote_multimodal: &self.remote_multimodal,
2713 remote_cache_read_only: self.chrome_remote_cache_read_only_enabled(),
2714 remote_cache_main_doc_only: self.chrome_remote_cache_main_doc_only_enabled(),
2715 first_byte_timeout: &self.chrome_first_byte_timeout,
2716 first_byte_timeout_jitter: &self.chrome_first_byte_timeout_jitter,
2717 browser_dead: None,
2718 chrome_failover: Some(&self.chrome_failover),
2719 chrome_endpoint_url: self
2726 .chrome_failover
2727 .last_connected_url()
2728 .or(self.chrome_connection_url.as_deref()),
2729 enhancements: self.enhancements,
2730 prefer_native_markdown: self.prefer_native_markdown && self.native_markdown_safe(),
2731 }
2732 }
2733
2734 #[cfg(any(feature = "cache_request", feature = "chrome_remote_cache"))]
2736 pub(crate) fn get_cache_options(&self) -> Option<crate::utils::CacheOptions> {
2737 use crate::utils::CacheOptions;
2738 if !self.cache {
2739 return None;
2740 }
2741 let auth_token = self
2742 .headers
2743 .as_ref()
2744 .and_then(|headers| {
2745 headers
2746 .0
2747 .get("authorization")
2748 .or_else(|| headers.0.get("Authorization"))
2749 })
2750 .map(|s| s.to_owned());
2751
2752 #[cfg(feature = "cache_mem")]
2757 let skip_browser = true;
2758 #[cfg(not(feature = "cache_mem"))]
2759 let skip_browser = self.cache_skip_browser;
2760
2761 match auth_token {
2762 Some(token) if !token.is_empty() => {
2763 if let Ok(token_str) = token.to_str() {
2764 if skip_browser {
2765 Some(CacheOptions::SkipBrowserAuthorized(token_str.into()))
2766 } else {
2767 Some(CacheOptions::Authorized(token_str.into()))
2768 }
2769 } else if skip_browser {
2770 Some(CacheOptions::SkipBrowser)
2771 } else {
2772 Some(CacheOptions::Yes)
2773 }
2774 }
2775 _ => {
2776 if skip_browser {
2777 Some(CacheOptions::SkipBrowser)
2778 } else {
2779 Some(CacheOptions::Yes)
2780 }
2781 }
2782 }
2783 }
2784
2785 #[cfg(all(
2787 feature = "chrome",
2788 not(any(feature = "cache_request", feature = "chrome_remote_cache"))
2789 ))]
2790 pub(crate) fn get_cache_options(&self) -> Option<crate::utils::CacheOptions> {
2791 None
2792 }
2793
2794 #[cfg(not(any(
2796 feature = "cache_request",
2797 feature = "chrome_remote_cache",
2798 feature = "chrome"
2799 )))]
2800 #[allow(dead_code)]
2801 pub(crate) fn get_cache_options(&self) -> Option<crate::utils::CacheOptions> {
2802 None
2803 }
2804
2805 pub fn build(&self) -> Self {
2807 self.to_owned()
2808 }
2809
2810 #[cfg(feature = "search")]
2811 pub fn with_search_config(&mut self, search_config: Option<SearchConfig>) -> &mut Self {
2813 self.search_config = search_config.map(Box::new);
2814 self
2815 }
2816
2817 #[cfg(not(feature = "search"))]
2818 pub fn with_search_config(&mut self, _search_config: Option<()>) -> &mut Self {
2820 self
2821 }
2822
2823 #[cfg(feature = "spider_cloud")]
2825 pub fn with_spider_cloud(&mut self, api_key: &str) -> &mut Self {
2826 if is_placeholder_api_key(api_key) {
2827 log::warn!("Spider Cloud API key looks like a placeholder — skipping. Get a real key at https://spider.cloud");
2828 return self;
2829 }
2830 self.spider_cloud = Some(Box::new(SpiderCloudConfig::new(api_key)));
2831 self
2832 }
2833
2834 #[cfg(not(feature = "spider_cloud"))]
2836 pub fn with_spider_cloud(&mut self, _api_key: &str) -> &mut Self {
2837 self
2838 }
2839
2840 #[cfg(feature = "spider_cloud")]
2842 pub fn with_spider_cloud_config(&mut self, config: SpiderCloudConfig) -> &mut Self {
2843 self.spider_cloud = Some(Box::new(config));
2844 self
2845 }
2846
2847 #[cfg(not(feature = "spider_cloud"))]
2849 pub fn with_spider_cloud_config(&mut self, _config: ()) -> &mut Self {
2850 self
2851 }
2852
2853 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
2862 pub fn with_spider_browser(&mut self, api_key: &str) -> &mut Self {
2863 if is_placeholder_api_key(api_key) {
2864 log::warn!("Spider Browser Cloud API key looks like a placeholder — skipping. Get a real key at https://spider.cloud");
2865 return self;
2866 }
2867 let cfg = SpiderBrowserConfig::new(api_key);
2868 let peers = cfg.connection_urls();
2869 log::info!(
2870 "[spider-browser] configured {} browser peer(s); healthy-peer failover {}",
2871 peers.len(),
2872 if peers.len() > 1 {
2873 "enabled"
2874 } else {
2875 "disabled (single peer)"
2876 }
2877 );
2878 self.with_chrome_connections(peers);
2882 self.spider_browser = Some(Box::new(cfg));
2883 self
2884 }
2885
2886 #[cfg(not(all(feature = "spider_cloud", feature = "chrome")))]
2888 pub fn with_spider_browser(&mut self, _api_key: &str) -> &mut Self {
2889 self
2890 }
2891
2892 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
2895 pub fn with_spider_browser_config(&mut self, config: SpiderBrowserConfig) -> &mut Self {
2896 let peers = config.connection_urls();
2897 log::info!(
2898 "[spider-browser] configured {} browser peer(s); healthy-peer failover {}",
2899 peers.len(),
2900 if peers.len() > 1 {
2901 "enabled"
2902 } else {
2903 "disabled (single peer)"
2904 }
2905 );
2906 self.with_chrome_connections(peers);
2909 self.spider_browser = Some(Box::new(config));
2910 self
2911 }
2912
2913 #[cfg(not(all(feature = "spider_cloud", feature = "chrome")))]
2915 pub fn with_spider_browser_config(&mut self, _config: ()) -> &mut Self {
2916 self
2917 }
2918
2919 #[cfg(feature = "hedge")]
2921 pub fn with_hedge(&mut self, config: crate::utils::hedge::HedgeConfig) -> &mut Self {
2922 self.hedge = Some(config);
2923 self
2924 }
2925
2926 #[cfg(not(feature = "hedge"))]
2928 pub fn with_hedge(&mut self, _config: ()) -> &mut Self {
2929 self
2930 }
2931
2932 #[cfg(feature = "auto_throttle")]
2933 pub fn with_auto_throttle(
2935 &mut self,
2936 config: crate::utils::auto_throttle::AutoThrottleConfig,
2937 ) -> &mut Self {
2938 self.auto_throttle = Some(config);
2939 self
2940 }
2941
2942 #[cfg(not(feature = "auto_throttle"))]
2944 pub fn with_auto_throttle(&mut self, _config: ()) -> &mut Self {
2945 self
2946 }
2947
2948 #[cfg(feature = "etag_cache")]
2949 pub fn with_etag_cache(&mut self, enabled: bool) -> &mut Self {
2951 self.etag_cache = enabled;
2952 self
2953 }
2954
2955 #[cfg(not(feature = "etag_cache"))]
2957 pub fn with_etag_cache(&mut self, _enabled: bool) -> &mut Self {
2958 self
2959 }
2960
2961 #[cfg(feature = "warc")]
2962 pub fn with_warc(&mut self, config: crate::utils::warc::WarcConfig) -> &mut Self {
2964 self.warc = Some(config);
2965 self
2966 }
2967
2968 #[cfg(not(feature = "warc"))]
2970 pub fn with_warc(&mut self, _config: ()) -> &mut Self {
2971 self
2972 }
2973}
2974
2975#[cfg(feature = "search")]
2977#[derive(Debug, Clone, PartialEq)]
2978#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2979pub struct SearchConfig {
2980 pub provider: SearchProviderType,
2982 pub api_key: String,
2984 pub api_url: Option<String>,
2986 pub default_options: Option<SearchOptions>,
2988}
2989
2990#[cfg(feature = "search")]
2991impl SearchConfig {
2992 pub fn new(provider: SearchProviderType, api_key: impl Into<String>) -> Self {
2994 Self {
2995 provider,
2996 api_key: api_key.into(),
2997 api_url: None,
2998 default_options: None,
2999 }
3000 }
3001
3002 pub fn with_api_url(mut self, url: impl Into<String>) -> Self {
3004 self.api_url = Some(url.into());
3005 self
3006 }
3007
3008 pub fn with_default_options(mut self, options: SearchOptions) -> Self {
3010 self.default_options = Some(options);
3011 self
3012 }
3013
3014 pub fn is_enabled(&self) -> bool {
3018 !self.api_key.is_empty() || self.api_url.is_some()
3019 }
3020}
3021
3022#[cfg(feature = "search")]
3024#[derive(Debug, Clone, Default, PartialEq, Eq)]
3025#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3026pub enum SearchProviderType {
3027 #[default]
3029 Serper,
3030 Brave,
3032 Bing,
3034 Tavily,
3036}
3037
3038#[cfg(feature = "spider_cloud")]
3042#[derive(Debug, Clone, Default, PartialEq, Eq)]
3043#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3044pub enum SpiderCloudMode {
3045 #[default]
3049 Proxy,
3050 Api,
3053 Unblocker,
3056 Fallback,
3059 Smart,
3064}
3065
3066#[cfg(feature = "spider_cloud")]
3068#[derive(Debug, Clone, Default, PartialEq, Eq)]
3069#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3070pub enum SpiderCloudReturnFormat {
3071 #[default]
3073 #[cfg_attr(feature = "serde", serde(rename = "raw"))]
3074 Raw,
3075 #[cfg_attr(feature = "serde", serde(rename = "markdown"))]
3077 Markdown,
3078 #[cfg_attr(feature = "serde", serde(rename = "commonmark"))]
3080 CommonMark,
3081 #[cfg_attr(feature = "serde", serde(rename = "text"))]
3083 Text,
3084 #[cfg_attr(feature = "serde", serde(rename = "bytes"))]
3086 Bytes,
3087}
3088
3089#[cfg(feature = "spider_cloud")]
3090impl SpiderCloudReturnFormat {
3091 pub fn as_str(&self) -> &'static str {
3093 match self {
3094 Self::Raw => "raw",
3095 Self::Markdown => "markdown",
3096 Self::CommonMark => "commonmark",
3097 Self::Text => "text",
3098 Self::Bytes => "bytes",
3099 }
3100 }
3101}
3102
3103#[cfg(feature = "spider_cloud")]
3104impl std::fmt::Display for SpiderCloudReturnFormat {
3105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3106 f.write_str(self.as_str())
3107 }
3108}
3109
3110#[cfg(feature = "spider_cloud")]
3111impl From<&str> for SpiderCloudReturnFormat {
3112 fn from(s: &str) -> Self {
3113 match s {
3114 "markdown" | "Markdown" | "MARKDOWN" => Self::Markdown,
3115 "commonmark" | "CommonMark" | "COMMONMARK" => Self::CommonMark,
3116 "text" | "Text" | "TEXT" => Self::Text,
3117 "bytes" | "Bytes" | "BYTES" => Self::Bytes,
3118 _ => Self::Raw,
3119 }
3120 }
3121}
3122
3123#[cfg(feature = "spider_cloud")]
3128#[derive(Debug, Clone, PartialEq, Eq)]
3129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3130pub struct SpiderCloudConfig {
3131 pub api_key: String,
3133 #[cfg_attr(feature = "serde", serde(default))]
3135 pub mode: SpiderCloudMode,
3136 #[cfg_attr(
3138 feature = "serde",
3139 serde(default = "SpiderCloudConfig::default_api_url")
3140 )]
3141 pub api_url: String,
3142 #[cfg_attr(
3144 feature = "serde",
3145 serde(default = "SpiderCloudConfig::default_proxy_url")
3146 )]
3147 pub proxy_url: String,
3148 #[cfg_attr(feature = "serde", serde(default))]
3150 pub return_format: SpiderCloudReturnFormat,
3151 #[cfg_attr(
3158 feature = "serde",
3159 serde(default, skip_serializing_if = "Option::is_none")
3160 )]
3161 pub return_formats: Option<Vec<SpiderCloudReturnFormat>>,
3162 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3164 pub extra_params: Option<hashbrown::HashMap<String, serde_json::Value>>,
3165}
3166
3167#[cfg(feature = "spider_cloud")]
3168impl Default for SpiderCloudConfig {
3169 fn default() -> Self {
3170 Self {
3171 api_key: String::new(),
3172 mode: SpiderCloudMode::default(),
3173 api_url: Self::default_api_url(),
3174 proxy_url: Self::default_proxy_url(),
3175 return_format: SpiderCloudReturnFormat::default(),
3176 return_formats: None,
3177 extra_params: None,
3178 }
3179 }
3180}
3181
3182#[cfg(feature = "spider_cloud")]
3183impl SpiderCloudConfig {
3184 pub fn new(api_key: impl Into<String>) -> Self {
3186 Self {
3187 api_key: api_key.into(),
3188 ..Default::default()
3189 }
3190 }
3191
3192 pub fn with_mode(mut self, mode: SpiderCloudMode) -> Self {
3194 self.mode = mode;
3195 self
3196 }
3197
3198 pub fn with_api_url(mut self, url: impl Into<String>) -> Self {
3200 self.api_url = url.into();
3201 self
3202 }
3203
3204 pub fn with_proxy_url(mut self, url: impl Into<String>) -> Self {
3206 self.proxy_url = url.into();
3207 self
3208 }
3209
3210 pub fn with_return_format(mut self, fmt: impl Into<SpiderCloudReturnFormat>) -> Self {
3218 self.return_format = fmt.into();
3219 self
3220 }
3221
3222 pub fn with_return_formats(mut self, formats: Vec<SpiderCloudReturnFormat>) -> Self {
3235 let mut seen = Vec::with_capacity(formats.len());
3237 for f in formats {
3238 if !seen.contains(&f) {
3239 seen.push(f);
3240 }
3241 }
3242 if let Some(first) = seen.first() {
3243 self.return_format = first.clone();
3244 }
3245 self.return_formats = Some(seen);
3246 self
3247 }
3248
3249 pub fn has_multiple_formats(&self) -> bool {
3251 self.return_formats.as_ref().is_some_and(|f| f.len() > 1)
3252 }
3253
3254 pub fn with_extra_params(
3256 mut self,
3257 params: hashbrown::HashMap<String, serde_json::Value>,
3258 ) -> Self {
3259 self.extra_params = Some(params);
3260 self
3261 }
3262
3263 pub fn should_fallback(&self, status_code: u16, body: Option<&[u8]>) -> bool {
3277 match self.mode {
3278 SpiderCloudMode::Api | SpiderCloudMode::Unblocker => false, SpiderCloudMode::Proxy => false, SpiderCloudMode::Fallback | SpiderCloudMode::Smart => {
3281 if matches!(status_code, 403 | 429 | 503 | 520..=530) {
3283 return true;
3284 }
3285 if status_code >= 500 {
3286 return true;
3287 }
3288
3289 if self.mode == SpiderCloudMode::Smart {
3291 if let Some(body) = body {
3292 if body.is_empty() {
3294 return true;
3295 }
3296
3297 let check_len = body.len().min(4096);
3300 let snippet = String::from_utf8_lossy(&body[..check_len]);
3301 let lower = snippet.to_lowercase();
3302
3303 if lower.contains("cf-browser-verification")
3305 || lower.contains("cloudflare") && lower.contains("challenge-platform")
3306 {
3307 return true;
3308 }
3309
3310 if lower.contains("captcha") && lower.contains("challenge")
3312 || lower.contains("please verify you are a human")
3313 || lower.contains("access denied") && lower.contains("automated")
3314 || lower.contains("bot detection")
3315 {
3316 return true;
3317 }
3318
3319 if lower.contains("distil_r_captcha")
3321 || lower.contains("_imperva")
3322 || lower.contains("akamai") && lower.contains("bot manager")
3323 {
3324 return true;
3325 }
3326 }
3327 }
3328
3329 false
3330 }
3331 }
3332 }
3333
3334 pub fn fallback_route(&self) -> &'static str {
3340 match self.mode {
3341 SpiderCloudMode::Smart | SpiderCloudMode::Unblocker => "unblocker",
3342 _ => "crawl",
3343 }
3344 }
3345
3346 pub fn uses_proxy(&self) -> bool {
3348 matches!(
3349 self.mode,
3350 SpiderCloudMode::Proxy | SpiderCloudMode::Fallback | SpiderCloudMode::Smart
3351 )
3352 }
3353
3354 fn default_api_url() -> String {
3355 "https://api.spider.cloud".to_string()
3356 }
3357
3358 fn default_proxy_url() -> String {
3359 "https://proxy.spider.cloud".to_string()
3360 }
3361}
3362
3363#[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3373#[derive(Debug, Clone, PartialEq, Eq)]
3374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
3375pub struct SpiderBrowserConfig {
3376 pub api_key: String,
3378 #[cfg_attr(
3380 feature = "serde",
3381 serde(default = "SpiderBrowserConfig::default_wss_url")
3382 )]
3383 pub wss_url: String,
3384 #[cfg_attr(
3391 feature = "serde",
3392 serde(default, skip_serializing_if = "Option::is_none")
3393 )]
3394 pub wss_urls: Option<Vec<String>>,
3395 #[cfg_attr(feature = "serde", serde(default))]
3397 pub stealth: bool,
3398 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3400 pub browser: Option<String>,
3401 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3403 pub country: Option<String>,
3404 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
3406 pub extra_params: Option<Vec<(String, String)>>,
3407}
3408
3409#[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3410impl Default for SpiderBrowserConfig {
3411 fn default() -> Self {
3412 Self {
3413 api_key: String::new(),
3414 wss_url: Self::default_wss_url(),
3415 wss_urls: None,
3416 stealth: false,
3417 browser: None,
3418 country: None,
3419 extra_params: None,
3420 }
3421 }
3422}
3423
3424#[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3425impl SpiderBrowserConfig {
3426 pub fn new(api_key: impl Into<String>) -> Self {
3428 Self {
3429 api_key: api_key.into(),
3430 ..Default::default()
3431 }
3432 }
3433
3434 pub fn with_wss_url(mut self, url: impl Into<String>) -> Self {
3436 self.wss_url = url.into();
3437 self
3438 }
3439
3440 pub fn with_wss_urls(mut self, urls: Vec<String>) -> Self {
3447 self.wss_urls = Some(urls);
3448 self
3449 }
3450
3451 pub fn with_stealth(mut self, stealth: bool) -> Self {
3453 self.stealth = stealth;
3454 self
3455 }
3456
3457 pub fn with_browser(mut self, browser: impl Into<String>) -> Self {
3459 self.browser = Some(browser.into());
3460 self
3461 }
3462
3463 pub fn with_country(mut self, country: impl Into<String>) -> Self {
3465 self.country = Some(country.into());
3466 self
3467 }
3468
3469 pub fn with_extra_params(mut self, params: Vec<(String, String)>) -> Self {
3471 self.extra_params = Some(params);
3472 self
3473 }
3474
3475 pub fn connection_url(&self) -> String {
3480 self.build_connection_url(&self.wss_url)
3481 }
3482
3483 pub fn connection_urls(&self) -> Vec<String> {
3491 match self.wss_urls {
3492 Some(ref bases) if !bases.is_empty() => {
3493 bases.iter().map(|b| self.build_connection_url(b)).collect()
3494 }
3495 _ => vec![self.connection_url()],
3496 }
3497 }
3498
3499 fn build_connection_url(&self, base: &str) -> String {
3505 let mut url = base.to_string();
3506
3507 if url.contains('?') {
3509 url.push('&');
3510 } else {
3511 url.push('?');
3512 }
3513 url.push_str("token=");
3514 url.push_str(&self.api_key);
3515
3516 if self.stealth {
3517 url.push_str("&stealth=true");
3518 }
3519 if let Some(ref browser) = self.browser {
3520 url.push_str("&browser=");
3521 url.push_str(browser);
3522 }
3523 if let Some(ref country) = self.country {
3524 url.push_str("&country=");
3525 url.push_str(country);
3526 }
3527 if let Some(ref extra) = self.extra_params {
3528 for (k, v) in extra {
3529 url.push('&');
3530 url.push_str(k);
3531 url.push('=');
3532 url.push_str(v);
3533 }
3534 }
3535
3536 url
3537 }
3538
3539 fn default_wss_url() -> String {
3540 "wss://browser.spider.cloud/v1/browser".to_string()
3541 }
3542}
3543
3544#[cfg(all(test, feature = "chrome"))]
3545mod native_markdown_gate_tests {
3546 use super::*;
3547
3548 #[test]
3549 fn native_markdown_gate_matrix() {
3550 let mut no_budget = Configuration::default();
3551 no_budget.prefer_native_markdown = true;
3552 assert!(!no_budget.chrome_fetch_params().prefer_native_markdown);
3553
3554 let mut limit_five = Configuration::default();
3555 limit_five.prefer_native_markdown = true;
3556 limit_five.with_limit(5);
3557 limit_five.configure_budget();
3558 assert!(!limit_five.chrome_fetch_params().prefer_native_markdown);
3559
3560 let mut page_links = Configuration::default();
3561 page_links.prefer_native_markdown = true;
3562 page_links.with_limit(1);
3563 page_links.return_page_links = true;
3564 page_links.configure_budget();
3565 assert!(!page_links.chrome_fetch_params().prefer_native_markdown);
3566
3567 let mut full_resources = Configuration::default();
3568 full_resources.prefer_native_markdown = true;
3569 full_resources.with_limit(1);
3570 full_resources.full_resources = true;
3571 full_resources.configure_budget();
3572 assert!(!full_resources.chrome_fetch_params().prefer_native_markdown);
3573
3574 let mut clean = Configuration::default();
3575 clean.prefer_native_markdown = true;
3576 clean.with_limit(1);
3577 clean.configure_budget();
3578 assert!(clean.chrome_fetch_params().prefer_native_markdown);
3579 }
3580}
3581
3582#[cfg(test)]
3583mod tests {
3584 use super::*;
3585
3586 #[test]
3587 fn test_configuration_defaults() {
3588 let config = Configuration::default();
3589 assert!(!config.respect_robots_txt);
3590 assert!(!config.subdomains);
3591 assert!(!config.tld);
3592 assert_eq!(config.delay, 0);
3593 assert!(config.user_agent.is_none());
3594 assert!(config.blacklist_url.is_none());
3595 assert!(config.whitelist_url.is_none());
3596 assert!(config.proxies.is_none());
3597 assert!(!config.http2_prior_knowledge);
3598 }
3599
3600 #[test]
3601 fn test_redirect_policy_variants() {
3602 assert_eq!(RedirectPolicy::default(), RedirectPolicy::Loose);
3603 let strict = RedirectPolicy::Strict;
3604 let none = RedirectPolicy::None;
3605 assert_ne!(strict, RedirectPolicy::Loose);
3606 assert_ne!(none, RedirectPolicy::Loose);
3607 assert_ne!(strict, none);
3608 }
3609
3610 #[test]
3611 fn test_redirect_limit_is_opt_in_for_chrome_path() {
3612 let fresh = Configuration::default();
3614 assert!(
3615 !fresh.redirect_limit_set,
3616 "Configuration::default() must not claim the redirect_limit was set"
3617 );
3618
3619 let mut opt_in = Configuration::default();
3621 opt_in.with_redirect_limit(3);
3622 assert!(opt_in.redirect_limit_set);
3623 assert_eq!(opt_in.redirect_limit, 3);
3624 }
3625
3626 #[test]
3627 fn test_proxy_ignore_variants() {
3628 assert_eq!(ProxyIgnore::default(), ProxyIgnore::No);
3629 let chrome = ProxyIgnore::Chrome;
3630 let http = ProxyIgnore::Http;
3631 assert_ne!(chrome, ProxyIgnore::No);
3632 assert_ne!(http, ProxyIgnore::No);
3633 assert_ne!(chrome, http);
3634 }
3635
3636 #[test]
3637 fn test_request_proxy_construction() {
3638 let proxy = RequestProxy {
3639 addr: "http://proxy.example.com:8080".to_string(),
3640 ignore: ProxyIgnore::No,
3641 };
3642 assert_eq!(proxy.addr, "http://proxy.example.com:8080");
3643 assert_eq!(proxy.ignore, ProxyIgnore::No);
3644 }
3645
3646 #[test]
3647 fn test_request_proxy_default() {
3648 let proxy = RequestProxy::default();
3649 assert!(proxy.addr.is_empty());
3650 assert_eq!(proxy.ignore, ProxyIgnore::No);
3651 }
3652
3653 #[test]
3654 fn test_configuration_blacklist_setup() {
3655 let mut config = Configuration::default();
3656 config.blacklist_url = Some(vec![
3657 "https://example.com/private".into(),
3658 "https://example.com/admin".into(),
3659 ]);
3660 assert_eq!(config.blacklist_url.as_ref().unwrap().len(), 2);
3661 }
3662
3663 #[test]
3664 fn test_configuration_whitelist_setup() {
3665 let mut config = Configuration::default();
3666 config.whitelist_url = Some(vec!["https://example.com/public".into()]);
3667 assert_eq!(config.whitelist_url.as_ref().unwrap().len(), 1);
3668 }
3669
3670 #[test]
3671 fn test_configuration_external_domains() {
3672 let mut config = Configuration::default();
3673 config.external_domains_caseless = Arc::new(
3674 [
3675 case_insensitive_string::CaseInsensitiveString::from("Example.Com"),
3676 case_insensitive_string::CaseInsensitiveString::from("OTHER.org"),
3677 ]
3678 .into_iter()
3679 .collect(),
3680 );
3681 assert_eq!(config.external_domains_caseless.len(), 2);
3682 assert!(config.external_domains_caseless.contains(
3683 &case_insensitive_string::CaseInsensitiveString::from("example.com")
3684 ));
3685 }
3686
3687 #[test]
3688 fn test_configuration_budget() {
3689 let mut config = Configuration::default();
3690 let mut budget = hashbrown::HashMap::new();
3691 budget.insert(
3692 case_insensitive_string::CaseInsensitiveString::from("/path"),
3693 100u32,
3694 );
3695 config.budget = Some(budget);
3696 assert!(config.budget.is_some());
3697 assert_eq!(
3698 config.budget.as_ref().unwrap().get(
3699 &case_insensitive_string::CaseInsensitiveString::from("/path")
3700 ),
3701 Some(&100u32)
3702 );
3703 }
3704
3705 #[cfg(not(feature = "regex"))]
3706 #[test]
3707 fn test_allow_list_set_default() {
3708 let allow_list = AllowListSet::default();
3709 assert!(allow_list.0.is_empty());
3710 }
3711
3712 #[cfg(feature = "agent")]
3713 #[test]
3714 fn test_build_remote_multimodal_engine_preserves_dual_models() {
3715 use crate::features::automation::{
3716 ModelEndpoint, RemoteMultimodalConfigs, VisionRouteMode,
3717 };
3718
3719 let mut config = Configuration::default();
3720 let mm = RemoteMultimodalConfigs::new(
3721 "https://api.example.com/v1/chat/completions",
3722 "primary-model",
3723 )
3724 .with_vision_model(ModelEndpoint::new("vision-model").with_api_key("vision-key"))
3725 .with_text_model(
3726 ModelEndpoint::new("text-model")
3727 .with_api_url("https://text.example.com/v1/chat/completions")
3728 .with_api_key("text-key"),
3729 )
3730 .with_vision_route_mode(VisionRouteMode::TextFirst);
3731 config.remote_multimodal = Some(Box::new(mm));
3732
3733 let engine = config
3734 .build_remote_multimodal_engine()
3735 .expect("engine should be built");
3736
3737 assert_eq!(
3738 engine.vision_model.as_ref().map(|m| m.model_name.as_str()),
3739 Some("vision-model")
3740 );
3741 assert_eq!(
3742 engine.text_model.as_ref().map(|m| m.model_name.as_str()),
3743 Some("text-model")
3744 );
3745 assert_eq!(engine.vision_route_mode, VisionRouteMode::TextFirst);
3746 }
3747
3748 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3749 #[test]
3750 fn test_spider_browser_config_defaults() {
3751 let cfg = SpiderBrowserConfig::new("test-key");
3752 assert_eq!(cfg.api_key, "test-key");
3753 assert_eq!(cfg.wss_url, "wss://browser.spider.cloud/v1/browser");
3754 assert!(!cfg.stealth);
3755 assert!(cfg.browser.is_none());
3756 assert!(cfg.country.is_none());
3757 assert!(cfg.extra_params.is_none());
3758 }
3759
3760 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3761 #[test]
3762 fn test_spider_browser_connection_url_basic() {
3763 let cfg = SpiderBrowserConfig::new("sk-abc123");
3764 assert_eq!(
3765 cfg.connection_url(),
3766 "wss://browser.spider.cloud/v1/browser?token=sk-abc123"
3767 );
3768 }
3769
3770 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3771 #[test]
3772 fn test_spider_browser_connection_url_full() {
3773 let cfg = SpiderBrowserConfig::new("sk-abc123")
3774 .with_stealth(true)
3775 .with_browser("chrome")
3776 .with_country("us")
3777 .with_extra_params(vec![("timeout".into(), "30000".into())]);
3778 assert_eq!(
3779 cfg.connection_url(),
3780 "wss://browser.spider.cloud/v1/browser?token=sk-abc123&stealth=true&browser=chrome&country=us&timeout=30000"
3781 );
3782 }
3783
3784 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3785 #[test]
3786 fn test_spider_browser_connection_url_custom_wss() {
3787 let cfg = SpiderBrowserConfig::new("key")
3788 .with_wss_url("wss://custom.browser.example.com/v1/browser");
3789 assert_eq!(
3790 cfg.connection_url(),
3791 "wss://custom.browser.example.com/v1/browser?token=key"
3792 );
3793 }
3794
3795 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3796 #[test]
3797 fn test_with_spider_browser_sets_chrome_connection() {
3798 let mut config = Configuration::default();
3799 config.with_spider_browser("my-api-key");
3800 assert_eq!(
3801 config.chrome_connection_url.as_deref(),
3802 Some("wss://browser.spider.cloud/v1/browser?token=my-api-key")
3803 );
3804 assert!(config.spider_browser.is_some());
3805 }
3806
3807 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3808 #[test]
3809 fn test_with_spider_browser_config_stealth() {
3810 let mut config = Configuration::default();
3811 let browser_cfg = SpiderBrowserConfig::new("key")
3812 .with_stealth(true)
3813 .with_country("gb");
3814 config.with_spider_browser_config(browser_cfg);
3815 assert_eq!(
3816 config.chrome_connection_url.as_deref(),
3817 Some("wss://browser.spider.cloud/v1/browser?token=key&stealth=true&country=gb")
3818 );
3819 }
3820
3821 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3822 #[test]
3823 fn test_spider_browser_connection_urls_single_default() {
3824 let cfg = SpiderBrowserConfig::new("sk-abc123");
3826 assert_eq!(cfg.connection_urls(), vec![cfg.connection_url()]);
3827 assert_eq!(cfg.connection_urls().len(), 1);
3828 }
3829
3830 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3831 #[test]
3832 fn test_spider_browser_connection_urls_multi_peer() {
3833 let cfg = SpiderBrowserConfig::new("sk-abc123")
3835 .with_stealth(true)
3836 .with_wss_urls(vec![
3837 "wss://browser-a.spider.cloud/v1/browser".into(),
3838 "wss://browser-b.spider.cloud/v1/browser".into(),
3839 ]);
3840 assert_eq!(
3841 cfg.connection_urls(),
3842 vec![
3843 "wss://browser-a.spider.cloud/v1/browser?token=sk-abc123&stealth=true".to_string(),
3844 "wss://browser-b.spider.cloud/v1/browser?token=sk-abc123&stealth=true".to_string(),
3845 ]
3846 );
3847 }
3848
3849 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3850 #[test]
3851 fn test_with_spider_browser_config_multi_peer_uses_failover() {
3852 let mut config = Configuration::default();
3854 let browser_cfg = SpiderBrowserConfig::new("key").with_wss_urls(vec![
3855 "wss://browser-a.spider.cloud/v1/browser".into(),
3856 "wss://browser-b.spider.cloud/v1/browser".into(),
3857 ]);
3858 config.with_spider_browser_config(browser_cfg);
3859 assert_eq!(
3860 config.chrome_connection_urls.as_ref().map(|u| u.len()),
3861 Some(2),
3862 "multi-peer browser config must route through chrome_connection_urls (failover)"
3863 );
3864 assert!(
3865 config.chrome_connection_url.is_none(),
3866 "multi-peer config must not pin a single chrome_connection_url"
3867 );
3868 assert!(config.spider_browser.is_some());
3869 }
3870
3871 #[cfg(all(feature = "spider_cloud", feature = "chrome"))]
3872 #[test]
3873 fn test_with_spider_browser_config_single_peer_single_path() {
3874 let mut config = Configuration::default();
3876 config.with_spider_browser_config(SpiderBrowserConfig::new("key"));
3877 assert!(config.chrome_connection_url.is_some());
3878 assert!(config.chrome_connection_urls.is_none());
3879 }
3880
3881 #[test]
3882 fn enhancement_map_defaults_are_empty() {
3883 let s = EnhancementSettings::new();
3884 assert!(!s.is_customized());
3885 for e in CrawlEnhancement::ALL {
3886 assert_eq!(s.get(e), None, "{e:?} should have no override by default");
3887 }
3888 assert_eq!(EnhancementSettings::default(), s);
3889 }
3890
3891 #[test]
3892 fn enhancement_map_per_section_override() {
3893 let mut s = EnhancementSettings::new();
3894 s.set(CrawlEnhancement::DnsGuard, false)
3895 .set(CrawlEnhancement::RenderUpgrade, true);
3896 assert_eq!(s.get(CrawlEnhancement::DnsGuard), Some(false));
3897 assert_eq!(s.get(CrawlEnhancement::RenderUpgrade), Some(true));
3898 assert_eq!(s.get(CrawlEnhancement::PointerAssist), None);
3900 assert!(s.is_customized());
3901 s.clear(CrawlEnhancement::DnsGuard);
3903 assert_eq!(s.get(CrawlEnhancement::DnsGuard), None);
3904 }
3905
3906 #[test]
3907 fn enhancement_map_set_all_and_all_off() {
3908 let mut s = EnhancementSettings::new();
3909 s.set_all(false);
3910 for e in CrawlEnhancement::ALL {
3911 assert_eq!(s.get(e), Some(false));
3912 }
3913 assert_eq!(EnhancementSettings::all_off(), s);
3914 s.set_all(true);
3915 for e in CrawlEnhancement::ALL {
3916 assert_eq!(s.get(e), Some(true));
3917 }
3918 }
3919
3920 #[test]
3921 fn enhancement_settings_is_copy() {
3922 let mut a = EnhancementSettings::new();
3924 a.set(CrawlEnhancement::DnsHedge, false);
3925 let b = a; assert_eq!(a.get(CrawlEnhancement::DnsHedge), Some(false));
3927 assert_eq!(b.get(CrawlEnhancement::DnsHedge), Some(false));
3928 }
3929
3930 #[cfg(feature = "chrome")]
3931 #[test]
3932 fn enhancement_override_wins_over_env_default() {
3933 let mut s = EnhancementSettings::new();
3935 s.set(CrawlEnhancement::DnsGuard, false);
3936 assert!(!s.enabled(CrawlEnhancement::DnsGuard));
3937 s.set(CrawlEnhancement::DnsGuard, true);
3938 assert!(s.enabled(CrawlEnhancement::DnsGuard));
3939 assert_eq!(
3941 EnhancementSettings::new().enabled(CrawlEnhancement::PointerAssist),
3942 CrawlEnhancement::PointerAssist.env_default()
3943 );
3944 }
3945
3946 #[cfg(feature = "chrome")]
3947 #[test]
3948 fn opt_out_flag_disable_tokens() {
3949 use crate::utils::opt_out_flag;
3950 assert!(opt_out_flag(None));
3952 assert!(opt_out_flag(Some("")));
3953 assert!(opt_out_flag(Some("1")));
3954 assert!(opt_out_flag(Some("true")));
3955 assert!(opt_out_flag(Some("on")));
3956 assert!(opt_out_flag(Some("anything")));
3957 assert!(!opt_out_flag(Some("0")));
3959 assert!(!opt_out_flag(Some(" 0 ")));
3960 assert!(!opt_out_flag(Some("false")));
3961 assert!(!opt_out_flag(Some("FALSE")));
3962 assert!(!opt_out_flag(Some("off")));
3963 assert!(!opt_out_flag(Some(" Off ")));
3964 }
3965}