Skip to main content

playwright_rs/protocol/
route.rs

1// Route protocol object
2//
3// Represents a route handler for network interception.
4// Routes are created when page.route() or context.route() matches a request.
5//
6// See: https://playwright.dev/docs/api/class-route
7
8use crate::error::Result;
9use crate::protocol::Request;
10use crate::protocol::api_request_context::{APIRequestContext, InnerFetchOptions};
11use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
12use crate::server::connection::downcast_parent;
13use serde_json::{Value, json};
14use std::any::Any;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Mutex};
17
18/// Route represents a network route handler.
19///
20/// Routes allow intercepting, aborting, continuing, or fulfilling network requests.
21///
22/// See: <https://playwright.dev/docs/api/class-route>
23#[derive(Clone)]
24pub struct Route {
25    base: ChannelOwnerImpl,
26    /// Tracks whether the route has been fully handled (abort/continue/fulfill).
27    /// Used by fallback() to signal that handler chaining should continue.
28    handled: Arc<AtomicBool>,
29    /// APIRequestContext for performing fetch operations.
30    /// Set by the route event dispatcher (Page or BrowserContext).
31    api_request_context: Arc<Mutex<Option<APIRequestContext>>>,
32}
33
34impl Route {
35    /// Creates a new Route from protocol initialization
36    ///
37    /// This is called by the object factory when the server sends a `__create__` message
38    /// for a Route object.
39    pub fn new(
40        parent: Arc<dyn ChannelOwner>,
41        type_name: String,
42        guid: Arc<str>,
43        initializer: Value,
44    ) -> Result<Self> {
45        let base = ChannelOwnerImpl::new(
46            ParentOrConnection::Parent(parent.clone()),
47            type_name,
48            guid,
49            initializer,
50        );
51
52        Ok(Self {
53            base,
54            handled: Arc::new(AtomicBool::new(false)),
55            api_request_context: Arc::new(Mutex::new(None)),
56        })
57    }
58
59    /// Returns whether this route was fully handled by a handler.
60    ///
61    /// Returns `false` if the handler called `fallback()`, indicating the next
62    /// matching handler should be tried.
63    pub(crate) fn was_handled(&self) -> bool {
64        self.handled.load(Ordering::SeqCst)
65    }
66
67    /// Sets the APIRequestContext for this route, enabling `fetch()`.
68    ///
69    /// Called by the route event dispatcher (Page or BrowserContext) when
70    /// dispatching the route to a handler.
71    pub(crate) fn set_api_request_context(&self, ctx: APIRequestContext) {
72        *self.api_request_context.lock().unwrap() = Some(ctx);
73    }
74
75    /// Returns the request that is being routed.
76    ///
77    /// See: <https://playwright.dev/docs/api/class-route#route-request>
78    pub fn request(&self) -> Request {
79        // The Route's parent is the Request object
80        if let Some(request) = downcast_parent::<Request>(self) {
81            return request;
82        }
83
84        // Fallback: Create a stub Request from initializer data
85        // This should rarely happen in practice
86        let request_data = self
87            .initializer()
88            .get("request")
89            .cloned()
90            .unwrap_or_else(|| {
91                serde_json::json!({
92                    "url": "",
93                    "method": "GET"
94                })
95            });
96
97        let parent = self
98            .parent()
99            .unwrap_or_else(|| Arc::new(self.clone()) as Arc<dyn ChannelOwner>);
100
101        let request_guid = request_data
102            .get("guid")
103            .and_then(|v| v.as_str())
104            .unwrap_or("request-stub");
105
106        Request::new(
107            parent,
108            "Request".to_string(),
109            Arc::from(request_guid),
110            request_data,
111        )
112        .expect("stub Request construction cannot fail")
113    }
114
115    /// Aborts the route's request.
116    ///
117    /// # Arguments
118    ///
119    /// * `error_code` - Optional error code (default: "failed")
120    ///
121    /// Available error codes:
122    /// - "aborted" - User-initiated cancellation
123    /// - "accessdenied" - Permission denied
124    /// - "addressunreachable" - Host unreachable
125    /// - "blockedbyclient" - Client blocked request
126    /// - "connectionaborted", "connectionclosed", "connectionfailed", "connectionrefused", "connectionreset"
127    /// - "internetdisconnected"
128    /// - "namenotresolved"
129    /// - "timedout"
130    /// - "failed" - Generic error (default)
131    ///
132    /// See: <https://playwright.dev/docs/api/class-route#route-abort>
133    pub async fn abort(&self, error_code: Option<&str>) -> Result<()> {
134        self.handled.store(true, Ordering::SeqCst);
135        let params = json!({
136            "errorCode": error_code.unwrap_or("failed")
137        });
138
139        self.channel()
140            .send::<_, serde_json::Value>("abort", params)
141            .await
142            .map(|_| ())
143    }
144
145    /// Continues the route's request with optional modifications.
146    ///
147    /// This is a final action — no other route handlers will run for this request.
148    /// Use `fallback()` instead if you want subsequent handlers to have a chance.
149    ///
150    /// # Arguments
151    ///
152    /// * `overrides` - Optional modifications to apply to the request
153    ///
154    /// See: <https://playwright.dev/docs/api/class-route#route-continue>
155    pub async fn continue_(&self, overrides: Option<ContinueOptions>) -> Result<()> {
156        self.handled.store(true, Ordering::SeqCst);
157        self.continue_internal(overrides, false).await
158    }
159
160    /// Continues the route's request, allowing subsequent handlers to run.
161    ///
162    /// Unlike `continue_()`, `fallback()` yields to the next matching handler in the
163    /// chain before the request reaches the network. This enables middleware-like
164    /// handler composition where multiple handlers can inspect and modify a request.
165    ///
166    /// # Arguments
167    ///
168    /// * `overrides` - Optional modifications to apply to the request
169    ///
170    /// See: <https://playwright.dev/docs/api/class-route#route-fallback>
171    pub async fn fallback(&self, overrides: Option<ContinueOptions>) -> Result<()> {
172        // Don't set handled — signals to the dispatcher to try the next handler
173        self.continue_internal(overrides, true).await
174    }
175
176    /// Internal implementation shared by continue_() and fallback()
177    async fn continue_internal(
178        &self,
179        overrides: Option<ContinueOptions>,
180        is_fallback: bool,
181    ) -> Result<()> {
182        let params = super::route_params::continue_params(overrides, is_fallback);
183
184        self.channel()
185            .send::<_, serde_json::Value>("continue", params)
186            .await
187            .map(|_| ())
188    }
189
190    /// Fulfills the route's request with a custom response.
191    ///
192    /// # Arguments
193    ///
194    /// * `options` - Response configuration (status, headers, body, etc.)
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the driver rejects the command, for example when
199    /// the route has already been handled or the page has closed.
200    ///
201    /// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
202    pub async fn fulfill(&self, options: impl Into<Option<FulfillOptions>>) -> Result<()> {
203        let options = options.into();
204        self.handled.store(true, Ordering::SeqCst);
205        let opts = options.unwrap_or_default();
206
207        let params = super::route_params::fulfill_params(opts);
208
209        self.channel()
210            .send::<_, serde_json::Value>("fulfill", params)
211            .await
212            .map(|_| ())
213    }
214
215    /// Performs the request and fetches result without fulfilling it, so that the
216    /// response can be modified and then fulfilled.
217    ///
218    /// Delegates to `APIRequestContext.inner_fetch()` using the request's URL and
219    /// any provided overrides.
220    ///
221    /// # Arguments
222    ///
223    /// * `options` - Optional overrides for the fetch request
224    ///
225    /// See: <https://playwright.dev/docs/api/class-route#route-fetch>
226    pub async fn fetch(&self, options: impl Into<Option<FetchOptions>>) -> Result<FetchResponse> {
227        let options = options.into();
228        self.handled.store(true, Ordering::SeqCst);
229
230        let api_ctx = self
231            .api_request_context
232            .lock()
233            .unwrap()
234            .clone()
235            .ok_or_else(|| {
236                crate::error::Error::ProtocolError(
237                    "No APIRequestContext available for route.fetch(). \
238                     This can happen if the route was not dispatched through \
239                     a BrowserContext with an associated request context."
240                        .to_string(),
241                )
242            })?;
243
244        let request = self.request();
245        let opts = options.unwrap_or_default();
246
247        // Use the original request URL unless overridden
248        let url = opts.url.unwrap_or_else(|| request.url().to_string());
249
250        let inner_opts = InnerFetchOptions {
251            method: opts.method.or_else(|| Some(request.method().to_string())),
252            headers: opts.headers,
253            post_data: opts.post_data,
254            post_data_bytes: opts.post_data_bytes,
255            max_redirects: opts.max_redirects,
256            max_retries: opts.max_retries,
257            timeout: opts.timeout,
258        };
259
260        api_ctx.inner_fetch(&url, Some(inner_opts)).await
261    }
262}
263
264/// Checks if a URL matches a Playwright URL glob.
265///
266/// Delegates to the shared port of the driver's globber, so the client picks
267/// the same handler the server picked when it decided to report the request.
268/// This previously used the `glob` crate, whose filesystem semantics differ
269/// in ways that matter for URLs: `?` matched any single character, `[…]` was
270/// a character class, and `{a,b}` alternation was unsupported. A pattern the
271/// server matched but the client did not left the route event unanswered and
272/// the request hanging until it timed out.
273///
274/// - `*` matches any characters except `/`
275/// - `**` matches any characters including `/`
276/// - `{a,b}` is alternation; `\` escapes the next character
277/// - everything else, `?` included, is literal
278pub(crate) fn matches_pattern(pattern: &str, url: &str) -> bool {
279    crate::protocol::glob::glob_match(pattern, url)
280}
281
282/// Behavior when removing route handlers via `unroute_all()`.
283///
284/// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
285#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286#[non_exhaustive]
287pub enum UnrouteBehavior {
288    /// Wait for in-flight handlers to complete before removing
289    Wait,
290    /// Stop handlers and ignore any errors they throw
291    IgnoreErrors,
292    /// Default behavior (does not wait, does not ignore errors)
293    Default,
294}
295
296/// Response from `route.fetch()`, allowing inspection and modification before fulfillment.
297///
298/// See: <https://playwright.dev/docs/api/class-apiresponse>
299#[derive(Debug, Clone)]
300#[non_exhaustive]
301pub struct FetchResponse {
302    /// HTTP status code
303    pub status: u16,
304    /// HTTP status text
305    pub status_text: String,
306    /// Response headers as name-value pairs
307    pub headers: Vec<(String, String)>,
308    /// Response body as bytes
309    pub body: Vec<u8>,
310}
311
312impl FetchResponse {
313    /// Returns the HTTP status code
314    pub fn status(&self) -> u16 {
315        self.status
316    }
317
318    /// Returns the status text
319    pub fn status_text(&self) -> &str {
320        &self.status_text
321    }
322
323    /// Returns response headers
324    pub fn headers(&self) -> &[(String, String)] {
325        &self.headers
326    }
327
328    /// Returns the response body as bytes
329    pub fn body(&self) -> &[u8] {
330        &self.body
331    }
332
333    /// Returns the response body as text
334    pub fn text(&self) -> Result<String> {
335        String::from_utf8(self.body.clone()).map_err(|e| {
336            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
337        })
338    }
339
340    /// Returns the response body parsed as JSON
341    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
342        serde_json::from_slice(&self.body).map_err(|e| {
343            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
344        })
345    }
346
347    /// Returns true if status is in 200-299 range
348    pub fn ok(&self) -> bool {
349        (200..300).contains(&self.status)
350    }
351}
352
353/// Options for continuing a request with modifications.
354///
355/// Allows modifying headers, method, post data, and URL when continuing a route.
356/// Used by both `continue_()` and `fallback()`.
357///
358/// See: <https://playwright.dev/docs/api/class-route#route-continue>
359#[derive(Debug, Clone, Default)]
360#[non_exhaustive]
361pub struct ContinueOptions {
362    /// Modified request headers
363    pub headers: Option<std::collections::HashMap<String, String>>,
364    /// Modified request method (GET, POST, etc.)
365    pub method: Option<String>,
366    /// Modified POST data as string
367    pub post_data: Option<String>,
368    /// Modified POST data as bytes
369    pub post_data_bytes: Option<Vec<u8>>,
370    /// Modified request URL (must have same protocol)
371    pub url: Option<String>,
372}
373
374impl ContinueOptions {
375    /// Creates a new builder for ContinueOptions
376    pub fn builder() -> ContinueOptionsBuilder {
377        ContinueOptionsBuilder::default()
378    }
379}
380
381/// Builder for ContinueOptions
382#[derive(Debug, Clone, Default)]
383pub struct ContinueOptionsBuilder {
384    headers: Option<std::collections::HashMap<String, String>>,
385    method: Option<String>,
386    post_data: Option<String>,
387    post_data_bytes: Option<Vec<u8>>,
388    url: Option<String>,
389}
390
391impl ContinueOptionsBuilder {
392    /// Sets the request headers
393    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
394        self.headers = Some(headers);
395        self
396    }
397
398    /// Sets the request method
399    pub fn method(mut self, method: String) -> Self {
400        self.method = Some(method);
401        self
402    }
403
404    /// Sets the POST data as a string
405    pub fn post_data(mut self, post_data: String) -> Self {
406        self.post_data = Some(post_data);
407        self.post_data_bytes = None; // Clear bytes if setting string
408        self
409    }
410
411    /// Sets the POST data as bytes
412    pub fn post_data_bytes(mut self, post_data_bytes: Vec<u8>) -> Self {
413        self.post_data_bytes = Some(post_data_bytes);
414        self.post_data = None; // Clear string if setting bytes
415        self
416    }
417
418    /// Sets the request URL (must have same protocol as original)
419    pub fn url(mut self, url: String) -> Self {
420        self.url = Some(url);
421        self
422    }
423
424    /// Builds the ContinueOptions
425    pub fn build(self) -> ContinueOptions {
426        ContinueOptions {
427            headers: self.headers,
428            method: self.method,
429            post_data: self.post_data,
430            post_data_bytes: self.post_data_bytes,
431            url: self.url,
432        }
433    }
434}
435
436/// Options for fulfilling a route with a custom response.
437///
438/// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
439#[derive(Debug, Clone, Default)]
440#[non_exhaustive]
441pub struct FulfillOptions {
442    /// HTTP status code (default: 200)
443    pub status: Option<u16>,
444    /// Response headers
445    pub headers: Option<std::collections::HashMap<String, String>>,
446    /// Response body as bytes
447    pub body: Option<Vec<u8>>,
448    /// Content-Type header value
449    pub content_type: Option<String>,
450}
451
452impl FulfillOptions {
453    /// Creates a new FulfillOptions builder
454    pub fn builder() -> FulfillOptionsBuilder {
455        FulfillOptionsBuilder::default()
456    }
457}
458
459/// Builder for FulfillOptions
460#[derive(Debug, Clone, Default)]
461pub struct FulfillOptionsBuilder {
462    status: Option<u16>,
463    headers: Option<std::collections::HashMap<String, String>>,
464    body: Option<Vec<u8>>,
465    content_type: Option<String>,
466}
467
468impl FulfillOptionsBuilder {
469    /// Sets the HTTP status code
470    pub fn status(mut self, status: u16) -> Self {
471        self.status = Some(status);
472        self
473    }
474
475    /// Sets the response headers
476    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
477        self.headers = Some(headers);
478        self
479    }
480
481    /// Sets the response body from bytes
482    pub fn body(mut self, body: Vec<u8>) -> Self {
483        self.body = Some(body);
484        self
485    }
486
487    /// Sets the response body from a string
488    pub fn body_string(mut self, body: impl Into<String>) -> Self {
489        self.body = Some(body.into().into_bytes());
490        self
491    }
492
493    /// Sets the response body from JSON (automatically sets content-type to application/json)
494    pub fn json(mut self, value: &impl serde::Serialize) -> Result<Self> {
495        let json_str = serde_json::to_string(value).map_err(|e| {
496            crate::error::Error::ProtocolError(format!("JSON serialization failed: {}", e))
497        })?;
498        self.body = Some(json_str.into_bytes());
499        self.content_type = Some("application/json".to_string());
500        Ok(self)
501    }
502
503    /// Sets the Content-Type header
504    pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
505        self.content_type = Some(content_type.into());
506        self
507    }
508
509    /// Builds the FulfillOptions
510    pub fn build(self) -> FulfillOptions {
511        FulfillOptions {
512            status: self.status,
513            headers: self.headers,
514            body: self.body,
515            content_type: self.content_type,
516        }
517    }
518}
519
520/// Options for fetching a route's request.
521///
522/// See: <https://playwright.dev/docs/api/class-route#route-fetch>
523#[derive(Debug, Clone, Default)]
524#[non_exhaustive]
525pub struct FetchOptions {
526    /// Modified request headers
527    pub headers: Option<std::collections::HashMap<String, String>>,
528    /// Modified request method (GET, POST, etc.)
529    pub method: Option<String>,
530    /// Modified POST data as string
531    pub post_data: Option<String>,
532    /// Modified POST data as bytes
533    pub post_data_bytes: Option<Vec<u8>>,
534    /// Modified request URL
535    pub url: Option<String>,
536    /// Maximum number of redirects to follow (default: 20)
537    pub max_redirects: Option<u32>,
538    /// Maximum number of retries (default: 0)
539    pub max_retries: Option<u32>,
540    /// Request timeout in milliseconds
541    pub timeout: Option<f64>,
542}
543
544impl FetchOptions {
545    /// Creates a new FetchOptions builder
546    pub fn builder() -> FetchOptionsBuilder {
547        FetchOptionsBuilder::default()
548    }
549}
550
551/// Builder for FetchOptions
552#[derive(Debug, Clone, Default)]
553pub struct FetchOptionsBuilder {
554    headers: Option<std::collections::HashMap<String, String>>,
555    method: Option<String>,
556    post_data: Option<String>,
557    post_data_bytes: Option<Vec<u8>>,
558    url: Option<String>,
559    max_redirects: Option<u32>,
560    max_retries: Option<u32>,
561    timeout: Option<f64>,
562}
563
564impl FetchOptionsBuilder {
565    /// Sets the request headers
566    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
567        self.headers = Some(headers);
568        self
569    }
570
571    /// Sets the request method
572    pub fn method(mut self, method: String) -> Self {
573        self.method = Some(method);
574        self
575    }
576
577    /// Sets the POST data as a string
578    pub fn post_data(mut self, post_data: String) -> Self {
579        self.post_data = Some(post_data);
580        self.post_data_bytes = None;
581        self
582    }
583
584    /// Sets the POST data as bytes
585    pub fn post_data_bytes(mut self, post_data_bytes: Vec<u8>) -> Self {
586        self.post_data_bytes = Some(post_data_bytes);
587        self.post_data = None;
588        self
589    }
590
591    /// Sets the request URL
592    pub fn url(mut self, url: String) -> Self {
593        self.url = Some(url);
594        self
595    }
596
597    /// Sets the maximum number of redirects to follow
598    pub fn max_redirects(mut self, n: u32) -> Self {
599        self.max_redirects = Some(n);
600        self
601    }
602
603    /// Sets the maximum number of retries
604    pub fn max_retries(mut self, n: u32) -> Self {
605        self.max_retries = Some(n);
606        self
607    }
608
609    /// Sets the request timeout in milliseconds
610    pub fn timeout(mut self, ms: f64) -> Self {
611        self.timeout = Some(ms);
612        self
613    }
614
615    /// Builds the FetchOptions
616    pub fn build(self) -> FetchOptions {
617        FetchOptions {
618            headers: self.headers,
619            method: self.method,
620            post_data: self.post_data,
621            post_data_bytes: self.post_data_bytes,
622            url: self.url,
623            max_redirects: self.max_redirects,
624            max_retries: self.max_retries,
625            timeout: self.timeout,
626        }
627    }
628}
629
630impl ChannelOwner for Route {
631    fn guid(&self) -> &str {
632        self.base.guid()
633    }
634
635    fn type_name(&self) -> &str {
636        self.base.type_name()
637    }
638
639    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
640        self.base.parent()
641    }
642
643    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
644        self.base.connection()
645    }
646
647    fn initializer(&self) -> &Value {
648        self.base.initializer()
649    }
650
651    fn channel(&self) -> &crate::server::channel::Channel {
652        self.base.channel()
653    }
654
655    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
656        self.base.dispose(reason)
657    }
658
659    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
660        self.base.adopt(child)
661    }
662
663    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
664        self.base.add_child(guid, child)
665    }
666
667    fn remove_child(&self, guid: &str) {
668        self.base.remove_child(guid)
669    }
670
671    fn on_event(&self, _method: &str, _params: Value) {
672        // Route events will be handled in future phases
673    }
674
675    fn was_collected(&self) -> bool {
676        self.base.was_collected()
677    }
678
679    fn as_any(&self) -> &dyn Any {
680        self
681    }
682}
683
684impl std::fmt::Debug for Route {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        f.debug_struct("Route")
687            .field("guid", &self.guid())
688            .field("request", &self.request().guid())
689            .finish()
690    }
691}