zendriver_interception/types.rs
1//! Public types for the Fetch interception API.
2//!
3//! - [`RequestStage`] selects which lifecycle point Chrome pauses on.
4//! - [`ResourceType`] mirrors Chrome's `Network.ResourceType` enum.
5//! - [`AbortReason`] mirrors Chrome's `Network.ErrorReason` enum used by
6//! `Fetch.failRequest`.
7//! - [`RequestInfo`] / [`ResponseInfo`] / [`RequestOverrides`] carry the
8//! payloads surfaced to user code via the rule + stream APIs.
9
10/// The lifecycle stage at which Chrome pauses an intercepted request.
11///
12/// Maps to the `stage` field of CDP's `Fetch.RequestPattern`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
14pub enum RequestStage {
15 /// Pause before the request is sent.
16 Request,
17 /// Pause after the response headers have been received.
18 Response,
19}
20
21impl RequestStage {
22 /// CDP wire-string for this stage (`"Request"` / `"Response"`).
23 #[must_use]
24 pub fn as_cdp_str(&self) -> &'static str {
25 match self {
26 Self::Request => "Request",
27 Self::Response => "Response",
28 }
29 }
30}
31
32/// Resource type classification for an intercepted request.
33///
34/// Mirrors Chrome's [`Network.ResourceType`] enum used by `Fetch.RequestPattern`
35/// and the `resourceType` field on `Fetch.requestPaused` events.
36///
37/// [`Network.ResourceType`]: https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ResourceType
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
39pub enum ResourceType {
40 Document,
41 Stylesheet,
42 Image,
43 Media,
44 Font,
45 Script,
46 TextTrack,
47 XHR,
48 Fetch,
49 EventSource,
50 WebSocket,
51 Manifest,
52 SignedExchange,
53 Ping,
54 CSPViolationReport,
55 Preflight,
56 Other,
57}
58
59impl ResourceType {
60 /// CDP wire-string for this resource type, matching the
61 /// `Network.ResourceType` enum names exactly (e.g. `"XHR"`, `"Stylesheet"`).
62 #[must_use]
63 pub fn as_cdp_str(&self) -> &'static str {
64 match self {
65 Self::Document => "Document",
66 Self::Stylesheet => "Stylesheet",
67 Self::Image => "Image",
68 Self::Media => "Media",
69 Self::Font => "Font",
70 Self::Script => "Script",
71 Self::TextTrack => "TextTrack",
72 Self::XHR => "XHR",
73 Self::Fetch => "Fetch",
74 Self::EventSource => "EventSource",
75 Self::WebSocket => "WebSocket",
76 Self::Manifest => "Manifest",
77 Self::SignedExchange => "SignedExchange",
78 Self::Ping => "Ping",
79 Self::CSPViolationReport => "CSPViolationReport",
80 Self::Preflight => "Preflight",
81 Self::Other => "Other",
82 }
83 }
84}
85
86/// Reason supplied to `Fetch.failRequest` when aborting an intercepted request.
87///
88/// Mirrors Chrome's [`Network.ErrorReason`] enum verbatim.
89///
90/// [`Network.ErrorReason`]: https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-ErrorReason
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
92pub enum AbortReason {
93 Failed,
94 Aborted,
95 TimedOut,
96 AccessDenied,
97 ConnectionClosed,
98 ConnectionReset,
99 ConnectionRefused,
100 ConnectionAborted,
101 ConnectionFailed,
102 NameNotResolved,
103 InternetDisconnected,
104 AddressUnreachable,
105 BlockedByClient,
106 BlockedByResponse,
107}
108
109impl AbortReason {
110 /// CDP wire-string for this abort reason, matching the
111 /// `Network.ErrorReason` enum names exactly.
112 #[must_use]
113 pub fn as_cdp_str(&self) -> &'static str {
114 match self {
115 Self::Failed => "Failed",
116 Self::Aborted => "Aborted",
117 Self::TimedOut => "TimedOut",
118 Self::AccessDenied => "AccessDenied",
119 Self::ConnectionClosed => "ConnectionClosed",
120 Self::ConnectionReset => "ConnectionReset",
121 Self::ConnectionRefused => "ConnectionRefused",
122 Self::ConnectionAborted => "ConnectionAborted",
123 Self::ConnectionFailed => "ConnectionFailed",
124 Self::NameNotResolved => "NameNotResolved",
125 Self::InternetDisconnected => "InternetDisconnected",
126 Self::AddressUnreachable => "AddressUnreachable",
127 Self::BlockedByClient => "BlockedByClient",
128 Self::BlockedByResponse => "BlockedByResponse",
129 }
130 }
131}
132
133impl std::fmt::Display for AbortReason {
134 /// Renders as the CDP wire string (e.g. `"BlockedByClient"`), matching
135 /// how Chrome reports the reason on the wire and in log output.
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.write_str(self.as_cdp_str())
138 }
139}
140
141/// Information about an intercepted request, surfaced to rule closures and
142/// stream consumers.
143///
144/// Headers are a `Vec<(name, value)>` rather than a `HashMap` so duplicates
145/// (multiple `Set-Cookie`, multi-value `Cookie`, etc.) and Chrome's emission
146/// order survive the round-trip into user code and back through
147/// [`RequestOverrides`]. CDP's underlying wire shape is a `[{name, value}]`
148/// array; this type matches that shape.
149#[derive(Debug, Clone)]
150pub struct RequestInfo {
151 /// Full request URL (post-redirect resolution by Chrome).
152 pub url: String,
153 /// HTTP method (`GET`, `POST`, ...).
154 pub method: String,
155 /// Request headers as Chrome reported them. Order is Chrome's emission
156 /// order; duplicates are preserved.
157 pub headers: Vec<(String, String)>,
158 /// Request body, if any. Sourced from `postDataEntries` (binary-safe)
159 /// when present, otherwise the UTF-8 bytes of `postData`.
160 pub post_data: Option<Vec<u8>>,
161 /// Chrome's classification of the request's resource type.
162 pub resource_type: ResourceType,
163}
164
165/// Information about a response paused at the `Response` stage.
166///
167/// Headers are a `Vec<(name, value)>` so duplicate-keyed response headers
168/// (notably `Set-Cookie`) are not silently merged into a single value.
169#[derive(Debug, Clone)]
170pub struct ResponseInfo {
171 /// HTTP status code.
172 pub status: u16,
173 /// HTTP status line text (e.g. `"OK"`, `"Not Found"`).
174 pub status_text: String,
175 /// Response headers in Chrome's emission order; duplicates preserved.
176 pub headers: Vec<(String, String)>,
177}
178
179/// Per-field overrides for `Fetch.continueResponse`.
180///
181/// Applied to an upstream response paused at the `Response` stage to rewrite
182/// its status line and/or headers while keeping Chrome's original body
183/// (contrast with [`PausedRequest::respond`](crate::PausedRequest::respond),
184/// which serves a fully synthetic body). All fields are optional — `None`
185/// leaves Chrome's original value unchanged. Use [`Default`] to start empty.
186///
187/// Header semantics are CDP-faithful *replacement*, not merge: when `headers`
188/// is `Some`, the supplied set becomes the entire response header block, so
189/// include every header you want forwarded. `None` keeps Chrome's headers.
190#[derive(Debug, Clone, Default)]
191pub struct ResponseOverrides {
192 /// Replace the HTTP status code (`responseCode`). `None` keeps Chrome's.
193 pub status: Option<u16>,
194 /// Replace the HTTP status line text (`responsePhrase`, e.g. `"OK"`).
195 pub phrase: Option<String>,
196 /// Replace the full response header set (CDP semantics: *replacement*,
197 /// not merge). Order is preserved on the wire.
198 pub headers: Option<Vec<(String, String)>>,
199}
200
201/// Per-field overrides for `Fetch.continueRequest`.
202///
203/// All fields are optional — `None` means "leave Chrome's original value
204/// unchanged". Use [`Default`] to start with an empty override set.
205#[derive(Debug, Clone, Default)]
206pub struct RequestOverrides {
207 /// Replace the request URL.
208 pub url: Option<String>,
209 /// Replace the HTTP method.
210 pub method: Option<String>,
211 /// Replace the full header set (CDP semantics: this is *replacement*, not
212 /// merge — include every header you want sent). Order is preserved
213 /// on the wire.
214 pub headers: Option<Vec<(String, String)>>,
215 /// Replace the request body.
216 pub post_data: Option<Vec<u8>>,
217}
218
219#[cfg(test)]
220#[allow(clippy::panic, clippy::unwrap_used)]
221mod tests {
222 use super::*;
223
224 /// Snapshot every enum variant against its CDP wire string. Catches
225 /// silent typos that would otherwise only surface in live CDP traffic.
226 #[test]
227 fn enum_cdp_strings_snapshot() {
228 let pairs = serde_json::json!({
229 "RequestStage": [
230 ["Request", RequestStage::Request.as_cdp_str()],
231 ["Response", RequestStage::Response.as_cdp_str()],
232 ],
233 "ResourceType": [
234 ["Document", ResourceType::Document.as_cdp_str()],
235 ["Stylesheet", ResourceType::Stylesheet.as_cdp_str()],
236 ["Image", ResourceType::Image.as_cdp_str()],
237 ["Media", ResourceType::Media.as_cdp_str()],
238 ["Font", ResourceType::Font.as_cdp_str()],
239 ["Script", ResourceType::Script.as_cdp_str()],
240 ["TextTrack", ResourceType::TextTrack.as_cdp_str()],
241 ["XHR", ResourceType::XHR.as_cdp_str()],
242 ["Fetch", ResourceType::Fetch.as_cdp_str()],
243 ["EventSource", ResourceType::EventSource.as_cdp_str()],
244 ["WebSocket", ResourceType::WebSocket.as_cdp_str()],
245 ["Manifest", ResourceType::Manifest.as_cdp_str()],
246 ["SignedExchange", ResourceType::SignedExchange.as_cdp_str()],
247 ["Ping", ResourceType::Ping.as_cdp_str()],
248 ["CSPViolationReport", ResourceType::CSPViolationReport.as_cdp_str()],
249 ["Preflight", ResourceType::Preflight.as_cdp_str()],
250 ["Other", ResourceType::Other.as_cdp_str()],
251 ],
252 "AbortReason": [
253 ["Failed", AbortReason::Failed.as_cdp_str()],
254 ["Aborted", AbortReason::Aborted.as_cdp_str()],
255 ["TimedOut", AbortReason::TimedOut.as_cdp_str()],
256 ["AccessDenied", AbortReason::AccessDenied.as_cdp_str()],
257 ["ConnectionClosed", AbortReason::ConnectionClosed.as_cdp_str()],
258 ["ConnectionReset", AbortReason::ConnectionReset.as_cdp_str()],
259 ["ConnectionRefused", AbortReason::ConnectionRefused.as_cdp_str()],
260 ["ConnectionAborted", AbortReason::ConnectionAborted.as_cdp_str()],
261 ["ConnectionFailed", AbortReason::ConnectionFailed.as_cdp_str()],
262 ["NameNotResolved", AbortReason::NameNotResolved.as_cdp_str()],
263 ["InternetDisconnected", AbortReason::InternetDisconnected.as_cdp_str()],
264 ["AddressUnreachable", AbortReason::AddressUnreachable.as_cdp_str()],
265 ["BlockedByClient", AbortReason::BlockedByClient.as_cdp_str()],
266 ["BlockedByResponse", AbortReason::BlockedByResponse.as_cdp_str()],
267 ],
268 });
269 insta::assert_yaml_snapshot!("enum_cdp_strings", pairs);
270 }
271
272 #[test]
273 fn abort_reason_display_matches_cdp_string() {
274 for reason in [
275 AbortReason::Failed,
276 AbortReason::BlockedByClient,
277 AbortReason::NameNotResolved,
278 ] {
279 assert_eq!(reason.to_string(), reason.as_cdp_str());
280 }
281 }
282}