Skip to main content

nodejs/stdlib/
fetch.rs

1//! The WHATWG Fetch globals: `fetch`, `Headers`, `Request`, `Response`, `Blob`,
2//! `FormData`, `AbortController` and `AbortSignal`.
3//!
4//! `fetch` is not a re-implementation of an HTTP client: it drives the SAME
5//! exchange `http.request`/`https.request` already perform (`http::exchange` /
6//! `https::exchange`), on a background thread, and hands the parsed result back
7//! over the host's I/O channel — the pattern every other async native in this
8//! frontend uses. That keeps one HTTP/1.1 wire implementation, one chunked
9//! decoder and one TLS configuration in the process.
10//!
11//! A body is held as raw BYTES (`@@body`, a JS array of byte numbers) rather
12//! than as a string, so `arrayBuffer()`/`bytes()` are exact for a binary
13//! response and `text()` decodes UTF-8 once, at the point the caller asks for
14//! text. A `Response` is fully buffered by the time the promise settles, so the
15//! body accessors are already-settled promises — the streaming `body`
16//! `ReadableStream` is the one part of the interface this does not provide.
17
18use crate::host::{with_host, JsObj};
19use fusevm::Value;
20use indexmap::IndexMap;
21
22/// The classes this module constructs, as bare globals.
23pub const CLASSES: &[&str] = &[
24    "Headers",
25    "Request",
26    "Response",
27    "Blob",
28    "File",
29    "FormData",
30    "AbortController",
31    "AbortSignal",
32];
33
34pub const HEADERS_METHODS: &[&str] = &[
35    "get",
36    "getSetCookie",
37    "has",
38    "set",
39    "append",
40    "delete",
41    "forEach",
42    "keys",
43    "values",
44    "entries",
45    "@@iterator",
46];
47pub const RESPONSE_METHODS: &[&str] = &[
48    "text",
49    "json",
50    "arrayBuffer",
51    "bytes",
52    "blob",
53    "formData",
54    "clone",
55];
56pub const REQUEST_METHODS: &[&str] = &[
57    "text",
58    "json",
59    "arrayBuffer",
60    "bytes",
61    "blob",
62    "formData",
63    "clone",
64];
65pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice", "stream"];
66pub const FORM_DATA_METHODS: &[&str] = &[
67    "append",
68    "delete",
69    "get",
70    "getAll",
71    "has",
72    "set",
73    "forEach",
74    "keys",
75    "values",
76    "entries",
77    "@@iterator",
78];
79pub const ABORT_CONTROLLER_METHODS: &[&str] = &["abort"];
80pub const ABORT_SIGNAL_METHODS: &[&str] =
81    &["throwIfAborted", "addEventListener", "removeEventListener"];
82
83pub fn methods_for(tag: &str) -> &'static [&'static str] {
84    match tag {
85        "Headers" => HEADERS_METHODS,
86        "Response" => RESPONSE_METHODS,
87        "Request" => REQUEST_METHODS,
88        "Blob" | "File" => BLOB_METHODS,
89        "FormData" => FORM_DATA_METHODS,
90        "AbortController" => ABORT_CONTROLLER_METHODS,
91        "AbortSignal" => ABORT_SIGNAL_METHODS,
92        _ => &[],
93    }
94}
95
96pub fn is_class(name: &str) -> bool {
97    CLASSES.contains(&name)
98}
99
100// ── small helpers ────────────────────────────────────────────────────────────
101
102fn str_of(v: &Value) -> String {
103    with_host(|h| h.str_of(v))
104}
105
106fn new_str(s: impl Into<String>) -> Value {
107    let s = s.into();
108    with_host(|h| h.new_str(s))
109}
110
111fn prop(recv: &Value, key: &str) -> Option<Value> {
112    with_host(|h| match h.get(recv) {
113        Some(JsObj::Object(p)) => p.get(key).cloned(),
114        _ => None,
115    })
116    .filter(|v| !matches!(v, Value::Undef))
117}
118
119fn set_prop(recv: &Value, key: &str, val: Value) {
120    with_host(|h| {
121        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
122            p.insert(key.to_string(), val);
123        }
124    });
125}
126
127/// Wrap a synchronous outcome in an already-settled Promise. Same shape as
128/// `fs/promises`: the body of a fetched `Response` is already in memory, so
129/// `text()`/`json()` have nothing to wait for.
130fn settled(result: Result<Value, String>) -> Value {
131    let p = with_host(|h| h.new_promise());
132    let id = with_host(|h| h.promise_id(&p).unwrap_or(0));
133    match result {
134        Ok(v) => crate::host::resolve_promise_val(id, v),
135        Err(e) => {
136            let ev = with_host(|h| crate::builtins::synth_error(h, &e));
137            crate::host::reject_promise_val(id, ev);
138        }
139    }
140    p
141}
142
143/// The bytes stored under `@@body`, as a Rust byte vector.
144fn body_bytes(recv: &Value) -> Vec<u8> {
145    let Some(arr) = prop(recv, "@@body") else {
146        return Vec::new();
147    };
148    with_host(|h| match h.get(&arr) {
149        Some(JsObj::Array(items)) => items.iter().map(|v| h.to_number(v) as u8).collect(),
150        _ => Vec::new(),
151    })
152}
153
154fn store_body(bytes: &[u8]) -> Value {
155    with_host(|h| h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect()))
156}
157
158/// A `Uint8Array` over `bytes` — what `bytes()` answers and what
159/// `arrayBuffer()` is built from.
160fn to_uint8array(bytes: &[u8]) -> Result<Value, String> {
161    let arr = with_host(|h| h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect()));
162    super::typedarray::construct("Uint8Array", &[arr])
163}
164
165// ── Headers ──────────────────────────────────────────────────────────────────
166//
167// The entry list is kept in insertion order with LOWERCASED names, which is what
168// the Fetch standard's header list is and what makes `get`/`has` case-
169// insensitive without a second index.
170
171fn headers_entries(recv: &Value) -> Vec<(String, String)> {
172    let Some(arr) = prop(recv, "@@entries") else {
173        return Vec::new();
174    };
175    with_host(|h| match h.get(&arr) {
176        Some(JsObj::Array(items)) => items
177            .iter()
178            .filter_map(|pair| match h.get(pair) {
179                Some(JsObj::Array(kv)) if kv.len() == 2 => {
180                    Some((h.str_of(&kv[0]), h.str_of(&kv[1])))
181                }
182                _ => None,
183            })
184            .collect(),
185        _ => Vec::new(),
186    })
187}
188
189fn set_headers_entries(recv: &Value, entries: &[(String, String)]) {
190    let arr = with_host(|h| {
191        let pairs: Vec<Value> = entries
192            .iter()
193            .map(|(k, v)| {
194                let k = h.new_str(k.clone());
195                let v = h.new_str(v.clone());
196                h.new_array(vec![k, v])
197            })
198            .collect();
199        h.new_array(pairs)
200    });
201    set_prop(recv, "@@entries", arr);
202}
203
204/// `new Headers(init)`. `init` is another `Headers`, an array of `[name, value]`
205/// pairs, or a plain object.
206pub fn construct_headers(args: &[Value]) -> Result<Value, String> {
207    let obj = with_host(|h| {
208        let mut m = IndexMap::new();
209        m.insert("@@native".into(), h.new_str("Headers"));
210        let empty = h.new_array(Vec::new());
211        m.insert("@@entries".into(), empty);
212        h.new_object(m)
213    });
214    with_host(|h| h.hide_prop(&obj, "@@entries"));
215    if let Some(init) = args.first().filter(|v| !matches!(v, Value::Undef)) {
216        for (k, v) in init_header_entries(init) {
217            append_header(&obj, &k, &v);
218        }
219    }
220    Ok(obj)
221}
222
223/// The `(name, value)` pairs a `HeadersInit` contributes, in order.
224fn init_header_entries(init: &Value) -> Vec<(String, String)> {
225    if super::native_tag(init).as_deref() == Some("Headers") {
226        return headers_entries(init);
227    }
228    // An array of `[name, value]` pairs.
229    if with_host(|h| matches!(h.get(init), Some(JsObj::Array(_)))) {
230        return with_host(|h| match h.get(init) {
231            Some(JsObj::Array(items)) => items
232                .iter()
233                .filter_map(|pair| match h.get(pair) {
234                    Some(JsObj::Array(kv)) if kv.len() >= 2 => {
235                        Some((h.str_of(&kv[0]).to_ascii_lowercase(), h.str_of(&kv[1])))
236                    }
237                    _ => None,
238                })
239                .collect(),
240            _ => Vec::new(),
241        });
242    }
243    // A plain object: every own enumerable string key.
244    with_host(|h| match h.get(init) {
245        Some(JsObj::Object(p)) => p
246            .iter()
247            .filter(|(k, _)| !k.starts_with("@@"))
248            .map(|(k, v)| (k.to_ascii_lowercase(), h.str_of(v)))
249            .collect(),
250        _ => Vec::new(),
251    })
252}
253
254fn append_header(recv: &Value, name: &str, value: &str) {
255    let mut e = headers_entries(recv);
256    e.push((name.to_ascii_lowercase(), value.to_string()));
257    set_headers_entries(recv, &e);
258}
259
260fn set_header(recv: &Value, name: &str, value: &str) {
261    let name = name.to_ascii_lowercase();
262    let mut e = headers_entries(recv);
263    match e.iter().position(|(k, _)| *k == name) {
264        Some(i) => {
265            e[i].1 = value.to_string();
266            e.retain({
267                let mut seen = 0;
268                move |(k, _)| {
269                    if *k != name {
270                        return true;
271                    }
272                    seen += 1;
273                    seen == 1
274                }
275            });
276        }
277        None => e.push((name, value.to_string())),
278    }
279    set_headers_entries(recv, &e);
280}
281
282/// `Headers.get`: every value for `name`, joined with `", "` — the standard's
283/// combined value, which is why a repeated header reads back as one string.
284fn get_header(recv: &Value, name: &str) -> Option<String> {
285    let name = name.to_ascii_lowercase();
286    let vals: Vec<String> = headers_entries(recv)
287        .into_iter()
288        .filter(|(k, _)| *k == name)
289        .map(|(_, v)| v)
290        .collect();
291    (!vals.is_empty()).then(|| vals.join(", "))
292}
293
294pub fn headers_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
295    match method {
296        "get" => Ok(match get_header(recv, &super::arg_str(args, 0)) {
297            Some(v) => new_str(v),
298            None => with_host(|h| h.null()),
299        }),
300        "getSetCookie" => {
301            let vals: Vec<Value> = headers_entries(recv)
302                .into_iter()
303                .filter(|(k, _)| k == "set-cookie")
304                .map(|(_, v)| new_str(v))
305                .collect();
306            Ok(with_host(|h| h.new_array(vals)))
307        }
308        "has" => {
309            let name = super::arg_str(args, 0).to_ascii_lowercase();
310            Ok(Value::Bool(
311                headers_entries(recv).iter().any(|(k, _)| *k == name),
312            ))
313        }
314        "set" => {
315            set_header(recv, &super::arg_str(args, 0), &super::arg_str(args, 1));
316            Ok(Value::Undef)
317        }
318        "append" => {
319            append_header(recv, &super::arg_str(args, 0), &super::arg_str(args, 1));
320            Ok(Value::Undef)
321        }
322        "delete" => {
323            let name = super::arg_str(args, 0).to_ascii_lowercase();
324            let mut e = headers_entries(recv);
325            e.retain(|(k, _)| *k != name);
326            set_headers_entries(recv, &e);
327            Ok(Value::Undef)
328        }
329        // Iteration is over the SORTED, combined header list (the standard's
330        // "sorted and combined" order), not insertion order.
331        "forEach" => {
332            let cb = args.first().cloned().unwrap_or(Value::Undef);
333            for (k, v) in sorted_combined(recv) {
334                let (kv, vv) = (new_str(k), new_str(v));
335                crate::host::invoke(&cb, vec![vv, kv, recv.clone()], None)?;
336            }
337            Ok(Value::Undef)
338        }
339        "keys" | "values" | "entries" | "@@iterator" => {
340            let items: Vec<Value> = sorted_combined(recv)
341                .into_iter()
342                .map(|(k, v)| match method {
343                    "keys" => new_str(k),
344                    "values" => new_str(v),
345                    // `entries` and the default iterator both yield pairs.
346                    _ => {
347                        let (kv, vv) = (new_str(k), new_str(v));
348                        with_host(|h| h.new_array(vec![kv, vv]))
349                    }
350                })
351                .collect();
352            Ok(with_host(|h| {
353                h.alloc(JsObj::Iter {
354                    items,
355                    idx: 0,
356                    array: None,
357                })
358            }))
359        }
360        _ => Err(crate::host::type_error(&format!(
361            "{method} is not a function"
362        ))),
363    }
364}
365
366/// The header list sorted by name with same-named values combined, which is the
367/// order every `Headers` iteration method observes.
368fn sorted_combined(recv: &Value) -> Vec<(String, String)> {
369    let mut names: Vec<String> = Vec::new();
370    for (k, _) in headers_entries(recv) {
371        if !names.contains(&k) {
372            names.push(k);
373        }
374    }
375    names.sort();
376    names
377        .into_iter()
378        .filter_map(|n| get_header(recv, &n).map(|v| (n, v)))
379        .collect()
380}
381
382// ── Response / Request bodies ────────────────────────────────────────────────
383
384fn build_response(
385    status: u16,
386    status_text: &str,
387    headers: &[(String, String)],
388    body: &[u8],
389    url: &str,
390) -> Value {
391    let headers_obj = construct_headers(&[]).unwrap_or(Value::Undef);
392    for (k, v) in headers {
393        append_header(&headers_obj, k, v);
394    }
395    let body_arr = store_body(body);
396    let obj = with_host(|h| {
397        let mut m = IndexMap::new();
398        m.insert("@@native".into(), h.new_str("Response"));
399        m.insert("status".into(), Value::Float(status as f64));
400        m.insert("statusText".into(), h.new_str(status_text.to_string()));
401        // 2xx (and only 2xx) is `ok` — 304 is not.
402        m.insert(
403            "ok".into(),
404            Value::Bool((200..300).contains(&(status as u32))),
405        );
406        m.insert("redirected".into(), Value::Bool(false));
407        m.insert("type".into(), h.new_str("basic"));
408        m.insert("url".into(), h.new_str(url.to_string()));
409        m.insert("bodyUsed".into(), Value::Bool(false));
410        m.insert("headers".into(), headers_obj);
411        m.insert("@@body".into(), body_arr);
412        h.new_object(m)
413    });
414    with_host(|h| h.hide_prop(&obj, "@@body"));
415    obj
416}
417
418/// `new Response(body, init)`.
419pub fn construct_response(args: &[Value]) -> Result<Value, String> {
420    let body = body_init_bytes(args.first());
421    let init = args.get(1).cloned().unwrap_or(Value::Undef);
422    let status = match prop(&init, "status") {
423        Some(v) => with_host(|h| h.to_number(&v)) as u16,
424        None => 200,
425    };
426    let status_text = prop(&init, "statusText")
427        .map(|v| str_of(&v))
428        .unwrap_or_default();
429    let mut headers: Vec<(String, String)> = Vec::new();
430    if let Some(hv) = prop(&init, "headers") {
431        headers = init_header_entries(&hv);
432    }
433    Ok(build_response(status, &status_text, &headers, &body, ""))
434}
435
436/// `new Request(input, init)`.
437pub fn construct_request(args: &[Value]) -> Result<Value, String> {
438    let input = args.first().cloned().unwrap_or(Value::Undef);
439    let url = if super::native_tag(&input).as_deref() == Some("Request") {
440        prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
441    } else {
442        str_of(&input)
443    };
444    let init = args.get(1).cloned().unwrap_or(Value::Undef);
445    let method = prop(&init, "method")
446        .map(|v| str_of(&v).to_ascii_uppercase())
447        .unwrap_or_else(|| "GET".into());
448    let headers_obj = construct_headers(&[])?;
449    if let Some(hv) = prop(&init, "headers") {
450        for (k, v) in init_header_entries(&hv) {
451            append_header(&headers_obj, &k, &v);
452        }
453    }
454    let body = body_init_bytes(prop(&init, "body").as_ref());
455    let body_arr = store_body(&body);
456    let obj = with_host(|h| {
457        let mut m = IndexMap::new();
458        m.insert("@@native".into(), h.new_str("Request"));
459        m.insert("url".into(), h.new_str(url));
460        m.insert("method".into(), h.new_str(method));
461        m.insert("headers".into(), headers_obj);
462        m.insert("bodyUsed".into(), Value::Bool(false));
463        m.insert("@@body".into(), body_arr);
464        h.new_object(m)
465    });
466    with_host(|h| h.hide_prop(&obj, "@@body"));
467    Ok(obj)
468}
469
470/// The bytes a `BodyInit` contributes: a string encodes as UTF-8, a typed array
471/// / Buffer / `Blob` supplies its bytes, and anything else stringifies.
472fn body_init_bytes(v: Option<&Value>) -> Vec<u8> {
473    let Some(v) = v.filter(|v| !matches!(v, Value::Undef)) else {
474        return Vec::new();
475    };
476    if with_host(|h| h.is_null(v)) {
477        return Vec::new();
478    }
479    match super::native_tag(v).as_deref() {
480        Some("Blob") | Some("File") | Some("Response") | Some("Request") => return body_bytes(v),
481        Some("TypedArray") | Some("Buffer") => {
482            if let Some(e) = super::typedarray::elems_of(v) {
483                return e.iter().map(|x| *x as u8).collect();
484            }
485        }
486        _ => {}
487    }
488    if super::native_tag(v).as_deref() == Some("URLSearchParams") {
489        return str_of(v).into_bytes();
490    }
491    str_of(v).into_bytes()
492}
493
494/// The body accessors shared by `Response`, `Request` and `Blob`. Each answers a
495/// settled Promise, because the body is already fully buffered.
496pub fn body_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
497    match method {
498        "text" => {
499            set_prop(recv, "bodyUsed", Value::Bool(true));
500            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
501            Ok(settled(Ok(new_str(s))))
502        }
503        "json" => {
504            set_prop(recv, "bodyUsed", Value::Bool(true));
505            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
506            let parsed = crate::builtins::call_builtin_function("JSON.parse", vec![new_str(s)]);
507            Ok(settled(parsed))
508        }
509        "bytes" => {
510            set_prop(recv, "bodyUsed", Value::Bool(true));
511            Ok(settled(to_uint8array(&body_bytes(recv))))
512        }
513        "arrayBuffer" => {
514            set_prop(recv, "bodyUsed", Value::Bool(true));
515            let bytes = body_bytes(recv);
516            let buf = with_host(|h| {
517                let mut m = IndexMap::new();
518                m.insert("@@native".into(), h.new_str("ArrayBuffer"));
519                m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
520                let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
521                m.insert("@@bytes".into(), arr);
522                h.new_object(m)
523            });
524            with_host(|h| h.hide_prop(&buf, "@@bytes"));
525            Ok(settled(Ok(buf)))
526        }
527        "blob" => {
528            set_prop(recv, "bodyUsed", Value::Bool(true));
529            let bytes = body_bytes(recv);
530            let ct = prop(recv, "headers")
531                .and_then(|h| get_header(&h, "content-type"))
532                .unwrap_or_default();
533            Ok(settled(Ok(new_blob(&bytes, &ct, None))))
534        }
535        "formData" => {
536            set_prop(recv, "bodyUsed", Value::Bool(true));
537            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
538            let fd = construct_form_data(&[])?;
539            // Only the urlencoded form is decoded here; a multipart body needs
540            // the boundary parser this does not have.
541            for pair in s.split('&').filter(|p| !p.is_empty()) {
542                let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
543                form_append(&fd, &percent_decode(k), &new_str(percent_decode(v)));
544            }
545            Ok(settled(Ok(fd)))
546        }
547        "clone" => {
548            let bytes = body_bytes(recv);
549            let clone = crate::builtins::deep_clone(recv)?;
550            set_prop(&clone, "@@body", store_body(&bytes));
551            set_prop(&clone, "bodyUsed", Value::Bool(false));
552            Ok(clone)
553        }
554        "slice" => {
555            let bytes = body_bytes(recv);
556            let start = args
557                .first()
558                .map(|v| with_host(|h| h.to_number(v)) as usize)
559                .unwrap_or(0)
560                .min(bytes.len());
561            let end = args
562                .get(1)
563                .filter(|v| !matches!(v, Value::Undef))
564                .map(|v| with_host(|h| h.to_number(v)) as usize)
565                .unwrap_or(bytes.len())
566                .clamp(start, bytes.len());
567            let ct = args.get(2).map(str_of).unwrap_or_default();
568            Ok(new_blob(&bytes[start..end], &ct, None))
569        }
570        "stream" => Err(crate::host::type_error(
571            "Response.body streaming is not implemented on this build",
572        )),
573        _ => Err(crate::host::type_error(&format!(
574            "{method} is not a function"
575        ))),
576    }
577}
578
579/// Throw `v` as the JS exception value (rather than as an internal message), so
580/// `catch (e)` receives the object the spec says it should.
581fn throw_js(v: Value) -> String {
582    let msg = with_host(|h| h.str_of(&v));
583    with_host(|h| h.exc = Some(v));
584    msg
585}
586
587fn percent_decode(s: &str) -> String {
588    let bytes = s.replace('+', " ").into_bytes();
589    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
590    let mut i = 0;
591    while i < bytes.len() {
592        if bytes[i] == b'%' && i + 2 < bytes.len() {
593            if let Ok(b) = u8::from_str_radix(&String::from_utf8_lossy(&bytes[i + 1..i + 3]), 16) {
594                out.push(b);
595                i += 3;
596                continue;
597            }
598        }
599        out.push(bytes[i]);
600        i += 1;
601    }
602    String::from_utf8_lossy(&out).into_owned()
603}
604
605// ── Blob / File ──────────────────────────────────────────────────────────────
606
607fn new_blob(bytes: &[u8], content_type: &str, file_name: Option<&str>) -> Value {
608    let body = store_body(bytes);
609    let obj = with_host(|h| {
610        let mut m = IndexMap::new();
611        m.insert(
612            "@@native".into(),
613            h.new_str(if file_name.is_some() { "File" } else { "Blob" }),
614        );
615        m.insert("size".into(), Value::Float(bytes.len() as f64));
616        m.insert("type".into(), h.new_str(content_type.to_ascii_lowercase()));
617        if let Some(n) = file_name {
618            m.insert("name".into(), h.new_str(n.to_string()));
619            m.insert("lastModified".into(), Value::Float(0.0));
620        }
621        m.insert("@@body".into(), body);
622        h.new_object(m)
623    });
624    with_host(|h| h.hide_prop(&obj, "@@body"));
625    obj
626}
627
628pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
629    let mut bytes = Vec::new();
630    if let Some(parts) = args.first() {
631        for p in crate::host::iter_all(parts).unwrap_or_default() {
632            bytes.extend(body_init_bytes(Some(&p)));
633        }
634    }
635    let ct = args
636        .get(1)
637        .and_then(|o| prop(o, "type"))
638        .map(|v| str_of(&v))
639        .unwrap_or_default();
640    Ok(new_blob(&bytes, &ct, None))
641}
642
643/// `new File(parts, name[, options])` — a `Blob` that also carries a name.
644pub fn construct_file(args: &[Value]) -> Result<Value, String> {
645    let mut bytes = Vec::new();
646    if let Some(parts) = args.first() {
647        for p in crate::host::iter_all(parts).unwrap_or_default() {
648            bytes.extend(body_init_bytes(Some(&p)));
649        }
650    }
651    let name = super::arg_str(args, 1);
652    let ct = args
653        .get(2)
654        .and_then(|o| prop(o, "type"))
655        .map(|v| str_of(&v))
656        .unwrap_or_default();
657    Ok(new_blob(&bytes, &ct, Some(&name)))
658}
659
660// ── FormData ─────────────────────────────────────────────────────────────────
661
662pub fn construct_form_data(_args: &[Value]) -> Result<Value, String> {
663    let obj = with_host(|h| {
664        let mut m = IndexMap::new();
665        m.insert("@@native".into(), h.new_str("FormData"));
666        let empty = h.new_array(Vec::new());
667        m.insert("@@entries".into(), empty);
668        h.new_object(m)
669    });
670    with_host(|h| h.hide_prop(&obj, "@@entries"));
671    Ok(obj)
672}
673
674fn form_entries(recv: &Value) -> Vec<(String, Value)> {
675    let Some(arr) = prop(recv, "@@entries") else {
676        return Vec::new();
677    };
678    with_host(|h| match h.get(&arr) {
679        Some(JsObj::Array(items)) => items
680            .iter()
681            .filter_map(|pair| match h.get(pair) {
682                Some(JsObj::Array(kv)) if kv.len() == 2 => Some((h.str_of(&kv[0]), kv[1].clone())),
683                _ => None,
684            })
685            .collect(),
686        _ => Vec::new(),
687    })
688}
689
690fn set_form_entries(recv: &Value, entries: &[(String, Value)]) {
691    let arr = with_host(|h| {
692        let pairs: Vec<Value> = entries
693            .iter()
694            .map(|(k, v)| {
695                let k = h.new_str(k.clone());
696                h.new_array(vec![k, v.clone()])
697            })
698            .collect();
699        h.new_array(pairs)
700    });
701    set_prop(recv, "@@entries", arr);
702}
703
704fn form_append(recv: &Value, name: &str, value: &Value) {
705    let mut e = form_entries(recv);
706    e.push((name.to_string(), value.clone()));
707    set_form_entries(recv, &e);
708}
709
710pub fn form_data_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
711    let name = super::arg_str(args, 0);
712    match method {
713        "append" => {
714            form_append(recv, &name, args.get(1).unwrap_or(&Value::Undef));
715            Ok(Value::Undef)
716        }
717        "set" => {
718            let mut e = form_entries(recv);
719            e.retain(|(k, _)| *k != name);
720            e.push((name, args.get(1).cloned().unwrap_or(Value::Undef)));
721            set_form_entries(recv, &e);
722            Ok(Value::Undef)
723        }
724        "delete" => {
725            let mut e = form_entries(recv);
726            e.retain(|(k, _)| *k != name);
727            set_form_entries(recv, &e);
728            Ok(Value::Undef)
729        }
730        "get" => Ok(form_entries(recv)
731            .into_iter()
732            .find(|(k, _)| *k == name)
733            .map(|(_, v)| v)
734            .unwrap_or_else(|| with_host(|h| h.null()))),
735        "getAll" => {
736            let vals: Vec<Value> = form_entries(recv)
737                .into_iter()
738                .filter(|(k, _)| *k == name)
739                .map(|(_, v)| v)
740                .collect();
741            Ok(with_host(|h| h.new_array(vals)))
742        }
743        "has" => Ok(Value::Bool(
744            form_entries(recv).iter().any(|(k, _)| *k == name),
745        )),
746        "forEach" => {
747            let cb = args.first().cloned().unwrap_or(Value::Undef);
748            for (k, v) in form_entries(recv) {
749                let kv = new_str(k);
750                crate::host::invoke(&cb, vec![v, kv, recv.clone()], None)?;
751            }
752            Ok(Value::Undef)
753        }
754        "keys" | "values" | "entries" | "@@iterator" => {
755            let items: Vec<Value> = form_entries(recv)
756                .into_iter()
757                .map(|(k, v)| match method {
758                    "keys" => new_str(k),
759                    "values" => v,
760                    _ => {
761                        let kv = new_str(k);
762                        with_host(|h| h.new_array(vec![kv, v]))
763                    }
764                })
765                .collect();
766            Ok(with_host(|h| {
767                h.alloc(JsObj::Iter {
768                    items,
769                    idx: 0,
770                    array: None,
771                })
772            }))
773        }
774        _ => Err(crate::host::type_error(&format!(
775            "{method} is not a function"
776        ))),
777    }
778}
779
780// ── AbortController / AbortSignal ────────────────────────────────────────────
781
782pub fn new_abort_signal() -> Value {
783    with_host(|h| {
784        let mut m = IndexMap::new();
785        m.insert("@@native".into(), h.new_str("AbortSignal"));
786        m.insert("@@aborted".into(), Value::Bool(false));
787        m.insert("@@reason".into(), Value::Undef);
788        m.insert("@@onabort".into(), h.null());
789        h.new_object(m)
790    })
791}
792
793pub fn construct_abort_controller(_args: &[Value]) -> Result<Value, String> {
794    let signal = new_abort_signal();
795    Ok(with_host(|h| {
796        let mut m = IndexMap::new();
797        m.insert("@@native".into(), h.new_str("AbortController"));
798        m.insert("@@signal".into(), signal);
799        h.new_object(m)
800    }))
801}
802
803/// The `DOMException`-shaped reason an abort carries. node's `AbortSignal`
804/// rejects with a `DOMException` whose `name` is `AbortError`/`TimeoutError`;
805/// `synth_error` only knows the ECMAScript error classes, so the name, the
806/// constructor label and the `stack` head are stamped on afterwards.
807fn dom_exception(name: &str, message: &str) -> Value {
808    let msg = with_host(|h| h.new_str(message.to_string()));
809    let nm = with_host(|h| h.new_str(name.to_string()));
810    crate::builtins::dom_exception(&[msg, nm])
811}
812
813/// Fire an `AbortSignal.timeout` deadline: abort the signal at heap index `idx`
814/// with a `TimeoutError`. Reached from the `@@aborttimeout:<idx>` thunk.
815pub fn fire_timeout_abort(idx: u32) -> Result<Value, String> {
816    let signal = Value::Obj(idx);
817    let e = dom_exception("TimeoutError", "The operation was aborted due to timeout");
818    abort_signal(&signal, e)?;
819    Ok(Value::Undef)
820}
821
822/// Mark a signal aborted and run its `onabort` / `abort` listeners.
823fn abort_signal(signal: &Value, reason: Value) -> Result<(), String> {
824    if matches!(prop(signal, "@@aborted"), Some(Value::Bool(true))) {
825        return Ok(());
826    }
827    let reason = if matches!(reason, Value::Undef) {
828        dom_exception("AbortError", "This operation was aborted")
829    } else {
830        reason
831    };
832    set_prop(signal, "@@aborted", Value::Bool(true));
833    set_prop(signal, "@@reason", reason);
834    if let Some(cb) = prop(signal, "@@onabort") {
835        if with_host(|h| crate::host::is_callable(h, &cb)) {
836            crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
837        }
838    }
839    if let Some(list) = prop(signal, "@@abortListeners") {
840        for cb in crate::host::iter_all(&list).unwrap_or_default() {
841            crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
842        }
843    }
844    Ok(())
845}
846
847pub fn abort_controller_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
848    match method {
849        "abort" => {
850            let signal = prop(recv, "@@signal").unwrap_or(Value::Undef);
851            abort_signal(&signal, args.first().cloned().unwrap_or(Value::Undef))?;
852            Ok(Value::Undef)
853        }
854        _ => Err(crate::host::type_error(&format!(
855            "{method} is not a function"
856        ))),
857    }
858}
859
860pub fn abort_signal_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
861    match method {
862        "throwIfAborted" => {
863            if matches!(prop(recv, "@@aborted"), Some(Value::Bool(true))) {
864                let reason = prop(recv, "@@reason").unwrap_or(Value::Undef);
865                return Err(throw_js(reason));
866            }
867            Ok(Value::Undef)
868        }
869        "addEventListener" => {
870            if super::arg_str(args, 0) == "abort" {
871                let cb = args.get(1).cloned().unwrap_or(Value::Undef);
872                let existing = prop(recv, "@@abortListeners");
873                let list = match existing {
874                    Some(l) => l,
875                    None => {
876                        let l = with_host(|h| h.new_array(Vec::new()));
877                        set_prop(recv, "@@abortListeners", l.clone());
878                        with_host(|h| h.hide_prop(recv, "@@abortListeners"));
879                        l
880                    }
881                };
882                with_host(|h| {
883                    if let Some(JsObj::Array(items)) = h.get_mut(&list) {
884                        items.push(cb);
885                    }
886                });
887            }
888            Ok(Value::Undef)
889        }
890        "removeEventListener" => Ok(Value::Undef),
891        _ => Err(crate::host::type_error(&format!(
892            "{method} is not a function"
893        ))),
894    }
895}
896
897/// `AbortSignal.abort(reason)` / `AbortSignal.timeout(ms)`.
898pub fn abort_signal_static(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
899    match method {
900        "abort" => {
901            let s = new_abort_signal();
902            let r = abort_signal(&s, args.first().cloned().unwrap_or(Value::Undef));
903            Some(r.map(|_| s))
904        }
905        "timeout" => {
906            let s = new_abort_signal();
907            // The abort is a real scheduled macrotask: a native continuation
908            // thunk (the `@@`-prefixed convention `@@presolve:<id>` already
909            // uses) carries the signal's heap index to the timer callback.
910            let ms = super::arg_num(args, 0);
911            let Value::Obj(idx) = s else {
912                return Some(Ok(s));
913            };
914            let cb = with_host(|h| h.alloc(JsObj::Builtin(format!("@@aborttimeout:{idx}"))));
915            with_host(|h| h.add_timer(ms, cb, Vec::new(), None));
916            Some(Ok(s))
917        }
918        _ => None,
919    }
920}
921
922// ── fetch ────────────────────────────────────────────────────────────────────
923
924/// `fetch(input[, init])` — a Promise for a fully buffered `Response`.
925pub fn fetch(args: &[Value]) -> Result<Value, String> {
926    let promise = with_host(|h| h.new_promise());
927    let id = with_host(|h| h.promise_id(&promise).unwrap_or(0));
928
929    let input = args.first().cloned().unwrap_or(Value::Undef);
930    let init = args.get(1).cloned().unwrap_or(Value::Undef);
931
932    // `input` is a URL string, a `URL`, or a `Request` (whose method/headers/
933    // body seed the request, and `init` overrides them).
934    let from_request = super::native_tag(&input).as_deref() == Some("Request");
935    let url = if from_request {
936        prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
937    } else {
938        str_of(&input)
939    };
940
941    let mut method = if from_request {
942        prop(&input, "method")
943            .map(|v| str_of(&v))
944            .unwrap_or_else(|| "GET".into())
945    } else {
946        "GET".into()
947    };
948    if let Some(m) = prop(&init, "method") {
949        method = str_of(&m);
950    }
951    method = method.to_ascii_uppercase();
952
953    let mut headers: Vec<(String, String)> = Vec::new();
954    if from_request {
955        if let Some(h) = prop(&input, "headers") {
956            headers = headers_entries(&h);
957        }
958    }
959    if let Some(hv) = prop(&init, "headers") {
960        headers.extend(init_header_entries(&hv));
961    }
962
963    let body = match prop(&init, "body") {
964        Some(b) => body_init_bytes(Some(&b)),
965        None if from_request => body_bytes(&input),
966        None => Vec::new(),
967    };
968
969    // An already-aborted signal rejects before any connection is made.
970    if let Some(sig) = prop(&init, "signal") {
971        if matches!(prop(&sig, "@@aborted"), Some(Value::Bool(true))) {
972            let reason = prop(&sig, "@@reason")
973                .unwrap_or_else(|| dom_exception("AbortError", "This operation was aborted"));
974            crate::host::reject_promise_val(id, reason);
975            return Ok(promise);
976        }
977    }
978
979    let Some(target) = parse_target(&url) else {
980        crate::host::reject_promise_val(id, fetch_failed("unknown scheme"));
981        return Ok(promise);
982    };
983
984    let wire = build_request_bytes(&target, &method, &headers, &body);
985    let io_tx = with_host(|h| h.io_sender());
986    with_host(|h| h.incr_handle());
987    let url_for_response = url.clone();
988    std::thread::spawn(move || {
989        let raw = if target.tls {
990            let config = crate::stdlib::tls::client_config(true);
991            crate::stdlib::https::exchange(&target.host, target.port, &target.host, config, &wire)
992        } else {
993            crate::stdlib::http::exchange(&target.host, target.port, &wire)
994        };
995        let _ = io_tx.send(Box::new(move || {
996            with_host(|h| h.decr_handle());
997            match raw {
998                Ok(raw) => {
999                    let parsed = crate::stdlib::http::parse_raw_response(&raw);
1000                    let (status, message, headers, body) =
1001                        (parsed.status, parsed.message, parsed.headers, parsed.body);
1002                    let resp = build_response(status, &message, &headers, &body, &url_for_response);
1003                    crate::host::resolve_promise_val(id, resp);
1004                }
1005                Err(msg) => crate::host::reject_promise_val(id, fetch_failed(&msg)),
1006            }
1007            Ok(())
1008        }));
1009    });
1010    Ok(promise)
1011}
1012
1013/// node reports EVERY fetch transport failure as `TypeError: fetch failed` and
1014/// puts the detail on `.cause` as a nested `Error`, so the reason is available
1015/// without the message itself varying by platform.
1016fn fetch_failed(cause: &str) -> Value {
1017    with_host(|h| {
1018        let e = crate::builtins::synth_error(h, "TypeError: fetch failed");
1019        let c = crate::builtins::synth_error(h, &format!("Error: {cause}"));
1020        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
1021            p.insert("cause".into(), c);
1022        }
1023        e
1024    })
1025}
1026
1027struct Target {
1028    host: String,
1029    port: u16,
1030    path: String,
1031    tls: bool,
1032}
1033
1034fn parse_target(url: &str) -> Option<Target> {
1035    // Only `http`/`https` are fetchable here; every other scheme (`file:`,
1036    // `data:`, `blob:`) rejects as an unknown scheme.
1037    let (tls, rest) = match url.strip_prefix("https://") {
1038        Some(r) => (true, r),
1039        None => (false, url.strip_prefix("http://")?),
1040    };
1041    let (authority, path) = match rest.find('/') {
1042        Some(i) => (&rest[..i], rest[i..].to_string()),
1043        None => (rest, "/".to_string()),
1044    };
1045    let authority = authority.split('@').next_back().unwrap_or(authority);
1046    let (host, port) = match authority.rsplit_once(':') {
1047        Some((h, p)) => match p.parse::<u16>() {
1048            Ok(n) => (h.to_string(), n),
1049            Err(_) => (authority.to_string(), if tls { 443 } else { 80 }),
1050        },
1051        None => (authority.to_string(), if tls { 443 } else { 80 }),
1052    };
1053    if host.is_empty() {
1054        return None;
1055    }
1056    Some(Target {
1057        host,
1058        port,
1059        path,
1060        tls,
1061    })
1062}
1063
1064fn build_request_bytes(
1065    target: &Target,
1066    method: &str,
1067    headers: &[(String, String)],
1068    body: &[u8],
1069) -> Vec<u8> {
1070    let mut has_host = false;
1071    let mut has_len = false;
1072    let mut block = String::new();
1073    for (k, v) in headers {
1074        if k.eq_ignore_ascii_case("host") {
1075            has_host = true;
1076        }
1077        if k.eq_ignore_ascii_case("content-length") {
1078            has_len = true;
1079        }
1080        if k.eq_ignore_ascii_case("connection") {
1081            continue;
1082        }
1083        block.push_str(&format!("{k}: {v}\r\n"));
1084    }
1085    let default_port = if target.tls { 443 } else { 80 };
1086    let host_header = if target.port == default_port {
1087        target.host.clone()
1088    } else {
1089        format!("{}:{}", target.host, target.port)
1090    };
1091    let mut req = format!("{method} {} HTTP/1.1\r\n", target.path);
1092    if !has_host {
1093        req.push_str(&format!("Host: {host_header}\r\n"));
1094    }
1095    req.push_str(&block);
1096    if !has_len && !body.is_empty() {
1097        req.push_str(&format!("Content-Length: {}\r\n", body.len()));
1098    }
1099    // Every exchange reads to EOF, so the connection must not be kept alive.
1100    req.push_str("Connection: close\r\n\r\n");
1101    let mut wire = req.into_bytes();
1102    wire.extend_from_slice(body);
1103    wire
1104}
1105
1106// ── dispatch ─────────────────────────────────────────────────────────────────
1107
1108pub fn instance_call(
1109    tag: &str,
1110    recv: &Value,
1111    method: &str,
1112    args: &[Value],
1113) -> Result<Value, String> {
1114    match tag {
1115        "Headers" => headers_call(recv, method, args),
1116        "FormData" => form_data_call(recv, method, args),
1117        "AbortController" => abort_controller_call(recv, method, args),
1118        "AbortSignal" => abort_signal_call(recv, method, args),
1119        _ => body_call(recv, method, args),
1120    }
1121}
1122
1123pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
1124    Some(match name {
1125        "Headers" => construct_headers(args),
1126        "Request" => construct_request(args),
1127        "Response" => construct_response(args),
1128        "Blob" => construct_blob(args),
1129        "File" => construct_file(args),
1130        "FormData" => construct_form_data(args),
1131        "AbortController" => construct_abort_controller(args),
1132        "AbortSignal" => Err(crate::host::type_error(
1133            "Illegal constructor: use AbortSignal.abort() or AbortSignal.timeout()",
1134        )),
1135        _ => return None,
1136    })
1137}
1138
1139/// The static methods of these classes (`Response.json`, `AbortSignal.abort`, …).
1140pub fn static_call(ns: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
1141    match ns {
1142        "AbortSignal" => abort_signal_static(method, args),
1143        "Response" => match method {
1144            "json" => {
1145                let body = crate::builtins::call_builtin_function(
1146                    "JSON.stringify",
1147                    vec![args.first()?.clone()],
1148                )
1149                .map(|v| str_of(&v))
1150                .unwrap_or_default();
1151                let init = args.get(1).cloned().unwrap_or(Value::Undef);
1152                let status = match prop(&init, "status") {
1153                    Some(v) => with_host(|h| h.to_number(&v)) as u16,
1154                    None => 200,
1155                };
1156                let mut headers =
1157                    vec![("content-type".to_string(), "application/json".to_string())];
1158                if let Some(hv) = prop(&init, "headers") {
1159                    headers.extend(init_header_entries(&hv));
1160                }
1161                Some(Ok(build_response(
1162                    status,
1163                    "",
1164                    &headers,
1165                    body.as_bytes(),
1166                    "",
1167                )))
1168            }
1169            "error" => Some(Ok(build_response(0, "", &[], &[], ""))),
1170            "redirect" => {
1171                let loc = super::arg_str(args, 0);
1172                let status = match args.get(1) {
1173                    Some(v) => with_host(|h| h.to_number(v)) as u16,
1174                    None => 302,
1175                };
1176                Some(Ok(build_response(
1177                    status,
1178                    "",
1179                    &[("location".into(), loc)],
1180                    &[],
1181                    "",
1182                )))
1183            }
1184            _ => None,
1185        },
1186        _ => None,
1187    }
1188}
1189
1190pub const RESPONSE_STATICS: &[&str] = &["json", "error", "redirect"];
1191pub const ABORT_SIGNAL_STATICS: &[&str] = &["abort", "timeout"];