Skip to main content

playwright_rs/protocol/
web_socket_route.rs

1//! WebSocketRoute protocol object — represents an intercepted WebSocket connection.
2//!
3//! `WebSocketRoute` is created by the Playwright server when a WebSocket connection
4//! matches a pattern registered via [`crate::protocol::Page::route_web_socket`] or
5//! [`crate::protocol::BrowserContext::route_web_socket`].
6//!
7//! # Example
8//!
9//! ```no_run
10//! use playwright_rs::protocol::Playwright;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//!     let playwright = Playwright::launch().await?;
15//!     let browser = playwright.chromium().launch().await?;
16//!     let page = browser.new_page().await?;
17//!
18//!     // Intercept all WebSocket connections and proxy them to the real server
19//!     page.route_web_socket("ws://**", |route| {
20//!         Box::pin(async move {
21//!             route.connect_to_server().await?;
22//!             Ok(())
23//!         })
24//!     })
25//!     .await?;
26//!
27//!     browser.close().await?;
28//!     Ok(())
29//! }
30//! ```
31//!
32//! See: <https://playwright.dev/docs/api/class-websocketroute>
33
34use crate::error::Result;
35use crate::server::channel::Channel;
36use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
37use serde_json::Value;
38use std::any::Any;
39use std::future::Future;
40use std::pin::Pin;
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::sync::{Arc, Mutex};
43
44/// Represents an intercepted WebSocket connection.
45///
46/// `WebSocketRoute` is passed to handlers registered via
47/// [`crate::protocol::Page::route_web_socket`] or [`crate::protocol::BrowserContext::route_web_socket`].
48/// The handler must call [`connect_to_server`](WebSocketRoute::connect_to_server)
49/// to forward the connection to the real server, or [`close`](WebSocketRoute::close)
50/// to terminate it.
51///
52/// See: <https://playwright.dev/docs/api/class-websocketroute>
53#[derive(Clone)]
54pub struct WebSocketRoute {
55    base: ChannelOwnerImpl,
56    /// The WebSocket URL being intercepted.
57    url: String,
58    /// Message handlers registered via on_message().
59    message_handlers: Arc<Mutex<Vec<WebSocketRouteMessageHandler>>>,
60    /// Close handlers registered via on_close().
61    close_handlers: Arc<Mutex<Vec<WebSocketRouteCloseHandler>>>,
62    /// Set by `connect_to_server`; decides whether page messages without a
63    /// handler are forwarded to the real server or dropped.
64    connected: Arc<AtomicBool>,
65}
66
67/// Type alias for boxed WebSocketRoute message handler future.
68type WebSocketRouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
69
70/// Message handler type.
71type WebSocketRouteMessageHandler =
72    Arc<dyn Fn(String) -> WebSocketRouteHandlerFuture + Send + Sync>;
73
74/// Close handler type.
75type WebSocketRouteCloseHandler = Arc<dyn Fn() -> WebSocketRouteHandlerFuture + Send + Sync>;
76
77impl WebSocketRoute {
78    /// Creates a new `WebSocketRoute` object.
79    pub fn new(
80        parent: Arc<dyn ChannelOwner>,
81        type_name: String,
82        guid: Arc<str>,
83        initializer: Value,
84    ) -> Result<Self> {
85        let url = initializer["url"].as_str().unwrap_or("").to_string();
86        let base = ChannelOwnerImpl::new(
87            ParentOrConnection::Parent(parent),
88            type_name,
89            guid,
90            initializer,
91        );
92        Ok(Self {
93            base,
94            url,
95            message_handlers: Arc::new(Mutex::new(Vec::new())),
96            close_handlers: Arc::new(Mutex::new(Vec::new())),
97            connected: Arc::new(AtomicBool::new(false)),
98        })
99    }
100
101    /// Returns the URL of the intercepted WebSocket connection.
102    ///
103    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-url>
104    pub fn url(&self) -> &str {
105        &self.url
106    }
107
108    /// Returns the WebSocket subprotocols the page requested (the
109    /// `Sec-WebSocket-Protocol` values) when opening this socket. Empty if none
110    /// were requested.
111    ///
112    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-protocols>
113    pub fn protocols(&self) -> Vec<String> {
114        self.base
115            .initializer()
116            .get("protocols")
117            .and_then(|v| v.as_array())
118            .map(|arr| {
119                arr.iter()
120                    .filter_map(|x| x.as_str().map(String::from))
121                    .collect()
122            })
123            .unwrap_or_default()
124    }
125
126    /// Connects this WebSocket to the actual server.
127    ///
128    /// After calling this method, all messages sent by the page are forwarded to
129    /// the server, and all messages sent by the server are forwarded to the page.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if the RPC call fails.
134    ///
135    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-connect-to-server>
136    pub async fn connect_to_server(&self) -> Result<()> {
137        if self.connected.swap(true, Ordering::SeqCst) {
138            return Err(crate::error::Error::InvalidArgument(
139                "Already connected to the server".to_string(),
140            ));
141        }
142        self.base
143            .channel()
144            .send_no_result("connect", serde_json::json!({}))
145            .await
146    }
147
148    /// Whether `connect_to_server` has been called on this route.
149    pub fn is_connected(&self) -> bool {
150        self.connected.load(Ordering::SeqCst)
151    }
152
153    /// Runs after every route handler, as upstream does: a handler that
154    /// neither connected to the server nor closed the socket leaves the
155    /// page's socket reporting itself open, so the page can send into a
156    /// mocked socket.
157    pub(crate) async fn after_handle(&self) {
158        if self.is_connected() {
159            return;
160        }
161        if let Err(e) = self
162            .base
163            .channel()
164            .send_no_result("ensureOpened", serde_json::json!({}))
165            .await
166        {
167            tracing::debug!("WebSocketRoute ensureOpened failed: {}", e);
168        }
169    }
170
171    /// Closes the WebSocket connection.
172    ///
173    /// # Arguments
174    ///
175    /// * `options` — Optional close code and reason.
176    ///
177    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-close>
178    pub async fn close(
179        &self,
180        options: impl Into<Option<WebSocketRouteCloseOptions>>,
181    ) -> Result<()> {
182        let options = options.into();
183        let opts = options.unwrap_or_default();
184        let mut params = serde_json::Map::new();
185        if let Some(code) = opts.code {
186            params.insert("code".to_string(), serde_json::json!(code));
187        }
188        if let Some(reason) = opts.reason {
189            params.insert("reason".to_string(), serde_json::json!(reason));
190        }
191        // A close initiated by the handler is reported to the page as a clean
192        // close, as upstream's `close()` does.
193        params.insert("wasClean".to_string(), serde_json::json!(true));
194        self.base
195            .channel()
196            .send_no_result("closePage", Value::Object(params))
197            .await
198    }
199
200    /// Sends a text message to the page.
201    ///
202    /// # Arguments
203    ///
204    /// * `message` — The text message to send.
205    ///
206    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-send>
207    pub async fn send(&self, message: &str) -> Result<()> {
208        self.base
209            .channel()
210            .send_no_result(
211                "sendToPage",
212                serde_json::json!({ "message": message, "isBase64": false }),
213            )
214            .await
215    }
216
217    /// Registers a handler for messages sent from the page.
218    ///
219    /// # Arguments
220    ///
221    /// * `handler` — Async closure that receives the message payload as a `String`.
222    ///
223    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-on-message>
224    pub async fn on_message<F>(&self, handler: F) -> Result<()>
225    where
226        F: Fn(String) -> WebSocketRouteHandlerFuture + Send + Sync + 'static,
227    {
228        let handler_arc = Arc::new(handler);
229        self.message_handlers.lock().unwrap().push(handler_arc);
230        Ok(())
231    }
232
233    /// Registers a handler for when the WebSocket is closed by the page.
234    ///
235    /// See: <https://playwright.dev/docs/api/class-websocketroute#web-socket-route-on-close>
236    pub async fn on_close<F>(&self, handler: F) -> Result<()>
237    where
238        F: Fn() -> WebSocketRouteHandlerFuture + Send + Sync + 'static,
239    {
240        let handler_arc = Arc::new(handler);
241        self.close_handlers.lock().unwrap().push(handler_arc);
242        Ok(())
243    }
244
245    /// Dispatches an incoming server-side event to registered handlers.
246    ///
247    /// Without a handler the route behaves like a transparent proxy once
248    /// `connect_to_server` has been called: page messages go to the server,
249    /// server messages and closes go to the page, and a page close closes
250    /// the server side. That is upstream's client-side contract; the driver
251    /// itself forwards nothing.
252    pub(crate) fn handle_event(&self, event: &str, params: &Value) {
253        match event {
254            "messageFromPage" => {
255                let handlers = self.message_handlers.lock().unwrap().clone();
256                if handlers.is_empty() {
257                    if self.is_connected() {
258                        self.forward("sendToServer", params);
259                    }
260                    return;
261                }
262                let payload = params["message"].as_str().unwrap_or("").to_string();
263                for handler in handlers {
264                    let p = payload.clone();
265                    tokio::spawn(async move {
266                        let _ = handler(p).await;
267                    });
268                }
269            }
270            "messageFromServer" => self.forward("sendToPage", params),
271            "closePage" => {
272                let handlers = self.close_handlers.lock().unwrap().clone();
273                if handlers.is_empty() {
274                    self.forward("closeServer", params);
275                    return;
276                }
277                for handler in handlers {
278                    tokio::spawn(async move {
279                        let _ = handler().await;
280                    });
281                }
282            }
283            "closeServer" => self.forward("closePage", params),
284            _ => {}
285        }
286    }
287
288    /// Re-sends an event's payload as the mirror-image command; the params
289    /// carry the same fields the command takes. Not `Channel::notify`: a
290    /// forward that fails because the other side already closed is the
291    /// normal end of a socket, so it logs at `debug`, not `warn`.
292    fn forward(&self, method: &'static str, params: &Value) {
293        let channel = self.base.channel().clone();
294        let params = params.clone();
295        tokio::spawn(async move {
296            if let Err(e) = channel.send_no_result(method, params).await {
297                tracing::debug!("WebSocketRoute {} forward failed: {}", method, e);
298            }
299        });
300    }
301}
302
303/// Options for [`WebSocketRoute::close`].
304#[derive(Debug, Default, Clone)]
305#[non_exhaustive]
306pub struct WebSocketRouteCloseOptions {
307    /// WebSocket close code (e.g. 1000 for normal closure).
308    pub code: Option<u16>,
309    /// Human-readable close reason.
310    pub reason: Option<String>,
311}
312
313impl WebSocketRouteCloseOptions {
314    /// WebSocket close code (e.g. 1000 for normal closure).
315    pub fn code(mut self, code: u16) -> Self {
316        self.code = Some(code);
317        self
318    }
319
320    /// Human-readable close reason.
321    pub fn reason(mut self, reason: impl Into<String>) -> Self {
322        self.reason = Some(reason.into());
323        self
324    }
325}
326
327impl ChannelOwner for WebSocketRoute {
328    fn guid(&self) -> &str {
329        self.base.guid()
330    }
331
332    fn type_name(&self) -> &str {
333        self.base.type_name()
334    }
335
336    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
337        self.base.parent()
338    }
339
340    fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
341        self.base.connection()
342    }
343
344    fn initializer(&self) -> &Value {
345        self.base.initializer()
346    }
347
348    fn channel(&self) -> &Channel {
349        self.base.channel()
350    }
351
352    fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
353        self.base.dispose(reason)
354    }
355
356    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
357        self.base.adopt(child)
358    }
359
360    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
361        self.base.add_child(guid, child)
362    }
363
364    fn remove_child(&self, guid: &str) {
365        self.base.remove_child(guid)
366    }
367
368    fn on_event(&self, method: &str, params: Value) {
369        self.handle_event(method, &params);
370        self.base.on_event(method, params)
371    }
372
373    fn was_collected(&self) -> bool {
374        self.base.was_collected()
375    }
376
377    fn as_any(&self) -> &dyn Any {
378        self
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn close_options_setters_write_their_own_fields() {
388        let opts = WebSocketRouteCloseOptions::default()
389            .code(1000)
390            .reason("done");
391        assert_eq!(opts.code, Some(1000));
392        assert_eq!(opts.reason.as_deref(), Some("done"));
393    }
394}