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| h.alloc(JsObj::Iter { items, idx: 0 })))
353        }
354        _ => Err(crate::host::type_error(&format!(
355            "{method} is not a function"
356        ))),
357    }
358}
359
360/// The header list sorted by name with same-named values combined, which is the
361/// order every `Headers` iteration method observes.
362fn sorted_combined(recv: &Value) -> Vec<(String, String)> {
363    let mut names: Vec<String> = Vec::new();
364    for (k, _) in headers_entries(recv) {
365        if !names.contains(&k) {
366            names.push(k);
367        }
368    }
369    names.sort();
370    names
371        .into_iter()
372        .filter_map(|n| get_header(recv, &n).map(|v| (n, v)))
373        .collect()
374}
375
376// ── Response / Request bodies ────────────────────────────────────────────────
377
378fn build_response(
379    status: u16,
380    status_text: &str,
381    headers: &[(String, String)],
382    body: &[u8],
383    url: &str,
384) -> Value {
385    let headers_obj = construct_headers(&[]).unwrap_or(Value::Undef);
386    for (k, v) in headers {
387        append_header(&headers_obj, k, v);
388    }
389    let body_arr = store_body(body);
390    let obj = with_host(|h| {
391        let mut m = IndexMap::new();
392        m.insert("@@native".into(), h.new_str("Response"));
393        m.insert("status".into(), Value::Float(status as f64));
394        m.insert("statusText".into(), h.new_str(status_text.to_string()));
395        // 2xx (and only 2xx) is `ok` — 304 is not.
396        m.insert(
397            "ok".into(),
398            Value::Bool((200..300).contains(&(status as u32))),
399        );
400        m.insert("redirected".into(), Value::Bool(false));
401        m.insert("type".into(), h.new_str("basic"));
402        m.insert("url".into(), h.new_str(url.to_string()));
403        m.insert("bodyUsed".into(), Value::Bool(false));
404        m.insert("headers".into(), headers_obj);
405        m.insert("@@body".into(), body_arr);
406        h.new_object(m)
407    });
408    with_host(|h| h.hide_prop(&obj, "@@body"));
409    obj
410}
411
412/// `new Response(body, init)`.
413pub fn construct_response(args: &[Value]) -> Result<Value, String> {
414    let body = body_init_bytes(args.first());
415    let init = args.get(1).cloned().unwrap_or(Value::Undef);
416    let status = match prop(&init, "status") {
417        Some(v) => with_host(|h| h.to_number(&v)) as u16,
418        None => 200,
419    };
420    let status_text = prop(&init, "statusText")
421        .map(|v| str_of(&v))
422        .unwrap_or_default();
423    let mut headers: Vec<(String, String)> = Vec::new();
424    if let Some(hv) = prop(&init, "headers") {
425        headers = init_header_entries(&hv);
426    }
427    Ok(build_response(status, &status_text, &headers, &body, ""))
428}
429
430/// `new Request(input, init)`.
431pub fn construct_request(args: &[Value]) -> Result<Value, String> {
432    let input = args.first().cloned().unwrap_or(Value::Undef);
433    let url = if super::native_tag(&input).as_deref() == Some("Request") {
434        prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
435    } else {
436        str_of(&input)
437    };
438    let init = args.get(1).cloned().unwrap_or(Value::Undef);
439    let method = prop(&init, "method")
440        .map(|v| str_of(&v).to_ascii_uppercase())
441        .unwrap_or_else(|| "GET".into());
442    let headers_obj = construct_headers(&[])?;
443    if let Some(hv) = prop(&init, "headers") {
444        for (k, v) in init_header_entries(&hv) {
445            append_header(&headers_obj, &k, &v);
446        }
447    }
448    let body = body_init_bytes(prop(&init, "body").as_ref());
449    let body_arr = store_body(&body);
450    let obj = with_host(|h| {
451        let mut m = IndexMap::new();
452        m.insert("@@native".into(), h.new_str("Request"));
453        m.insert("url".into(), h.new_str(url));
454        m.insert("method".into(), h.new_str(method));
455        m.insert("headers".into(), headers_obj);
456        m.insert("bodyUsed".into(), Value::Bool(false));
457        m.insert("@@body".into(), body_arr);
458        h.new_object(m)
459    });
460    with_host(|h| h.hide_prop(&obj, "@@body"));
461    Ok(obj)
462}
463
464/// The bytes a `BodyInit` contributes: a string encodes as UTF-8, a typed array
465/// / Buffer / `Blob` supplies its bytes, and anything else stringifies.
466fn body_init_bytes(v: Option<&Value>) -> Vec<u8> {
467    let Some(v) = v.filter(|v| !matches!(v, Value::Undef)) else {
468        return Vec::new();
469    };
470    if with_host(|h| h.is_null(v)) {
471        return Vec::new();
472    }
473    match super::native_tag(v).as_deref() {
474        Some("Blob") | Some("File") | Some("Response") | Some("Request") => return body_bytes(v),
475        Some("TypedArray") | Some("Buffer") => {
476            if let Some(e) = super::typedarray::elems_of(v) {
477                return e.iter().map(|x| *x as u8).collect();
478            }
479        }
480        _ => {}
481    }
482    if super::native_tag(v).as_deref() == Some("URLSearchParams") {
483        return str_of(v).into_bytes();
484    }
485    str_of(v).into_bytes()
486}
487
488/// The body accessors shared by `Response`, `Request` and `Blob`. Each answers a
489/// settled Promise, because the body is already fully buffered.
490pub fn body_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
491    match method {
492        "text" => {
493            set_prop(recv, "bodyUsed", Value::Bool(true));
494            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
495            Ok(settled(Ok(new_str(s))))
496        }
497        "json" => {
498            set_prop(recv, "bodyUsed", Value::Bool(true));
499            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
500            let parsed = crate::builtins::call_builtin_function("JSON.parse", vec![new_str(s)]);
501            Ok(settled(parsed))
502        }
503        "bytes" => {
504            set_prop(recv, "bodyUsed", Value::Bool(true));
505            Ok(settled(to_uint8array(&body_bytes(recv))))
506        }
507        "arrayBuffer" => {
508            set_prop(recv, "bodyUsed", Value::Bool(true));
509            let bytes = body_bytes(recv);
510            let buf = with_host(|h| {
511                let mut m = IndexMap::new();
512                m.insert("@@native".into(), h.new_str("ArrayBuffer"));
513                m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
514                let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
515                m.insert("@@bytes".into(), arr);
516                h.new_object(m)
517            });
518            with_host(|h| h.hide_prop(&buf, "@@bytes"));
519            Ok(settled(Ok(buf)))
520        }
521        "blob" => {
522            set_prop(recv, "bodyUsed", Value::Bool(true));
523            let bytes = body_bytes(recv);
524            let ct = prop(recv, "headers")
525                .and_then(|h| get_header(&h, "content-type"))
526                .unwrap_or_default();
527            Ok(settled(Ok(new_blob(&bytes, &ct, None))))
528        }
529        "formData" => {
530            set_prop(recv, "bodyUsed", Value::Bool(true));
531            let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
532            let fd = construct_form_data(&[])?;
533            // Only the urlencoded form is decoded here; a multipart body needs
534            // the boundary parser this does not have.
535            for pair in s.split('&').filter(|p| !p.is_empty()) {
536                let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
537                form_append(&fd, &percent_decode(k), &new_str(percent_decode(v)));
538            }
539            Ok(settled(Ok(fd)))
540        }
541        "clone" => {
542            let bytes = body_bytes(recv);
543            let clone = crate::builtins::deep_clone(recv)?;
544            set_prop(&clone, "@@body", store_body(&bytes));
545            set_prop(&clone, "bodyUsed", Value::Bool(false));
546            Ok(clone)
547        }
548        "slice" => {
549            let bytes = body_bytes(recv);
550            let start = args
551                .first()
552                .map(|v| with_host(|h| h.to_number(v)) as usize)
553                .unwrap_or(0)
554                .min(bytes.len());
555            let end = args
556                .get(1)
557                .filter(|v| !matches!(v, Value::Undef))
558                .map(|v| with_host(|h| h.to_number(v)) as usize)
559                .unwrap_or(bytes.len())
560                .clamp(start, bytes.len());
561            let ct = args.get(2).map(str_of).unwrap_or_default();
562            Ok(new_blob(&bytes[start..end], &ct, None))
563        }
564        "stream" => Err(crate::host::type_error(
565            "Response.body streaming is not implemented on this build",
566        )),
567        _ => Err(crate::host::type_error(&format!(
568            "{method} is not a function"
569        ))),
570    }
571}
572
573/// Throw `v` as the JS exception value (rather than as an internal message), so
574/// `catch (e)` receives the object the spec says it should.
575fn throw_js(v: Value) -> String {
576    let msg = with_host(|h| h.str_of(&v));
577    with_host(|h| h.exc = Some(v));
578    msg
579}
580
581fn percent_decode(s: &str) -> String {
582    let bytes = s.replace('+', " ").into_bytes();
583    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
584    let mut i = 0;
585    while i < bytes.len() {
586        if bytes[i] == b'%' && i + 2 < bytes.len() {
587            if let Ok(b) = u8::from_str_radix(&String::from_utf8_lossy(&bytes[i + 1..i + 3]), 16) {
588                out.push(b);
589                i += 3;
590                continue;
591            }
592        }
593        out.push(bytes[i]);
594        i += 1;
595    }
596    String::from_utf8_lossy(&out).into_owned()
597}
598
599// ── Blob / File ──────────────────────────────────────────────────────────────
600
601fn new_blob(bytes: &[u8], content_type: &str, file_name: Option<&str>) -> Value {
602    let body = store_body(bytes);
603    let obj = with_host(|h| {
604        let mut m = IndexMap::new();
605        m.insert(
606            "@@native".into(),
607            h.new_str(if file_name.is_some() { "File" } else { "Blob" }),
608        );
609        m.insert("size".into(), Value::Float(bytes.len() as f64));
610        m.insert("type".into(), h.new_str(content_type.to_ascii_lowercase()));
611        if let Some(n) = file_name {
612            m.insert("name".into(), h.new_str(n.to_string()));
613            m.insert("lastModified".into(), Value::Float(0.0));
614        }
615        m.insert("@@body".into(), body);
616        h.new_object(m)
617    });
618    with_host(|h| h.hide_prop(&obj, "@@body"));
619    obj
620}
621
622pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
623    let mut bytes = Vec::new();
624    if let Some(parts) = args.first() {
625        for p in crate::host::iter_all(parts).unwrap_or_default() {
626            bytes.extend(body_init_bytes(Some(&p)));
627        }
628    }
629    let ct = args
630        .get(1)
631        .and_then(|o| prop(o, "type"))
632        .map(|v| str_of(&v))
633        .unwrap_or_default();
634    Ok(new_blob(&bytes, &ct, None))
635}
636
637/// `new File(parts, name[, options])` — a `Blob` that also carries a name.
638pub fn construct_file(args: &[Value]) -> Result<Value, String> {
639    let mut bytes = Vec::new();
640    if let Some(parts) = args.first() {
641        for p in crate::host::iter_all(parts).unwrap_or_default() {
642            bytes.extend(body_init_bytes(Some(&p)));
643        }
644    }
645    let name = super::arg_str(args, 1);
646    let ct = args
647        .get(2)
648        .and_then(|o| prop(o, "type"))
649        .map(|v| str_of(&v))
650        .unwrap_or_default();
651    Ok(new_blob(&bytes, &ct, Some(&name)))
652}
653
654// ── FormData ─────────────────────────────────────────────────────────────────
655
656pub fn construct_form_data(_args: &[Value]) -> Result<Value, String> {
657    let obj = with_host(|h| {
658        let mut m = IndexMap::new();
659        m.insert("@@native".into(), h.new_str("FormData"));
660        let empty = h.new_array(Vec::new());
661        m.insert("@@entries".into(), empty);
662        h.new_object(m)
663    });
664    with_host(|h| h.hide_prop(&obj, "@@entries"));
665    Ok(obj)
666}
667
668fn form_entries(recv: &Value) -> Vec<(String, Value)> {
669    let Some(arr) = prop(recv, "@@entries") else {
670        return Vec::new();
671    };
672    with_host(|h| match h.get(&arr) {
673        Some(JsObj::Array(items)) => items
674            .iter()
675            .filter_map(|pair| match h.get(pair) {
676                Some(JsObj::Array(kv)) if kv.len() == 2 => Some((h.str_of(&kv[0]), kv[1].clone())),
677                _ => None,
678            })
679            .collect(),
680        _ => Vec::new(),
681    })
682}
683
684fn set_form_entries(recv: &Value, entries: &[(String, Value)]) {
685    let arr = with_host(|h| {
686        let pairs: Vec<Value> = entries
687            .iter()
688            .map(|(k, v)| {
689                let k = h.new_str(k.clone());
690                h.new_array(vec![k, v.clone()])
691            })
692            .collect();
693        h.new_array(pairs)
694    });
695    set_prop(recv, "@@entries", arr);
696}
697
698fn form_append(recv: &Value, name: &str, value: &Value) {
699    let mut e = form_entries(recv);
700    e.push((name.to_string(), value.clone()));
701    set_form_entries(recv, &e);
702}
703
704pub fn form_data_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
705    let name = super::arg_str(args, 0);
706    match method {
707        "append" => {
708            form_append(recv, &name, args.get(1).unwrap_or(&Value::Undef));
709            Ok(Value::Undef)
710        }
711        "set" => {
712            let mut e = form_entries(recv);
713            e.retain(|(k, _)| *k != name);
714            e.push((name, args.get(1).cloned().unwrap_or(Value::Undef)));
715            set_form_entries(recv, &e);
716            Ok(Value::Undef)
717        }
718        "delete" => {
719            let mut e = form_entries(recv);
720            e.retain(|(k, _)| *k != name);
721            set_form_entries(recv, &e);
722            Ok(Value::Undef)
723        }
724        "get" => Ok(form_entries(recv)
725            .into_iter()
726            .find(|(k, _)| *k == name)
727            .map(|(_, v)| v)
728            .unwrap_or_else(|| with_host(|h| h.null()))),
729        "getAll" => {
730            let vals: Vec<Value> = form_entries(recv)
731                .into_iter()
732                .filter(|(k, _)| *k == name)
733                .map(|(_, v)| v)
734                .collect();
735            Ok(with_host(|h| h.new_array(vals)))
736        }
737        "has" => Ok(Value::Bool(
738            form_entries(recv).iter().any(|(k, _)| *k == name),
739        )),
740        "forEach" => {
741            let cb = args.first().cloned().unwrap_or(Value::Undef);
742            for (k, v) in form_entries(recv) {
743                let kv = new_str(k);
744                crate::host::invoke(&cb, vec![v, kv, recv.clone()], None)?;
745            }
746            Ok(Value::Undef)
747        }
748        "keys" | "values" | "entries" | "@@iterator" => {
749            let items: Vec<Value> = form_entries(recv)
750                .into_iter()
751                .map(|(k, v)| match method {
752                    "keys" => new_str(k),
753                    "values" => v,
754                    _ => {
755                        let kv = new_str(k);
756                        with_host(|h| h.new_array(vec![kv, v]))
757                    }
758                })
759                .collect();
760            Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
761        }
762        _ => Err(crate::host::type_error(&format!(
763            "{method} is not a function"
764        ))),
765    }
766}
767
768// ── AbortController / AbortSignal ────────────────────────────────────────────
769
770pub fn new_abort_signal() -> Value {
771    with_host(|h| {
772        let mut m = IndexMap::new();
773        m.insert("@@native".into(), h.new_str("AbortSignal"));
774        m.insert("@@aborted".into(), Value::Bool(false));
775        m.insert("@@reason".into(), Value::Undef);
776        m.insert("@@onabort".into(), h.null());
777        h.new_object(m)
778    })
779}
780
781pub fn construct_abort_controller(_args: &[Value]) -> Result<Value, String> {
782    let signal = new_abort_signal();
783    Ok(with_host(|h| {
784        let mut m = IndexMap::new();
785        m.insert("@@native".into(), h.new_str("AbortController"));
786        m.insert("@@signal".into(), signal);
787        h.new_object(m)
788    }))
789}
790
791/// The `DOMException`-shaped reason an abort carries. node's `AbortSignal`
792/// rejects with a `DOMException` whose `name` is `AbortError`/`TimeoutError`;
793/// `synth_error` only knows the ECMAScript error classes, so the name, the
794/// constructor label and the `stack` head are stamped on afterwards.
795fn dom_exception(name: &str, message: &str) -> Value {
796    let msg = with_host(|h| h.new_str(message.to_string()));
797    let nm = with_host(|h| h.new_str(name.to_string()));
798    crate::builtins::dom_exception(&[msg, nm])
799}
800
801/// Fire an `AbortSignal.timeout` deadline: abort the signal at heap index `idx`
802/// with a `TimeoutError`. Reached from the `@@aborttimeout:<idx>` thunk.
803pub fn fire_timeout_abort(idx: u32) -> Result<Value, String> {
804    let signal = Value::Obj(idx);
805    let e = dom_exception("TimeoutError", "The operation was aborted due to timeout");
806    abort_signal(&signal, e)?;
807    Ok(Value::Undef)
808}
809
810/// Mark a signal aborted and run its `onabort` / `abort` listeners.
811fn abort_signal(signal: &Value, reason: Value) -> Result<(), String> {
812    if matches!(prop(signal, "@@aborted"), Some(Value::Bool(true))) {
813        return Ok(());
814    }
815    let reason = if matches!(reason, Value::Undef) {
816        dom_exception("AbortError", "This operation was aborted")
817    } else {
818        reason
819    };
820    set_prop(signal, "@@aborted", Value::Bool(true));
821    set_prop(signal, "@@reason", reason);
822    if let Some(cb) = prop(signal, "@@onabort") {
823        if with_host(|h| crate::host::is_callable(h, &cb)) {
824            crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
825        }
826    }
827    if let Some(list) = prop(signal, "@@abortListeners") {
828        for cb in crate::host::iter_all(&list).unwrap_or_default() {
829            crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
830        }
831    }
832    Ok(())
833}
834
835pub fn abort_controller_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
836    match method {
837        "abort" => {
838            let signal = prop(recv, "@@signal").unwrap_or(Value::Undef);
839            abort_signal(&signal, args.first().cloned().unwrap_or(Value::Undef))?;
840            Ok(Value::Undef)
841        }
842        _ => Err(crate::host::type_error(&format!(
843            "{method} is not a function"
844        ))),
845    }
846}
847
848pub fn abort_signal_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
849    match method {
850        "throwIfAborted" => {
851            if matches!(prop(recv, "@@aborted"), Some(Value::Bool(true))) {
852                let reason = prop(recv, "@@reason").unwrap_or(Value::Undef);
853                return Err(throw_js(reason));
854            }
855            Ok(Value::Undef)
856        }
857        "addEventListener" => {
858            if super::arg_str(args, 0) == "abort" {
859                let cb = args.get(1).cloned().unwrap_or(Value::Undef);
860                let existing = prop(recv, "@@abortListeners");
861                let list = match existing {
862                    Some(l) => l,
863                    None => {
864                        let l = with_host(|h| h.new_array(Vec::new()));
865                        set_prop(recv, "@@abortListeners", l.clone());
866                        with_host(|h| h.hide_prop(recv, "@@abortListeners"));
867                        l
868                    }
869                };
870                with_host(|h| {
871                    if let Some(JsObj::Array(items)) = h.get_mut(&list) {
872                        items.push(cb);
873                    }
874                });
875            }
876            Ok(Value::Undef)
877        }
878        "removeEventListener" => Ok(Value::Undef),
879        _ => Err(crate::host::type_error(&format!(
880            "{method} is not a function"
881        ))),
882    }
883}
884
885/// `AbortSignal.abort(reason)` / `AbortSignal.timeout(ms)`.
886pub fn abort_signal_static(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
887    match method {
888        "abort" => {
889            let s = new_abort_signal();
890            let r = abort_signal(&s, args.first().cloned().unwrap_or(Value::Undef));
891            Some(r.map(|_| s))
892        }
893        "timeout" => {
894            let s = new_abort_signal();
895            // The abort is a real scheduled macrotask: a native continuation
896            // thunk (the `@@`-prefixed convention `@@presolve:<id>` already
897            // uses) carries the signal's heap index to the timer callback.
898            let ms = super::arg_num(args, 0);
899            let Value::Obj(idx) = s else {
900                return Some(Ok(s));
901            };
902            let cb = with_host(|h| h.alloc(JsObj::Builtin(format!("@@aborttimeout:{idx}"))));
903            with_host(|h| h.add_timer(ms, cb, Vec::new(), None));
904            Some(Ok(s))
905        }
906        _ => None,
907    }
908}
909
910// ── fetch ────────────────────────────────────────────────────────────────────
911
912/// `fetch(input[, init])` — a Promise for a fully buffered `Response`.
913pub fn fetch(args: &[Value]) -> Result<Value, String> {
914    let promise = with_host(|h| h.new_promise());
915    let id = with_host(|h| h.promise_id(&promise).unwrap_or(0));
916
917    let input = args.first().cloned().unwrap_or(Value::Undef);
918    let init = args.get(1).cloned().unwrap_or(Value::Undef);
919
920    // `input` is a URL string, a `URL`, or a `Request` (whose method/headers/
921    // body seed the request, and `init` overrides them).
922    let from_request = super::native_tag(&input).as_deref() == Some("Request");
923    let url = if from_request {
924        prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
925    } else {
926        str_of(&input)
927    };
928
929    let mut method = if from_request {
930        prop(&input, "method")
931            .map(|v| str_of(&v))
932            .unwrap_or_else(|| "GET".into())
933    } else {
934        "GET".into()
935    };
936    if let Some(m) = prop(&init, "method") {
937        method = str_of(&m);
938    }
939    method = method.to_ascii_uppercase();
940
941    let mut headers: Vec<(String, String)> = Vec::new();
942    if from_request {
943        if let Some(h) = prop(&input, "headers") {
944            headers = headers_entries(&h);
945        }
946    }
947    if let Some(hv) = prop(&init, "headers") {
948        headers.extend(init_header_entries(&hv));
949    }
950
951    let body = match prop(&init, "body") {
952        Some(b) => body_init_bytes(Some(&b)),
953        None if from_request => body_bytes(&input),
954        None => Vec::new(),
955    };
956
957    // An already-aborted signal rejects before any connection is made.
958    if let Some(sig) = prop(&init, "signal") {
959        if matches!(prop(&sig, "@@aborted"), Some(Value::Bool(true))) {
960            let reason = prop(&sig, "@@reason")
961                .unwrap_or_else(|| dom_exception("AbortError", "This operation was aborted"));
962            crate::host::reject_promise_val(id, reason);
963            return Ok(promise);
964        }
965    }
966
967    let Some(target) = parse_target(&url) else {
968        crate::host::reject_promise_val(id, fetch_failed("unknown scheme"));
969        return Ok(promise);
970    };
971
972    let wire = build_request_bytes(&target, &method, &headers, &body);
973    let io_tx = with_host(|h| h.io_sender());
974    with_host(|h| h.incr_handle());
975    let url_for_response = url.clone();
976    std::thread::spawn(move || {
977        let raw = if target.tls {
978            let config = crate::stdlib::tls::client_config(true);
979            crate::stdlib::https::exchange(&target.host, target.port, &target.host, config, &wire)
980        } else {
981            crate::stdlib::http::exchange(&target.host, target.port, &wire)
982        };
983        let _ = io_tx.send(Box::new(move || {
984            with_host(|h| h.decr_handle());
985            match raw {
986                Ok(raw) => {
987                    let parsed = crate::stdlib::http::parse_raw_response(&raw);
988                    let (status, message, headers, body) =
989                        (parsed.status, parsed.message, parsed.headers, parsed.body);
990                    let resp = build_response(status, &message, &headers, &body, &url_for_response);
991                    crate::host::resolve_promise_val(id, resp);
992                }
993                Err(msg) => crate::host::reject_promise_val(id, fetch_failed(&msg)),
994            }
995            Ok(())
996        }));
997    });
998    Ok(promise)
999}
1000
1001/// node reports EVERY fetch transport failure as `TypeError: fetch failed` and
1002/// puts the detail on `.cause` as a nested `Error`, so the reason is available
1003/// without the message itself varying by platform.
1004fn fetch_failed(cause: &str) -> Value {
1005    with_host(|h| {
1006        let e = crate::builtins::synth_error(h, "TypeError: fetch failed");
1007        let c = crate::builtins::synth_error(h, &format!("Error: {cause}"));
1008        if let Some(JsObj::Object(p)) = h.get_mut(&e) {
1009            p.insert("cause".into(), c);
1010        }
1011        e
1012    })
1013}
1014
1015struct Target {
1016    host: String,
1017    port: u16,
1018    path: String,
1019    tls: bool,
1020}
1021
1022fn parse_target(url: &str) -> Option<Target> {
1023    // Only `http`/`https` are fetchable here; every other scheme (`file:`,
1024    // `data:`, `blob:`) rejects as an unknown scheme.
1025    let (tls, rest) = match url.strip_prefix("https://") {
1026        Some(r) => (true, r),
1027        None => (false, url.strip_prefix("http://")?),
1028    };
1029    let (authority, path) = match rest.find('/') {
1030        Some(i) => (&rest[..i], rest[i..].to_string()),
1031        None => (rest, "/".to_string()),
1032    };
1033    let authority = authority.split('@').next_back().unwrap_or(authority);
1034    let (host, port) = match authority.rsplit_once(':') {
1035        Some((h, p)) => match p.parse::<u16>() {
1036            Ok(n) => (h.to_string(), n),
1037            Err(_) => (authority.to_string(), if tls { 443 } else { 80 }),
1038        },
1039        None => (authority.to_string(), if tls { 443 } else { 80 }),
1040    };
1041    if host.is_empty() {
1042        return None;
1043    }
1044    Some(Target {
1045        host,
1046        port,
1047        path,
1048        tls,
1049    })
1050}
1051
1052fn build_request_bytes(
1053    target: &Target,
1054    method: &str,
1055    headers: &[(String, String)],
1056    body: &[u8],
1057) -> Vec<u8> {
1058    let mut has_host = false;
1059    let mut has_len = false;
1060    let mut block = String::new();
1061    for (k, v) in headers {
1062        if k.eq_ignore_ascii_case("host") {
1063            has_host = true;
1064        }
1065        if k.eq_ignore_ascii_case("content-length") {
1066            has_len = true;
1067        }
1068        if k.eq_ignore_ascii_case("connection") {
1069            continue;
1070        }
1071        block.push_str(&format!("{k}: {v}\r\n"));
1072    }
1073    let default_port = if target.tls { 443 } else { 80 };
1074    let host_header = if target.port == default_port {
1075        target.host.clone()
1076    } else {
1077        format!("{}:{}", target.host, target.port)
1078    };
1079    let mut req = format!("{method} {} HTTP/1.1\r\n", target.path);
1080    if !has_host {
1081        req.push_str(&format!("Host: {host_header}\r\n"));
1082    }
1083    req.push_str(&block);
1084    if !has_len && !body.is_empty() {
1085        req.push_str(&format!("Content-Length: {}\r\n", body.len()));
1086    }
1087    // Every exchange reads to EOF, so the connection must not be kept alive.
1088    req.push_str("Connection: close\r\n\r\n");
1089    let mut wire = req.into_bytes();
1090    wire.extend_from_slice(body);
1091    wire
1092}
1093
1094// ── dispatch ─────────────────────────────────────────────────────────────────
1095
1096pub fn instance_call(
1097    tag: &str,
1098    recv: &Value,
1099    method: &str,
1100    args: &[Value],
1101) -> Result<Value, String> {
1102    match tag {
1103        "Headers" => headers_call(recv, method, args),
1104        "FormData" => form_data_call(recv, method, args),
1105        "AbortController" => abort_controller_call(recv, method, args),
1106        "AbortSignal" => abort_signal_call(recv, method, args),
1107        _ => body_call(recv, method, args),
1108    }
1109}
1110
1111pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
1112    Some(match name {
1113        "Headers" => construct_headers(args),
1114        "Request" => construct_request(args),
1115        "Response" => construct_response(args),
1116        "Blob" => construct_blob(args),
1117        "File" => construct_file(args),
1118        "FormData" => construct_form_data(args),
1119        "AbortController" => construct_abort_controller(args),
1120        "AbortSignal" => Err(crate::host::type_error(
1121            "Illegal constructor: use AbortSignal.abort() or AbortSignal.timeout()",
1122        )),
1123        _ => return None,
1124    })
1125}
1126
1127/// The static methods of these classes (`Response.json`, `AbortSignal.abort`, …).
1128pub fn static_call(ns: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
1129    match ns {
1130        "AbortSignal" => abort_signal_static(method, args),
1131        "Response" => match method {
1132            "json" => {
1133                let body = crate::builtins::call_builtin_function(
1134                    "JSON.stringify",
1135                    vec![args.first()?.clone()],
1136                )
1137                .map(|v| str_of(&v))
1138                .unwrap_or_default();
1139                let init = args.get(1).cloned().unwrap_or(Value::Undef);
1140                let status = match prop(&init, "status") {
1141                    Some(v) => with_host(|h| h.to_number(&v)) as u16,
1142                    None => 200,
1143                };
1144                let mut headers =
1145                    vec![("content-type".to_string(), "application/json".to_string())];
1146                if let Some(hv) = prop(&init, "headers") {
1147                    headers.extend(init_header_entries(&hv));
1148                }
1149                Some(Ok(build_response(
1150                    status,
1151                    "",
1152                    &headers,
1153                    body.as_bytes(),
1154                    "",
1155                )))
1156            }
1157            "error" => Some(Ok(build_response(0, "", &[], &[], ""))),
1158            "redirect" => {
1159                let loc = super::arg_str(args, 0);
1160                let status = match args.get(1) {
1161                    Some(v) => with_host(|h| h.to_number(v)) as u16,
1162                    None => 302,
1163                };
1164                Some(Ok(build_response(
1165                    status,
1166                    "",
1167                    &[("location".into(), loc)],
1168                    &[],
1169                    "",
1170                )))
1171            }
1172            _ => None,
1173        },
1174        _ => None,
1175    }
1176}
1177
1178pub const RESPONSE_STATICS: &[&str] = &["json", "error", "redirect"];
1179pub const ABORT_SIGNAL_STATICS: &[&str] = &["abort", "timeout"];