agent_first_http/sdk/fetch/
mod.rs1pub mod artifacts;
6pub(crate) mod deadline;
7pub(crate) mod page_classification;
8pub mod pipeline;
9pub mod result;
10pub mod wait;
11pub mod writer;
12
13pub type FetchCookie = cookie::Cookie<'static>;
14pub use cookie::SameSite as FetchCookieSameSite;
15pub use pipeline::{NetworkBodies, RenderMode};
16pub use result::{FetchError, FetchResult, PageKind};
17pub use wait::Wait;
18
19use std::collections::BTreeSet;
20use std::path::PathBuf;
21use std::time::Duration;
22
23use crate::sdk::client::Client;
24use crate::shared::artifacts::Artifact;
25use crate::shared::error::Error;
26use crate::shared::ids::TabId;
27
28pub const DEFAULT_NETWORK_BODY_MAX_BYTES: u64 = 10 * 1024 * 1024;
30
31#[derive(Clone)]
33pub struct FetchBuilder {
34 pub(crate) client: Client,
35 pub(crate) url: String,
36 pub(crate) render: RenderMode,
37 pub(crate) wait: Wait,
38 pub(crate) timeout: Duration,
39 pub(crate) want: BTreeSet<Artifact>,
40 pub(crate) want_explicit: bool,
41 pub(crate) tab: Option<TabId>,
42 pub(crate) keep_tab_open: bool,
46 pub(crate) request: RequestOptions,
47 pub(crate) out_dir: Option<PathBuf>,
48 pub(crate) readiness: ReadinessOptions,
49 pub(crate) network: NetworkCapture,
50 pub(crate) retry: RetryOptions,
51 pub(crate) http: HttpOptions,
52 pub(crate) cookie_jar: CookieJarOptions,
53}
54
55#[derive(Clone)]
58pub(crate) struct ReadinessOptions {
59 pub(crate) idle_ms: u64,
60 pub(crate) stable_ms: u64,
61 pub(crate) min_text_bytes: u64,
62 pub(crate) observe_main_wait_ms: u64,
67}
68
69#[derive(Clone)]
72pub(crate) struct NetworkCapture {
73 pub(crate) bodies: NetworkBodies,
74 pub(crate) body_max_bytes: u64,
75 pub(crate) redact: bool,
76 pub(crate) capture_ws: bool,
78 pub(crate) capture_sse: bool,
80}
81
82#[derive(Clone)]
85pub(crate) struct RetryOptions {
86 pub(crate) attempts: u32,
89 pub(crate) backoff_ms: u64,
93}
94
95#[derive(Clone)]
98pub(crate) struct HttpOptions {
99 pub(crate) proxy: Option<String>,
104 pub(crate) ca_cert: Option<PathBuf>,
108 pub(crate) tls_insecure: bool,
111 pub(crate) max_response_bytes: u64,
117}
118
119#[derive(Clone)]
121pub(crate) struct CookieJarOptions {
122 pub(crate) path: Option<PathBuf>,
128 pub(crate) warning: Option<String>,
129 pub(crate) disabled: bool,
134}
135
136#[derive(Clone, Debug, Default)]
137pub(crate) struct RequestOptions {
138 pub(crate) headers: Vec<(String, String)>,
139 pub(crate) user_agent: Option<String>,
140 pub(crate) cookies: Vec<FetchCookie>,
141 pub(crate) evaluate_after_wait: Vec<String>,
142 pub(crate) method: Option<String>,
145 pub(crate) body: Option<Vec<u8>>,
147 pub(crate) form: Vec<(String, String)>,
150}
151
152impl FetchBuilder {
153 pub(crate) fn new(client: Client, url: String) -> Self {
154 Self {
155 client,
156 url,
157 render: RenderMode::Auto,
158 wait: Wait::Auto,
159 timeout: Duration::from_secs(30),
160 want: Artifact::HTTP_DEFAULT.iter().copied().collect(),
161 want_explicit: false,
162 tab: None,
163 keep_tab_open: false,
164 request: RequestOptions::default(),
165 out_dir: None,
166 readiness: ReadinessOptions {
167 idle_ms: 800,
168 stable_ms: 500,
169 min_text_bytes: 32,
170 observe_main_wait_ms: 500,
171 },
172 network: NetworkCapture {
173 bodies: NetworkBodies::Off,
174 body_max_bytes: DEFAULT_NETWORK_BODY_MAX_BYTES,
175 redact: true,
176 capture_ws: false,
177 capture_sse: false,
178 },
179 retry: RetryOptions {
180 attempts: 0,
181 backoff_ms: 250,
182 },
183 http: HttpOptions {
184 proxy: None,
185 ca_cert: None,
186 tls_insecure: false,
187 max_response_bytes: 1_073_741_824,
188 },
189 cookie_jar: CookieJarOptions {
190 path: None,
191 warning: None,
192 disabled: false,
193 },
194 }
195 }
196
197 #[must_use]
198 pub fn render(mut self, mode: RenderMode) -> Self {
199 self.render = mode;
200 self
201 }
202
203 #[must_use]
204 pub fn wait(mut self, w: Wait) -> Self {
205 self.wait = w;
206 self
207 }
208
209 #[must_use]
210 pub fn timeout(mut self, d: Duration) -> Self {
211 self.timeout = d;
212 self
213 }
214
215 #[must_use]
216 pub fn readiness_idle_ms(mut self, ms: u64) -> Self {
217 self.readiness.idle_ms = ms;
218 self
219 }
220
221 #[must_use]
222 pub fn readiness_stable_ms(mut self, ms: u64) -> Self {
223 self.readiness.stable_ms = ms;
224 self
225 }
226
227 #[must_use]
228 pub fn readiness_min_text_bytes(mut self, bytes: u64) -> Self {
229 self.readiness.min_text_bytes = bytes;
230 self
231 }
232
233 #[must_use]
234 pub fn want<I: IntoIterator<Item = Artifact>>(mut self, items: I) -> Self {
235 self.want = items.into_iter().collect();
236 self.want_explicit = true;
237 self
238 }
239
240 #[must_use]
241 pub fn tab(mut self, tab: TabId) -> Self {
242 self.tab = Some(tab);
243 self
244 }
245
246 #[must_use]
248 pub fn keep_tab_open(mut self, keep: bool) -> Self {
249 self.keep_tab_open = keep;
250 self
251 }
252
253 #[must_use]
254 pub fn network_bodies(mut self, mode: NetworkBodies) -> Self {
255 self.network.bodies = mode;
256 self
257 }
258
259 #[must_use]
260 pub fn network_body_max_bytes(mut self, n: u64) -> Self {
261 self.network.body_max_bytes = n;
262 self
263 }
264
265 #[must_use]
266 pub fn network_redact(mut self, on: bool) -> Self {
267 self.network.redact = on;
268 self
269 }
270
271 #[must_use]
275 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
276 self.request.headers.push((name.into(), value.into()));
277 self
278 }
279
280 #[must_use]
282 pub fn headers<I, K, V>(mut self, headers: I) -> Self
283 where
284 I: IntoIterator<Item = (K, V)>,
285 K: Into<String>,
286 V: Into<String>,
287 {
288 self.request
289 .headers
290 .extend(headers.into_iter().map(|(k, v)| (k.into(), v.into())));
291 self
292 }
293
294 #[must_use]
296 pub fn user_agent(mut self, value: impl Into<String>) -> Self {
297 self.request.user_agent = Some(value.into());
298 self
299 }
300
301 #[must_use]
303 pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
304 self.request
305 .cookies
306 .push(cookie::Cookie::new(name.into(), value.into()));
307 self
308 }
309
310 #[must_use]
313 pub fn cookie_full(mut self, cookie: FetchCookie) -> Self {
314 self.request.cookies.push(cookie);
315 self
316 }
317
318 #[must_use]
320 pub fn cookies<I, K, V>(mut self, cookies: I) -> Self
321 where
322 I: IntoIterator<Item = (K, V)>,
323 K: Into<String>,
324 V: Into<String>,
325 {
326 self.request.cookies.extend(
327 cookies
328 .into_iter()
329 .map(|(k, v)| cookie::Cookie::new(k.into(), v.into())),
330 );
331 self
332 }
333
334 #[must_use]
337 pub fn evaluate_after_wait(mut self, js: impl Into<String>) -> Self {
338 self.request.evaluate_after_wait.push(js.into());
339 self
340 }
341
342 #[must_use]
343 pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
344 self.out_dir = Some(dir.into());
345 self
346 }
347
348 #[must_use]
355 pub fn cookie_jar(mut self, path: impl Into<PathBuf>) -> Self {
356 self.cookie_jar.path = Some(path.into());
357 self
358 }
359
360 #[must_use]
364 pub fn no_cookie_jar(mut self) -> Self {
365 self.cookie_jar.disabled = true;
366 self
367 }
368
369 #[must_use]
374 pub fn observe_main_wait_ms(mut self, ms: u64) -> Self {
375 self.readiness.observe_main_wait_ms = ms;
376 self
377 }
378
379 #[must_use]
384 pub fn max_response_bytes(mut self, bytes: u64) -> Self {
385 self.http.max_response_bytes = bytes;
386 self
387 }
388
389 #[must_use]
393 pub fn retry(mut self, n: u32) -> Self {
394 self.retry.attempts = n;
395 self
396 }
397
398 #[must_use]
400 pub fn backoff_ms(mut self, ms: u64) -> Self {
401 self.retry.backoff_ms = ms;
402 self
403 }
404
405 #[must_use]
411 pub fn proxy(mut self, url: impl Into<String>) -> Self {
412 self.http.proxy = Some(url.into());
413 self
414 }
415
416 #[must_use]
420 pub fn ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
421 self.http.ca_cert = Some(path.into());
422 self
423 }
424
425 #[must_use]
429 pub fn tls_insecure(mut self, on: bool) -> Self {
430 self.http.tls_insecure = on;
431 self
432 }
433
434 #[must_use]
436 pub fn method(mut self, m: impl Into<String>) -> Self {
437 self.request.method = Some(m.into());
438 self
439 }
440
441 #[must_use]
445 pub fn body(mut self, data: impl Into<Vec<u8>>) -> Self {
446 self.request.body = Some(data.into());
447 self
448 }
449
450 #[must_use]
454 pub fn form_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
455 self.request.form.push((key.into(), value.into()));
456 self
457 }
458
459 #[must_use]
462 pub fn capture_ws(mut self, on: bool) -> Self {
463 self.network.capture_ws = on;
464 self
465 }
466
467 #[must_use]
470 pub fn capture_sse(mut self, on: bool) -> Self {
471 self.network.capture_sse = on;
472 self
473 }
474
475 pub async fn send(self) -> Result<FetchResult, Error> {
479 self.send_detailed().await.map_err(FetchError::into_error)
480 }
481
482 pub async fn send_detailed(self) -> Result<FetchResult, FetchError> {
487 if self.retry.attempts == 0 {
488 return execute_once_with_timeout(self).await;
489 }
490 let max_attempts = self.retry.attempts.saturating_add(1);
491 let delay = std::time::Duration::from_millis(self.retry.backoff_ms);
492 let mut attempt: u32 = 0;
493 loop {
494 match execute_once_with_timeout(self.clone()).await {
495 Ok(r) => return Ok(r),
496 Err(e) if e.retryable && attempt + 1 < max_attempts => {
497 tokio::time::sleep(delay).await;
498 attempt += 1;
499 }
500 Err(e) => return Err(e),
501 }
502 }
503 }
504}
505
506async fn execute_once_with_timeout(builder: FetchBuilder) -> Result<FetchResult, FetchError> {
507 let timeout = builder.timeout;
508 let render_mode = builder.render.as_trace();
509 let deadline = deadline::FetchDeadline::new(timeout, render_mode);
510 match tokio::time::timeout(timeout, pipeline::execute(builder, deadline.clone())).await {
511 Ok(Ok(result)) => Ok(result),
512 Ok(Err(error)) => Err(FetchError::new(error, deadline.snapshot())),
513 Err(_) => {
514 let error = deadline.timeout_error();
515 Err(FetchError::new(error, deadline.snapshot()))
516 }
517 }
518}