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::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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum RenderMode {
26 None,
28 #[default]
30 Auto,
31 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#[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}