1use 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#[derive(JsLifetime, Trace)]
20#[rquickjs::class(rename = "Request")]
21pub struct RequestJs {
22 #[qjs(skip_trace)]
23 inner: CoreRequest,
24 #[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 #[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 #[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 #[qjs(rename = "serviceWorker")]
183 pub fn service_worker<'js>(&self, ctx: Ctx<'js>) -> Value<'js> {
184 let _ = self.inner.service_worker();
188 Value::new_null(ctx)
189 }
190}
191
192#[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 #[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 let text = self.inner.text().await.into_js()?;
292 ctx.json_parse(text)
293 }
294
295 #[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 #[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 #[qjs(rename = "frame")]
348 pub fn frame(&self) -> Option<crate::bindings::frame::FrameJs> {
349 self.request().frame()
350 }
351}
352
353#[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 #[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
439fn 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#[derive(JsLifetime, Trace)]
476#[rquickjs::class(rename = "Route")]
477pub struct RouteJs {
478 #[qjs(skip_trace)]
479 inner: StdMutex<Option<CoreRoute>>,
480}
481
482#[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 #[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 #[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 #[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 #[must_use]
549 pub fn new(inner: CoreRoute) -> Self {
550 Self {
551 inner: StdMutex::new(Some(inner)),
552 }
553 }
554}
555
556type 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
582fn 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}