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