Skip to main content

agent_first_http/sdk/fetch/pipeline/
mod.rs

1//! Render-mode state machine.
2//!
3//! `RenderMode::None` uses the HTTP fast path (no browser). `Auto` tries
4//! HTTP first and escalates to the browser on connect failure / 5xx;
5//! `Always` skips the entire HTTP attempt.
6
7mod browser;
8mod cookie_jar_resolve;
9mod http_only;
10mod request_opts;
11
12use std::time::Instant;
13
14use serde::{Deserialize, Serialize};
15
16use crate::sdk::fetch::result::{EscalationReason, FetchResult};
17use crate::sdk::fetch::FetchBuilder;
18use crate::shared::artifacts::ArtifactPaths;
19use crate::shared::error::{Error, ErrorCode};
20use crate::shared::ids::RequestId;
21
22/// `--render` modes from `architecture.md §5`.
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum RenderMode {
26    /// HTTP only; never start a browser.
27    None,
28    /// HTTP first; escalate to browser on connect/5xx/non-HTML.
29    #[default]
30    Auto,
31    /// Always use the browser.
32    Always,
33}
34
35impl RenderMode {
36    pub fn parse(s: &str) -> Result<Self, Error> {
37        match s {
38            "none" => Ok(Self::None),
39            "auto" => Ok(Self::Auto),
40            "always" => Ok(Self::Always),
41            other => Err(Error::new(
42                ErrorCode::InvalidArgument,
43                format!("--render: unknown mode {other:?}; expected none|auto|always"),
44            )),
45        }
46    }
47
48    pub(crate) fn as_trace(self) -> crate::sdk::fetch::result::TraceRenderMode {
49        match self {
50            Self::None => crate::sdk::fetch::result::TraceRenderMode::None,
51            Self::Auto => crate::sdk::fetch::result::TraceRenderMode::Auto,
52            Self::Always => crate::sdk::fetch::result::TraceRenderMode::Always,
53        }
54    }
55}
56
57/// `--network-bodies` modes from `architecture.md §8`.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum NetworkBodies {
61    #[default]
62    Off,
63    Xhr,
64    All,
65}
66
67impl NetworkBodies {
68    pub fn parse(s: &str) -> Result<Self, Error> {
69        match s {
70            "off" => Ok(Self::Off),
71            "xhr" => Ok(Self::Xhr),
72            "all" => Ok(Self::All),
73            other => Err(Error::new(
74                ErrorCode::InvalidArgument,
75                format!("--network-bodies: unknown {other:?}; expected off|xhr|all"),
76            )),
77        }
78    }
79}
80
81pub(crate) async fn execute(mut builder: FetchBuilder) -> Result<FetchResult, Error> {
82    cookie_jar_resolve::resolve_cookie_jar_path(&mut builder).await?;
83    let request_options = request_opts::PreparedRequestOptions::from_builder(&builder)?;
84    let request_id = RequestId::new_v4();
85    let out_root = builder
86        .out_dir
87        .clone()
88        .unwrap_or_else(|| std::path::PathBuf::from("./afhttp-out"));
89    let paths = ArtifactPaths::new(out_root, &request_id);
90
91    let start = Instant::now();
92    match builder.render {
93        RenderMode::None => {
94            browser::reject_http_only_evaluate(&request_options)?;
95            http_only::http_only(&builder, &request_options, request_id, &paths, start, None)
96                .await
97                .map(|o| o.result)
98        }
99        RenderMode::Auto => {
100            match http_only::http_only(
101                &builder,
102                &request_options,
103                request_id.clone(),
104                &paths,
105                start,
106                None,
107            )
108            .await
109            {
110                Ok(o) if o.result.status < 400 => {
111                    if http_only::looks_like_empty_html_shell(
112                        &o.body_bytes,
113                        o.content_type.as_deref(),
114                    ) {
115                        return browser::browser_path(
116                            builder,
117                            request_options,
118                            request_id,
119                            paths,
120                            start,
121                            Some(EscalationReason::EMPTY_HTML_SHELL.to_string()),
122                        )
123                        .await;
124                    }
125                    browser::reject_http_only_evaluate(&request_options)?;
126                    Ok(o.result)
127                }
128                Err(e) if e.error_code == ErrorCode::InvalidArgument => Err(e),
129                outcome => {
130                    let reason = match &outcome {
131                        Ok(o) => EscalationReason::http_status(o.result.status),
132                        Err(e) => EscalationReason::http_failed(e.error_code.as_str()),
133                    };
134                    browser::browser_path(
135                        builder,
136                        request_options,
137                        request_id,
138                        paths,
139                        start,
140                        Some(reason),
141                    )
142                    .await
143                }
144            }
145        }
146        RenderMode::Always => {
147            browser::browser_path(builder, request_options, request_id, paths, start, None).await
148        }
149    }
150}
151
152pub(super) fn sensitive_capture(builder: &FetchBuilder) -> Vec<String> {
153    let mut risks = Vec::new();
154    if !builder.network_redact {
155        risks.push("network_redact_off_may_expose_tokens_or_pii".to_string());
156    }
157    if builder.capture_ws {
158        risks.push("capture_ws_may_expose_tokens_or_pii".to_string());
159    }
160    if builder.capture_sse {
161        risks.push("capture_sse_may_expose_tokens_or_pii".to_string());
162    }
163    risks
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn render_mode_parses() {
172        assert_eq!(RenderMode::parse("none").unwrap(), RenderMode::None);
173        assert_eq!(RenderMode::parse("auto").unwrap(), RenderMode::Auto);
174        assert_eq!(RenderMode::parse("always").unwrap(), RenderMode::Always);
175        assert!(RenderMode::parse("rocket").is_err());
176    }
177
178    #[test]
179    fn network_bodies_parses() {
180        assert_eq!(NetworkBodies::parse("off").unwrap(), NetworkBodies::Off);
181        assert_eq!(NetworkBodies::parse("xhr").unwrap(), NetworkBodies::Xhr);
182        assert_eq!(NetworkBodies::parse("all").unwrap(), NetworkBodies::All);
183        assert!(NetworkBodies::parse("some").is_err());
184    }
185}