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