pub mod artifacts;
pub(crate) mod deadline;
pub(crate) mod page_classification;
pub mod pipeline;
pub mod result;
pub mod wait;
pub mod writer;
pub type FetchCookie = cookie::Cookie<'static>;
pub use cookie::SameSite as FetchCookieSameSite;
pub use pipeline::{NetworkBodies, RenderMode};
pub use result::{FetchError, FetchResult, PageKind};
pub use wait::Wait;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::time::Duration;
use crate::sdk::client::Client;
use crate::shared::artifacts::Artifact;
use crate::shared::error::Error;
use crate::shared::ids::TabId;
pub const DEFAULT_NETWORK_BODY_MAX_BYTES: u64 = 10 * 1024 * 1024;
#[derive(Clone)]
pub struct FetchBuilder {
pub(crate) client: Client,
pub(crate) url: String,
pub(crate) render: RenderMode,
pub(crate) wait: Wait,
pub(crate) timeout: Duration,
pub(crate) want: BTreeSet<Artifact>,
pub(crate) want_explicit: bool,
pub(crate) tab: Option<TabId>,
pub(crate) keep_tab_open: bool,
pub(crate) request: RequestOptions,
pub(crate) out_dir: Option<PathBuf>,
pub(crate) readiness: ReadinessOptions,
pub(crate) network: NetworkCapture,
pub(crate) retry: RetryOptions,
pub(crate) http: HttpOptions,
pub(crate) cookie_jar: CookieJarOptions,
}
#[derive(Clone)]
pub(crate) struct ReadinessOptions {
pub(crate) idle_ms: u64,
pub(crate) stable_ms: u64,
pub(crate) min_text_bytes: u64,
pub(crate) observe_main_wait_ms: u64,
}
#[derive(Clone)]
pub(crate) struct NetworkCapture {
pub(crate) bodies: NetworkBodies,
pub(crate) body_max_bytes: u64,
pub(crate) redact: bool,
pub(crate) capture_ws: bool,
pub(crate) capture_sse: bool,
}
#[derive(Clone)]
pub(crate) struct RetryOptions {
pub(crate) attempts: u32,
pub(crate) backoff_ms: u64,
}
#[derive(Clone)]
pub(crate) struct HttpOptions {
pub(crate) proxy: Option<String>,
pub(crate) ca_cert: Option<PathBuf>,
pub(crate) tls_insecure: bool,
pub(crate) max_response_bytes: u64,
}
#[derive(Clone)]
pub(crate) struct CookieJarOptions {
pub(crate) path: Option<PathBuf>,
pub(crate) warning: Option<String>,
pub(crate) disabled: bool,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct RequestOptions {
pub(crate) headers: Vec<(String, String)>,
pub(crate) user_agent: Option<String>,
pub(crate) cookies: Vec<FetchCookie>,
pub(crate) evaluate_after_wait: Vec<String>,
pub(crate) method: Option<String>,
pub(crate) body: Option<Vec<u8>>,
pub(crate) form: Vec<(String, String)>,
}
impl FetchBuilder {
pub(crate) fn new(client: Client, url: String) -> Self {
Self {
client,
url,
render: RenderMode::Auto,
wait: Wait::Auto,
timeout: Duration::from_secs(30),
want: Artifact::HTTP_DEFAULT.iter().copied().collect(),
want_explicit: false,
tab: None,
keep_tab_open: false,
request: RequestOptions::default(),
out_dir: None,
readiness: ReadinessOptions {
idle_ms: 800,
stable_ms: 500,
min_text_bytes: 32,
observe_main_wait_ms: 500,
},
network: NetworkCapture {
bodies: NetworkBodies::Off,
body_max_bytes: DEFAULT_NETWORK_BODY_MAX_BYTES,
redact: true,
capture_ws: false,
capture_sse: false,
},
retry: RetryOptions {
attempts: 0,
backoff_ms: 250,
},
http: HttpOptions {
proxy: None,
ca_cert: None,
tls_insecure: false,
max_response_bytes: 1_073_741_824,
},
cookie_jar: CookieJarOptions {
path: None,
warning: None,
disabled: false,
},
}
}
#[must_use]
pub fn render(mut self, mode: RenderMode) -> Self {
self.render = mode;
self
}
#[must_use]
pub fn wait(mut self, w: Wait) -> Self {
self.wait = w;
self
}
#[must_use]
pub fn timeout(mut self, d: Duration) -> Self {
self.timeout = d;
self
}
#[must_use]
pub fn readiness_idle_ms(mut self, ms: u64) -> Self {
self.readiness.idle_ms = ms;
self
}
#[must_use]
pub fn readiness_stable_ms(mut self, ms: u64) -> Self {
self.readiness.stable_ms = ms;
self
}
#[must_use]
pub fn readiness_min_text_bytes(mut self, bytes: u64) -> Self {
self.readiness.min_text_bytes = bytes;
self
}
#[must_use]
pub fn want<I: IntoIterator<Item = Artifact>>(mut self, items: I) -> Self {
self.want = items.into_iter().collect();
self.want_explicit = true;
self
}
#[must_use]
pub fn tab(mut self, tab: TabId) -> Self {
self.tab = Some(tab);
self
}
#[must_use]
pub fn keep_tab_open(mut self, keep: bool) -> Self {
self.keep_tab_open = keep;
self
}
#[must_use]
pub fn network_bodies(mut self, mode: NetworkBodies) -> Self {
self.network.bodies = mode;
self
}
#[must_use]
pub fn network_body_max_bytes(mut self, n: u64) -> Self {
self.network.body_max_bytes = n;
self
}
#[must_use]
pub fn network_redact(mut self, on: bool) -> Self {
self.network.redact = on;
self
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.request.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn headers<I, K, V>(mut self, headers: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
self.request
.headers
.extend(headers.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
#[must_use]
pub fn user_agent(mut self, value: impl Into<String>) -> Self {
self.request.user_agent = Some(value.into());
self
}
#[must_use]
pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.request
.cookies
.push(cookie::Cookie::new(name.into(), value.into()));
self
}
#[must_use]
pub fn cookie_full(mut self, cookie: FetchCookie) -> Self {
self.request.cookies.push(cookie);
self
}
#[must_use]
pub fn cookies<I, K, V>(mut self, cookies: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
self.request.cookies.extend(
cookies
.into_iter()
.map(|(k, v)| cookie::Cookie::new(k.into(), v.into())),
);
self
}
#[must_use]
pub fn evaluate_after_wait(mut self, js: impl Into<String>) -> Self {
self.request.evaluate_after_wait.push(js.into());
self
}
#[must_use]
pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.out_dir = Some(dir.into());
self
}
#[must_use]
pub fn cookie_jar(mut self, path: impl Into<PathBuf>) -> Self {
self.cookie_jar.path = Some(path.into());
self
}
#[must_use]
pub fn no_cookie_jar(mut self) -> Self {
self.cookie_jar.disabled = true;
self
}
#[must_use]
pub fn observe_main_wait_ms(mut self, ms: u64) -> Self {
self.readiness.observe_main_wait_ms = ms;
self
}
#[must_use]
pub fn max_response_bytes(mut self, bytes: u64) -> Self {
self.http.max_response_bytes = bytes;
self
}
#[must_use]
pub fn retry(mut self, n: u32) -> Self {
self.retry.attempts = n;
self
}
#[must_use]
pub fn backoff_ms(mut self, ms: u64) -> Self {
self.retry.backoff_ms = ms;
self
}
#[must_use]
pub fn proxy(mut self, url: impl Into<String>) -> Self {
self.http.proxy = Some(url.into());
self
}
#[must_use]
pub fn ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
self.http.ca_cert = Some(path.into());
self
}
#[must_use]
pub fn tls_insecure(mut self, on: bool) -> Self {
self.http.tls_insecure = on;
self
}
#[must_use]
pub fn method(mut self, m: impl Into<String>) -> Self {
self.request.method = Some(m.into());
self
}
#[must_use]
pub fn body(mut self, data: impl Into<Vec<u8>>) -> Self {
self.request.body = Some(data.into());
self
}
#[must_use]
pub fn form_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.request.form.push((key.into(), value.into()));
self
}
#[must_use]
pub fn capture_ws(mut self, on: bool) -> Self {
self.network.capture_ws = on;
self
}
#[must_use]
pub fn capture_sse(mut self, on: bool) -> Self {
self.network.capture_sse = on;
self
}
pub async fn send(self) -> Result<FetchResult, Error> {
self.send_detailed().await.map_err(FetchError::into_error)
}
#[allow(
clippy::result_large_err,
reason = "FetchResult is the larger variant; see the size assertion in this module"
)]
pub async fn send_detailed(self) -> Result<FetchResult, FetchError> {
if self.retry.attempts == 0 {
return execute_once_with_timeout(self).await;
}
let max_attempts = self.retry.attempts.saturating_add(1);
let delay = std::time::Duration::from_millis(self.retry.backoff_ms);
let mut attempt: u32 = 0;
loop {
match execute_once_with_timeout(self.clone()).await {
Ok(r) => return Ok(r),
Err(e) if e.retryable && attempt + 1 < max_attempts => {
tokio::time::sleep(delay).await;
attempt += 1;
}
Err(e) => return Err(e),
}
}
}
}
const _: () = assert!(
std::mem::size_of::<FetchResult>() >= std::mem::size_of::<FetchError>(),
"FetchError is now the larger variant; box it instead of allowing result_large_err"
);
#[allow(
clippy::result_large_err,
reason = "FetchResult is the larger variant; see the size assertion above"
)]
async fn execute_once_with_timeout(builder: FetchBuilder) -> Result<FetchResult, FetchError> {
let timeout = builder.timeout;
let render_mode = builder.render.as_trace();
let deadline = deadline::FetchDeadline::new(timeout, render_mode);
match tokio::time::timeout(timeout, pipeline::execute(builder, deadline.clone())).await {
Ok(Ok(result)) => Ok(result),
Ok(Err(error)) => Err(FetchError::new(error, deadline.snapshot())),
Err(_) => {
let error = deadline.timeout_error();
Err(FetchError::new(error, deadline.snapshot()))
}
}
}