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.continueRequest`.
180///
181/// All fields are optional — `None` means "leave Chrome's original value
182/// unchanged". Use [`Default`] to start with an empty override set.
183#[derive(Debug, Clone, Default)]
184pub struct RequestOverrides {
185    /// Replace the request URL.
186    pub url: Option<String>,
187    /// Replace the HTTP method.
188    pub method: Option<String>,
189    /// Replace the full header set (CDP semantics: this is *replacement*, not
190    /// merge — include every header you want sent). Order is preserved
191    /// on the wire.
192    pub headers: Option<Vec<(String, String)>>,
193    /// Replace the request body.
194    pub post_data: Option<Vec<u8>>,
195}
196
197#[cfg(test)]
198#[allow(clippy::panic, clippy::unwrap_used)]
199mod tests {
200    use super::*;
201
202    /// Snapshot every enum variant against its CDP wire string. Catches
203    /// silent typos that would otherwise only surface in live CDP traffic.
204    #[test]
205    fn enum_cdp_strings_snapshot() {
206        let pairs = serde_json::json!({
207            "RequestStage": [
208                ["Request", RequestStage::Request.as_cdp_str()],
209                ["Response", RequestStage::Response.as_cdp_str()],
210            ],
211            "ResourceType": [
212                ["Document", ResourceType::Document.as_cdp_str()],
213                ["Stylesheet", ResourceType::Stylesheet.as_cdp_str()],
214                ["Image", ResourceType::Image.as_cdp_str()],
215                ["Media", ResourceType::Media.as_cdp_str()],
216                ["Font", ResourceType::Font.as_cdp_str()],
217                ["Script", ResourceType::Script.as_cdp_str()],
218                ["TextTrack", ResourceType::TextTrack.as_cdp_str()],
219                ["XHR", ResourceType::XHR.as_cdp_str()],
220                ["Fetch", ResourceType::Fetch.as_cdp_str()],
221                ["EventSource", ResourceType::EventSource.as_cdp_str()],
222                ["WebSocket", ResourceType::WebSocket.as_cdp_str()],
223                ["Manifest", ResourceType::Manifest.as_cdp_str()],
224                ["SignedExchange", ResourceType::SignedExchange.as_cdp_str()],
225                ["Ping", ResourceType::Ping.as_cdp_str()],
226                ["CSPViolationReport", ResourceType::CSPViolationReport.as_cdp_str()],
227                ["Preflight", ResourceType::Preflight.as_cdp_str()],
228                ["Other", ResourceType::Other.as_cdp_str()],
229            ],
230            "AbortReason": [
231                ["Failed", AbortReason::Failed.as_cdp_str()],
232                ["Aborted", AbortReason::Aborted.as_cdp_str()],
233                ["TimedOut", AbortReason::TimedOut.as_cdp_str()],
234                ["AccessDenied", AbortReason::AccessDenied.as_cdp_str()],
235                ["ConnectionClosed", AbortReason::ConnectionClosed.as_cdp_str()],
236                ["ConnectionReset", AbortReason::ConnectionReset.as_cdp_str()],
237                ["ConnectionRefused", AbortReason::ConnectionRefused.as_cdp_str()],
238                ["ConnectionAborted", AbortReason::ConnectionAborted.as_cdp_str()],
239                ["ConnectionFailed", AbortReason::ConnectionFailed.as_cdp_str()],
240                ["NameNotResolved", AbortReason::NameNotResolved.as_cdp_str()],
241                ["InternetDisconnected", AbortReason::InternetDisconnected.as_cdp_str()],
242                ["AddressUnreachable", AbortReason::AddressUnreachable.as_cdp_str()],
243                ["BlockedByClient", AbortReason::BlockedByClient.as_cdp_str()],
244                ["BlockedByResponse", AbortReason::BlockedByResponse.as_cdp_str()],
245            ],
246        });
247        insta::assert_yaml_snapshot!("enum_cdp_strings", pairs);
248    }
249
250    #[test]
251    fn abort_reason_display_matches_cdp_string() {
252        for reason in [
253            AbortReason::Failed,
254            AbortReason::BlockedByClient,
255            AbortReason::NameNotResolved,
256        ] {
257            assert_eq!(reason.to_string(), reason.as_cdp_str());
258        }
259    }
260}