playwright_core/protocol/
route.rs

1// Route protocol object
2//
3// Represents a route handler for network interception.
4// Routes are created when page.route() matches a request.
5//
6// See: https://playwright.dev/docs/api/class-route
7
8use crate::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
9use crate::error::Result;
10use crate::protocol::Request;
11use serde_json::{json, Value};
12use std::any::Any;
13use std::sync::Arc;
14
15/// Route represents a network route handler.
16///
17/// Routes allow intercepting, aborting, continuing, or fulfilling network requests.
18///
19/// See: <https://playwright.dev/docs/api/class-route>
20#[derive(Clone)]
21pub struct Route {
22    base: ChannelOwnerImpl,
23}
24
25impl Route {
26    /// Creates a new Route from protocol initialization
27    ///
28    /// This is called by the object factory when the server sends a `__create__` message
29    /// for a Route object.
30    pub fn new(
31        parent: Arc<dyn ChannelOwner>,
32        type_name: String,
33        guid: Arc<str>,
34        initializer: Value,
35    ) -> Result<Self> {
36        let base = ChannelOwnerImpl::new(
37            ParentOrConnection::Parent(parent.clone()),
38            type_name,
39            guid,
40            initializer,
41        );
42
43        Ok(Self { base })
44    }
45
46    /// Returns the request that is being routed.
47    ///
48    /// See: <https://playwright.dev/docs/api/class-route#route-request>
49    pub fn request(&self) -> Request {
50        // The Route's parent is the Request object
51        // Try to downcast the parent to Request
52        if let Some(parent) = self.parent() {
53            if let Some(request) = parent.as_any().downcast_ref::<Request>() {
54                return request.clone();
55            }
56        }
57
58        // Fallback: Create a stub Request from initializer data
59        // This should rarely happen in practice
60        let request_data = self
61            .initializer()
62            .get("request")
63            .cloned()
64            .unwrap_or_else(|| {
65                serde_json::json!({
66                    "url": "",
67                    "method": "GET"
68                })
69            });
70
71        let parent = self
72            .parent()
73            .unwrap_or_else(|| Arc::new(self.clone()) as Arc<dyn ChannelOwner>);
74
75        let request_guid = request_data
76            .get("guid")
77            .and_then(|v| v.as_str())
78            .unwrap_or("request-stub");
79
80        Request::new(
81            parent,
82            "Request".to_string(),
83            Arc::from(request_guid),
84            request_data,
85        )
86        .unwrap()
87    }
88
89    /// Aborts the route's request.
90    ///
91    /// # Arguments
92    ///
93    /// * `error_code` - Optional error code (default: "failed")
94    ///
95    /// Available error codes:
96    /// - "aborted" - User-initiated cancellation
97    /// - "accessdenied" - Permission denied
98    /// - "addressunreachable" - Host unreachable
99    /// - "blockedbyclient" - Client blocked request
100    /// - "connectionaborted", "connectionclosed", "connectionfailed", "connectionrefused", "connectionreset"
101    /// - "internetdisconnected"
102    /// - "namenotresolved"
103    /// - "timedout"
104    /// - "failed" - Generic error (default)
105    ///
106    /// See: <https://playwright.dev/docs/api/class-route#route-abort>
107    pub async fn abort(&self, error_code: Option<&str>) -> Result<()> {
108        let params = json!({
109            "errorCode": error_code.unwrap_or("failed")
110        });
111
112        self.channel()
113            .send::<_, serde_json::Value>("abort", params)
114            .await
115            .map(|_| ())
116    }
117
118    /// Continues the route's request with optional modifications.
119    ///
120    /// # Arguments
121    ///
122    /// * `overrides` - Optional modifications to apply to the request
123    ///
124    /// See: <https://playwright.dev/docs/api/class-route#route-continue>
125    pub async fn continue_(&self, overrides: Option<ContinueOptions>) -> Result<()> {
126        let mut params = json!({
127            "isFallback": false
128        });
129
130        // Add overrides if provided
131        if let Some(opts) = overrides {
132            // Add headers
133            if let Some(headers) = opts.headers {
134                let headers_array: Vec<serde_json::Value> = headers
135                    .into_iter()
136                    .map(|(name, value)| json!({"name": name, "value": value}))
137                    .collect();
138                params["headers"] = json!(headers_array);
139            }
140
141            // Add method
142            if let Some(method) = opts.method {
143                params["method"] = json!(method);
144            }
145
146            // Add postData (string or binary)
147            if let Some(post_data) = opts.post_data {
148                params["postData"] = json!(post_data);
149            } else if let Some(post_data_bytes) = opts.post_data_bytes {
150                use base64::Engine;
151                let encoded = base64::engine::general_purpose::STANDARD.encode(&post_data_bytes);
152                params["postData"] = json!(encoded);
153            }
154
155            // Add URL
156            if let Some(url) = opts.url {
157                params["url"] = json!(url);
158            }
159        }
160
161        self.channel()
162            .send::<_, serde_json::Value>("continue", params)
163            .await
164            .map(|_| ())
165    }
166
167    /// Fulfills the route's request with a custom response.
168    ///
169    /// # Arguments
170    ///
171    /// * `options` - Response configuration (status, headers, body, etc.)
172    ///
173    /// # Known Limitations
174    ///
175    /// **Response body fulfillment is not supported in Playwright 1.49.0 - 1.56.1.**
176    ///
177    /// The route.fulfill() method can successfully send requests for status codes and headers,
178    /// but the response body is not transmitted to the browser JavaScript layer. This applies
179    /// to ALL request types (main document, fetch, XHR, etc.), not just document navigation.
180    ///
181    /// **Investigation Findings:**
182    /// - The protocol message is correctly formatted and accepted by the Playwright server
183    /// - The body bytes are present in the fulfill() call
184    /// - The Playwright server creates a Response object
185    /// - But the body content does not reach the browser's fetch/network API
186    ///
187    /// This appears to be a limitation or bug in the Playwright server implementation.
188    /// Tested with versions 1.49.0 and 1.56.1 (latest as of 2025-11-10).
189    ///
190    /// TODO: Periodically test with newer Playwright versions for fix.
191    /// Workaround: Mock responses at the HTTP server level rather than using network interception,
192    /// or wait for a newer Playwright version that supports response body fulfillment.
193    ///
194    /// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
195    pub async fn fulfill(&self, options: Option<FulfillOptions>) -> Result<()> {
196        let opts = options.unwrap_or_default();
197
198        // Build the response object for the protocol
199        let mut response = json!({
200            "status": opts.status.unwrap_or(200),
201            "headers": []
202        });
203
204        // Set headers - prepare them BEFORE adding body
205        let mut headers_map = opts.headers.unwrap_or_default();
206
207        // Set body if provided, and prepare headers
208        let body_bytes = opts.body.as_ref();
209        if let Some(body) = body_bytes {
210            let content_length = body.len().to_string();
211            headers_map.insert("content-length".to_string(), content_length);
212        }
213
214        // Add Content-Type if specified
215        if let Some(ref ct) = opts.content_type {
216            headers_map.insert("content-type".to_string(), ct.clone());
217        }
218
219        // Convert headers to protocol format
220        let headers_array: Vec<Value> = headers_map
221            .into_iter()
222            .map(|(name, value)| json!({"name": name, "value": value}))
223            .collect();
224        response["headers"] = json!(headers_array);
225
226        // Set body LAST, after all other fields
227        if let Some(body) = body_bytes {
228            // Send as plain string for text (UTF-8), base64 for binary
229            if let Ok(body_str) = std::str::from_utf8(body) {
230                response["body"] = json!(body_str);
231            } else {
232                use base64::Engine;
233                let encoded = base64::engine::general_purpose::STANDARD.encode(body);
234                response["body"] = json!(encoded);
235                response["isBase64"] = json!(true);
236            }
237        }
238
239        let params = json!({
240            "response": response
241        });
242
243        self.channel()
244            .send::<_, serde_json::Value>("fulfill", params)
245            .await
246            .map(|_| ())
247    }
248}
249
250/// Options for continuing a request with modifications.
251///
252/// Allows modifying headers, method, post data, and URL when continuing a route.
253///
254/// See: <https://playwright.dev/docs/api/class-route#route-continue>
255#[derive(Debug, Clone, Default)]
256pub struct ContinueOptions {
257    /// Modified request headers
258    pub headers: Option<std::collections::HashMap<String, String>>,
259    /// Modified request method (GET, POST, etc.)
260    pub method: Option<String>,
261    /// Modified POST data as string
262    pub post_data: Option<String>,
263    /// Modified POST data as bytes
264    pub post_data_bytes: Option<Vec<u8>>,
265    /// Modified request URL (must have same protocol)
266    pub url: Option<String>,
267}
268
269impl ContinueOptions {
270    /// Creates a new builder for ContinueOptions
271    pub fn builder() -> ContinueOptionsBuilder {
272        ContinueOptionsBuilder::default()
273    }
274}
275
276/// Builder for ContinueOptions
277#[derive(Debug, Clone, Default)]
278pub struct ContinueOptionsBuilder {
279    headers: Option<std::collections::HashMap<String, String>>,
280    method: Option<String>,
281    post_data: Option<String>,
282    post_data_bytes: Option<Vec<u8>>,
283    url: Option<String>,
284}
285
286impl ContinueOptionsBuilder {
287    /// Sets the request headers
288    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
289        self.headers = Some(headers);
290        self
291    }
292
293    /// Sets the request method
294    pub fn method(mut self, method: String) -> Self {
295        self.method = Some(method);
296        self
297    }
298
299    /// Sets the POST data as a string
300    pub fn post_data(mut self, post_data: String) -> Self {
301        self.post_data = Some(post_data);
302        self.post_data_bytes = None; // Clear bytes if setting string
303        self
304    }
305
306    /// Sets the POST data as bytes
307    pub fn post_data_bytes(mut self, post_data_bytes: Vec<u8>) -> Self {
308        self.post_data_bytes = Some(post_data_bytes);
309        self.post_data = None; // Clear string if setting bytes
310        self
311    }
312
313    /// Sets the request URL (must have same protocol as original)
314    pub fn url(mut self, url: String) -> Self {
315        self.url = Some(url);
316        self
317    }
318
319    /// Builds the ContinueOptions
320    pub fn build(self) -> ContinueOptions {
321        ContinueOptions {
322            headers: self.headers,
323            method: self.method,
324            post_data: self.post_data,
325            post_data_bytes: self.post_data_bytes,
326            url: self.url,
327        }
328    }
329}
330
331/// Options for fulfilling a route with a custom response.
332///
333/// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
334#[derive(Debug, Clone, Default)]
335pub struct FulfillOptions {
336    /// HTTP status code (default: 200)
337    pub status: Option<u16>,
338    /// Response headers
339    pub headers: Option<std::collections::HashMap<String, String>>,
340    /// Response body as bytes
341    pub body: Option<Vec<u8>>,
342    /// Content-Type header value
343    pub content_type: Option<String>,
344}
345
346impl FulfillOptions {
347    /// Creates a new FulfillOptions builder
348    pub fn builder() -> FulfillOptionsBuilder {
349        FulfillOptionsBuilder::default()
350    }
351}
352
353/// Builder for FulfillOptions
354#[derive(Debug, Clone, Default)]
355pub struct FulfillOptionsBuilder {
356    status: Option<u16>,
357    headers: Option<std::collections::HashMap<String, String>>,
358    body: Option<Vec<u8>>,
359    content_type: Option<String>,
360}
361
362impl FulfillOptionsBuilder {
363    /// Sets the HTTP status code
364    pub fn status(mut self, status: u16) -> Self {
365        self.status = Some(status);
366        self
367    }
368
369    /// Sets the response headers
370    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
371        self.headers = Some(headers);
372        self
373    }
374
375    /// Sets the response body from bytes
376    pub fn body(mut self, body: Vec<u8>) -> Self {
377        self.body = Some(body);
378        self
379    }
380
381    /// Sets the response body from a string
382    pub fn body_string(mut self, body: impl Into<String>) -> Self {
383        self.body = Some(body.into().into_bytes());
384        self
385    }
386
387    /// Sets the response body from JSON (automatically sets content-type to application/json)
388    pub fn json(mut self, value: &impl serde::Serialize) -> Result<Self> {
389        let json_str = serde_json::to_string(value).map_err(|e| {
390            crate::error::Error::ProtocolError(format!("JSON serialization failed: {}", e))
391        })?;
392        self.body = Some(json_str.into_bytes());
393        self.content_type = Some("application/json".to_string());
394        Ok(self)
395    }
396
397    /// Sets the Content-Type header
398    pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
399        self.content_type = Some(content_type.into());
400        self
401    }
402
403    /// Builds the FulfillOptions
404    pub fn build(self) -> FulfillOptions {
405        FulfillOptions {
406            status: self.status,
407            headers: self.headers,
408            body: self.body,
409            content_type: self.content_type,
410        }
411    }
412}
413
414impl ChannelOwner for Route {
415    fn guid(&self) -> &str {
416        self.base.guid()
417    }
418
419    fn type_name(&self) -> &str {
420        self.base.type_name()
421    }
422
423    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
424        self.base.parent()
425    }
426
427    fn connection(&self) -> Arc<dyn crate::connection::ConnectionLike> {
428        self.base.connection()
429    }
430
431    fn initializer(&self) -> &Value {
432        self.base.initializer()
433    }
434
435    fn channel(&self) -> &crate::channel::Channel {
436        self.base.channel()
437    }
438
439    fn dispose(&self, reason: crate::channel_owner::DisposeReason) {
440        self.base.dispose(reason)
441    }
442
443    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
444        self.base.adopt(child)
445    }
446
447    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
448        self.base.add_child(guid, child)
449    }
450
451    fn remove_child(&self, guid: &str) {
452        self.base.remove_child(guid)
453    }
454
455    fn on_event(&self, _method: &str, _params: Value) {
456        // Route events will be handled in future phases
457    }
458
459    fn was_collected(&self) -> bool {
460        self.base.was_collected()
461    }
462
463    fn as_any(&self) -> &dyn Any {
464        self
465    }
466}
467
468impl std::fmt::Debug for Route {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        f.debug_struct("Route")
471            .field("guid", &self.guid())
472            .field("request", &self.request().guid())
473            .finish()
474    }
475}