Skip to main content

playwright_rs/protocol/browser_context/
network.rs

1use super::{
2    BrowserContext, ContextWsRouteHandlerEntry, RouteHandlerEntry, RouteHandlerFuture,
3    WsRouteHandlerFuture,
4};
5use crate::error::{Error, Result};
6use crate::protocol::Route;
7use crate::protocol::in_flight::settle_removed;
8use crate::protocol::route::UnrouteBehavior;
9use crate::server::channel_owner::ChannelOwner;
10use std::future::Future;
11use std::sync::{Arc, Mutex};
12
13/// Routes, network and WebSocket interception, HAR replay.
14impl BrowserContext {
15    /// Registers a route handler for context-level network interception.
16    ///
17    /// Routes registered on a context apply to all pages within the context.
18    /// Page-level routes take precedence over context-level routes.
19    ///
20    /// # Arguments
21    ///
22    /// * `pattern` - URL pattern to match (supports glob patterns like "**/*.png")
23    /// * `handler` - Async closure that handles the route
24    ///
25    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route>
26    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
27    pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
28    where
29        F: Fn(Route) -> Fut + Send + Sync + 'static,
30        Fut: Future<Output = Result<()>> + Send + 'static,
31    {
32        let handler =
33            Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
34
35        self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
36            pattern: pattern.to_string(),
37            handler,
38            in_flight: Arc::default(),
39        });
40
41        self.enable_network_interception().await
42    }
43
44    /// Fulfills matching requests from every page in this context using an
45    /// in-process tower `Service`, such as an axum `Router` or a tower-http
46    /// `ServeDir`, with no socket.
47    ///
48    /// Each request whose URL matches `pattern` is rebuilt as an
49    /// `http::Request`, handed to a clone of `service`, and fulfilled with the
50    /// response. The [`route_service`](crate::protocol::route_service) module
51    /// documents what the service sees, the limits of route interception
52    /// compared with a real listener, and how to wait on a wasm frontend.
53    ///
54    /// # Arguments
55    ///
56    /// * `pattern` - URL pattern to match (supports glob patterns like `"https://app.example/**"`)
57    /// * `service` - Any [`RouteService`](crate::protocol::route_service::RouteService):
58    ///   an axum `Router`, a tower-http `ServeDir`, a `tower::service_fn`; cloned per request
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if network interception cannot be enabled. A service
63    /// that fails at request time aborts that request and logs the error; it
64    /// does not surface here.
65    ///
66    /// See: <https://playwright.dev/docs/mock>
67    #[cfg(feature = "route-service")]
68    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
69    pub async fn route_service<S: crate::protocol::route_service::RouteService>(
70        &self,
71        pattern: &str,
72        service: S,
73    ) -> Result<()> {
74        // Captured here, a hop away, so the service's view of the browser
75        // (its engine, its cookie jar) does not depend on walking each
76        // request's object chain.
77        let context = Some(self.clone());
78        self.route(pattern, move |route| {
79            crate::protocol::route_service::fulfill_from_service(
80                route,
81                service.clone(),
82                context.clone(),
83            )
84        })
85        .await
86    }
87
88    /// Removes route handler(s) matching the given URL pattern.
89    ///
90    /// # Arguments
91    ///
92    /// * `pattern` - URL pattern to remove handlers for
93    ///
94    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute>
95    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %pattern))]
96    pub async fn unroute(&self, pattern: &str) -> Result<()> {
97        self.route_handlers
98            .lock()
99            .unwrap()
100            .retain(|entry| entry.pattern != pattern);
101        self.enable_network_interception().await
102    }
103
104    /// Removes all registered route handlers.
105    ///
106    /// # Arguments
107    ///
108    /// * `behavior` - What to do about handler invocations still running:
109    ///   [`UnrouteBehavior::Wait`] returns only after they finish,
110    ///   [`UnrouteBehavior::IgnoreErrors`] lets them fail without a log line,
111    ///   and the default (`None`) neither waits nor silences them.
112    ///
113    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute-all>
114    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
115    pub async fn unroute_all(&self, behavior: Option<UnrouteBehavior>) -> Result<()> {
116        let removed = std::mem::take(&mut *self.route_handlers.lock().unwrap());
117        settle_removed(removed.iter().map(|entry| &entry.in_flight), behavior).await;
118        self.enable_network_interception().await
119    }
120
121    /// Replays network requests from a HAR file recorded previously.
122    ///
123    /// Requests matching `options.url` (or all requests if omitted) will be
124    /// served from the archive for every page in this context.  Unmatched
125    /// requests are either aborted or passed through depending on
126    /// `options.not_found` (`"abort"` is the default).
127    ///
128    /// # Arguments
129    ///
130    /// * `har_path` - Path to the `.har` file on disk
131    /// * `options` - Optional settings (url filter, not_found policy, update mode)
132    ///
133    /// # Errors
134    ///
135    /// Returns error if:
136    /// - `har_path` does not exist or cannot be read by the Playwright server
137    /// - The Playwright server fails to open the archive
138    ///
139    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har>
140    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
141    pub async fn route_from_har(
142        &self,
143        har_path: &str,
144        options: impl Into<Option<crate::protocol::RouteFromHarOptions>>,
145    ) -> Result<()> {
146        let options = options.into();
147        let opts = options.unwrap_or_default();
148        let not_found = opts.not_found.unwrap_or_else(|| "abort".to_string());
149        let url_filter = opts.url.clone();
150
151        let abs_path = std::path::Path::new(har_path).canonicalize().map_err(|e| {
152            Error::InvalidPath(format!(
153                "route_from_har: cannot resolve '{}': {}",
154                har_path, e
155            ))
156        })?;
157        let abs_str = abs_path.to_string_lossy().into_owned();
158
159        let connection = self.connection();
160        let local_utils = {
161            let all = connection.all_objects_sync();
162            all.into_iter()
163                .find(|o| o.type_name() == "LocalUtils")
164                .and_then(|o| {
165                    o.as_any()
166                        .downcast_ref::<crate::protocol::LocalUtils>()
167                        .cloned()
168                })
169                .ok_or_else(|| {
170                    Error::ProtocolError(
171                        "route_from_har: LocalUtils not found in connection registry".to_string(),
172                    )
173                })?
174        };
175
176        let har_id = local_utils.har_open(&abs_str).await?;
177
178        let pattern = url_filter.unwrap_or_else(|| "**/*".to_string());
179
180        let har_id_clone = har_id.clone();
181        let local_utils_clone = local_utils.clone();
182        let not_found_clone = not_found.clone();
183
184        self.route(&pattern, move |route| {
185            let har_id = har_id_clone.clone();
186            let local_utils = local_utils_clone.clone();
187            let not_found = not_found_clone.clone();
188            async move {
189                let request = route.request();
190                let req_url = request.url().to_string();
191                let req_method = request.method().to_string();
192
193                let headers = crate::protocol::route_params::header_array(request.header_pairs());
194
195                let lookup = local_utils
196                    .har_lookup(
197                        &har_id,
198                        &req_url,
199                        &req_method,
200                        headers,
201                        None,
202                        request.is_navigation_request(),
203                    )
204                    .await;
205
206                match lookup {
207                    Err(e) => {
208                        tracing::warn!("har_lookup error for {}: {}", req_url, e);
209                        route.continue_(None).await
210                    }
211                    Ok(result) => match result.action.as_str() {
212                        "redirect" => {
213                            let redirect_url = result.redirect_url.unwrap_or_default();
214                            let opts = crate::protocol::ContinueOptions::builder()
215                                .url(redirect_url)
216                                .build();
217                            route.continue_(Some(opts)).await
218                        }
219                        "fulfill" => {
220                            route
221                                .fulfill(Some(crate::protocol::route_params::har_fulfill_options(
222                                    result.status,
223                                    result.body.as_deref(),
224                                    result.headers.as_deref(),
225                                )))
226                                .await
227                        }
228                        _ => {
229                            if not_found == "fallback" {
230                                route.fallback(None).await
231                            } else {
232                                route.abort(None).await
233                            }
234                        }
235                    },
236                }
237            }
238        })
239        .await
240    }
241
242    /// Intercepts WebSocket connections matching the given URL pattern for all pages in this context.
243    ///
244    /// When a WebSocket connection from any page in this context matches `url`,
245    /// the `handler` is called with a [`WebSocketRoute`](crate::protocol::WebSocketRoute) object.
246    /// The handler must call [`connect_to_server`](crate::protocol::WebSocketRoute::connect_to_server)
247    /// to forward the connection to the real server, or
248    /// [`close`](crate::protocol::WebSocketRoute::close) to terminate it.
249    ///
250    /// # Arguments
251    ///
252    /// * `url` — URL glob pattern (e.g. `"ws://**"` or `"wss://example.com/ws"`).
253    /// * `handler` — Async closure receiving a `WebSocketRoute`.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the RPC call to enable interception fails.
258    ///
259    /// See: <https://playwright.dev/docs/api/class-browsercontext#browser-context-route-web-socket>
260    #[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), url = %url))]
261    pub async fn route_web_socket<F, Fut>(&self, url: &str, handler: F) -> Result<()>
262    where
263        F: Fn(crate::protocol::WebSocketRoute) -> Fut + Send + Sync + 'static,
264        Fut: Future<Output = Result<()>> + Send + 'static,
265    {
266        let handler = Arc::new(
267            move |route: crate::protocol::WebSocketRoute| -> WsRouteHandlerFuture {
268                Box::pin(handler(route))
269            },
270        );
271
272        self.ws_route_handlers
273            .lock()
274            .unwrap()
275            .push(ContextWsRouteHandlerEntry {
276                pattern: url.to_string(),
277                handler,
278            });
279
280        self.enable_ws_interception().await
281    }
282
283    /// Updates WebSocket interception patterns for this context.
284    async fn enable_ws_interception(&self) -> Result<()> {
285        let patterns: Vec<serde_json::Value> = self
286            .ws_route_handlers
287            .lock()
288            .unwrap()
289            .iter()
290            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
291            .collect();
292
293        self.channel()
294            .send_no_result(
295                "setWebSocketInterceptionPatterns",
296                serde_json::json!({ "patterns": patterns }),
297            )
298            .await
299    }
300
301    /// Updates network interception patterns for this context
302    async fn enable_network_interception(&self) -> Result<()> {
303        let patterns: Vec<serde_json::Value> = self
304            .route_handlers
305            .lock()
306            .unwrap()
307            .iter()
308            .map(|entry| serde_json::json!({ "glob": entry.pattern }))
309            .collect();
310
311        self.channel()
312            .send_no_result(
313                "setNetworkInterceptionPatterns",
314                serde_json::json!({ "patterns": patterns }),
315            )
316            .await
317    }
318
319    /// Handles a route event from the protocol
320    pub(super) async fn on_route_event(
321        route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
322        route: Route,
323    ) {
324        let handlers = route_handlers.lock().unwrap().clone();
325        let url = route.request().url().to_string();
326
327        for entry in handlers.iter().rev() {
328            if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
329                let handler = entry.handler.clone();
330                let running = entry.in_flight.enter();
331                let outcome = handler(route.clone()).await;
332                drop(running);
333                if let Err(e) = outcome {
334                    let quiet = entry.in_flight.ignores_errors();
335                    if !quiet {
336                        tracing::warn!("Context route handler error: {}", e);
337                    }
338                    // A handler that failed before reaching a route command
339                    // leaves the request pending; abort it so the browser
340                    // sees a failed request instead of waiting out its timeout.
341                    if !route.was_handled()
342                        && let Err(abort_error) = route.abort(Some("failed")).await
343                        && !quiet
344                    {
345                        tracing::warn!("aborting the unhandled route failed too: {}", abort_error);
346                    }
347                    break;
348                }
349                if !route.was_handled() {
350                    continue;
351                }
352                break;
353            }
354        }
355    }
356}