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) tab: Option<TabId>,
41 pub(crate) request: RequestOptions,
42 pub(crate) out_dir: Option<PathBuf>,
43 pub(crate) readiness: ReadinessOptions,
44 pub(crate) network: NetworkCapture,
45 pub(crate) retry: RetryOptions,
46 pub(crate) http: HttpOptions,
47 pub(crate) cookie_jar: CookieJarOptions,
48}
49
50#[derive(Clone)]
53pub(crate) struct ReadinessOptions {
54 pub(crate) idle_ms: u64,
55 pub(crate) stable_ms: u64,
56 pub(crate) min_text_bytes: u64,
57 pub(crate) observe_main_wait_ms: u64,
62}
63
64#[derive(Clone)]
67pub(crate) struct NetworkCapture {
68 pub(crate) bodies: NetworkBodies,
69 pub(crate) body_max_bytes: u64,
70 pub(crate) redact: bool,
71 pub(crate) capture_ws: bool,
73 pub(crate) capture_sse: bool,
75}
76
77#[derive(Clone)]
80pub(crate) struct RetryOptions {
81 pub(crate) attempts: u32,
84 pub(crate) backoff_ms: u64,
88}
89
90#[derive(Clone)]
93pub(crate) struct HttpOptions {
94 pub(crate) proxy: Option<String>,
99 pub(crate) ca_cert: Option<PathBuf>,
103 pub(crate) tls_insecure: bool,
106 pub(crate) max_response_bytes: u64,
112}
113
114#[derive(Clone)]
116pub(crate) struct CookieJarOptions {
117 pub(crate) path: Option<PathBuf>,
123 pub(crate) warning: Option<String>,
124 pub(crate) disabled: bool,
129}
130
131#[derive(Clone, Debug, Default)]
132pub(crate) struct RequestOptions {
133 pub(crate) headers: Vec<(String, String)>,
134 pub(crate) user_agent: Option<String>,
135 pub(crate) cookies: Vec<FetchCookie>,
136 pub(crate) evaluate_after_wait: Vec<String>,
137 pub(crate) method: Option<String>,
140 pub(crate) body: Option<Vec<u8>>,
142 pub(crate) form: Vec<(String, String)>,
145}
146
147impl FetchBuilder {
148 pub(crate) fn new(client: Client, url: String) -> Self {
149 Self {
150 client,
151 url,
152 render: RenderMode::Auto,
153 wait: Wait::Auto,
154 timeout: Duration::from_secs(30),
155 want: Artifact::ALL.iter().copied().collect(),
156 tab: None,
157 request: RequestOptions::default(),
158 out_dir: None,
159 readiness: ReadinessOptions {
160 idle_ms: 800,
161 stable_ms: 500,
162 min_text_bytes: 32,
163 observe_main_wait_ms: 500,
164 },
165 network: NetworkCapture {
166 bodies: NetworkBodies::Off,
167 body_max_bytes: DEFAULT_NETWORK_BODY_MAX_BYTES,
168 redact: true,
169 capture_ws: false,
170 capture_sse: false,
171 },
172 retry: RetryOptions {
173 attempts: 0,
174 backoff_ms: 250,
175 },
176 http: HttpOptions {
177 proxy: None,
178 ca_cert: None,
179 tls_insecure: false,
180 max_response_bytes: 1_073_741_824,
181 },
182 cookie_jar: CookieJarOptions {
183 path: None,
184 warning: None,
185 disabled: false,
186 },
187 }
188 }
189
190 #[must_use]
191 pub fn render(mut self, mode: RenderMode) -> Self {
192 self.render = mode;
193 self
194 }
195
196 #[must_use]
197 pub fn wait(mut self, w: Wait) -> Self {
198 self.wait = w;
199 self
200 }
201
202 #[must_use]
203 pub fn timeout(mut self, d: Duration) -> Self {
204 self.timeout = d;
205 self
206 }
207
208 #[must_use]
209 pub fn readiness_idle_ms(mut self, ms: u64) -> Self {
210 self.readiness.idle_ms = ms;
211 self
212 }
213
214 #[must_use]
215 pub fn readiness_stable_ms(mut self, ms: u64) -> Self {
216 self.readiness.stable_ms = ms;
217 self
218 }
219
220 #[must_use]
221 pub fn readiness_min_text_bytes(mut self, bytes: u64) -> Self {
222 self.readiness.min_text_bytes = bytes;
223 self
224 }
225
226 #[must_use]
227 pub fn want<I: IntoIterator<Item = Artifact>>(mut self, items: I) -> Self {
228 self.want = items.into_iter().collect();
229 self
230 }
231
232 #[must_use]
233 pub fn tab(mut self, tab: TabId) -> Self {
234 self.tab = Some(tab);
235 self
236 }
237
238 #[must_use]
239 pub fn network_bodies(mut self, mode: NetworkBodies) -> Self {
240 self.network.bodies = mode;
241 self
242 }
243
244 #[must_use]
245 pub fn network_body_max_bytes(mut self, n: u64) -> Self {
246 self.network.body_max_bytes = n;
247 self
248 }
249
250 #[must_use]
251 pub fn network_redact(mut self, on: bool) -> Self {
252 self.network.redact = on;
253 self
254 }
255
256 #[must_use]
260 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
261 self.request.headers.push((name.into(), value.into()));
262 self
263 }
264
265 #[must_use]
267 pub fn headers<I, K, V>(mut self, headers: I) -> Self
268 where
269 I: IntoIterator<Item = (K, V)>,
270 K: Into<String>,
271 V: Into<String>,
272 {
273 self.request
274 .headers
275 .extend(headers.into_iter().map(|(k, v)| (k.into(), v.into())));
276 self
277 }
278
279 #[must_use]
281 pub fn user_agent(mut self, value: impl Into<String>) -> Self {
282 self.request.user_agent = Some(value.into());
283 self
284 }
285
286 #[must_use]
288 pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
289 self.request
290 .cookies
291 .push(cookie::Cookie::new(name.into(), value.into()));
292 self
293 }
294
295 #[must_use]
298 pub fn cookie_full(mut self, cookie: FetchCookie) -> Self {
299 self.request.cookies.push(cookie);
300 self
301 }
302
303 #[must_use]
305 pub fn cookies<I, K, V>(mut self, cookies: I) -> Self
306 where
307 I: IntoIterator<Item = (K, V)>,
308 K: Into<String>,
309 V: Into<String>,
310 {
311 self.request.cookies.extend(
312 cookies
313 .into_iter()
314 .map(|(k, v)| cookie::Cookie::new(k.into(), v.into())),
315 );
316 self
317 }
318
319 #[must_use]
322 pub fn evaluate_after_wait(mut self, js: impl Into<String>) -> Self {
323 self.request.evaluate_after_wait.push(js.into());
324 self
325 }
326
327 #[must_use]
328 pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
329 self.out_dir = Some(dir.into());
330 self
331 }
332
333 #[must_use]
340 pub fn cookie_jar(mut self, path: impl Into<PathBuf>) -> Self {
341 self.cookie_jar.path = Some(path.into());
342 self
343 }
344
345 #[must_use]
349 pub fn no_cookie_jar(mut self) -> Self {
350 self.cookie_jar.disabled = true;
351 self
352 }
353
354 #[must_use]
359 pub fn observe_main_wait_ms(mut self, ms: u64) -> Self {
360 self.readiness.observe_main_wait_ms = ms;
361 self
362 }
363
364 #[must_use]
369 pub fn max_response_bytes(mut self, bytes: u64) -> Self {
370 self.http.max_response_bytes = bytes;
371 self
372 }
373
374 #[must_use]
378 pub fn retry(mut self, n: u32) -> Self {
379 self.retry.attempts = n;
380 self
381 }
382
383 #[must_use]
385 pub fn backoff_ms(mut self, ms: u64) -> Self {
386 self.retry.backoff_ms = ms;
387 self
388 }
389
390 #[must_use]
396 pub fn proxy(mut self, url: impl Into<String>) -> Self {
397 self.http.proxy = Some(url.into());
398 self
399 }
400
401 #[must_use]
405 pub fn ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
406 self.http.ca_cert = Some(path.into());
407 self
408 }
409
410 #[must_use]
414 pub fn tls_insecure(mut self, on: bool) -> Self {
415 self.http.tls_insecure = on;
416 self
417 }
418
419 #[must_use]
421 pub fn method(mut self, m: impl Into<String>) -> Self {
422 self.request.method = Some(m.into());
423 self
424 }
425
426 #[must_use]
430 pub fn body(mut self, data: impl Into<Vec<u8>>) -> Self {
431 self.request.body = Some(data.into());
432 self
433 }
434
435 #[must_use]
439 pub fn form_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
440 self.request.form.push((key.into(), value.into()));
441 self
442 }
443
444 #[must_use]
447 pub fn capture_ws(mut self, on: bool) -> Self {
448 self.network.capture_ws = on;
449 self
450 }
451
452 #[must_use]
455 pub fn capture_sse(mut self, on: bool) -> Self {
456 self.network.capture_sse = on;
457 self
458 }
459
460 pub async fn send(self) -> Result<FetchResult, Error> {
464 self.send_detailed().await.map_err(FetchError::into_error)
465 }
466
467 pub async fn send_detailed(self) -> Result<FetchResult, FetchError> {
472 if self.retry.attempts == 0 {
473 return execute_once_with_timeout(self).await;
474 }
475 let max_attempts = self.retry.attempts.saturating_add(1);
476 let delay = std::time::Duration::from_millis(self.retry.backoff_ms);
477 let mut attempt: u32 = 0;
478 loop {
479 match execute_once_with_timeout(self.clone()).await {
480 Ok(r) => return Ok(r),
481 Err(e) if e.retryable && attempt + 1 < max_attempts => {
482 tokio::time::sleep(delay).await;
483 attempt += 1;
484 }
485 Err(e) => return Err(e),
486 }
487 }
488 }
489}
490
491async fn execute_once_with_timeout(builder: FetchBuilder) -> Result<FetchResult, FetchError> {
492 let timeout = builder.timeout;
493 let render_mode = builder.render.as_trace();
494 let deadline = deadline::FetchDeadline::new(timeout, render_mode);
495 match tokio::time::timeout(timeout, pipeline::execute(builder, deadline.clone())).await {
496 Ok(Ok(result)) => Ok(result),
497 Ok(Err(error)) => Err(FetchError::new(error, deadline.snapshot())),
498 Err(_) => {
499 let error = deadline.timeout_error();
500 Err(FetchError::new(error, deadline.snapshot()))
501 }
502 }
503}