1use crate::engine::Engine;
4use crate::error::ApiError;
5use crate::types::{PdfParser, ScrapeOptions};
6
7pub const MAX_URL_LEN: usize = 8 * 1024;
9pub const MAX_SCRAPE_TIMEOUT_MS: u64 = 300_000;
11pub const MAX_WAIT_FOR_MS: u64 = 60_000;
13pub const MAX_CRAWL_LIMIT: usize = 100_000;
15pub const MAX_CRAWL_DEPTH: u64 = 100;
17pub const MAX_MAP_LIMIT: usize = 100_000;
19pub const MAX_URLS: usize = 10_000;
21
22pub fn check_url(raw: &str) -> Result<(), ApiError> {
23 if raw.len() > MAX_URL_LEN {
24 return Err(ApiError::invalid(format!(
25 "URL is too long: {} bytes, the maximum is {MAX_URL_LEN}",
26 raw.len()
27 )));
28 }
29 match url::Url::parse(raw) {
30 Ok(u) if matches!(u.scheme(), "http" | "https") && u.host().is_some() => Ok(()),
31 Ok(_) => Err(ApiError::invalid(format!(
32 "only http(s) URLs are supported: {raw}"
33 ))),
34 Err(e) => Err(ApiError::invalid(format!("invalid URL {raw:?}: {e}"))),
35 }
36}
37
38pub fn check_scrape_options(opts: &ScrapeOptions, engine: &dyn Engine) -> Result<(), ApiError> {
39 if opts.store_snapshot {
40 return Err(ApiError::unsupported(
41 "storeSnapshot is not supported; use the changeTracking format",
42 ));
43 }
44 if !opts.remove_base64_images {
45 return Err(ApiError::unsupported(
46 "removeBase64Images: false is not supported",
47 ));
48 }
49 if opts
50 .formats
51 .iter()
52 .filter_map(|f| f.options.get("schema"))
53 .any(|schema| !schema.is_object())
54 {
55 return Err(ApiError::invalid("formats[].schema must be an object"));
56 }
57 if opts.keep_session {
59 return Err(ApiError::invalid(
60 "keepSession is only available on POST /v2/scrape",
61 ));
62 }
63 if opts.discover_scripts
64 && (opts.engine.as_deref() != Some("http")
65 || !opts.wants("links")
66 || opts.egress.is_some()
67 || !opts.headers.is_empty()
68 || opts.if_none_match.is_some()
69 || opts.if_modified_since.is_some())
70 {
71 return Err(ApiError::unsupported(
72 "discoverScripts requires HTTP engine, links, and no custom headers/egress/conditional request",
73 ));
74 }
75 if let Some(p) = opts.proxy.as_deref()
76 && !matches!(p, "basic" | "auto")
77 {
78 let valid_url = url::Url::parse(p)
79 .ok()
80 .is_some_and(|u| matches!(u.scheme(), "http" | "https" | "socks5" | "socks5h"));
81 if !valid_url {
82 return Err(ApiError::unsupported(format!(
83 "proxy {p:?} is not supported: use \"basic\", \"auto\", or a valid http/https/socks5 URL"
84 )));
85 }
86 }
87 if let Some(e) = opts.engine.as_deref()
88 && !matches!(e, "http" | "browser" | "auto")
89 {
90 return Err(ApiError::invalid(format!(
91 "engine must be http, browser or auto, got {e:?}"
92 )));
93 }
94 if let Some(t) = opts.respond_timing.as_deref() {
95 if !crate::types::RESPOND_TIMINGS.contains(&t) {
96 return Err(ApiError::invalid(format!(
97 "respondTiming must be one of {}",
98 crate::types::RESPOND_TIMINGS.join(", ")
99 )));
100 }
101 if t != "html" && opts.engine.as_deref() == Some("http") {
102 return Err(ApiError::invalid(format!(
103 "respondTiming {t:?} waits for the rendered page and needs engine browser or auto"
104 )));
105 }
106 }
107 if let Some(c) = &opts.chunking {
108 const KINDS: [&str; 8] = [
109 "recursive",
110 "headings",
111 "paragraphs",
112 "sentences",
113 "fixed_tokens",
114 "sliding_window",
115 "overlapping",
116 "regex",
117 ];
118 let kind = c.get("type").and_then(|t| t.as_str());
119 if !kind.is_some_and(|k| KINDS.contains(&k)) {
120 return Err(ApiError::invalid(format!(
121 "chunking.type must be one of {}",
122 KINDS.join(", ")
123 )));
124 }
125 }
126 let supported = engine.supported_formats();
127 if let Some(f) = opts
128 .formats
129 .iter()
130 .find(|f| !supported.contains(&f.kind.as_str()))
131 {
132 return Err(ApiError::unsupported(format!(
133 "format {:?} is not supported by this build (supported: {})",
134 f.kind,
135 supported.join(", ")
136 )));
137 }
138 if !opts.actions.is_empty() {
139 if !engine.supports_actions() {
140 return Err(ApiError::unsupported(engine.actions_unavailable()));
141 }
142 if opts.engine.as_deref() == Some("http") {
143 return Err(ApiError::invalid(
144 "actions run in a browser and cannot be combined with engine \"http\"",
145 ));
146 }
147 if opts.respond_timing.as_deref() == Some("html") {
148 return Err(ApiError::invalid(
149 "actions run in a browser and cannot be combined with respondTiming \"html\"",
150 ));
151 }
152 }
153 if let Some(t) = opts.timeout {
154 if t < 1000 {
155 return Err(ApiError::invalid("timeout must be at least 1000 ms"));
156 }
157 if t > MAX_SCRAPE_TIMEOUT_MS {
158 return Err(ApiError::invalid(format!(
159 "timeout must not exceed {MAX_SCRAPE_TIMEOUT_MS} ms"
160 )));
161 }
162 if opts.wait_for > t / 2 {
163 return Err(ApiError::invalid("waitFor must not exceed half of timeout"));
164 }
165 }
166 if opts.wait_for > MAX_WAIT_FOR_MS {
167 return Err(ApiError::invalid(format!(
168 "waitFor must not exceed {MAX_WAIT_FOR_MS} ms"
169 )));
170 }
171 PdfParser::from_parsers(opts.parsers.as_ref())?;
173 Ok(())
174}
175
176pub fn check_url_count(n: usize) -> Result<(), ApiError> {
178 if n > MAX_URLS {
179 return Err(ApiError::invalid(format!(
180 "too many URLs: {n}, the maximum is {MAX_URLS}"
181 )));
182 }
183 Ok(())
184}
185
186pub const MAX_PARSE_FILE_BYTES: usize = 50 * 1024 * 1024;
188
189pub fn check_parse_options(opts: &ScrapeOptions, engine: &dyn Engine) -> Result<(), ApiError> {
192 check_scrape_options(opts, engine)?;
193 for kind in ["screenshot", "branding", "changeTracking"] {
194 if opts.wants(kind) {
195 return Err(ApiError::unsupported(format!(
196 "parse uploads do not support the {kind} format"
197 )));
198 }
199 }
200 let unsupported = [
201 (!opts.actions.is_empty(), "actions"),
202 (opts.wait_for > 0, "waitFor"),
203 (opts.location.is_some(), "location"),
204 (opts.mobile, "mobile"),
205 (
206 opts.engine.as_deref() == Some("browser"),
207 "engine \"browser\"",
208 ),
209 (opts.egress.is_some(), "egress"),
210 (opts.discover_scripts, "discoverScripts"),
211 (
212 opts.if_none_match.is_some() || opts.if_modified_since.is_some(),
213 "conditional requests",
214 ),
215 (
216 opts.proxy
217 .as_deref()
218 .is_some_and(|p| !matches!(p, "basic" | "auto")),
219 "proxy other than basic or auto",
220 ),
221 ];
222 if let Some((_, what)) = unsupported.iter().find(|(on, _)| *on) {
223 return Err(ApiError::unsupported(format!(
224 "parse uploads do not support {what}"
225 )));
226 }
227 Ok(())
228}