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}