1use std::sync::{Arc, Mutex};
43use std::time::Duration;
44
45use ferridriver::http_client::{HttpClient, RequestOptions};
46use rquickjs::atom::PredefinedAtom;
47use rquickjs::function::{Func, Opt};
48use rquickjs::{Coerced, Ctx, IntoJs, Object, Value, class::Class, class::Trace};
49
50use crate::bindings::convert::json_to_js;
51use crate::bindings::http_client::net_check;
52
53const MAX_FETCH_BODY_BYTES: usize = 64 * 1024 * 1024;
59
60const FETCH_BODY_DRAIN_TIMEOUT: Duration = Duration::from_secs(120);
64
65#[derive(Clone, Default)]
77pub(crate) struct NetPolicy(Arc<Mutex<Option<Arc<[String]>>>>);
78
79impl NetPolicy {
80 fn lock(&self) -> std::sync::MutexGuard<'_, Option<Arc<[String]>>> {
81 self.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
82 }
83
84 pub(crate) fn current(&self) -> Option<Arc<[String]>> {
86 self.lock().clone()
87 }
88
89 pub(crate) fn swap(&self, next: Option<Arc<[String]>>) -> Option<Arc<[String]>> {
92 std::mem::replace(&mut *self.lock(), next)
93 }
94}
95
96pub(crate) struct NetPolicyUd(pub(crate) NetPolicy);
98
99#[allow(unsafe_code)]
101unsafe impl rquickjs::JsLifetime<'_> for NetPolicyUd {
102 type Changed<'to> = NetPolicyUd;
103}
104
105pub(crate) fn active_net(ctx: &Ctx<'_>) -> Option<Arc<[String]>> {
109 ctx.userdata::<NetPolicyUd>().and_then(|u| u.0.current())
110}
111
112#[derive(Trace)]
120#[rquickjs::class(rename = "Headers")]
121pub struct HeadersJs {
122 #[qjs(skip_trace)]
125 pairs: Vec<(String, String)>,
126}
127
128#[derive(Clone, Copy)]
129enum IterKind {
130 Entries,
131 Keys,
132 Values,
133}
134
135fn make_header_iter<'js>(
143 ctx: &Ctx<'js>,
144 data: Arc<Vec<(String, String)>>,
145 pos: Arc<std::sync::atomic::AtomicUsize>,
146 kind: IterKind,
147) -> rquickjs::Result<Object<'js>> {
148 let it = Object::new(ctx.clone())?;
149 {
150 let data = data.clone();
151 let pos = pos.clone();
152 it.set(
153 PredefinedAtom::Next,
154 Func::from(move |ctx: Ctx<'js>| -> rquickjs::Result<Object<'js>> {
155 let r = Object::new(ctx.clone())?;
156 let i = pos.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
157 if let Some((k, v)) = data.get(i) {
158 let value: Value<'js> = match kind {
159 IterKind::Entries => {
160 let a = rquickjs::Array::new(ctx.clone())?;
161 a.set(0, k.clone())?;
162 a.set(1, v.clone())?;
163 a.into_value()
164 },
165 IterKind::Keys => k.clone().into_js(&ctx)?,
166 IterKind::Values => v.clone().into_js(&ctx)?,
167 };
168 r.set(PredefinedAtom::Value, value)?;
169 r.set(PredefinedAtom::Done, false)?;
170 } else {
171 pos.store(data.len(), std::sync::atomic::Ordering::Relaxed);
172 r.set(PredefinedAtom::Value, Value::new_undefined(ctx.clone()))?;
173 r.set(PredefinedAtom::Done, true)?;
174 }
175 Ok(r)
176 }),
177 )?;
178 }
179 {
180 let data = data.clone();
181 let pos = pos.clone();
182 it.set(
183 PredefinedAtom::SymbolIterator,
184 Func::from(move |ctx: Ctx<'js>| make_header_iter(&ctx, data.clone(), pos.clone(), kind)),
185 )?;
186 }
187 Ok(it)
188}
189
190fn new_header_iter<'js>(ctx: &Ctx<'js>, data: Vec<(String, String)>, kind: IterKind) -> rquickjs::Result<Object<'js>> {
192 make_header_iter(
193 ctx,
194 Arc::new(data),
195 Arc::new(std::sync::atomic::AtomicUsize::new(0)),
196 kind,
197 )
198}
199
200fn is_header_name(name: &str) -> bool {
202 !name.is_empty()
203 && name.bytes().all(|b| {
204 matches!(b,
205 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+'
206 | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
207 | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z')
208 })
209}
210
211fn is_header_value(value: &str) -> bool {
214 value
215 .chars()
216 .all(|c| c == '\t' || c == ' ' || ('\u{21}'..='\u{7E}').contains(&c) || c == '\u{0C}' || c == '\u{00A0}')
217}
218
219fn normalize_header_value(text: &str) -> String {
224 let input = text.as_bytes();
225 let mut out: Vec<u8> = Vec::with_capacity(input.len());
226 let mut read = 0;
227 while read < input.len() && (input[read] == b' ' || input[read] == b'\t') {
228 read += 1;
229 }
230 let mut pending: Option<u8> = None;
231 while read < input.len() {
232 match input[read] {
233 b'\r'
234 if read + 2 < input.len()
235 && input[read + 1] == b'\n'
236 && (input[read + 2] == b' ' || input[read + 2] == b'\t') =>
237 {
238 pending = Some(input[read + 2]);
239 read += 3;
240 },
241 b'\r' | b'\n' => read += 1,
242 b' ' | b'\t' => {
243 pending = Some(input[read]);
244 read += 1;
245 },
246 byte => {
247 if let Some(ws) = pending.take()
248 && !out.is_empty()
249 {
250 out.push(ws);
251 }
252 out.push(byte);
253 read += 1;
254 },
255 }
256 }
257 while matches!(out.last(), Some(b' ' | b'\t')) {
258 out.pop();
259 }
260 String::from_utf8_lossy(&out).into_owned()
261}
262
263#[derive(Trace)]
272#[rquickjs::class(rename = "Response")]
273pub struct FetchResponseJs {
274 #[qjs(skip_trace)]
275 status: u16,
276 #[qjs(skip_trace)]
277 status_text: String,
278 #[qjs(skip_trace)]
279 url: String,
280 #[qjs(skip_trace)]
281 headers: Vec<(String, String)>,
282 #[qjs(skip_trace)]
283 body: Vec<u8>,
284 #[qjs(skip_trace)]
285 redirected: bool,
286 #[qjs(skip_trace)]
287 type_: &'static str,
288 #[qjs(skip_trace)]
289 body_used: bool,
290 #[qjs(skip_trace)]
295 net: Option<Arc<tokio::sync::Mutex<Option<ferridriver::http_client::HttpStreamResponse>>>>,
296}
297
298#[derive(Trace)]
306#[rquickjs::class(rename = "Request")]
307pub struct FetchRequestJs {
308 #[qjs(skip_trace)]
309 url: String,
310 #[qjs(skip_trace)]
311 method: String,
312 #[qjs(skip_trace)]
313 headers: Vec<(String, String)>,
314 #[qjs(skip_trace)]
315 body: Vec<u8>,
316 #[qjs(skip_trace)]
317 redirect: String,
318 #[qjs(skip_trace)]
319 credentials: String,
320 #[qjs(skip_trace)]
321 body_used: bool,
322}
323
324#[allow(unsafe_code)]
326unsafe impl rquickjs::JsLifetime<'_> for HeadersJs {
327 type Changed<'to> = HeadersJs;
328}
329#[allow(unsafe_code)]
330unsafe impl rquickjs::JsLifetime<'_> for FetchResponseJs {
331 type Changed<'to> = FetchResponseJs;
332}
333#[allow(unsafe_code)]
334unsafe impl rquickjs::JsLifetime<'_> for FetchRequestJs {
335 type Changed<'to> = FetchRequestJs;
336}
337
338fn extract_body<'js>(ctx: &Ctx<'js>, v: &Value<'js>) -> (Vec<u8>, Option<&'static str>) {
343 if v.is_undefined() || v.is_null() {
344 return (Vec::new(), None);
345 }
346 if let Some(s) = v.as_string().and_then(|s| s.to_string().ok()) {
347 return (s.into_bytes(), Some("text/plain;charset=UTF-8"));
348 }
349 if v.is_object() {
350 if let Ok(j) = crate::bindings::convert::serde_from_js::<serde_json::Value>(ctx, v.clone()) {
351 return (j.to_string().into_bytes(), Some("application/json"));
352 }
353 }
354 (Vec::new(), None)
355}
356
357fn init_headers(init: Option<&Object<'_>>, default_ct: Option<&'static str>) -> Vec<(String, String)> {
360 let mut pairs = init
361 .and_then(|o| o.get::<_, Value<'_>>("headers").ok())
362 .map(|v| header_pairs_from(&v))
363 .unwrap_or_default();
364 if let Some(ct) = default_ct
365 && !pairs.iter().any(|(k, _)| k == "content-type")
366 {
367 pairs.push(("content-type".to_string(), ct.to_string()));
368 }
369 pairs
370}
371
372fn header_pairs_from(v: &Value<'_>) -> Vec<(String, String)> {
377 if let Ok(h) = Class::<HeadersJs>::from_value(v) {
378 return h.borrow().pairs.clone();
379 }
380 let mut acc = HeadersJs { pairs: Vec::new() };
381 if let Some(arr) = v.as_array() {
382 for i in 0..arr.len() {
383 if let Ok(entry) = arr.get::<Value<'_>>(i)
384 && let Some(pair) = entry.as_array()
385 && pair.len() == 2
386 && let (Ok(k), Ok(val)) = (pair.get::<Coerced<String>>(0), pair.get::<Coerced<String>>(1))
387 && is_header_name(&k.0)
388 {
389 acc.append_normalized(k.0.to_ascii_lowercase(), normalize_header_value(&val.0));
390 }
391 }
392 return acc.pairs;
393 }
394 if let Some(obj) = v.as_object()
395 && let Ok(keys) = obj.keys::<String>().collect::<rquickjs::Result<Vec<_>>>()
396 {
397 for k in keys {
398 if let Ok(val) = obj.get::<_, Coerced<String>>(k.as_str())
399 && is_header_name(&k)
400 {
401 acc.append_normalized(k.to_ascii_lowercase(), normalize_header_value(&val.0));
402 }
403 }
404 }
405 acc.pairs
406}
407
408impl HeadersJs {
409 fn append_normalized(&mut self, name_lc: String, value: String) {
413 if name_lc == "set-cookie" {
414 self.pairs.push((name_lc, value));
415 return;
416 }
417 if let Some(i) = self.pairs.iter().position(|(k, _)| k == &name_lc) {
418 self.pairs[i].1 = format!("{}, {value}", self.pairs[i].1);
422 } else {
423 self.pairs.push((name_lc, value));
424 }
425 }
426
427 pub(crate) fn from_pairs<I: IntoIterator<Item = (String, String)>>(it: I) -> Self {
430 let mut h = Self { pairs: Vec::new() };
431 for (k, v) in it {
432 h.append_normalized(k.to_ascii_lowercase(), normalize_header_value(&v));
433 }
434 h
435 }
436
437 fn sorted(&self) -> Vec<(String, String)> {
440 let mut v = self.pairs.clone();
441 v.sort_by(|a, b| a.0.cmp(&b.0));
442 v
443 }
444
445 fn check_name(ctx: &Ctx<'_>, name: &str) -> rquickjs::Result<String> {
446 if is_header_name(name) {
447 Ok(name.to_ascii_lowercase())
448 } else {
449 Err(rquickjs::Exception::throw_type(
450 ctx,
451 &format!("Invalid header name: {name:?}"),
452 ))
453 }
454 }
455
456 fn check_value(ctx: &Ctx<'_>, raw: &str) -> rquickjs::Result<String> {
457 let v = normalize_header_value(raw);
458 if is_header_value(&v) {
459 Ok(v)
460 } else {
461 Err(rquickjs::Exception::throw_type(ctx, "Invalid header value"))
462 }
463 }
464
465 fn fill_from_value<'js>(&mut self, ctx: &Ctx<'js>, v: &Value<'js>) -> rquickjs::Result<()> {
466 if let Ok(other) = Class::<HeadersJs>::from_value(v) {
467 for (k, val) in &other.borrow().pairs {
468 self.append_normalized(k.clone(), val.clone());
469 }
470 return Ok(());
471 }
472 if let Some(arr) = v.as_array() {
473 for i in 0..arr.len() {
474 let entry = arr.get::<Value<'js>>(i)?;
475 let pair = entry
476 .as_array()
477 .ok_or_else(|| rquickjs::Exception::throw_type(ctx, "Header init entry is not a [name, value] pair"))?;
478 if pair.len() != 2 {
479 return Err(rquickjs::Exception::throw_type(
480 ctx,
481 "Header init entry must be a [name, value] pair",
482 ));
483 }
484 let name = Self::check_name(ctx, &pair.get::<Coerced<String>>(0)?.0)?;
485 let value = Self::check_value(ctx, &pair.get::<Coerced<String>>(1)?.0)?;
486 self.append_normalized(name, value);
487 }
488 return Ok(());
489 }
490 if let Some(obj) = v.as_object() {
491 for k in obj.keys::<String>().collect::<rquickjs::Result<Vec<_>>>()? {
492 let name = Self::check_name(ctx, &k)?;
493 let value = Self::check_value(ctx, &obj.get::<_, Coerced<String>>(k.as_str())?.0)?;
494 self.append_normalized(name, value);
495 }
496 }
497 Ok(())
498 }
499}
500
501#[rquickjs::methods]
502impl HeadersJs {
503 #[qjs(constructor)]
504 pub fn new<'js>(ctx: Ctx<'js>, init: Opt<Value<'js>>) -> rquickjs::Result<Self> {
505 let mut h = Self { pairs: Vec::new() };
506 if let Some(v) = init.0 {
507 if v.is_null() || v.is_number() {
508 return Err(rquickjs::Exception::throw_type(
509 &ctx,
510 "Failed to construct 'Headers': invalid init",
511 ));
512 }
513 if !v.is_undefined() {
514 h.fill_from_value(&ctx, &v)?;
515 }
516 }
517 Ok(h)
518 }
519
520 #[qjs(rename = "append")]
521 pub fn append(&mut self, ctx: Ctx<'_>, name: String, value: Coerced<String>) -> rquickjs::Result<()> {
522 let n = Self::check_name(&ctx, &name)?;
523 let v = Self::check_value(&ctx, &value.0)?;
524 self.append_normalized(n, v);
525 Ok(())
526 }
527
528 #[qjs(rename = "set")]
529 pub fn set(&mut self, ctx: Ctx<'_>, name: String, value: Coerced<String>) -> rquickjs::Result<()> {
530 let n = Self::check_name(&ctx, &name)?;
531 let v = Self::check_value(&ctx, &value.0)?;
532 self.pairs.retain(|(k, _)| k != &n);
533 self.pairs.push((n, v));
534 Ok(())
535 }
536
537 #[qjs(rename = "get")]
538 pub fn get<'js>(&self, ctx: Ctx<'js>, name: String) -> rquickjs::Result<Value<'js>> {
539 let n = Self::check_name(&ctx, &name)?;
540 let matches: Vec<&str> = self
541 .pairs
542 .iter()
543 .filter(|(k, _)| k == &n)
544 .map(|(_, v)| v.as_str())
545 .collect();
546 if matches.is_empty() {
547 Ok(Value::new_null(ctx))
548 } else {
549 matches.join(", ").into_js(&ctx)
550 }
551 }
552
553 #[qjs(rename = "getSetCookie")]
554 pub fn get_set_cookie(&self) -> Vec<String> {
555 self
556 .pairs
557 .iter()
558 .filter(|(k, _)| k == "set-cookie")
559 .map(|(_, v)| v.clone())
560 .collect()
561 }
562
563 #[qjs(rename = "has")]
564 pub fn has(&self, ctx: Ctx<'_>, name: String) -> rquickjs::Result<bool> {
565 let n = Self::check_name(&ctx, &name)?;
566 Ok(self.pairs.iter().any(|(k, _)| k == &n))
567 }
568
569 #[qjs(rename = "delete")]
570 pub fn delete(&mut self, ctx: Ctx<'_>, name: String) -> rquickjs::Result<()> {
571 let n = Self::check_name(&ctx, &name)?;
572 self.pairs.retain(|(k, _)| k != &n);
573 Ok(())
574 }
575
576 #[qjs(rename = "entries")]
577 pub fn entries<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
578 new_header_iter(&ctx, self.sorted(), IterKind::Entries)
579 }
580
581 #[qjs(rename = "keys")]
582 pub fn keys<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
583 new_header_iter(&ctx, self.sorted(), IterKind::Keys)
584 }
585
586 #[qjs(rename = "values")]
587 pub fn values<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
588 new_header_iter(&ctx, self.sorted(), IterKind::Values)
589 }
590
591 #[qjs(rename = PredefinedAtom::SymbolIterator)]
592 pub fn js_iterator<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Object<'js>> {
593 new_header_iter(&ctx, self.sorted(), IterKind::Entries)
594 }
595
596 #[qjs(rename = "forEach")]
597 pub fn for_each(&self, cb: rquickjs::Function<'_>) -> rquickjs::Result<()> {
598 for (k, v) in self.sorted() {
599 cb.call::<_, ()>((v, k))?;
600 }
601 Ok(())
602 }
603}
604
605impl FetchResponseJs {
606 fn from_stream(
609 status: u16,
610 status_text: String,
611 url: String,
612 headers: Vec<(String, String)>,
613 redirected: bool,
614 stream: ferridriver::http_client::HttpStreamResponse,
615 ) -> Self {
616 Self {
617 status,
618 status_text,
619 url,
620 headers,
621 body: Vec::new(),
622 redirected,
623 type_: "basic",
624 body_used: false,
625 net: Some(Arc::new(tokio::sync::Mutex::new(Some(stream)))),
626 }
627 }
628
629 async fn consume(&mut self, ctx: &Ctx<'_>) -> rquickjs::Result<Vec<u8>> {
633 if self.body_used {
634 return Err(rquickjs::Exception::throw_type(ctx, "Body has already been consumed"));
635 }
636 self.body_used = true;
637 if let Some(net) = &self.net {
638 let mut guard = net.lock().await;
639 let mut out = Vec::new();
640 if let Some(resp) = guard.as_mut() {
641 let drained = tokio::time::timeout(FETCH_BODY_DRAIN_TIMEOUT, async {
642 while let Some(chunk) = resp.chunk().await.map_err(|e| e.to_string())? {
643 if out.len() + chunk.len() > MAX_FETCH_BODY_BYTES {
644 return Err(format!("response body exceeded {MAX_FETCH_BODY_BYTES} bytes"));
645 }
646 out.extend_from_slice(&chunk);
647 }
648 Ok::<(), String>(())
649 })
650 .await;
651 *guard = None;
654 match drained {
655 Ok(Ok(())) => {},
656 Ok(Err(msg)) => return Err(rquickjs::Exception::throw_type(ctx, &msg)),
657 Err(_) => {
658 return Err(rquickjs::Exception::throw_type(ctx, "response body read timed out"));
659 },
660 }
661 return Ok(out);
662 }
663 *guard = None;
664 return Ok(out);
665 }
666 Ok(std::mem::take(&mut self.body))
667 }
668}
669
670#[rquickjs::methods]
671impl FetchResponseJs {
672 #[qjs(constructor)]
675 pub fn new<'js>(ctx: Ctx<'js>, body: Opt<Value<'js>>, init: Opt<Object<'js>>) -> rquickjs::Result<Self> {
676 let init = init.0;
677 let status = match init.as_ref().and_then(|o| o.get::<_, i64>("status").ok()) {
678 Some(s) if !(200..=599).contains(&s) => {
679 return Err(rquickjs::Exception::throw_range(
680 &ctx,
681 "Failed to construct 'Response': status is outside the range [200, 599]",
682 ));
683 },
684 Some(s) => s as u16,
685 None => 200,
686 };
687 let status_text = init
688 .as_ref()
689 .and_then(|o| o.get::<_, String>("statusText").ok())
690 .unwrap_or_default();
691 let has_body = body.0.as_ref().is_some_and(|v| !v.is_null() && !v.is_undefined());
694 if has_body && matches!(status, 204 | 205 | 304) {
695 return Err(rquickjs::Exception::throw_type(
696 &ctx,
697 "Failed to construct 'Response': Response with null body status cannot have body",
698 ));
699 }
700 let (bytes, default_ct) = body.0.map_or((Vec::new(), None), |v| extract_body(&ctx, &v));
701 Ok(Self {
702 status,
703 status_text,
704 url: String::new(),
705 headers: init_headers(init.as_ref(), default_ct),
706 body: bytes,
707 redirected: false,
708 type_: "default",
709 body_used: false,
710 net: None,
711 })
712 }
713
714 #[qjs(static, rename = "json")]
716 pub fn json_static<'js>(ctx: Ctx<'js>, data: Value<'js>, init: Opt<Object<'js>>) -> rquickjs::Result<Self> {
717 let init = init.0;
718 let json: serde_json::Value = crate::bindings::convert::serde_from_js(&ctx, data)?;
719 let status = init
720 .as_ref()
721 .and_then(|o| o.get::<_, i64>("status").ok())
722 .unwrap_or(200) as u16;
723 let status_text = init
724 .as_ref()
725 .and_then(|o| o.get::<_, String>("statusText").ok())
726 .unwrap_or_default();
727 Ok(Self {
728 status,
729 status_text,
730 url: String::new(),
731 headers: init_headers(init.as_ref(), Some("application/json")),
732 body: json.to_string().into_bytes(),
733 redirected: false,
734 type_: "default",
735 body_used: false,
736 net: None,
737 })
738 }
739
740 #[qjs(static, rename = "error")]
742 pub fn error() -> Self {
743 Self {
744 status: 0,
745 status_text: String::new(),
746 url: String::new(),
747 headers: Vec::new(),
748 body: Vec::new(),
749 redirected: false,
750 type_: "error",
751 body_used: false,
752 net: None,
753 }
754 }
755
756 #[qjs(static, rename = "redirect")]
759 pub fn redirect(ctx: Ctx<'_>, url: String, status: Opt<i64>) -> rquickjs::Result<Self> {
760 let status = status.0.unwrap_or(302);
761 if ![301, 302, 303, 307, 308].contains(&status) {
762 return Err(rquickjs::Exception::throw_range(&ctx, "Invalid redirect status code"));
763 }
764 Ok(Self {
765 status: status as u16,
766 status_text: String::new(),
767 url: String::new(),
768 headers: vec![("location".to_string(), url)],
769 body: Vec::new(),
770 redirected: false,
771 type_: "default",
772 body_used: false,
773 net: None,
774 })
775 }
776
777 #[qjs(get, rename = "status")]
778 pub fn status(&self) -> u16 {
779 self.status
780 }
781 #[qjs(get, rename = "ok")]
782 pub fn ok(&self) -> bool {
783 (200..300).contains(&self.status)
784 }
785 #[qjs(get, rename = "statusText")]
786 pub fn status_text(&self) -> String {
787 self.status_text.clone()
788 }
789 #[qjs(get, rename = "url")]
790 pub fn url(&self) -> String {
791 self.url.clone()
792 }
793 #[qjs(get, rename = "redirected")]
794 pub fn redirected(&self) -> bool {
795 self.redirected
796 }
797 #[qjs(get, rename = "type")]
798 pub fn type_(&self) -> String {
799 self.type_.to_string()
800 }
801 #[qjs(get, rename = "bodyUsed")]
802 pub fn body_used(&self) -> bool {
803 self.body_used
804 }
805
806 #[qjs(get, rename = "headers")]
807 pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, HeadersJs>> {
808 Class::instance(ctx, HeadersJs::from_pairs(self.headers.iter().cloned()))
809 }
810
811 #[qjs(get, rename = "body")]
817 pub fn body<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, crate::bindings::streams::ReadableStreamJs>> {
818 let stream = match &self.net {
819 Some(net) => crate::bindings::streams::ReadableStreamJs::from_net(net.clone()),
820 None => crate::bindings::streams::ReadableStreamJs::from_bytes(self.body.clone()),
821 };
822 Class::instance(ctx, stream)
823 }
824
825 #[qjs(rename = "text")]
826 pub async fn text(&mut self, ctx: Ctx<'_>) -> rquickjs::Result<String> {
827 let b = self.consume(&ctx).await?;
828 Ok(String::from_utf8_lossy(&b).into_owned())
829 }
830
831 #[qjs(rename = "json")]
832 pub async fn json<'js>(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
833 let b = self.consume(&ctx).await?;
834 let v: serde_json::Value = serde_json::from_slice(&b)
835 .map_err(|e| rquickjs::Error::new_from_js_message("Response.json", "Error", e.to_string()))?;
836 json_to_js(&ctx, &v)
837 }
838
839 #[qjs(rename = "arrayBuffer")]
840 pub async fn array_buffer<'js>(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
841 let b = self.consume(&ctx).await?;
842 rquickjs::ArrayBuffer::new(ctx.clone(), b).map(rquickjs::ArrayBuffer::into_value)
843 }
844
845 #[qjs(rename = "clone")]
846 pub fn clone_(&self, ctx: Ctx<'_>) -> rquickjs::Result<Self> {
847 if self.body_used {
848 return Err(rquickjs::Exception::throw_type(&ctx, "Cannot clone a used Response"));
849 }
850 if self.net.is_some() {
851 return Err(rquickjs::Exception::throw_type(
854 &ctx,
855 "Cannot clone a streaming Response (body is not buffered)",
856 ));
857 }
858 Ok(Self {
859 status: self.status,
860 status_text: self.status_text.clone(),
861 url: self.url.clone(),
862 headers: self.headers.clone(),
863 body: self.body.clone(),
864 redirected: self.redirected,
865 type_: self.type_,
866 body_used: false,
867 net: None,
868 })
869 }
870}
871
872#[rquickjs::methods]
873impl FetchRequestJs {
874 #[qjs(constructor)]
878 pub fn new<'js>(ctx: Ctx<'js>, input: Value<'js>, init: Opt<Object<'js>>) -> Self {
879 let init = init.0;
880 let mut req = if let Ok(other) = Class::<FetchRequestJs>::from_value(&input) {
881 let o = other.borrow();
882 Self {
883 url: o.url.clone(),
884 method: o.method.clone(),
885 headers: o.headers.clone(),
886 body: o.body.clone(),
887 redirect: o.redirect.clone(),
888 credentials: o.credentials.clone(),
889 body_used: false,
890 }
891 } else {
892 Self {
893 url: input.as_string().and_then(|s| s.to_string().ok()).unwrap_or_default(),
894 method: "GET".to_string(),
895 headers: Vec::new(),
896 body: Vec::new(),
897 redirect: "follow".to_string(),
898 credentials: "same-origin".to_string(),
899 body_used: false,
900 }
901 };
902 if let Some(o) = init.as_ref() {
903 if let Ok(m) = o.get::<_, String>("method") {
904 req.method = m.to_ascii_uppercase();
905 }
906 if let Ok(r) = o.get::<_, String>("redirect") {
907 req.redirect = r;
908 }
909 if let Ok(c) = o.get::<_, String>("credentials") {
910 req.credentials = c;
911 }
912 let (bytes, default_ct) = o
913 .get::<_, Value<'_>>("body")
914 .ok()
915 .map_or((Vec::new(), None), |v| extract_body(&ctx, &v));
916 if !bytes.is_empty() {
917 req.body = bytes;
918 }
919 req.headers = {
920 let mut h = init_headers(init.as_ref(), default_ct);
921 if h.is_empty() {
922 std::mem::take(&mut req.headers)
923 } else {
924 if let Ok(existing) = Class::<FetchRequestJs>::from_value(&input) {
925 for (k, v) in &existing.borrow().headers {
926 if !h.iter().any(|(hk, _)| hk == k) {
927 h.push((k.clone(), v.clone()));
928 }
929 }
930 }
931 h
932 }
933 };
934 }
935 req
936 }
937
938 #[qjs(get, rename = "url")]
939 pub fn url(&self) -> String {
940 self.url.clone()
941 }
942 #[qjs(get, rename = "method")]
943 pub fn method(&self) -> String {
944 self.method.clone()
945 }
946 #[qjs(get, rename = "redirect")]
947 pub fn redirect(&self) -> String {
948 self.redirect.clone()
949 }
950 #[qjs(get, rename = "credentials")]
951 pub fn credentials(&self) -> String {
952 self.credentials.clone()
953 }
954 #[qjs(get, rename = "bodyUsed")]
955 pub fn body_used(&self) -> bool {
956 self.body_used
957 }
958 #[qjs(get, rename = "headers")]
959 pub fn headers<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, HeadersJs>> {
960 Class::instance(ctx, HeadersJs::from_pairs(self.headers.iter().cloned()))
961 }
962
963 #[qjs(rename = "text")]
964 pub fn text(&mut self, ctx: Ctx<'_>) -> rquickjs::Result<String> {
965 if self.body_used {
966 return Err(rquickjs::Exception::throw_type(&ctx, "Body has already been consumed"));
967 }
968 self.body_used = true;
969 Ok(String::from_utf8_lossy(&std::mem::take(&mut self.body)).into_owned())
970 }
971
972 #[qjs(rename = "json")]
973 pub fn json<'js>(&mut self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
974 if self.body_used {
975 return Err(rquickjs::Exception::throw_type(&ctx, "Body has already been consumed"));
976 }
977 self.body_used = true;
978 let v: serde_json::Value = serde_json::from_slice(&std::mem::take(&mut self.body))
979 .map_err(|e| rquickjs::Error::new_from_js_message("Request.json", "Error", e.to_string()))?;
980 json_to_js(&ctx, &v)
981 }
982
983 #[qjs(rename = "clone")]
984 pub fn clone_(&self, ctx: Ctx<'_>) -> rquickjs::Result<Self> {
985 if self.body_used {
986 return Err(rquickjs::Exception::throw_type(&ctx, "Cannot clone a used Request"));
987 }
988 Ok(Self {
989 url: self.url.clone(),
990 method: self.method.clone(),
991 headers: self.headers.clone(),
992 body: self.body.clone(),
993 redirect: self.redirect.clone(),
994 credentials: self.credentials.clone(),
995 body_used: false,
996 })
997 }
998}
999
1000pub fn install(ctx: &Ctx<'_>, cx: Arc<HttpClient>) -> rquickjs::Result<()> {
1004 let f = rquickjs::Function::new(ctx.clone(), move |ctx, input, init| {
1009 do_fetch(ctx, input, init, cx.clone())
1010 })?;
1011 ctx.globals().set("fetch", f)?;
1012 Ok(())
1013}
1014
1015fn do_fetch<'js>(
1016 ctx: Ctx<'js>,
1017 input: Value<'js>,
1018 init: Opt<Object<'js>>,
1019 cx: Arc<HttpClient>,
1020) -> rquickjs::Result<Value<'js>> {
1021 {
1022 let req = Class::<FetchRequestJs>::from_value(&input).ok();
1026 let url = req
1027 .as_ref()
1028 .map(|r| r.borrow().url.clone())
1029 .or_else(|| input.as_string().and_then(|s| s.to_string().ok()))
1030 .or_else(|| input.as_object().and_then(|o| o.get::<_, String>("url").ok()))
1031 .unwrap_or_default();
1032 let net = active_net(&ctx);
1037 let init = init.0;
1038 let method = init
1039 .as_ref()
1040 .and_then(|o| o.get::<_, String>("method").ok())
1041 .or_else(|| req.as_ref().map(|r| r.borrow().method.clone()));
1042 let mut headers_vec: Vec<(String, String)> = init
1043 .as_ref()
1044 .and_then(|o| o.get::<_, Value<'_>>("headers").ok())
1045 .map(|v| header_pairs_from(&v))
1046 .or_else(|| req.as_ref().map(|r| r.borrow().headers.clone()))
1047 .unwrap_or_default();
1048 let body_val = init.as_ref().and_then(|o| o.get::<_, Value<'_>>("body").ok());
1053 let (data, json_data, body_ct, force_ct) = if let Some(b) = &body_val {
1054 if let Some(s) = b.as_string().and_then(|s| s.to_string().ok()) {
1055 (Some(s.into_bytes()), None, None, false)
1056 } else if let Ok(fd) = Class::<crate::bindings::form_data::FormDataJs>::from_value(b) {
1057 let (bytes, ct) = fd.borrow().to_multipart();
1058 (Some(bytes), None, Some(ct), true)
1059 } else if let Some((bytes, ct)) = crate::bindings::blob::BlobJs::from_js_blob(b) {
1060 (Some(bytes), None, (!ct.is_empty()).then_some(ct), false)
1061 } else if b.is_object() {
1062 let j: Option<serde_json::Value> = crate::bindings::convert::serde_from_js(&ctx, b.clone()).ok();
1063 (None, j, None, false)
1064 } else {
1065 (None, None, None, false)
1066 }
1067 } else {
1068 match req.as_ref().map(|r| r.borrow().body.clone()) {
1069 Some(b) if !b.is_empty() => (Some(b), None, None, false),
1070 _ => (None, None, None, false),
1071 }
1072 };
1073 if let Some(ct) = body_ct {
1074 let has_ct = headers_vec.iter().any(|(k, _)| k == "content-type");
1075 if force_ct {
1076 headers_vec.retain(|(k, _)| k != "content-type");
1077 headers_vec.push(("content-type".to_string(), ct));
1078 } else if !has_ct {
1079 headers_vec.push(("content-type".to_string(), ct));
1080 }
1081 }
1082 let headers = (!headers_vec.is_empty()).then_some(headers_vec);
1083 let redirect = init
1090 .as_ref()
1091 .and_then(|o| o.get::<_, String>("redirect").ok())
1092 .or_else(|| req.as_ref().map(|r| r.borrow().redirect.clone()));
1093 let max_redirects = match redirect.as_deref() {
1094 Some("manual" | "error") => Some(0),
1095 _ => None,
1096 };
1097 let signal = init
1100 .as_ref()
1101 .and_then(|o| o.get::<_, Value<'_>>("signal").ok())
1102 .and_then(|v| Class::<crate::bindings::abort::AbortSignalJs<'js>>::from_value(&v).ok())
1103 .map(|s| crate::bindings::abort::AbortSignalJs::inner_of(&s));
1104 let promised = rquickjs::promise::Promised::from(async move {
1105 if let Some(list) = net.as_deref()
1106 && let Err(msg) = net_check(list, &url)
1107 {
1108 return Err(rquickjs::Error::new_from_js_message("fetch", "Error", msg));
1109 }
1110 let opts = RequestOptions {
1111 method,
1112 headers,
1113 data,
1114 json_data,
1115 max_redirects,
1116 net_guard: Some(ferridriver::http_client::NetGuard {
1122 allowlist: net.clone(),
1123 block_metadata: true,
1124 block_private: false,
1125 }),
1126 ..Default::default()
1127 };
1128 if let Some(sig) = &signal
1129 && sig.is_aborted()
1130 {
1131 return Err(rquickjs::Error::new_from_js_message(
1132 "fetch",
1133 "AbortError",
1134 sig.reason_message(),
1135 ));
1136 }
1137 let fut = cx.fetch_stream(&url, Some(opts));
1140 let resp = match &signal {
1141 Some(sig) => {
1142 tokio::select! {
1143 r = fut => r.map_err(|e| rquickjs::Error::new_from_js_message("fetch", "Error", e.to_string()))?,
1144 () = sig.aborted() => {
1145 return Err(rquickjs::Error::new_from_js_message("fetch", "AbortError", sig.reason_message()));
1146 }
1147 }
1148 },
1149 None => fut
1150 .await
1151 .map_err(|e| rquickjs::Error::new_from_js_message("fetch", "Error", e.to_string()))?,
1152 };
1153 let final_url = resp.url().to_string();
1154 let redirected = !final_url.is_empty() && final_url != url;
1157 let out = FetchResponseJs::from_stream(
1158 resp.status(),
1159 resp.status_text().to_string(),
1160 final_url,
1161 resp.headers().to_vec(),
1162 redirected,
1163 resp,
1164 );
1165 Ok::<_, rquickjs::Error>(out)
1166 });
1167 promised.into_js(&ctx)
1168 }
1169}