viewpoint_cdp/protocol/fetch/
mod.rs

1//! Fetch domain types.
2//!
3//! The Fetch domain allows intercepting network requests, modifying them,
4//! and providing custom responses. It's the primary mechanism for request
5//! routing and mocking in browser automation.
6
7use serde::{Deserialize, Serialize};
8
9use super::network::{Request, ResourceType};
10
11/// Unique request identifier for the Fetch domain.
12pub type RequestId = String;
13
14/// Response HTTP header entry.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct HeaderEntry {
17    /// Header name.
18    pub name: String,
19    /// Header value.
20    pub value: String,
21}
22
23/// Stage at which to begin intercepting requests.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum RequestStage {
26    /// Intercept before the request is sent.
27    #[default]
28    Request,
29    /// Intercept after the response is received (but before response body is received).
30    Response,
31}
32
33/// Request pattern for interception.
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35#[serde(rename_all = "camelCase")]
36pub struct RequestPattern {
37    /// Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed.
38    /// Escape character is backslash. Omitting is equivalent to "*".
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub url_pattern: Option<String>,
41
42    /// If set, only requests for matching resource types will be intercepted.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub resource_type: Option<ResourceType>,
45
46    /// Stage at which to begin intercepting requests. Default is Request.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub request_stage: Option<RequestStage>,
49}
50
51impl RequestPattern {
52    /// Create a new request pattern matching all URLs.
53    pub fn all() -> Self {
54        Self::default()
55    }
56
57    /// Create a new request pattern matching the specified URL pattern.
58    pub fn url(pattern: impl Into<String>) -> Self {
59        Self {
60            url_pattern: Some(pattern.into()),
61            ..Default::default()
62        }
63    }
64
65    /// Set the resource type filter.
66    #[must_use]
67    pub fn with_resource_type(mut self, resource_type: ResourceType) -> Self {
68        self.resource_type = Some(resource_type);
69        self
70    }
71
72    /// Set the request stage.
73    #[must_use]
74    pub fn with_stage(mut self, stage: RequestStage) -> Self {
75        self.request_stage = Some(stage);
76        self
77    }
78}
79
80// =============================================================================
81// Commands
82// =============================================================================
83
84/// Parameters for Fetch.enable.
85#[derive(Debug, Clone, Serialize, Default)]
86#[serde(rename_all = "camelCase")]
87pub struct EnableParams {
88    /// If specified, only requests matching any of these patterns will produce
89    /// fetchRequested event and will be paused until client's response.
90    /// If not set, all requests will be affected.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub patterns: Option<Vec<RequestPattern>>,
93
94    /// If true, authRequired events will be issued and requests will be paused
95    /// expecting a call to continueWithAuth.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub handle_auth_requests: Option<bool>,
98}
99
100/// Parameters for Fetch.disable.
101#[derive(Debug, Clone, Serialize, Default)]
102pub struct DisableParams {}
103
104/// Parameters for Fetch.continueRequest.
105#[derive(Debug, Clone, Serialize, Default)]
106#[serde(rename_all = "camelCase")]
107pub struct ContinueRequestParams {
108    /// An id the client received in requestPaused event.
109    pub request_id: RequestId,
110
111    /// If set, the request url will be modified in a way that's not observable by page.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub url: Option<String>,
114
115    /// If set, the request method is overridden.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub method: Option<String>,
118
119    /// If set, overrides the post data in the request.
120    /// (Encoded as a base64 string when passed over JSON)
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub post_data: Option<String>,
123
124    /// If set, overrides the request headers.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub headers: Option<Vec<HeaderEntry>>,
127
128    /// If set, overrides response interception behavior for this request.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub intercept_response: Option<bool>,
131}
132
133/// Parameters for Fetch.fulfillRequest.
134#[derive(Debug, Clone, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct FulfillRequestParams {
137    /// An id the client received in requestPaused event.
138    pub request_id: RequestId,
139
140    /// An HTTP response code.
141    pub response_code: i32,
142
143    /// Response headers.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub response_headers: Option<Vec<HeaderEntry>>,
146
147    /// Alternative way of specifying response headers as a \0-separated
148    /// series of name: value pairs.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub binary_response_headers: Option<String>,
151
152    /// A response body. If absent, original response body will be used if
153    /// the request is intercepted at the response stage and empty body
154    /// will be used if the request is intercepted at the request stage.
155    /// (Encoded as a base64 string when passed over JSON)
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub body: Option<String>,
158
159    /// A textual representation of responseCode.
160    /// If absent, a standard phrase matching responseCode is used.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub response_phrase: Option<String>,
163}
164
165/// Parameters for Fetch.failRequest.
166#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct FailRequestParams {
169    /// An id the client received in requestPaused event.
170    pub request_id: RequestId,
171
172    /// Causes the request to fail with the given reason.
173    pub error_reason: ErrorReason,
174}
175
176/// Parameters for Fetch.getResponseBody.
177#[derive(Debug, Clone, Serialize)]
178#[serde(rename_all = "camelCase")]
179pub struct GetResponseBodyParams {
180    /// Identifier for the intercepted request to get body for.
181    pub request_id: RequestId,
182}
183
184/// Result for Fetch.getResponseBody.
185#[derive(Debug, Clone, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct GetResponseBodyResult {
188    /// Response body.
189    pub body: String,
190
191    /// True, if content was sent as base64.
192    pub base64_encoded: bool,
193}
194
195/// Parameters for Fetch.continueWithAuth.
196#[derive(Debug, Clone, Serialize)]
197#[serde(rename_all = "camelCase")]
198pub struct ContinueWithAuthParams {
199    /// An id the client received in authRequired event.
200    pub request_id: RequestId,
201
202    /// Response to with an authChallenge.
203    pub auth_challenge_response: AuthChallengeResponse,
204}
205
206/// Parameters for Fetch.continueResponse (experimental).
207#[derive(Debug, Clone, Serialize, Default)]
208#[serde(rename_all = "camelCase")]
209pub struct ContinueResponseParams {
210    /// An id the client received in requestPaused event.
211    pub request_id: RequestId,
212
213    /// An HTTP response code. If absent, original response code will be used.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub response_code: Option<i32>,
216
217    /// A textual representation of responseCode.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub response_phrase: Option<String>,
220
221    /// Response headers. If absent, original response headers will be used.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub response_headers: Option<Vec<HeaderEntry>>,
224
225    /// Alternative way of specifying response headers.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub binary_response_headers: Option<String>,
228}
229
230// =============================================================================
231// Events
232// =============================================================================
233
234/// Event: Fetch.requestPaused
235///
236/// Issued when the domain is enabled and the request URL matches the
237/// specified filter. The request is paused until the client responds
238/// with one of continueRequest, failRequest or fulfillRequest.
239#[derive(Debug, Clone, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct RequestPausedEvent {
242    /// Each request the page makes will have a unique id.
243    pub request_id: RequestId,
244
245    /// The details of the request.
246    pub request: Request,
247
248    /// The id of the frame that initiated the request.
249    pub frame_id: String,
250
251    /// How the requested resource will be used.
252    pub resource_type: ResourceType,
253
254    /// Response error if intercepted at response stage.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub response_error_reason: Option<ErrorReason>,
257
258    /// Response code if intercepted at response stage.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub response_status_code: Option<i32>,
261
262    /// Response status text if intercepted at response stage.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub response_status_text: Option<String>,
265
266    /// Response headers if intercepted at the response stage.
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub response_headers: Option<Vec<HeaderEntry>>,
269
270    /// If the intercepted request had a corresponding Network.requestWillBeSent event,
271    /// then this networkId will be the same as the requestId in that event.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub network_id: Option<String>,
274
275    /// If the request is due to a redirect response from the server,
276    /// the id of the request that has caused the redirect.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub redirected_request_id: Option<RequestId>,
279}
280
281impl RequestPausedEvent {
282    /// Check if this event is at the response stage.
283    pub fn is_response_stage(&self) -> bool {
284        self.response_error_reason.is_some() || self.response_status_code.is_some()
285    }
286
287    /// Check if this event is at the request stage.
288    pub fn is_request_stage(&self) -> bool {
289        !self.is_response_stage()
290    }
291
292    /// Check if this is a redirect response.
293    pub fn is_redirect(&self) -> bool {
294        if let Some(code) = self.response_status_code {
295            matches!(code, 301 | 302 | 303 | 307 | 308)
296                && self.response_headers.as_ref().is_some_and(|headers| {
297                    headers
298                        .iter()
299                        .any(|h| h.name.eq_ignore_ascii_case("location"))
300                })
301        } else {
302            false
303        }
304    }
305}
306
307/// Event: Fetch.authRequired
308///
309/// Issued when the domain is enabled with handleAuthRequests set to true.
310/// The request is paused until client responds with continueWithAuth.
311#[derive(Debug, Clone, Deserialize)]
312#[serde(rename_all = "camelCase")]
313pub struct AuthRequiredEvent {
314    /// Each request the page makes will have a unique id.
315    pub request_id: RequestId,
316
317    /// The details of the request.
318    pub request: Request,
319
320    /// The id of the frame that initiated the request.
321    pub frame_id: String,
322
323    /// How the requested resource will be used.
324    pub resource_type: ResourceType,
325
326    /// Details of the Authorization Challenge encountered.
327    pub auth_challenge: AuthChallenge,
328}
329
330// =============================================================================
331// Types
332// =============================================================================
333
334/// Network level fetch failure reason.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
336pub enum ErrorReason {
337    /// Generic failure.
338    #[default]
339    Failed,
340    /// Request was aborted.
341    Aborted,
342    /// Request timed out.
343    TimedOut,
344    /// Access was denied.
345    AccessDenied,
346    /// Connection was closed.
347    ConnectionClosed,
348    /// Connection was reset.
349    ConnectionReset,
350    /// Connection was refused.
351    ConnectionRefused,
352    /// Connection was aborted.
353    ConnectionAborted,
354    /// Connection failed.
355    ConnectionFailed,
356    /// Name could not be resolved.
357    NameNotResolved,
358    /// Internet is disconnected.
359    InternetDisconnected,
360    /// Address is unreachable.
361    AddressUnreachable,
362    /// Blocked by client.
363    BlockedByClient,
364    /// Blocked by response.
365    BlockedByResponse,
366}
367
368impl ErrorReason {
369    /// Get the CDP string representation of this error reason.
370    pub fn as_str(&self) -> &'static str {
371        match self {
372            Self::Failed => "Failed",
373            Self::Aborted => "Aborted",
374            Self::TimedOut => "TimedOut",
375            Self::AccessDenied => "AccessDenied",
376            Self::ConnectionClosed => "ConnectionClosed",
377            Self::ConnectionReset => "ConnectionReset",
378            Self::ConnectionRefused => "ConnectionRefused",
379            Self::ConnectionAborted => "ConnectionAborted",
380            Self::ConnectionFailed => "ConnectionFailed",
381            Self::NameNotResolved => "NameNotResolved",
382            Self::InternetDisconnected => "InternetDisconnected",
383            Self::AddressUnreachable => "AddressUnreachable",
384            Self::BlockedByClient => "BlockedByClient",
385            Self::BlockedByResponse => "BlockedByResponse",
386        }
387    }
388}
389
390/// Authorization challenge for HTTP status code 401 or 407.
391#[derive(Debug, Clone, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct AuthChallenge {
394    /// Source of the authentication challenge.
395    pub source: AuthChallengeSource,
396
397    /// Origin of the challenger.
398    pub origin: String,
399
400    /// The authentication scheme used, such as basic or digest.
401    pub scheme: String,
402
403    /// The realm of the challenge. May be empty.
404    pub realm: String,
405}
406
407/// Source of the authentication challenge.
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409pub enum AuthChallengeSource {
410    /// Server authentication.
411    Server,
412    /// Proxy authentication.
413    Proxy,
414}
415
416/// Response to an `AuthChallenge`.
417#[derive(Debug, Clone, Serialize)]
418#[serde(rename_all = "camelCase")]
419pub struct AuthChallengeResponse {
420    /// The decision on what to do in response to the authorization challenge.
421    pub response: AuthChallengeResponseType,
422
423    /// The username to provide, possibly empty.
424    /// Should only be set if response is `ProvideCredentials`.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub username: Option<String>,
427
428    /// The password to provide, possibly empty.
429    /// Should only be set if response is `ProvideCredentials`.
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub password: Option<String>,
432}
433
434/// The decision on what to do in response to the authorization challenge.
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
436pub enum AuthChallengeResponseType {
437    /// Defer to the default behavior of the net stack.
438    Default,
439    /// Cancel the authentication.
440    CancelAuth,
441    /// Provide credentials.
442    ProvideCredentials,
443}
444
445impl AuthChallengeResponse {
446    /// Create a default response (defer to browser).
447    pub fn default_response() -> Self {
448        Self {
449            response: AuthChallengeResponseType::Default,
450            username: None,
451            password: None,
452        }
453    }
454
455    /// Create a cancel response.
456    pub fn cancel() -> Self {
457        Self {
458            response: AuthChallengeResponseType::CancelAuth,
459            username: None,
460            password: None,
461        }
462    }
463
464    /// Create a response providing credentials.
465    pub fn provide_credentials(username: impl Into<String>, password: impl Into<String>) -> Self {
466        Self {
467            response: AuthChallengeResponseType::ProvideCredentials,
468            username: Some(username.into()),
469            password: Some(password.into()),
470        }
471    }
472}
473
474// Unit tests moved to tests/integration_tests.rs