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, ¶ms);
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}