Skip to main content

ferridriver_script/bindings/
network.rs

1//! QuickJS bindings for `ferridriver::network::{Request, Response, WebSocket}`.
2//!
3//! Each wrapper is a thin pass-through to the core type per Rule 1.
4//! Method names mirror Playwright's `client/network.ts` exactly so
5//! scripts use the same `request.url()`, `response.body()`,
6//! `webSocket.waitForEvent()` shapes as Playwright tests.
7
8use ferridriver::network::{
9  Request as CoreRequest, Response as CoreResponse, WebSocket as CoreWebSocket, WebSocketEvent, WebSocketPayload,
10};
11use ferridriver::route::{ContinueOverrides, FulfillResponse, Route as CoreRoute};
12use rquickjs::{Ctx, JsLifetime, Value, class::Trace};
13use std::sync::{Arc, Mutex as StdMutex};
14
15use crate::bindings::convert::{FerriResultExt, serde_from_js, serde_to_js};
16
17// ── RequestJs ────────────────────────────────────────────────────────────────
18
19#[derive(JsLifetime, Trace)]
20#[rquickjs::class(rename = "Request")]
21pub struct RequestJs {
22  #[qjs(skip_trace)]
23  inner: CoreRequest,
24  /// Owning page reference, used by `frame()` to resolve frame_id via
25  /// the page's frame cache. `None` when the wrapper was constructed
26  /// without page context.
27  #[qjs(skip_trace)]
28  page: Option<Arc<ferridriver::Page>>,
29}
30
31impl RequestJs {
32  #[must_use]
33  pub fn new(inner: CoreRequest) -> Self {
34    Self { inner, page: None }
35  }
36
37  #[must_use]
38  pub fn new_with_page(inner: CoreRequest, page: Arc<ferridriver::Page>) -> Self {
39    Self {
40      inner,
41      page: Some(page),
42    }
43  }
44}
45
46#[rquickjs::methods]
47impl RequestJs {
48  #[qjs(rename = "url")]
49  pub fn url(&self) -> String {
50    self.inner.url().to_string()
51  }
52
53  #[qjs(rename = "method")]
54  pub fn method(&self) -> String {
55    self.inner.method().to_string()
56  }
57
58  #[qjs(rename = "resourceType")]
59  pub fn resource_type(&self) -> String {
60    self.inner.resource_type().to_string()
61  }
62
63  #[qjs(rename = "isNavigationRequest")]
64  pub fn is_navigation_request(&self) -> bool {
65    self.inner.is_navigation_request()
66  }
67
68  #[qjs(rename = "postData")]
69  pub fn post_data(&self) -> Option<String> {
70    self.inner.post_data()
71  }
72
73  /// Mirrors Playwright `request.postDataBuffer(): Buffer | null`.
74  /// QuickJS has no native `Buffer`, so the raw body bytes are returned
75  /// base64-encoded (same convention as `response.body()`); `null` when
76  /// the request has no post body.
77  #[qjs(rename = "postDataBuffer")]
78  pub fn post_data_buffer(&self) -> Option<String> {
79    self
80      .inner
81      .post_data_buffer()
82      .map(|b| base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b))
83  }
84
85  #[qjs(rename = "postDataJSON")]
86  pub fn post_data_json<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
87    let v = self.inner.post_data_json().into_js()?;
88    let v = v.unwrap_or(serde_json::Value::Null);
89    serde_to_js(&ctx, &v)
90  }
91
92  #[qjs(rename = "headers")]
93  pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
94    serde_to_js(&ctx, &self.inner.headers())
95  }
96
97  #[qjs(rename = "headersArray")]
98  pub async fn headers_array<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
99    let arr = self.inner.headers_array().await;
100    let pairs: Vec<(String, String)> = arr.into_iter().map(|h| (h.name, h.value)).collect();
101    crate::bindings::convert::name_value_array_to_js(&ctx, &pairs)
102  }
103
104  #[qjs(rename = "allHeaders")]
105  pub async fn all_headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
106    let h = self.inner.all_headers().await.into_js()?;
107    serde_to_js(&ctx, &h)
108  }
109
110  #[qjs(rename = "headerValue")]
111  pub async fn header_value(&self, name: String) -> rquickjs::Result<Option<String>> {
112    self.inner.header_value(&name).await.into_js()
113  }
114
115  #[qjs(rename = "failure")]
116  pub fn failure<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
117    match self.inner.failure() {
118      Some(error_text) => {
119        let o = rquickjs::Object::new(ctx.clone())?;
120        o.set("errorText", error_text)?;
121        Ok(o.into_value())
122      },
123      None => Ok(Value::new_null(ctx.clone())),
124    }
125  }
126
127  #[qjs(rename = "timing")]
128  pub fn timing<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
129    serde_to_js(&ctx, &self.inner.timing())
130  }
131
132  #[qjs(rename = "sizes")]
133  pub async fn sizes<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
134    let sizes = self.inner.sizes().await.into_js()?;
135    serde_to_js(&ctx, &sizes)
136  }
137
138  #[qjs(rename = "redirectedFrom")]
139  pub fn redirected_from(&self) -> Option<RequestJs> {
140    self.inner.redirected_from().map(|r| match self.page.as_ref() {
141      Some(page) => RequestJs::new_with_page(r, page.clone()),
142      None => RequestJs::new(r),
143    })
144  }
145
146  #[qjs(rename = "redirectedTo")]
147  pub fn redirected_to(&self) -> Option<RequestJs> {
148    self.inner.redirected_to().map(|r| match self.page.as_ref() {
149      Some(page) => RequestJs::new_with_page(r, page.clone()),
150      None => RequestJs::new(r),
151    })
152  }
153
154  #[qjs(rename = "response")]
155  pub async fn response(&self) -> rquickjs::Result<Option<ResponseJs>> {
156    let resp = self.inner.response().await.into_js()?;
157    Ok(resp.map(|r| match self.page.as_ref() {
158      Some(page) => ResponseJs::new_with_page(r, page.clone()),
159      None => ResponseJs::new(r),
160    }))
161  }
162
163  /// Mirrors Playwright `request.frame(): Frame`. Resolves the
164  /// initiating frame_id via the owning page's frame cache. Returns
165  /// `null` when no frame context is attached.
166  #[qjs(rename = "frame")]
167  pub fn frame(&self) -> Option<crate::bindings::frame::FrameJs> {
168    let page = self.page.as_ref()?;
169    let frame_id = self.inner.frame_id()?;
170    for f in page.frames() {
171      if f.frame_id() == frame_id {
172        return Some(crate::bindings::frame::FrameJs::new(f));
173      }
174    }
175    None
176  }
177
178  /// Mirrors Playwright `request.serviceWorker(): Worker | null`.
179  /// Backed by `Request::service_worker` which always returns `None`
180  /// today (Tier-2 §2.7 hasn't landed). Surface kept stable so flipping
181  /// the implementation on later is non-breaking.
182  #[qjs(rename = "serviceWorker")]
183  pub fn service_worker<'js>(&self, ctx: Ctx<'js>) -> Value<'js> {
184    // Query the core accessor so this stays self-aware once §2.7 fills
185    // in the Worker class — the binding then maps `Some(worker)` to a
186    // real WorkerJs instance.
187    let _ = self.inner.service_worker();
188    Value::new_null(ctx)
189  }
190}
191
192// ── ResponseJs ───────────────────────────────────────────────────────────────
193
194#[derive(JsLifetime, Trace)]
195#[rquickjs::class(rename = "Response")]
196pub struct ResponseJs {
197  #[qjs(skip_trace)]
198  inner: CoreResponse,
199  #[qjs(skip_trace)]
200  page: Option<Arc<ferridriver::Page>>,
201}
202
203impl ResponseJs {
204  #[must_use]
205  pub fn new(inner: CoreResponse) -> Self {
206    Self { inner, page: None }
207  }
208
209  #[must_use]
210  pub fn new_with_page(inner: CoreResponse, page: Arc<ferridriver::Page>) -> Self {
211    Self {
212      inner,
213      page: Some(page),
214    }
215  }
216}
217
218#[rquickjs::methods]
219impl ResponseJs {
220  #[qjs(rename = "url")]
221  pub fn url(&self) -> String {
222    self.inner.url().to_string()
223  }
224
225  #[qjs(rename = "status")]
226  pub fn status(&self) -> i32 {
227    i32::try_from(self.inner.status()).unwrap_or(i32::MAX)
228  }
229
230  #[qjs(rename = "statusText")]
231  pub fn status_text(&self) -> String {
232    self.inner.status_text().to_string()
233  }
234
235  #[qjs(rename = "ok")]
236  pub fn ok(&self) -> bool {
237    self.inner.ok()
238  }
239
240  #[qjs(rename = "fromServiceWorker")]
241  pub fn is_from_service_worker(&self) -> bool {
242    self.inner.is_from_service_worker()
243  }
244
245  #[qjs(rename = "headers")]
246  pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
247    serde_to_js(&ctx, &self.inner.headers())
248  }
249
250  #[qjs(rename = "allHeaders")]
251  pub async fn all_headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
252    let h = self.inner.all_headers().await.into_js()?;
253    serde_to_js(&ctx, &h)
254  }
255
256  #[qjs(rename = "headersArray")]
257  pub async fn headers_array<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
258    let arr = self.inner.headers_array().await;
259    let pairs: Vec<(String, String)> = arr.into_iter().map(|h| (h.name, h.value)).collect();
260    crate::bindings::convert::name_value_array_to_js(&ctx, &pairs)
261  }
262
263  #[qjs(rename = "headerValue")]
264  pub async fn header_value(&self, name: String) -> rquickjs::Result<Option<String>> {
265    self.inner.header_value(&name).await.into_js()
266  }
267
268  #[qjs(rename = "headerValues")]
269  pub async fn header_values(&self, name: String) -> rquickjs::Result<Vec<String>> {
270    self.inner.header_values(&name).await.into_js()
271  }
272
273  /// Response body as base64-encoded string. QuickJS does not have
274  /// `Buffer`; scripts decode if they need raw bytes.
275  #[qjs(rename = "body")]
276  pub async fn body<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
277    let bytes = self.inner.body().await.into_js()?;
278    let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
279    serde_to_js(&ctx, &encoded)
280  }
281
282  #[qjs(rename = "text")]
283  pub async fn text(&self) -> rquickjs::Result<String> {
284    self.inner.text().await.into_js()
285  }
286
287  #[qjs(rename = "json")]
288  pub async fn json<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
289    // Body -> JS via QuickJS's C JSON parser; no serde_json::Value
290    // middle allocation, no dependence on the JS `JSON` global.
291    let text = self.inner.text().await.into_js()?;
292    ctx.json_parse(text)
293  }
294
295  /// Mirrors Playwright `response.finished()`. Resolves to `null` on
296  /// success, throws on failure.
297  #[qjs(rename = "finished")]
298  pub async fn finished<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
299    match self.inner.finished().await {
300      Ok(()) => Ok(Value::new_null(ctx.clone())),
301      Err(e) => Err(rquickjs::Error::new_from_js_message(
302        "Response.finished failure",
303        "Error",
304        e.to_string(),
305      )),
306    }
307  }
308
309  #[qjs(rename = "serverAddr")]
310  pub async fn server_addr<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
311    match self.inner.server_addr().await {
312      Some(a) => {
313        let o = rquickjs::Object::new(ctx.clone())?;
314        o.set("ipAddress", a.ip_address)?;
315        o.set("port", a.port)?;
316        Ok(o.into_value())
317      },
318      None => Ok(Value::new_null(ctx.clone())),
319    }
320  }
321
322  #[qjs(rename = "securityDetails")]
323  pub async fn security_details<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
324    match self.inner.security_details().await {
325      Some(s) => serde_to_js(&ctx, &s),
326      None => Ok(Value::new_null(ctx.clone())),
327    }
328  }
329
330  /// Mirrors Playwright `response.httpVersion(): Promise<string>`.
331  /// Empty string when the backend did not report a protocol version.
332  #[qjs(rename = "httpVersion")]
333  pub async fn http_version(&self) -> String {
334    self.inner.http_version().await.unwrap_or_default()
335  }
336
337  #[qjs(rename = "request")]
338  pub fn request(&self) -> RequestJs {
339    match self.page.as_ref() {
340      Some(page) => RequestJs::new_with_page(self.inner.request(), page.clone()),
341      None => RequestJs::new(self.inner.request()),
342    }
343  }
344
345  /// Mirrors Playwright `response.frame(): Frame`. Convenience for
346  /// `response.request().frame()`.
347  #[qjs(rename = "frame")]
348  pub fn frame(&self) -> Option<crate::bindings::frame::FrameJs> {
349    self.request().frame()
350  }
351}
352
353// ── WebSocketJs ──────────────────────────────────────────────────────────────
354
355#[derive(JsLifetime, Trace)]
356#[rquickjs::class(rename = "WebSocket")]
357pub struct WebSocketJs {
358  #[qjs(skip_trace)]
359  inner: CoreWebSocket,
360}
361
362impl WebSocketJs {
363  #[must_use]
364  pub fn new(inner: CoreWebSocket) -> Self {
365    Self { inner }
366  }
367}
368
369#[rquickjs::methods]
370impl WebSocketJs {
371  #[qjs(rename = "url")]
372  pub fn url(&self) -> String {
373    self.inner.url().to_string()
374  }
375
376  #[qjs(rename = "isClosed")]
377  pub fn is_closed(&self) -> bool {
378    self.inner.is_closed()
379  }
380
381  /// Mirrors Playwright `webSocket.waitForEvent(event, options?)`.
382  /// Resolves with `{ event, payload, error }`.
383  #[qjs(rename = "waitForEvent")]
384  pub async fn wait_for_event<'js>(
385    &self,
386    ctx: Ctx<'js>,
387    event: String,
388    timeout_ms: Option<f64>,
389  ) -> rquickjs::Result<Value<'js>> {
390    let timeout = std::time::Duration::from_millis(
391      #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
392      {
393        timeout_ms.unwrap_or(30000.0) as u64
394      },
395    );
396    let mut rx = self.inner.subscribe();
397    let event_lc = event.to_ascii_lowercase();
398    let deadline = tokio::time::Instant::now() + timeout;
399    loop {
400      let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
401      if remaining.is_zero() {
402        return Err(rquickjs::Error::new_from_js_message(
403          "WebSocket.waitForEvent",
404          "TimeoutError",
405          format!(
406            "Timeout {}ms exceeded while waiting for WebSocket event {event:?}",
407            timeout.as_millis()
408          ),
409        ));
410      }
411      match tokio::time::timeout(remaining, rx.recv()).await {
412        Ok(Ok(ev)) => {
413          if let Some(v) = ws_event_to_js(&ctx, &event_lc, &ev)? {
414            return Ok(v);
415          }
416        },
417        Ok(Err(_)) => {
418          return Err(rquickjs::Error::new_from_js_message(
419            "WebSocket.waitForEvent",
420            "Error",
421            "WebSocket channel closed".to_string(),
422          ));
423        },
424        Err(_) => {
425          return Err(rquickjs::Error::new_from_js_message(
426            "WebSocket.waitForEvent",
427            "TimeoutError",
428            format!(
429              "Timeout {}ms exceeded while waiting for WebSocket event {event:?}",
430              timeout.as_millis()
431            ),
432          ));
433        },
434      }
435    }
436  }
437}
438
439/// Build the `{ event, payload, error }` JS object for a matched
440/// WebSocket event directly — no serde_json::Value middle allocation.
441fn ws_event_to_js<'js>(ctx: &Ctx<'js>, name: &str, ev: &WebSocketEvent) -> rquickjs::Result<Option<Value<'js>>> {
442  let make = |event: &str, payload: Value<'js>, error: Value<'js>| -> rquickjs::Result<Value<'js>> {
443    let o = rquickjs::Object::new(ctx.clone())?;
444    o.set("event", event)?;
445    o.set("payload", payload)?;
446    o.set("error", error)?;
447    Ok(o.into_value())
448  };
449  let null = || Value::new_null(ctx.clone());
450  let js_str =
451    |s: &str| -> rquickjs::Result<Value<'js>> { Ok(rquickjs::String::from_str(ctx.clone(), s)?.into_value()) };
452  let payload = |p: &WebSocketPayload| -> rquickjs::Result<Value<'js>> {
453    match p {
454      WebSocketPayload::Text(s) => js_str(s),
455      WebSocketPayload::Binary(b) => js_str(&base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b)),
456    }
457  };
458  Ok(match (name, ev) {
459    ("framesent", WebSocketEvent::FrameSent(p)) => Some(make("framesent", payload(p)?, null())?),
460    ("framereceived", WebSocketEvent::FrameReceived(p)) => Some(make("framereceived", payload(p)?, null())?),
461    ("socketerror", WebSocketEvent::Error(msg)) => Some(make("socketerror", null(), js_str(msg)?)?),
462    ("close", WebSocketEvent::Close) => Some(make("close", null(), null())?),
463    _ => None,
464  })
465}
466
467// ── RouteJs ──────────────────────────────────────────────────────────────────
468//
469// Mirrors Playwright's `Route` interface: `fulfill`, `continue`, `abort`,
470// plus the `request()` getter. The handler-callback path (registering
471// the JS function with `page.route(matcher, fn)`) lives in `bindings/page.rs`
472// — `RouteJs` itself is the per-invocation wrapper passed into the
473// handler.
474
475#[derive(JsLifetime, Trace)]
476#[rquickjs::class(rename = "Route")]
477pub struct RouteJs {
478  #[qjs(skip_trace)]
479  inner: StdMutex<Option<CoreRoute>>,
480}
481
482/// `Request` returned by `route.request()` (Playwright parity) — a
483/// read-only snapshot of the intercepted request's url / method /
484/// headers / postData / resourceType so handlers can `route.request()
485/// .headers()['x-foo']` exactly like Playwright code does.
486#[derive(JsLifetime, Trace)]
487#[rquickjs::class(rename = "RouteRequest")]
488pub struct RouteRequestJs {
489  #[qjs(skip_trace)]
490  url: String,
491  #[qjs(skip_trace)]
492  method: String,
493  #[qjs(skip_trace)]
494  headers: rustc_hash::FxHashMap<String, String>,
495  #[qjs(skip_trace)]
496  post_data: Option<String>,
497  #[qjs(skip_trace)]
498  resource_type: String,
499}
500
501#[rquickjs::methods]
502impl RouteRequestJs {
503  #[qjs(rename = "url")]
504  pub fn url(&self) -> String {
505    self.url.clone()
506  }
507  #[qjs(rename = "method")]
508  pub fn method(&self) -> String {
509    self.method.clone()
510  }
511  /// Playwright: `request.headers(): Record<string, string>`.
512  #[qjs(rename = "headers")]
513  pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
514    serde_to_js(&ctx, &self.headers)
515  }
516  /// Playwright: `request.headersArray(): { name, value }[]`.
517  #[qjs(rename = "headersArray")]
518  pub fn headers_array<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
519    let pairs: Vec<(&str, &str)> = self.headers.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
520    crate::bindings::convert::name_value_array_to_js(&ctx, &pairs)
521  }
522  /// Playwright: `request.headerValue(name): Promise<string | null>`.
523  #[qjs(rename = "headerValue")]
524  pub fn header_value(&self, name: String) -> Option<String> {
525    let lower = name.to_ascii_lowercase();
526    self
527      .headers
528      .iter()
529      .find(|(k, _)| k.to_ascii_lowercase() == lower)
530      .map(|(_, v)| v.clone())
531  }
532  #[qjs(rename = "postData")]
533  pub fn post_data(&self) -> Option<String> {
534    self.post_data.clone()
535  }
536  #[qjs(rename = "resourceType")]
537  pub fn resource_type(&self) -> String {
538    self.resource_type.clone()
539  }
540}
541
542impl RouteJs {
543  /// Construct a wrapper around a paused-route handle. The handle is
544  /// consumed on the first call to `fulfill` / `continue` / `abort`;
545  /// subsequent calls become no-ops. If the JS callback returns
546  /// without invoking any of the three, the inner [`CoreRoute`]'s
547  /// own `Drop` falls open and continues the request.
548  #[must_use]
549  pub fn new(inner: CoreRoute) -> Self {
550    Self {
551      inner: StdMutex::new(Some(inner)),
552    }
553  }
554}
555
556/// WHATWG/Playwright `Headers` — `{ [name: string]: string }` record.
557/// Deserialised as a map then flattened to pairs at the call site so
558/// the core API (which takes `Vec<(String, String)>`) sees the
559/// expected shape. Accepting the record is REQUIRED for Playwright
560/// parity: `route.fulfill({ headers: { 'x-from': 'route' } })` is
561/// the documented form (see `client/network.ts`).
562type JsHeadersMap = std::collections::BTreeMap<String, String>;
563
564#[derive(serde::Deserialize, Debug, Default)]
565#[serde(rename_all = "camelCase", default)]
566struct JsFulfillOptions {
567  status: Option<i32>,
568  body: Option<String>,
569  content_type: Option<String>,
570  headers: Option<JsHeadersMap>,
571}
572
573#[derive(serde::Deserialize, Debug, Default)]
574#[serde(rename_all = "camelCase", default)]
575struct JsContinueOptions {
576  url: Option<String>,
577  method: Option<String>,
578  headers: Option<JsHeadersMap>,
579  post_data: Option<String>,
580}
581
582/// Lower the Playwright `Headers` record (`{name: value}`) the bindings
583/// accept down to the `Vec<(String, String)>` core expects.
584fn headers_to_pairs(map: Option<JsHeadersMap>) -> Vec<(String, String)> {
585  map.map(|m| m.into_iter().collect()).unwrap_or_default()
586}
587
588#[rquickjs::methods]
589impl RouteJs {
590  /// Playwright: `route.request(): Request` — the intercepted request,
591  /// inspectable as a real Request class (`.url()`, `.method()`,
592  /// `.headers()`, `.headerValue(name)`, `.headersArray()`,
593  /// `.postData()`, `.resourceType()`). LLM-generated Playwright code
594  /// uses `route.request().headers()['x-foo']` constantly; previously
595  /// `route.request` was undefined and the handler silently threw,
596  /// falling through to the network (`Failed to fetch`).
597  #[qjs(rename = "request")]
598  pub fn request(&self) -> RouteRequestJs {
599    let snap = self
600      .inner
601      .lock()
602      .ok()
603      .and_then(|g| g.as_ref().map(|r| r.request().clone()));
604    if let Some(r) = snap {
605      RouteRequestJs {
606        url: r.url,
607        method: r.method,
608        headers: r.headers,
609        post_data: r.post_data,
610        resource_type: r.resource_type,
611      }
612    } else {
613      RouteRequestJs {
614        url: String::new(),
615        method: String::new(),
616        headers: rustc_hash::FxHashMap::default(),
617        post_data: None,
618        resource_type: String::new(),
619      }
620    }
621  }
622
623  /// Mirrors Playwright `route.url(): string`.
624  #[qjs(rename = "url")]
625  pub fn url(&self) -> String {
626    self
627      .inner
628      .lock()
629      .ok()
630      .and_then(|g| g.as_ref().map(|r| r.request().url.clone()))
631      .unwrap_or_default()
632  }
633
634  /// Mirrors Playwright `route.request().method()`.
635  #[qjs(rename = "method")]
636  pub fn method(&self) -> String {
637    self
638      .inner
639      .lock()
640      .ok()
641      .and_then(|g| g.as_ref().map(|r| r.request().method.clone()))
642      .unwrap_or_default()
643  }
644
645  /// Mirrors Playwright `route.request().resourceType()`.
646  #[qjs(rename = "resourceType")]
647  pub fn resource_type(&self) -> String {
648    self
649      .inner
650      .lock()
651      .ok()
652      .and_then(|g| g.as_ref().map(|r| r.request().resource_type.clone()))
653      .unwrap_or_default()
654  }
655
656  /// Mirrors Playwright `route.request().postData(): string | null`.
657  #[qjs(rename = "postData")]
658  pub fn post_data(&self) -> Option<String> {
659    self
660      .inner
661      .lock()
662      .ok()
663      .and_then(|g| g.as_ref().and_then(|r| r.request().post_data.clone()))
664  }
665
666  /// Headers as a plain JS object (`Record<string, string>`).
667  #[qjs(rename = "headers")]
668  pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
669    let map: rustc_hash::FxHashMap<String, String> = self
670      .inner
671      .lock()
672      .ok()
673      .and_then(|g| g.as_ref().map(|r| r.request().headers.clone()))
674      .unwrap_or_default();
675    serde_to_js(&ctx, &map)
676  }
677
678  /// Mirrors Playwright `route.fulfill(options?)`.
679  #[qjs(rename = "fulfill")]
680  pub fn fulfill<'js>(&self, ctx: Ctx<'js>, options: rquickjs::function::Opt<Value<'js>>) -> rquickjs::Result<()> {
681    let opts: JsFulfillOptions = match options.0 {
682      Some(v) if !v.is_undefined() && !v.is_null() => serde_from_js(&ctx, v)?,
683      _ => JsFulfillOptions::default(),
684    };
685    let route = self
686      .inner
687      .lock()
688      .ok()
689      .and_then(|mut g| g.take())
690      .ok_or_else(|| rquickjs::Error::new_from_js_message("Route", "Error", "Route already handled".to_string()))?;
691    let mut headers: Vec<(String, String)> = headers_to_pairs(opts.headers);
692    if let Some(ct) = opts.content_type.clone() {
693      if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("content-type")) {
694        headers.push(("content-type".to_string(), ct));
695      }
696    }
697    let body_bytes = opts.body.unwrap_or_default().into_bytes();
698    route.fulfill(FulfillResponse {
699      status: opts.status.unwrap_or(200),
700      headers,
701      body: body_bytes,
702      content_type: opts.content_type,
703    });
704    Ok(())
705  }
706
707  /// Mirrors Playwright `route.continue(options?)`.
708  #[qjs(rename = "continue")]
709  pub fn continue_<'js>(&self, ctx: Ctx<'js>, options: rquickjs::function::Opt<Value<'js>>) -> rquickjs::Result<()> {
710    let opts: JsContinueOptions = match options.0 {
711      Some(v) if !v.is_undefined() && !v.is_null() => serde_from_js(&ctx, v)?,
712      _ => JsContinueOptions::default(),
713    };
714    let route = self
715      .inner
716      .lock()
717      .ok()
718      .and_then(|mut g| g.take())
719      .ok_or_else(|| rquickjs::Error::new_from_js_message("Route", "Error", "Route already handled".to_string()))?;
720    route.continue_route(ContinueOverrides {
721      url: opts.url,
722      method: opts.method,
723      headers: opts.headers.map(|m| m.into_iter().collect()),
724      post_data: opts.post_data.map(String::into_bytes),
725    });
726    Ok(())
727  }
728
729  /// Mirrors Playwright `route.fallback(options?)`
730  /// (`client/network.ts`): hand the request to the next matching
731  /// handler with the given overrides applied. ferridriver dispatches a
732  /// single handler per matched route, so `fallback` resolves the route
733  /// by continuing the request with the overrides applied (with no
734  /// overrides this is the unmodified request, matching the end state
735  /// Playwright's `fallback` reaches once no further handler claims it).
736  #[qjs(rename = "fallback")]
737  pub fn fallback<'js>(&self, ctx: Ctx<'js>, options: rquickjs::function::Opt<Value<'js>>) -> rquickjs::Result<()> {
738    let opts: JsContinueOptions = match options.0 {
739      Some(v) if !v.is_undefined() && !v.is_null() => serde_from_js(&ctx, v)?,
740      _ => JsContinueOptions::default(),
741    };
742    let route = self
743      .inner
744      .lock()
745      .ok()
746      .and_then(|mut g| g.take())
747      .ok_or_else(|| rquickjs::Error::new_from_js_message("Route", "Error", "Route already handled".to_string()))?;
748    route.fallback(ContinueOverrides {
749      url: opts.url,
750      method: opts.method,
751      headers: opts.headers.map(|m| m.into_iter().collect()),
752      post_data: opts.post_data.map(String::into_bytes),
753    });
754    Ok(())
755  }
756
757  /// Mirrors Playwright `route.abort(errorCode?)`.
758  #[qjs(rename = "abort")]
759  pub fn abort(&self, error_code: Option<String>) -> rquickjs::Result<()> {
760    let route = self
761      .inner
762      .lock()
763      .ok()
764      .and_then(|mut g| g.take())
765      .ok_or_else(|| rquickjs::Error::new_from_js_message("Route", "Error", "Route already handled".to_string()))?;
766    route.abort(&error_code.unwrap_or_else(|| "blockedbyclient".to_string()));
767    Ok(())
768  }
769}