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 mut params = json!({
183            "isFallback": is_fallback
184        });
185
186        // Add overrides if provided
187        if let Some(opts) = overrides {
188            // Add headers
189            if let Some(headers) = opts.headers {
190                let headers_array: Vec<serde_json::Value> = headers
191                    .into_iter()
192                    .map(|(name, value)| json!({"name": name, "value": value}))
193                    .collect();
194                params["headers"] = json!(headers_array);
195            }
196
197            // Add method
198            if let Some(method) = opts.method {
199                params["method"] = json!(method);
200            }
201
202            // Add postData (string or binary)
203            if let Some(post_data) = opts.post_data {
204                params["postData"] = json!(post_data);
205            } else if let Some(post_data_bytes) = opts.post_data_bytes {
206                use base64::Engine;
207                let encoded = base64::engine::general_purpose::STANDARD.encode(&post_data_bytes);
208                params["postData"] = json!(encoded);
209            }
210
211            // Add URL
212            if let Some(url) = opts.url {
213                params["url"] = json!(url);
214            }
215        }
216
217        self.channel()
218            .send::<_, serde_json::Value>("continue", params)
219            .await
220            .map(|_| ())
221    }
222
223    /// Fulfills the route's request with a custom response.
224    ///
225    /// # Arguments
226    ///
227    /// * `options` - Response configuration (status, headers, body, etc.)
228    ///
229    /// # Known Limitations
230    ///
231    /// **Response body fulfillment is not supported in Playwright 1.49.0 - 1.60.0.**
232    ///
233    /// The route.fulfill() method can successfully send requests for status codes and headers,
234    /// but the response body is not transmitted to the browser JavaScript layer. This applies
235    /// to ALL request types (main document, fetch, XHR, etc.), not just document navigation.
236    ///
237    /// **Investigation Findings:**
238    /// - The protocol message is correctly formatted and accepted by the Playwright server
239    /// - The body bytes are present in the fulfill() call
240    /// - The Playwright server creates a Response object
241    /// - But the body content does not reach the browser's fetch/network API
242    ///
243    /// This appears to be a limitation or bug in the Playwright server implementation.
244    /// Tested with versions 1.49.0, 1.56.1, 1.58.2, 1.59.1, and 1.60.0 (latest as of 2026-05-16).
245    ///
246    /// TODO: Periodically test with newer Playwright versions for fix.
247    /// Workaround: Mock responses at the HTTP server level rather than using network interception,
248    /// or wait for a newer Playwright version that supports response body fulfillment.
249    ///
250    /// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
251    pub async fn fulfill(&self, options: impl Into<Option<FulfillOptions>>) -> Result<()> {
252        let options = options.into();
253        self.handled.store(true, Ordering::SeqCst);
254        let opts = options.unwrap_or_default();
255
256        // Build the response object for the protocol
257        let mut response = json!({
258            "status": opts.status.unwrap_or(200),
259            "headers": []
260        });
261
262        // Set headers - prepare them BEFORE adding body
263        let mut headers_map = opts.headers.unwrap_or_default();
264
265        // Set body if provided, and prepare headers
266        let body_bytes = opts.body.as_ref();
267        if let Some(body) = body_bytes {
268            let content_length = body.len().to_string();
269            headers_map.insert("content-length".to_string(), content_length);
270        }
271
272        // Add Content-Type if specified
273        if let Some(ref ct) = opts.content_type {
274            headers_map.insert("content-type".to_string(), ct.clone());
275        }
276
277        // Convert headers to protocol format
278        let headers_array: Vec<Value> = headers_map
279            .into_iter()
280            .map(|(name, value)| json!({"name": name, "value": value}))
281            .collect();
282        response["headers"] = json!(headers_array);
283
284        // Set body LAST, after all other fields
285        if let Some(body) = body_bytes {
286            // Send as plain string for text (UTF-8), base64 for binary
287            if let Ok(body_str) = std::str::from_utf8(body) {
288                response["body"] = json!(body_str);
289            } else {
290                use base64::Engine;
291                let encoded = base64::engine::general_purpose::STANDARD.encode(body);
292                response["body"] = json!(encoded);
293                response["isBase64"] = json!(true);
294            }
295        }
296
297        let params = json!({
298            "response": response
299        });
300
301        self.channel()
302            .send::<_, serde_json::Value>("fulfill", params)
303            .await
304            .map(|_| ())
305    }
306
307    /// Performs the request and fetches result without fulfilling it, so that the
308    /// response can be modified and then fulfilled.
309    ///
310    /// Delegates to `APIRequestContext.inner_fetch()` using the request's URL and
311    /// any provided overrides.
312    ///
313    /// # Arguments
314    ///
315    /// * `options` - Optional overrides for the fetch request
316    ///
317    /// See: <https://playwright.dev/docs/api/class-route#route-fetch>
318    pub async fn fetch(&self, options: impl Into<Option<FetchOptions>>) -> Result<FetchResponse> {
319        let options = options.into();
320        self.handled.store(true, Ordering::SeqCst);
321
322        let api_ctx = self
323            .api_request_context
324            .lock()
325            .unwrap()
326            .clone()
327            .ok_or_else(|| {
328                crate::error::Error::ProtocolError(
329                    "No APIRequestContext available for route.fetch(). \
330                     This can happen if the route was not dispatched through \
331                     a BrowserContext with an associated request context."
332                        .to_string(),
333                )
334            })?;
335
336        let request = self.request();
337        let opts = options.unwrap_or_default();
338
339        // Use the original request URL unless overridden
340        let url = opts.url.unwrap_or_else(|| request.url().to_string());
341
342        let inner_opts = InnerFetchOptions {
343            method: opts.method.or_else(|| Some(request.method().to_string())),
344            headers: opts.headers,
345            post_data: opts.post_data,
346            post_data_bytes: opts.post_data_bytes,
347            max_redirects: opts.max_redirects,
348            max_retries: opts.max_retries,
349            timeout: opts.timeout,
350        };
351
352        api_ctx.inner_fetch(&url, Some(inner_opts)).await
353    }
354}
355
356/// Checks if a URL matches a glob pattern.
357///
358/// Supports standard glob patterns:
359/// - `*` matches any characters except `/`
360/// - `**` matches any characters including `/`
361/// - `?` matches a single character
362pub(crate) fn matches_pattern(pattern: &str, url: &str) -> bool {
363    use glob::Pattern;
364
365    match Pattern::new(pattern) {
366        Ok(glob_pattern) => glob_pattern.matches(url),
367        Err(_) => {
368            // If pattern is invalid, fall back to exact string match
369            pattern == url
370        }
371    }
372}
373
374/// Behavior when removing route handlers via `unroute_all()`.
375///
376/// See: <https://playwright.dev/docs/api/class-page#page-unroute-all>
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378#[non_exhaustive]
379pub enum UnrouteBehavior {
380    /// Wait for in-flight handlers to complete before removing
381    Wait,
382    /// Stop handlers and ignore any errors they throw
383    IgnoreErrors,
384    /// Default behavior (does not wait, does not ignore errors)
385    Default,
386}
387
388/// Response from `route.fetch()`, allowing inspection and modification before fulfillment.
389///
390/// See: <https://playwright.dev/docs/api/class-apiresponse>
391#[derive(Debug, Clone)]
392#[non_exhaustive]
393pub struct FetchResponse {
394    /// HTTP status code
395    pub status: u16,
396    /// HTTP status text
397    pub status_text: String,
398    /// Response headers as name-value pairs
399    pub headers: Vec<(String, String)>,
400    /// Response body as bytes
401    pub body: Vec<u8>,
402}
403
404impl FetchResponse {
405    /// Returns the HTTP status code
406    pub fn status(&self) -> u16 {
407        self.status
408    }
409
410    /// Returns the status text
411    pub fn status_text(&self) -> &str {
412        &self.status_text
413    }
414
415    /// Returns response headers
416    pub fn headers(&self) -> &[(String, String)] {
417        &self.headers
418    }
419
420    /// Returns the response body as bytes
421    pub fn body(&self) -> &[u8] {
422        &self.body
423    }
424
425    /// Returns the response body as text
426    pub fn text(&self) -> Result<String> {
427        String::from_utf8(self.body.clone()).map_err(|e| {
428            crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
429        })
430    }
431
432    /// Returns the response body parsed as JSON
433    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
434        serde_json::from_slice(&self.body).map_err(|e| {
435            crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
436        })
437    }
438
439    /// Returns true if status is in 200-299 range
440    pub fn ok(&self) -> bool {
441        (200..300).contains(&self.status)
442    }
443}
444
445/// Options for continuing a request with modifications.
446///
447/// Allows modifying headers, method, post data, and URL when continuing a route.
448/// Used by both `continue_()` and `fallback()`.
449///
450/// See: <https://playwright.dev/docs/api/class-route#route-continue>
451#[derive(Debug, Clone, Default)]
452#[non_exhaustive]
453pub struct ContinueOptions {
454    /// Modified request headers
455    pub headers: Option<std::collections::HashMap<String, String>>,
456    /// Modified request method (GET, POST, etc.)
457    pub method: Option<String>,
458    /// Modified POST data as string
459    pub post_data: Option<String>,
460    /// Modified POST data as bytes
461    pub post_data_bytes: Option<Vec<u8>>,
462    /// Modified request URL (must have same protocol)
463    pub url: Option<String>,
464}
465
466impl ContinueOptions {
467    /// Creates a new builder for ContinueOptions
468    pub fn builder() -> ContinueOptionsBuilder {
469        ContinueOptionsBuilder::default()
470    }
471}
472
473/// Builder for ContinueOptions
474#[derive(Debug, Clone, Default)]
475pub struct ContinueOptionsBuilder {
476    headers: Option<std::collections::HashMap<String, String>>,
477    method: Option<String>,
478    post_data: Option<String>,
479    post_data_bytes: Option<Vec<u8>>,
480    url: Option<String>,
481}
482
483impl ContinueOptionsBuilder {
484    /// Sets the request headers
485    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
486        self.headers = Some(headers);
487        self
488    }
489
490    /// Sets the request method
491    pub fn method(mut self, method: String) -> Self {
492        self.method = Some(method);
493        self
494    }
495
496    /// Sets the POST data as a string
497    pub fn post_data(mut self, post_data: String) -> Self {
498        self.post_data = Some(post_data);
499        self.post_data_bytes = None; // Clear bytes if setting string
500        self
501    }
502
503    /// Sets the POST data as bytes
504    pub fn post_data_bytes(mut self, post_data_bytes: Vec<u8>) -> Self {
505        self.post_data_bytes = Some(post_data_bytes);
506        self.post_data = None; // Clear string if setting bytes
507        self
508    }
509
510    /// Sets the request URL (must have same protocol as original)
511    pub fn url(mut self, url: String) -> Self {
512        self.url = Some(url);
513        self
514    }
515
516    /// Builds the ContinueOptions
517    pub fn build(self) -> ContinueOptions {
518        ContinueOptions {
519            headers: self.headers,
520            method: self.method,
521            post_data: self.post_data,
522            post_data_bytes: self.post_data_bytes,
523            url: self.url,
524        }
525    }
526}
527
528/// Options for fulfilling a route with a custom response.
529///
530/// See: <https://playwright.dev/docs/api/class-route#route-fulfill>
531#[derive(Debug, Clone, Default)]
532#[non_exhaustive]
533pub struct FulfillOptions {
534    /// HTTP status code (default: 200)
535    pub status: Option<u16>,
536    /// Response headers
537    pub headers: Option<std::collections::HashMap<String, String>>,
538    /// Response body as bytes
539    pub body: Option<Vec<u8>>,
540    /// Content-Type header value
541    pub content_type: Option<String>,
542}
543
544impl FulfillOptions {
545    /// Creates a new FulfillOptions builder
546    pub fn builder() -> FulfillOptionsBuilder {
547        FulfillOptionsBuilder::default()
548    }
549}
550
551/// Builder for FulfillOptions
552#[derive(Debug, Clone, Default)]
553pub struct FulfillOptionsBuilder {
554    status: Option<u16>,
555    headers: Option<std::collections::HashMap<String, String>>,
556    body: Option<Vec<u8>>,
557    content_type: Option<String>,
558}
559
560impl FulfillOptionsBuilder {
561    /// Sets the HTTP status code
562    pub fn status(mut self, status: u16) -> Self {
563        self.status = Some(status);
564        self
565    }
566
567    /// Sets the response headers
568    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
569        self.headers = Some(headers);
570        self
571    }
572
573    /// Sets the response body from bytes
574    pub fn body(mut self, body: Vec<u8>) -> Self {
575        self.body = Some(body);
576        self
577    }
578
579    /// Sets the response body from a string
580    pub fn body_string(mut self, body: impl Into<String>) -> Self {
581        self.body = Some(body.into().into_bytes());
582        self
583    }
584
585    /// Sets the response body from JSON (automatically sets content-type to application/json)
586    pub fn json(mut self, value: &impl serde::Serialize) -> Result<Self> {
587        let json_str = serde_json::to_string(value).map_err(|e| {
588            crate::error::Error::ProtocolError(format!("JSON serialization failed: {}", e))
589        })?;
590        self.body = Some(json_str.into_bytes());
591        self.content_type = Some("application/json".to_string());
592        Ok(self)
593    }
594
595    /// Sets the Content-Type header
596    pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
597        self.content_type = Some(content_type.into());
598        self
599    }
600
601    /// Builds the FulfillOptions
602    pub fn build(self) -> FulfillOptions {
603        FulfillOptions {
604            status: self.status,
605            headers: self.headers,
606            body: self.body,
607            content_type: self.content_type,
608        }
609    }
610}
611
612/// Options for fetching a route's request.
613///
614/// See: <https://playwright.dev/docs/api/class-route#route-fetch>
615#[derive(Debug, Clone, Default)]
616#[non_exhaustive]
617pub struct FetchOptions {
618    /// Modified request headers
619    pub headers: Option<std::collections::HashMap<String, String>>,
620    /// Modified request method (GET, POST, etc.)
621    pub method: Option<String>,
622    /// Modified POST data as string
623    pub post_data: Option<String>,
624    /// Modified POST data as bytes
625    pub post_data_bytes: Option<Vec<u8>>,
626    /// Modified request URL
627    pub url: Option<String>,
628    /// Maximum number of redirects to follow (default: 20)
629    pub max_redirects: Option<u32>,
630    /// Maximum number of retries (default: 0)
631    pub max_retries: Option<u32>,
632    /// Request timeout in milliseconds
633    pub timeout: Option<f64>,
634}
635
636impl FetchOptions {
637    /// Creates a new FetchOptions builder
638    pub fn builder() -> FetchOptionsBuilder {
639        FetchOptionsBuilder::default()
640    }
641}
642
643/// Builder for FetchOptions
644#[derive(Debug, Clone, Default)]
645pub struct FetchOptionsBuilder {
646    headers: Option<std::collections::HashMap<String, String>>,
647    method: Option<String>,
648    post_data: Option<String>,
649    post_data_bytes: Option<Vec<u8>>,
650    url: Option<String>,
651    max_redirects: Option<u32>,
652    max_retries: Option<u32>,
653    timeout: Option<f64>,
654}
655
656impl FetchOptionsBuilder {
657    /// Sets the request headers
658    pub fn headers(mut self, headers: std::collections::HashMap<String, String>) -> Self {
659        self.headers = Some(headers);
660        self
661    }
662
663    /// Sets the request method
664    pub fn method(mut self, method: String) -> Self {
665        self.method = Some(method);
666        self
667    }
668
669    /// Sets the POST data as a string
670    pub fn post_data(mut self, post_data: String) -> Self {
671        self.post_data = Some(post_data);
672        self.post_data_bytes = None;
673        self
674    }
675
676    /// Sets the POST data as bytes
677    pub fn post_data_bytes(mut self, post_data_bytes: Vec<u8>) -> Self {
678        self.post_data_bytes = Some(post_data_bytes);
679        self.post_data = None;
680        self
681    }
682
683    /// Sets the request URL
684    pub fn url(mut self, url: String) -> Self {
685        self.url = Some(url);
686        self
687    }
688
689    /// Sets the maximum number of redirects to follow
690    pub fn max_redirects(mut self, n: u32) -> Self {
691        self.max_redirects = Some(n);
692        self
693    }
694
695    /// Sets the maximum number of retries
696    pub fn max_retries(mut self, n: u32) -> Self {
697        self.max_retries = Some(n);
698        self
699    }
700
701    /// Sets the request timeout in milliseconds
702    pub fn timeout(mut self, ms: f64) -> Self {
703        self.timeout = Some(ms);
704        self
705    }
706
707    /// Builds the FetchOptions
708    pub fn build(self) -> FetchOptions {
709        FetchOptions {
710            headers: self.headers,
711            method: self.method,
712            post_data: self.post_data,
713            post_data_bytes: self.post_data_bytes,
714            url: self.url,
715            max_redirects: self.max_redirects,
716            max_retries: self.max_retries,
717            timeout: self.timeout,
718        }
719    }
720}
721
722impl ChannelOwner for Route {
723    fn guid(&self) -> &str {
724        self.base.guid()
725    }
726
727    fn type_name(&self) -> &str {
728        self.base.type_name()
729    }
730
731    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
732        self.base.parent()
733    }
734
735    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
736        self.base.connection()
737    }
738
739    fn initializer(&self) -> &Value {
740        self.base.initializer()
741    }
742
743    fn channel(&self) -> &crate::server::channel::Channel {
744        self.base.channel()
745    }
746
747    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
748        self.base.dispose(reason)
749    }
750
751    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
752        self.base.adopt(child)
753    }
754
755    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
756        self.base.add_child(guid, child)
757    }
758
759    fn remove_child(&self, guid: &str) {
760        self.base.remove_child(guid)
761    }
762
763    fn on_event(&self, _method: &str, _params: Value) {
764        // Route events will be handled in future phases
765    }
766
767    fn was_collected(&self) -> bool {
768        self.base.was_collected()
769    }
770
771    fn as_any(&self) -> &dyn Any {
772        self
773    }
774}
775
776impl std::fmt::Debug for Route {
777    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
778        f.debug_struct("Route")
779            .field("guid", &self.guid())
780            .field("request", &self.request().guid())
781            .finish()
782    }
783}