Skip to main content

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}