agent_first_http/sdk/fetch/pipeline/
mod.rs1mod 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::deadline::FetchDeadline;
17use crate::sdk::fetch::result::{EscalationReason, FetchResult, RenderDecision};
18use crate::sdk::fetch::FetchBuilder;
19use crate::shared::artifacts::ArtifactPaths;
20use crate::shared::error::{Error, ErrorCode};
21use crate::shared::ids::RequestId;
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum RenderMode {
27 None,
29 #[default]
31 Auto,
32 Always,
34}
35
36impl RenderMode {
37 pub fn parse(s: &str) -> Result<Self, Error> {
38 match s {
39 "none" => Ok(Self::None),
40 "auto" => Ok(Self::Auto),
41 "always" => Ok(Self::Always),
42 other => Err(Error::new(
43 ErrorCode::InvalidArgument,
44 format!("--render: unknown mode {other:?}; expected none|auto|always"),
45 )),
46 }
47 }
48
49 pub(crate) fn as_trace(self) -> crate::sdk::fetch::result::TraceRenderMode {
50 match self {
51 Self::None => crate::sdk::fetch::result::TraceRenderMode::None,
52 Self::Auto => crate::sdk::fetch::result::TraceRenderMode::Auto,
53 Self::Always => crate::sdk::fetch::result::TraceRenderMode::Always,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum NetworkBodies {
62 #[default]
63 Off,
64 Xhr,
65 All,
66}
67
68impl NetworkBodies {
69 pub fn parse(s: &str) -> Result<Self, Error> {
70 match s {
71 "off" => Ok(Self::Off),
72 "xhr" => Ok(Self::Xhr),
73 "all" => Ok(Self::All),
74 other => Err(Error::new(
75 ErrorCode::InvalidArgument,
76 format!("--network-bodies: unknown {other:?}; expected off|xhr|all"),
77 )),
78 }
79 }
80}
81
82pub(crate) async fn execute(
83 mut builder: FetchBuilder,
84 deadline: FetchDeadline,
85) -> Result<FetchResult, Error> {
86 deadline.update_trace(|trace| {
87 trace.render_mode = builder.render.as_trace();
88 trace.sensitive_capture = sensitive_capture(&builder);
89 });
90 deadline
91 .run_result(
92 "resolve_cookie_jar",
93 ErrorCode::NavigationTimeout,
94 cookie_jar_resolve::resolve_cookie_jar_path(&mut builder),
95 )
96 .await?;
97 deadline.update_trace(|trace| {
98 trace.cookie_jar_file = builder.cookie_jar.path.clone();
99 trace.cookie_jar_warning = builder.cookie_jar.warning.clone();
100 });
101 let request_options = deadline
102 .run_result(
103 "prepare_request",
104 ErrorCode::NavigationTimeout,
105 std::future::ready(request_opts::PreparedRequestOptions::from_builder(&builder)),
106 )
107 .await?;
108 let request_id = RequestId::new_v4();
109 let out_root = builder
110 .out_dir
111 .clone()
112 .unwrap_or_else(|| std::path::PathBuf::from("./afhttp-out"));
113 let paths = ArtifactPaths::new(out_root, &request_id);
114
115 let start = Instant::now();
116 match builder.render {
117 RenderMode::None => {
118 deadline.update_trace(|trace| {
119 trace.render_decision = RenderDecision::HttpOnly;
120 trace.render_used = false;
121 });
122 browser::reject_http_only_evaluate(&request_options)?;
123 http_only::http_only(
124 &builder,
125 &request_options,
126 request_id,
127 &paths,
128 start,
129 None,
130 &deadline,
131 )
132 .await
133 .map(|o| o.result)
134 }
135 RenderMode::Auto => {
136 deadline.update_trace(|trace| {
137 trace.render_decision = RenderDecision::HttpOnly;
138 trace.render_used = false;
139 });
140 match http_only::http_only(
141 &builder,
142 &request_options,
143 request_id.clone(),
144 &paths,
145 start,
146 None,
147 &deadline,
148 )
149 .await
150 {
151 Ok(o) if o.result.status < 400 => {
152 if let Some(classification) =
153 http_only::classify_http_body(&o.body_bytes, o.content_type.as_deref())
154 {
155 let reason = classification.code.as_str().to_string();
156 deadline.update_trace(|trace| {
157 trace.render_decision = RenderDecision::Browser;
158 trace.render_used = true;
159 trace.escalation_reason = Some(reason.clone());
160 });
161 return browser::browser_path(
162 builder,
163 request_options,
164 request_id,
165 paths,
166 start,
167 Some(reason),
168 &deadline,
169 )
170 .await;
171 }
172 if http_only::looks_like_empty_html_shell(
173 &o.body_bytes,
174 o.content_type.as_deref(),
175 ) {
176 deadline.update_trace(|trace| {
177 trace.render_decision = RenderDecision::Browser;
178 trace.render_used = true;
179 trace.escalation_reason =
180 Some(EscalationReason::EMPTY_HTML_SHELL.to_string());
181 });
182 return browser::browser_path(
183 builder,
184 request_options,
185 request_id,
186 paths,
187 start,
188 Some(EscalationReason::EMPTY_HTML_SHELL.to_string()),
189 &deadline,
190 )
191 .await;
192 }
193 browser::reject_http_only_evaluate(&request_options)?;
194 Ok(o.result)
195 }
196 Err(e) if e.error_code == ErrorCode::InvalidArgument => Err(e),
197 outcome => {
198 let reason = match &outcome {
199 Ok(o) => EscalationReason::http_status(o.result.status),
200 Err(e) => EscalationReason::http_failed(e.error_code.as_str()),
201 };
202 deadline.update_trace(|trace| {
203 trace.render_decision = RenderDecision::Browser;
204 trace.render_used = true;
205 trace.escalation_reason = Some(reason.clone());
206 });
207 browser::browser_path(
208 builder,
209 request_options,
210 request_id,
211 paths,
212 start,
213 Some(reason),
214 &deadline,
215 )
216 .await
217 }
218 }
219 }
220 RenderMode::Always => {
221 deadline.update_trace(|trace| {
222 trace.render_decision = RenderDecision::Browser;
223 trace.render_used = true;
224 });
225 browser::browser_path(
226 builder,
227 request_options,
228 request_id,
229 paths,
230 start,
231 None,
232 &deadline,
233 )
234 .await
235 }
236 }
237}
238
239pub(super) fn sensitive_capture(builder: &FetchBuilder) -> Vec<String> {
240 let mut risks = Vec::new();
241 if !builder.network.redact {
242 risks.push("network_redact_off_may_expose_tokens_or_pii".to_string());
243 }
244 if builder.network.capture_ws {
245 risks.push("capture_ws_may_expose_tokens_or_pii".to_string());
246 }
247 if builder.network.capture_sse {
248 risks.push("capture_sse_may_expose_tokens_or_pii".to_string());
249 }
250 risks
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn render_mode_parses() {
259 assert_eq!(RenderMode::parse("none").unwrap(), RenderMode::None);
260 assert_eq!(RenderMode::parse("auto").unwrap(), RenderMode::Auto);
261 assert_eq!(RenderMode::parse("always").unwrap(), RenderMode::Always);
262 assert!(RenderMode::parse("rocket").is_err());
263 }
264
265 #[test]
266 fn network_bodies_parses() {
267 assert_eq!(NetworkBodies::parse("off").unwrap(), NetworkBodies::Off);
268 assert_eq!(NetworkBodies::parse("xhr").unwrap(), NetworkBodies::Xhr);
269 assert_eq!(NetworkBodies::parse("all").unwrap(), NetworkBodies::All);
270 assert!(NetworkBodies::parse("some").is_err());
271 }
272}