Skip to main content

nodejs/stdlib/
http.rs

1//! Node `http` module: an HTTP/1.1 server built on top of `net`.
2//!
3//! `http.createServer(requestListener)` returns a `net.Server` with an
4//! `http`-specific per-connection hook. As bytes arrive on a connection (posted
5//! by the socket reader thread and dispatched on the main thread by
6//! `net::on_socket_data` → `http::feed`), we buffer them, parse complete
7//! HTTP/1.1 requests, and for each one build an `IncomingMessage` (`req`) and a
8//! `ServerResponse` (`res`) and call the user's `(req, res)` listener on the main
9//! thread. `res.end` serializes a valid HTTP/1.1 response and writes it straight
10//! back to the socket via `net::socket_write_id`, keeping the connection alive.
11
12use crate::host::{invoke, with_host, JsObj};
13use fusevm::Value;
14use indexmap::IndexMap;
15use std::collections::HashMap;
16use std::io::{Read, Write};
17use std::net::TcpStream;
18
19/// `http` module functions routed through `stdlib::call`.
20pub const MODULE_METHODS: &[&str] = &[
21    "createServer",
22    "request",
23    "get",
24    "validateHeaderName",
25    "validateHeaderValue",
26    "setMaxIdleHTTPParsers",
27    "setGlobalProxyFromEnv",
28];
29
30/// Instance methods of a client `ClientRequest` (parent wires the `ClientRequest`
31/// tag via `native_tag`/`instance_call`).
32pub const CLIENT_REQUEST_METHODS: &[&str] = &[
33    "write",
34    "end",
35    "setHeader",
36    "getHeader",
37    "removeHeader",
38    "abort",
39    "destroy",
40    "setTimeout",
41    "flushHeaders",
42];
43
44/// The HTTP request methods Node exposes as `http.METHODS` (router derives its
45/// per-verb helpers by lowercasing this list).
46const METHODS: &[&str] = &[
47    "ACL",
48    "BIND",
49    "CHECKOUT",
50    "CONNECT",
51    "COPY",
52    "DELETE",
53    "GET",
54    "HEAD",
55    "LINK",
56    "LOCK",
57    "M-SEARCH",
58    "MERGE",
59    "MKACTIVITY",
60    "MKCALENDAR",
61    "MKCOL",
62    "MOVE",
63    "NOTIFY",
64    "OPTIONS",
65    "PATCH",
66    "POST",
67    "PROPFIND",
68    "PROPPATCH",
69    "PURGE",
70    "PUT",
71    "QUERY",
72    "REBIND",
73    "REPORT",
74    "SEARCH",
75    "SOURCE",
76    "SUBSCRIBE",
77    "TRACE",
78    "UNBIND",
79    "UNLINK",
80    "UNLOCK",
81    "UNSUBSCRIBE",
82];
83
84/// Non-function `http` module constants (`http.METHODS`, `http.STATUS_CODES`),
85/// reachable via `namespace_property` → `stdlib::constant`.
86pub fn constant(name: &str) -> Option<Value> {
87    match name {
88        "METHODS" => Some(with_host(|h| {
89            let items = METHODS.iter().map(|m| h.new_str(*m)).collect();
90            h.new_array(items)
91        })),
92        "STATUS_CODES" => Some(with_host(|h| {
93            let mut m = IndexMap::new();
94            for (code, msg) in crate::stdlib::http::status_table() {
95                m.insert(code.to_string(), h.new_str(*msg));
96            }
97            h.new_object(m)
98        })),
99        // The request/response constructors express augments
100        // (`Object.create(http.IncomingMessage.prototype)`): exposed as builtin
101        // ctor namespaces so `.prototype` resolves.
102        "IncomingMessage" => Some(with_host(|h| {
103            h.alloc(JsObj::Builtin("IncomingMessage".into()))
104        })),
105        "ServerResponse" => Some(with_host(|h| {
106            h.alloc(JsObj::Builtin("ServerResponse".into()))
107        })),
108        // Client/server constructors exposed as builtin ctor namespaces so
109        // `.prototype` resolves and `new http.X(...)` routes to `http::construct`.
110        // `http.Server` uses a distinct builtin name to disambiguate from
111        // `net.Server` in the shared `stdlib::construct`.
112        "Agent" => Some(with_host(|h| h.alloc(JsObj::Builtin("Agent".into())))),
113        "Server" => Some(with_host(|h| h.alloc(JsObj::Builtin("http.Server".into())))),
114        "ClientRequest" => Some(with_host(|h| {
115            h.alloc(JsObj::Builtin("ClientRequest".into()))
116        })),
117        "OutgoingMessage" => Some(with_host(|h| {
118            h.alloc(JsObj::Builtin("OutgoingMessage".into()))
119        })),
120        "globalAgent" => Some(construct_agent(&[])),
121        _ => None,
122    }
123}
124
125/// `stdlib::construct` entry for `http` classes. Parent wires this in.
126pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
127    match name {
128        "Agent" => Some(Ok(construct_agent(args))),
129        // `new http.Server([requestListener])`.
130        "http.Server" => Some(Ok(create_server(
131            args.first()
132                .cloned()
133                .filter(|v| with_host(|h| crate::host::is_callable(h, v))),
134        ))),
135        _ => None,
136    }
137}
138
139/// The status-code → reason-phrase table shared by `STATUS_CODES` and response
140/// serialization.
141pub fn status_table() -> &'static [(u16, &'static str)] {
142    &[
143        (200, "OK"),
144        (201, "Created"),
145        (202, "Accepted"),
146        (204, "No Content"),
147        (301, "Moved Permanently"),
148        (302, "Found"),
149        (303, "See Other"),
150        (304, "Not Modified"),
151        (307, "Temporary Redirect"),
152        (308, "Permanent Redirect"),
153        (400, "Bad Request"),
154        (401, "Unauthorized"),
155        (403, "Forbidden"),
156        (404, "Not Found"),
157        (405, "Method Not Allowed"),
158        (406, "Not Acceptable"),
159        (409, "Conflict"),
160        (410, "Gone"),
161        (411, "Length Required"),
162        (413, "Payload Too Large"),
163        (414, "URI Too Long"),
164        (415, "Unsupported Media Type"),
165        (422, "Unprocessable Entity"),
166        (429, "Too Many Requests"),
167        (500, "Internal Server Error"),
168        (501, "Not Implemented"),
169        (502, "Bad Gateway"),
170        (503, "Service Unavailable"),
171        (504, "Gateway Timeout"),
172    ]
173}
174
175// ── per-connection parse state ───────────────────────────────────────────────
176
177/// Main-thread state for one live HTTP connection, keyed by the `net` socket id.
178struct HttpConn {
179    /// The underlying `net.Socket` object.
180    socket: Value,
181    /// The user's `requestListener` (`(req, res) => …`).
182    listener: Value,
183    /// Bytes received but not yet consumed into a complete request.
184    buf: Vec<u8>,
185}
186
187/// Main-thread state for one in-flight response, keyed by a fresh response id
188/// (stored on the `res` object as `@@resid`).
189struct ResState {
190    /// Socket id to write the serialized response to.
191    sock_id: u64,
192    /// Status code (`writeHead`/`res.statusCode`).
193    status: u16,
194    /// Optional custom status message.
195    message: Option<String>,
196    /// Response headers in insertion order; name kept as given, matched
197    /// case-insensitively.
198    headers: Vec<(String, String)>,
199    /// Accumulated body bytes (`write`/`end`).
200    body: Vec<u8>,
201}
202
203thread_local! {
204    static CONNS: std::cell::RefCell<HashMap<u64, HttpConn>> =
205        std::cell::RefCell::new(HashMap::new());
206    static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
207        std::cell::RefCell::new(HashMap::new());
208    static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
209}
210
211fn next_resid() -> u64 {
212    NEXT_RESID.with(|c| {
213        let id = c.get();
214        c.set(id + 1);
215        id
216    })
217}
218
219// ── module: http.createServer ────────────────────────────────────────────────
220
221/// `stdlib::call` entry for `http.<method>`.
222pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
223    match method {
224        "createServer" => Some(Ok(create_server(args.first().cloned()))),
225        "request" => Some(request(args, false)),
226        "get" => Some(request(args, true)),
227        "validateHeaderName" => Some(validate_header_name(args)),
228        "validateHeaderValue" => Some(validate_header_value(args)),
229        // No pooling/proxy substrate: accepted no-ops (match Node's `undefined`).
230        "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv" => Some(Ok(Value::Undef)),
231        _ => None,
232    }
233}
234
235/// `http.validateHeaderName(name[, label])` — throws `ERR_INVALID_HTTP_TOKEN`
236/// unless `name` is a valid RFC 7230 field-name token, else returns `undefined`.
237fn validate_header_name(args: &[Value]) -> Result<Value, String> {
238    let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
239    if is_valid_token(&name) {
240        Ok(Value::Undef)
241    } else {
242        Err(crate::host::type_error(&format!(
243            "Header name must be a valid HTTP token [\"{name}\"]"
244        )))
245    }
246}
247
248/// `http.validateHeaderValue(name, value)` — throws if `value` is `undefined`
249/// (`ERR_HTTP_INVALID_HEADER_VALUE`) or contains an invalid character
250/// (`ERR_INVALID_CHAR`), else returns `undefined`.
251fn validate_header_value(args: &[Value]) -> Result<Value, String> {
252    let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
253    let raw = args.get(1).cloned().unwrap_or(Value::Undef);
254    if matches!(raw, Value::Undef) {
255        return Err(crate::host::type_error(&format!(
256            "Invalid value \"undefined\" for header \"{name}\""
257        )));
258    }
259    let value = with_host(|h| h.str_of(&raw));
260    if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
261        return Err(crate::host::type_error(&format!(
262            "Invalid character in header content [\"{name}\"]"
263        )));
264    }
265    Ok(Value::Undef)
266}
267
268/// RFC 7230 field-name token: one or more of the `tchar` set.
269fn is_valid_token(s: &str) -> bool {
270    !s.is_empty()
271        && s.bytes().all(|b| {
272            b.is_ascii_alphanumeric()
273                || matches!(
274                    b,
275                    b'!' | b'#'
276                        | b'$'
277                        | b'%'
278                        | b'&'
279                        | b'\''
280                        | b'*'
281                        | b'+'
282                        | b'-'
283                        | b'.'
284                        | b'^'
285                        | b'_'
286                        | b'`'
287                        | b'|'
288                        | b'~'
289                )
290        })
291}
292
293/// `new http.Agent([options])` — a minimal connection-pool holder. We open a
294/// fresh connection per request (no reuse), so the agent only carries its
295/// configuration for the JS side to read back.
296pub fn construct_agent(args: &[Value]) -> Value {
297    let pairs = args
298        .first()
299        .filter(|v| matches!(v, Value::Obj(_)))
300        .map(object_pairs)
301        .unwrap_or_default();
302    with_host(|h| {
303        let mut m = IndexMap::new();
304        m.insert("@@native".into(), h.new_str("Agent"));
305        m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
306        m.insert("maxFreeSockets".into(), Value::Float(256.0));
307        m.insert("sockets".into(), h.new_object(IndexMap::new()));
308        m.insert("requests".into(), h.new_object(IndexMap::new()));
309        for (k, v) in pairs {
310            m.insert(k, h.new_str(v));
311        }
312        h.new_object(m)
313    })
314}
315
316/// Build an HTTP server: a `net.Server` whose per-connection hook wires up the
317/// request parser and remembers the `requestListener`.
318pub fn create_server(request_listener: Option<Value>) -> Value {
319    let server = super::net::create_server(None);
320    let listener = request_listener.unwrap_or(Value::Undef);
321    let hook = std::rc::Rc::new(
322        move |_server: &Value, socket: &Value| -> Result<(), String> {
323            let sock_id = socket_id_of(socket);
324            CONNS.with(|c| {
325                c.borrow_mut().insert(
326                    sock_id,
327                    HttpConn {
328                        socket: socket.clone(),
329                        listener: listener.clone(),
330                        buf: Vec::new(),
331                    },
332                );
333            });
334            Ok(())
335        },
336    );
337    super::net::set_conn_hook(&server, hook);
338    server
339}
340
341fn socket_id_of(socket: &Value) -> u64 {
342    with_host(|h| match h.get(socket) {
343        Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
344        _ => 0,
345    })
346}
347
348/// Discard an HTTP connection when its socket closes.
349pub fn drop_conn(sock_id: u64) {
350    CONNS.with(|c| {
351        c.borrow_mut().remove(&sock_id);
352    });
353}
354
355// ── request parsing ──────────────────────────────────────────────────────────
356
357/// Feed freshly received socket bytes into the HTTP parser. No-op for a socket
358/// that is not an HTTP connection (plain `net`). Runs on the main thread.
359pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
360    let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
361    if !is_http {
362        return Ok(());
363    }
364    CONNS.with(|c| {
365        c.borrow_mut()
366            .get_mut(&sock_id)
367            .unwrap()
368            .buf
369            .extend_from_slice(bytes)
370    });
371
372    // Parse and dispatch every complete request currently buffered (pipelining).
373    loop {
374        let (listener, socket, parsed) = CONNS.with(|c| {
375            let mut c = c.borrow_mut();
376            let conn = c.get_mut(&sock_id).unwrap();
377            match parse_request(&conn.buf) {
378                Some((req, consumed)) => {
379                    conn.buf.drain(..consumed);
380                    (conn.listener.clone(), conn.socket.clone(), Some(req))
381                }
382                None => (Value::Undef, Value::Undef, None),
383            }
384        });
385        let Some(parsed) = parsed else { break };
386
387        let req = build_incoming(&parsed);
388        let res = build_response(sock_id);
389        if with_host(|h| crate::host::is_callable(h, &listener)) {
390            invoke(&listener, vec![req.clone(), res], None)?;
391        }
392        // Body streaming: emit any request body then `end` for downstream readers.
393        if !parsed.body.is_empty() {
394            let chunk = super::buffer::from_bytes(&parsed.body);
395            super::events::instance_call(
396                &req,
397                "emit",
398                vec![with_host(|h| h.new_str("data")), chunk],
399            )?;
400        }
401        super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
402        let _ = socket; // (kept for future keep-alive bookkeeping)
403    }
404    Ok(())
405}
406
407/// A fully parsed HTTP request.
408struct ParsedReq {
409    method: String,
410    url: String,
411    http_version: String,
412    /// Lowercased header name → value (last wins, like Node for simple headers).
413    headers: Vec<(String, String)>,
414    body: Vec<u8>,
415}
416
417/// Try to parse one complete request from `buf`. Returns the request and the
418/// number of bytes consumed, or `None` if more bytes are needed.
419fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
420    // Header block ends at the first CRLFCRLF.
421    let head_end = find_subslice(buf, b"\r\n\r\n")?;
422    let head = &buf[..head_end];
423    let body_start = head_end + 4;
424
425    let head_str = String::from_utf8_lossy(head);
426    let mut lines = head_str.split("\r\n");
427    let request_line = lines.next()?;
428    let mut parts = request_line.split(' ');
429    let method = parts.next()?.to_string();
430    let url = parts.next()?.to_string();
431    let version = parts.next().unwrap_or("HTTP/1.1");
432    let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
433
434    let mut headers: Vec<(String, String)> = Vec::new();
435    let mut content_length = 0usize;
436    for line in lines {
437        if line.is_empty() {
438            continue;
439        }
440        if let Some((k, v)) = line.split_once(':') {
441            let name = k.trim().to_ascii_lowercase();
442            let value = v.trim().to_string();
443            if name == "content-length" {
444                content_length = value.parse().unwrap_or(0);
445            }
446            headers.push((name, value));
447        }
448    }
449
450    // Need the full body before dispatching.
451    if buf.len() < body_start + content_length {
452        return None;
453    }
454    let body = buf[body_start..body_start + content_length].to_vec();
455    Some((
456        ParsedReq {
457            method,
458            url,
459            http_version,
460            headers,
461            body,
462        },
463        body_start + content_length,
464    ))
465}
466
467fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
468    haystack.windows(needle.len()).position(|w| w == needle)
469}
470
471// ── IncomingMessage (req) ────────────────────────────────────────────────────
472
473fn build_incoming(req: &ParsedReq) -> Value {
474    let headers_obj = with_host(|h| {
475        let mut m = IndexMap::new();
476        for (k, v) in &req.headers {
477            m.insert(k.clone(), h.new_str(v.clone()));
478        }
479        h.new_object(m)
480    });
481    let mut extra = IndexMap::new();
482    extra.insert(
483        "method".into(),
484        with_host(|h| h.new_str(req.method.clone())),
485    );
486    extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
487    extra.insert(
488        "httpVersion".into(),
489        with_host(|h| h.new_str(req.http_version.clone())),
490    );
491    extra.insert("headers".into(), headers_obj);
492    super::net::new_emitter_object("IncomingMessage", extra)
493}
494
495// ── ServerResponse (res) ─────────────────────────────────────────────────────
496
497fn build_response(sock_id: u64) -> Value {
498    let resid = next_resid();
499    RESPONSES.with(|r| {
500        r.borrow_mut().insert(
501            resid,
502            ResState {
503                sock_id,
504                status: 200,
505                message: None,
506                headers: Vec::new(),
507                body: Vec::new(),
508            },
509        );
510    });
511    let mut extra = IndexMap::new();
512    extra.insert("@@resid".into(), Value::Float(resid as f64));
513    extra.insert("statusCode".into(), Value::Float(200.0));
514    super::net::new_emitter_object("ServerResponse", extra)
515}
516
517fn resid_of(res: &Value) -> Option<u64> {
518    with_host(|h| match h.get(res) {
519        Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
520        _ => None,
521    })
522}
523
524/// Instance dispatch for `IncomingMessage`/`ServerResponse` (EventEmitter methods
525/// handled by `net`'s shared emitter delegation via `stdlib::instance_call`).
526pub fn instance_call(
527    tag: &str,
528    recv: &Value,
529    method: &str,
530    args: Vec<Value>,
531) -> Result<Value, String> {
532    // EventEmitter surface is shared with `net` sockets.
533    if matches!(
534        method,
535        "on" | "addListener"
536            | "prependListener"
537            | "once"
538            | "prependOnceListener"
539            | "emit"
540            | "removeListener"
541            | "off"
542            | "removeAllListeners"
543            | "listenerCount"
544            | "eventNames"
545    ) {
546        return super::events::instance_call(recv, method, args);
547    }
548    match tag {
549        "IncomingMessage" => Err(crate::host::type_error(&format!(
550            "req.{method} is not a function"
551        ))),
552        "ServerResponse" => response_call(recv, method, args),
553        "ClientRequest" => client_request_call(recv, method, args),
554        // Minimal Agent: no live sockets to tear down.
555        "Agent" => match method {
556            "destroy" => Ok(Value::Undef),
557            "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
558            _ => Err(crate::host::type_error(&format!(
559                "agent.{method} is not a function"
560            ))),
561        },
562        _ => Err(crate::host::type_error(&format!(
563            "{method} is not a function"
564        ))),
565    }
566}
567
568fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
569    let Some(resid) = resid_of(res) else {
570        return Err(crate::host::type_error("invalid ServerResponse"));
571    };
572    match method {
573        "writeHead" => {
574            let status =
575                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
576            // Second arg is either the status message (string) or the headers obj.
577            let mut message: Option<String> = None;
578            let mut headers_arg: Option<Value> = None;
579            if let Some(a) = args.get(1) {
580                if with_host(|h| h.as_str(a)).is_some() {
581                    message = Some(with_host(|h| h.str_of(a)));
582                } else if !matches!(a, Value::Undef) {
583                    headers_arg = Some(a.clone());
584                }
585            }
586            if let Some(a) = args.get(2) {
587                if !matches!(a, Value::Undef) {
588                    headers_arg = Some(a.clone());
589                }
590            }
591            let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
592            RESPONSES.with(|r| {
593                if let Some(st) = r.borrow_mut().get_mut(&resid) {
594                    st.status = status;
595                    st.message = message;
596                    for (k, v) in header_pairs {
597                        upsert_header(&mut st.headers, &k, v);
598                    }
599                }
600            });
601            // Mirror onto the JS prop so `res.statusCode` reads reflect it.
602            set_res_prop(res, "statusCode", Value::Float(status as f64));
603            Ok(res.clone())
604        }
605        "setHeader" => {
606            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
607            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
608            RESPONSES.with(|r| {
609                if let Some(st) = r.borrow_mut().get_mut(&resid) {
610                    upsert_header(&mut st.headers, &k, v);
611                }
612            });
613            Ok(Value::Undef)
614        }
615        "getHeader" => {
616            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
617                .to_ascii_lowercase();
618            let val = RESPONSES.with(|r| {
619                r.borrow().get(&resid).and_then(|st| {
620                    st.headers
621                        .iter()
622                        .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
623                        .map(|(_, v)| v.clone())
624                })
625            });
626            Ok(val
627                .map(|v| with_host(|h| h.new_str(v)))
628                .unwrap_or(Value::Undef))
629        }
630        "removeHeader" => {
631            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
632            RESPONSES.with(|r| {
633                if let Some(st) = r.borrow_mut().get_mut(&resid) {
634                    st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
635                }
636            });
637            Ok(Value::Undef)
638        }
639        "write" => {
640            let bytes = value_bytes(args.first());
641            RESPONSES.with(|r| {
642                if let Some(st) = r.borrow_mut().get_mut(&resid) {
643                    st.body.extend_from_slice(&bytes);
644                }
645            });
646            Ok(Value::Bool(true))
647        }
648        "end" => {
649            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
650                let bytes = value_bytes(Some(chunk));
651                RESPONSES.with(|r| {
652                    if let Some(st) = r.borrow_mut().get_mut(&resid) {
653                        st.body.extend_from_slice(&bytes);
654                    }
655                });
656            }
657            finish_response(res, resid)?;
658            Ok(res.clone())
659        }
660        _ => Err(crate::host::type_error(&format!(
661            "res.{method} is not a function"
662        ))),
663    }
664}
665
666/// Serialize and write the response, then emit `finish`.
667fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
668    // Reconcile status with a possible `res.statusCode = n` assignment.
669    let js_status = with_host(|h| match h.get(res) {
670        Some(JsObj::Object(p)) => p.get("statusCode").map(|v| h.to_number(v) as u16),
671        _ => None,
672    });
673    let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
674    let Some(mut st) = st else { return Ok(()) };
675    if let Some(s) = js_status {
676        st.status = s;
677    }
678    let payload = serialize_response(&mut st);
679    super::net::socket_write_id(st.sock_id, &payload);
680    super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
681    Ok(())
682}
683
684/// Build the raw HTTP/1.1 response bytes: status line, headers (adding
685/// `Content-Length` and `Connection: keep-alive` if the user did not set framing
686/// headers), CRLFCRLF, then the body.
687fn serialize_response(st: &mut ResState) -> Vec<u8> {
688    let reason = st
689        .message
690        .clone()
691        .unwrap_or_else(|| status_text(st.status).to_string());
692    let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
693
694    let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
695    let chunked = st.headers.iter().any(|(k, v)| {
696        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
697    });
698
699    for (k, v) in &st.headers {
700        out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
701    }
702    if !chunked && !has("content-length") {
703        out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
704    }
705    if !has("connection") {
706        out.extend_from_slice(b"Connection: keep-alive\r\n");
707    }
708    out.extend_from_slice(b"\r\n");
709    out.extend_from_slice(&st.body);
710    out
711}
712
713// ── helpers ──────────────────────────────────────────────────────────────────
714
715/// Insert or replace a header (case-insensitive name match), preserving order.
716fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
717    if let Some(slot) = headers
718        .iter_mut()
719        .find(|(k, _)| k.eq_ignore_ascii_case(name))
720    {
721        slot.1 = value;
722    } else {
723        headers.push((name.to_string(), value));
724    }
725}
726
727/// Enumerable string key/value pairs of a plain object (for the `writeHead`
728/// headers argument).
729fn object_pairs(obj: &Value) -> Vec<(String, String)> {
730    with_host(|h| match h.get(obj) {
731        Some(JsObj::Object(p)) => p
732            .iter()
733            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
734            .map(|(k, v)| (k.clone(), h.str_of(v)))
735            .collect(),
736        _ => Vec::new(),
737    })
738}
739
740/// Set a JS-visible prop on the response object.
741fn set_res_prop(res: &Value, key: &str, val: Value) {
742    with_host(|h| {
743        if let Some(JsObj::Object(p)) = h.get_mut(res) {
744            p.insert(key.to_string(), val);
745        }
746    });
747}
748
749/// Raw bytes of a `write`/`end` argument: a Buffer's bytes, or a string's UTF-8.
750fn value_bytes(v: Option<&Value>) -> Vec<u8> {
751    let Some(v) = v else { return Vec::new() };
752    let is_buffer =
753        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
754    if is_buffer {
755        return with_host(|h| match h.get(v) {
756            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
757                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
758                _ => Vec::new(),
759            },
760            _ => Vec::new(),
761        });
762    }
763    with_host(|h| h.str_of(v)).into_bytes()
764}
765
766// ── client: http.request / http.get ──────────────────────────────────────────
767//
768// Mirrors the `https` client (https.rs), but over a plain `TcpStream` on port 80.
769// `.end()` (or `http.get` immediately) spawns a background thread that connects,
770// writes the request, reads the full response to EOF (the request forces
771// `Connection: close`), parses it, and posts an `IoTask` that emits `response`
772// with an `IncomingMessage` (then its body `data`/`end`) on the main thread.
773
774/// State of an in-flight client request until `.end()` dispatches it.
775struct ClientReq {
776    host: String,
777    port: u16,
778    method: String,
779    path: String,
780    headers: Vec<(String, String)>,
781    body: Vec<u8>,
782    /// The `req` object (a `ClientRequest` emitter) for `response`/`error` events.
783    request: Value,
784    sent: bool,
785}
786
787thread_local! {
788    static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
789        std::cell::RefCell::new(HashMap::new());
790    static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
791}
792
793fn next_reqid() -> u64 {
794    NEXT_REQID.with(|c| {
795        let id = c.get();
796        c.set(id + 1);
797        id
798    })
799}
800
801fn get_prop(recv: &Value, key: &str) -> Option<Value> {
802    with_host(|h| match h.get(recv) {
803        Some(JsObj::Object(p)) => p.get(key).cloned(),
804        _ => None,
805    })
806}
807
808fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
809    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
810}
811
812/// `http.request(options|url[, cb])` / `http.get(url|options[, cb])`. Returns a
813/// `ClientRequest` (a `ClientRequest` emitter). `get` auto-sends via `.end()`.
814pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
815    let mut host = "localhost".to_string();
816    let mut port: u16 = 80;
817    let mut path = "/".to_string();
818    let mut method = "GET".to_string();
819    let mut headers: Vec<(String, String)> = Vec::new();
820    let mut cb: Option<Value> = None;
821
822    for a in args {
823        if with_host(|h| crate::host::is_callable(h, a)) {
824            cb = Some(a.clone());
825        } else if with_host(|h| h.as_str(a)).is_some() {
826            let url = with_host(|h| h.str_of(a));
827            parse_url(&url, &mut host, &mut port, &mut path);
828        } else if matches!(a, Value::Obj(_)) {
829            for key in ["hostname", "host"] {
830                if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
831                    host = with_host(|h| h.str_of(&v));
832                }
833            }
834            if let Some(v) = get_prop(a, "port") {
835                let n = with_host(|h| h.to_number(&v));
836                if !n.is_nan() {
837                    port = n as u16;
838                }
839            }
840            if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
841                path = with_host(|h| h.str_of(&v));
842            }
843            if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
844            {
845                method = with_host(|h| h.str_of(&v));
846            }
847            if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
848                for (k, val) in object_pairs(&hv) {
849                    headers.push((k, val));
850                }
851            }
852        }
853    }
854    if is_get {
855        method = "GET".to_string();
856    }
857
858    let reqid = next_reqid();
859    let mut extra = IndexMap::new();
860    extra.insert("@@reqid".into(), Value::Float(reqid as f64));
861    extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
862    extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
863    let request = super::net::new_emitter_object("ClientRequest", extra);
864    // The `cb` is registered as the `response` listener (Node semantics).
865    if let Some(cb) = cb {
866        super::events::instance_call(
867            &request,
868            "on",
869            vec![with_host(|h| h.new_str("response")), cb],
870        )?;
871    }
872    CLIENT_REQS.with(|c| {
873        c.borrow_mut().insert(
874            reqid,
875            ClientReq {
876                host,
877                port,
878                method,
879                path,
880                headers,
881                body: Vec::new(),
882                request: request.clone(),
883                sent: false,
884            },
885        );
886    });
887    if is_get {
888        dispatch_request(reqid)?;
889    }
890    Ok(request)
891}
892
893/// Parse an `http://host[:port][/path]` URL into its parts (defaults port 80).
894fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
895    let rest = url.strip_prefix("http://").unwrap_or(url);
896    let (authority, p) = match rest.find('/') {
897        Some(i) => (&rest[..i], &rest[i..]),
898        None => (rest, "/"),
899    };
900    *path = if p.is_empty() {
901        "/".to_string()
902    } else {
903        p.to_string()
904    };
905    if let Some((h, port_str)) = authority.rsplit_once(':') {
906        *host = h.to_string();
907        if let Ok(n) = port_str.parse::<u16>() {
908            *port = n;
909        }
910    } else {
911        *host = authority.to_string();
912        *port = 80;
913    }
914}
915
916fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
917    let reqid = u64_prop(req, "@@reqid");
918    match method {
919        "write" => {
920            if let Some(id) = reqid {
921                let bytes = value_bytes(args.first());
922                CLIENT_REQS.with(|c| {
923                    if let Some(r) = c.borrow_mut().get_mut(&id) {
924                        r.body.extend_from_slice(&bytes);
925                    }
926                });
927            }
928            Ok(Value::Bool(true))
929        }
930        "end" => {
931            if let Some(id) = reqid {
932                if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
933                    let bytes = value_bytes(Some(chunk));
934                    CLIENT_REQS.with(|c| {
935                        if let Some(r) = c.borrow_mut().get_mut(&id) {
936                            r.body.extend_from_slice(&bytes);
937                        }
938                    });
939                }
940                dispatch_request(id)?;
941            }
942            Ok(req.clone())
943        }
944        "setHeader" => {
945            if let Some(id) = reqid {
946                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
947                let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
948                CLIENT_REQS.with(|c| {
949                    if let Some(r) = c.borrow_mut().get_mut(&id) {
950                        upsert_header(&mut r.headers, &k, v);
951                    }
952                });
953            }
954            Ok(Value::Undef)
955        }
956        "getHeader" => {
957            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
958                .to_ascii_lowercase();
959            let val = reqid.and_then(|id| {
960                CLIENT_REQS.with(|c| {
961                    c.borrow().get(&id).and_then(|r| {
962                        r.headers
963                            .iter()
964                            .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
965                            .map(|(_, v)| v.clone())
966                    })
967                })
968            });
969            Ok(val
970                .map(|v| with_host(|h| h.new_str(v)))
971                .unwrap_or(Value::Undef))
972        }
973        "removeHeader" => {
974            if let Some(id) = reqid {
975                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
976                CLIENT_REQS.with(|c| {
977                    if let Some(r) = c.borrow_mut().get_mut(&id) {
978                        r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
979                    }
980                });
981            }
982            Ok(Value::Undef)
983        }
984        "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
985        _ => Err(crate::host::type_error(&format!(
986            "req.{method} is not a function"
987        ))),
988    }
989}
990
991/// Spawn the blocking HTTP exchange for a client request on a background thread;
992/// the parsed response is posted back to the main thread.
993fn dispatch_request(reqid: u64) -> Result<(), String> {
994    let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
995    if sent {
996        return Ok(());
997    }
998    CLIENT_REQS.with(|c| {
999        if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1000            r.sent = true;
1001        }
1002    });
1003
1004    let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1005        let b = c.borrow();
1006        let r = b.get(&reqid).unwrap();
1007        (
1008            r.host.clone(),
1009            r.port,
1010            r.method.clone(),
1011            r.path.clone(),
1012            r.headers.clone(),
1013            r.body.clone(),
1014        )
1015    });
1016
1017    let io_tx = with_host(|h| h.io_sender());
1018    with_host(|h| h.incr_handle());
1019
1020    // Build the request bytes (force `Connection: close` for read-to-EOF).
1021    let mut has_host = false;
1022    let mut has_len = false;
1023    let mut header_block = String::new();
1024    for (k, v) in &headers {
1025        if k.eq_ignore_ascii_case("host") {
1026            has_host = true;
1027        }
1028        if k.eq_ignore_ascii_case("content-length") {
1029            has_len = true;
1030        }
1031        if k.eq_ignore_ascii_case("connection") {
1032            continue;
1033        }
1034        header_block.push_str(&format!("{k}: {v}\r\n"));
1035    }
1036    let host_header = if port == 80 {
1037        host.clone()
1038    } else {
1039        format!("{host}:{port}")
1040    };
1041    let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1042    if !has_host {
1043        request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1044    }
1045    request_bytes.push_str(&header_block);
1046    if !has_len && !body.is_empty() {
1047        request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1048    }
1049    request_bytes.push_str("Connection: close\r\n\r\n");
1050    let mut wire = request_bytes.into_bytes();
1051    wire.extend_from_slice(&body);
1052
1053    std::thread::spawn(move || match do_client_exchange(&host, port, &wire) {
1054        Ok(raw) => {
1055            let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1056        }
1057        Err(msg) => {
1058            let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1059        }
1060    });
1061    Ok(())
1062}
1063
1064/// The blocking TCP round-trip: connect, write the request, read to EOF.
1065fn do_client_exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1066    let mut stream = TcpStream::connect((host, port))
1067        .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1068    stream
1069        .write_all(request)
1070        .map_err(|e| format!("Error: http write: {e}"))?;
1071    stream
1072        .flush()
1073        .map_err(|e| format!("Error: http flush: {e}"))?;
1074    let mut raw = Vec::new();
1075    let mut buf = [0u8; 16384];
1076    loop {
1077        match stream.read(&mut buf) {
1078            Ok(0) => break,
1079            Ok(n) => raw.extend_from_slice(&buf[..n]),
1080            Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1081            Err(e) => {
1082                if raw.is_empty() {
1083                    return Err(format!("Error: http read: {e}"));
1084                }
1085                break;
1086            }
1087        }
1088    }
1089    Ok(raw)
1090}
1091
1092/// Parse the raw response and emit `response` (with an `IncomingMessage`), then
1093/// the body `data`/`end`. Runs on the main thread.
1094fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1095    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1096    with_host(|h| h.decr_handle());
1097    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1098    let Some(entry) = entry else { return Ok(()) };
1099
1100    let (status, message, http_version, headers, body) = parse_response(&raw);
1101
1102    let headers_obj = with_host(|h| {
1103        let mut m = IndexMap::new();
1104        for (k, v) in &headers {
1105            m.insert(k.clone(), h.new_str(v.clone()));
1106        }
1107        h.new_object(m)
1108    });
1109    let mut extra = IndexMap::new();
1110    extra.insert("statusCode".into(), Value::Float(status as f64));
1111    extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1112    extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1113    extra.insert("headers".into(), headers_obj);
1114    let res = super::net::new_emitter_object("IncomingMessage", extra);
1115
1116    super::events::instance_call(
1117        &entry.request,
1118        "emit",
1119        vec![with_host(|h| h.new_str("response")), res.clone()],
1120    )?;
1121    if !body.is_empty() {
1122        let chunk = super::buffer::from_bytes(&body);
1123        super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1124    }
1125    super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1126    Ok(())
1127}
1128
1129/// Emit `error` on the request when the exchange fails. Runs on the main thread.
1130fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1131    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1132    with_host(|h| h.decr_handle());
1133    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1134    if let Some(entry) = entry {
1135        let err = with_host(|h| {
1136            let mut m = IndexMap::new();
1137            m.insert("message".into(), h.new_str(msg.clone()));
1138            h.new_object(m)
1139        });
1140        super::events::instance_call(
1141            &entry.request,
1142            "emit",
1143            vec![with_host(|h| h.new_str("error")), err],
1144        )?;
1145    }
1146    Ok(())
1147}
1148
1149/// Parse a raw HTTP/1.1 response into `(status, message, version, headers, body)`.
1150/// Handles `Transfer-Encoding: chunked` and plain (Content-Length / to-EOF) bodies.
1151fn parse_response(raw: &[u8]) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1152    let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1153    let head = String::from_utf8_lossy(&raw[..head_end]);
1154    let body_start = (head_end + 4).min(raw.len());
1155    let mut lines = head.split("\r\n");
1156    let status_line = lines.next().unwrap_or("");
1157    let mut sp = status_line.splitn(3, ' ');
1158    let version = sp
1159        .next()
1160        .unwrap_or("HTTP/1.1")
1161        .strip_prefix("HTTP/")
1162        .unwrap_or("1.1")
1163        .to_string();
1164    let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1165    let message = sp.next().unwrap_or("").to_string();
1166
1167    let mut headers: Vec<(String, String)> = Vec::new();
1168    let mut chunked = false;
1169    for line in lines {
1170        if line.is_empty() {
1171            continue;
1172        }
1173        if let Some((k, v)) = line.split_once(':') {
1174            let name = k.trim().to_ascii_lowercase();
1175            let value = v.trim().to_string();
1176            if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1177                chunked = true;
1178            }
1179            headers.push((name, value));
1180        }
1181    }
1182    let raw_body = &raw[body_start..];
1183    let body = if chunked {
1184        decode_chunked(raw_body)
1185    } else {
1186        raw_body.to_vec()
1187    };
1188    (status, message, version, headers, body)
1189}
1190
1191/// Decode an HTTP/1.1 chunked body (best-effort; stops at the terminating
1192/// 0-chunk or when the input is exhausted).
1193fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1194    let mut out = Vec::new();
1195    while let Some(nl) = find_subslice(data, b"\r\n") {
1196        let size_line = String::from_utf8_lossy(&data[..nl]);
1197        let size_hex = size_line.split(';').next().unwrap_or("").trim();
1198        let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1199        if size == 0 {
1200            break;
1201        }
1202        let chunk_start = nl + 2;
1203        let chunk_end = (chunk_start + size).min(data.len());
1204        out.extend_from_slice(&data[chunk_start..chunk_end]);
1205        let next = chunk_end + 2;
1206        if next >= data.len() {
1207            break;
1208        }
1209        data = &data[next..];
1210    }
1211    out
1212}
1213
1214/// Standard reason phrase for common status codes (default `OK`).
1215fn status_text(code: u16) -> &'static str {
1216    match code {
1217        200 => "OK",
1218        201 => "Created",
1219        202 => "Accepted",
1220        204 => "No Content",
1221        301 => "Moved Permanently",
1222        302 => "Found",
1223        304 => "Not Modified",
1224        400 => "Bad Request",
1225        401 => "Unauthorized",
1226        403 => "Forbidden",
1227        404 => "Not Found",
1228        405 => "Method Not Allowed",
1229        409 => "Conflict",
1230        500 => "Internal Server Error",
1231        502 => "Bad Gateway",
1232        503 => "Service Unavailable",
1233        _ => "OK",
1234    }
1235}