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::coded_error(
243            "TypeError",
244            "ERR_INVALID_HTTP_TOKEN",
245            &format!("Header name must be a valid HTTP token [\"{name}\"]"),
246        ))
247    }
248}
249
250/// `http.validateHeaderValue(name, value)` — throws if `value` is `undefined`
251/// (`ERR_HTTP_INVALID_HEADER_VALUE`) or contains an invalid character
252/// (`ERR_INVALID_CHAR`), else returns `undefined`.
253fn validate_header_value(args: &[Value]) -> Result<Value, String> {
254    let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
255    let raw = args.get(1).cloned().unwrap_or(Value::Undef);
256    if matches!(raw, Value::Undef) {
257        return Err(crate::host::coded_error(
258            "TypeError",
259            "ERR_HTTP_INVALID_HEADER_VALUE",
260            &format!("Invalid value \"undefined\" for header \"{name}\""),
261        ));
262    }
263    let value = with_host(|h| h.str_of(&raw));
264    if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
265        return Err(crate::host::coded_error(
266            "TypeError",
267            "ERR_INVALID_CHAR",
268            &format!("Invalid character in header content [\"{name}\"]"),
269        ));
270    }
271    Ok(Value::Undef)
272}
273
274/// RFC 7230 field-name token: one or more of the `tchar` set.
275fn is_valid_token(s: &str) -> bool {
276    !s.is_empty()
277        && s.bytes().all(|b| {
278            b.is_ascii_alphanumeric()
279                || matches!(
280                    b,
281                    b'!' | b'#'
282                        | b'$'
283                        | b'%'
284                        | b'&'
285                        | b'\''
286                        | b'*'
287                        | b'+'
288                        | b'-'
289                        | b'.'
290                        | b'^'
291                        | b'_'
292                        | b'`'
293                        | b'|'
294                        | b'~'
295                )
296        })
297}
298
299/// `new http.Agent([options])` — a minimal connection-pool holder. We open a
300/// fresh connection per request (no reuse), so the agent only carries its
301/// configuration for the JS side to read back.
302pub fn construct_agent(args: &[Value]) -> Value {
303    let pairs = args
304        .first()
305        .filter(|v| matches!(v, Value::Obj(_)))
306        .map(object_pairs)
307        .unwrap_or_default();
308    with_host(|h| {
309        let mut m = IndexMap::new();
310        m.insert("@@native".into(), h.new_str("Agent"));
311        m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
312        m.insert("maxFreeSockets".into(), Value::Float(256.0));
313        m.insert("sockets".into(), h.new_object(IndexMap::new()));
314        m.insert("requests".into(), h.new_object(IndexMap::new()));
315        for (k, v) in pairs {
316            m.insert(k, h.new_str(v));
317        }
318        h.new_object(m)
319    })
320}
321
322/// Build an HTTP server: a `net.Server` whose per-connection hook wires up the
323/// request parser and remembers the `requestListener`.
324pub fn create_server(request_listener: Option<Value>) -> Value {
325    let server = super::net::create_server(None);
326    let listener = request_listener.unwrap_or(Value::Undef);
327    let hook = std::rc::Rc::new(
328        move |_server: &Value, socket: &Value| -> Result<(), String> {
329            let sock_id = socket_id_of(socket);
330            CONNS.with(|c| {
331                c.borrow_mut().insert(
332                    sock_id,
333                    HttpConn {
334                        socket: socket.clone(),
335                        listener: listener.clone(),
336                        buf: Vec::new(),
337                    },
338                );
339            });
340            Ok(())
341        },
342    );
343    super::net::set_conn_hook(&server, hook);
344    server
345}
346
347fn socket_id_of(socket: &Value) -> u64 {
348    with_host(|h| match h.get(socket) {
349        Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
350        _ => 0,
351    })
352}
353
354/// Discard an HTTP connection when its socket closes.
355pub fn drop_conn(sock_id: u64) {
356    CONNS.with(|c| {
357        c.borrow_mut().remove(&sock_id);
358    });
359}
360
361// ── request parsing ──────────────────────────────────────────────────────────
362
363/// Feed freshly received socket bytes into the HTTP parser. No-op for a socket
364/// that is not an HTTP connection (plain `net`). Runs on the main thread.
365pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
366    let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
367    if !is_http {
368        return Ok(());
369    }
370    CONNS.with(|c| {
371        c.borrow_mut()
372            .get_mut(&sock_id)
373            .unwrap()
374            .buf
375            .extend_from_slice(bytes)
376    });
377
378    // Parse and dispatch every complete request currently buffered (pipelining).
379    loop {
380        let (listener, socket, parsed) = CONNS.with(|c| {
381            let mut c = c.borrow_mut();
382            let conn = c.get_mut(&sock_id).unwrap();
383            match parse_request(&conn.buf) {
384                Some((req, consumed)) => {
385                    conn.buf.drain(..consumed);
386                    (conn.listener.clone(), conn.socket.clone(), Some(req))
387                }
388                None => (Value::Undef, Value::Undef, None),
389            }
390        });
391        let Some(parsed) = parsed else { break };
392
393        let req = build_incoming(&parsed);
394        let res = build_response(sock_id);
395        if with_host(|h| crate::host::is_callable(h, &listener)) {
396            invoke(&listener, vec![req.clone(), res], None)?;
397        }
398        // Body streaming: emit any request body then `end` for downstream readers.
399        // A listener that called `req.setEncoding(enc)` gets a decoded string
400        // chunk instead of a Buffer, matching Node's Readable.
401        if !parsed.body.is_empty() {
402            let encoding = with_host(|h| match h.get(&req) {
403                Some(JsObj::Object(p)) => p.get("@@encoding").map(|v| h.str_of(v)),
404                _ => None,
405            });
406            let chunk = match encoding {
407                Some(enc) => with_host(|h| {
408                    let s = super::buffer::encode_bytes(&parsed.body, &enc);
409                    h.new_str(s)
410                }),
411                None => super::buffer::from_bytes(&parsed.body),
412            };
413            super::events::instance_call(
414                &req,
415                "emit",
416                vec![with_host(|h| h.new_str("data")), chunk],
417            )?;
418        }
419        super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
420        let _ = socket; // (kept for future keep-alive bookkeeping)
421    }
422    Ok(())
423}
424
425/// A fully parsed HTTP request.
426struct ParsedReq {
427    method: String,
428    url: String,
429    http_version: String,
430    /// Lowercased header name → value (last wins, like Node for simple headers).
431    headers: Vec<(String, String)>,
432    body: Vec<u8>,
433}
434
435/// Try to parse one complete request from `buf`. Returns the request and the
436/// number of bytes consumed, or `None` if more bytes are needed.
437fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
438    // Header block ends at the first CRLFCRLF.
439    let head_end = find_subslice(buf, b"\r\n\r\n")?;
440    let head = &buf[..head_end];
441    let body_start = head_end + 4;
442
443    let head_str = String::from_utf8_lossy(head);
444    let mut lines = head_str.split("\r\n");
445    let request_line = lines.next()?;
446    let mut parts = request_line.split(' ');
447    let method = parts.next()?.to_string();
448    let url = parts.next()?.to_string();
449    let version = parts.next().unwrap_or("HTTP/1.1");
450    let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
451
452    let mut headers: Vec<(String, String)> = Vec::new();
453    let mut content_length = 0usize;
454    for line in lines {
455        if line.is_empty() {
456            continue;
457        }
458        if let Some((k, v)) = line.split_once(':') {
459            let name = k.trim().to_ascii_lowercase();
460            let value = v.trim().to_string();
461            if name == "content-length" {
462                content_length = value.parse().unwrap_or(0);
463            }
464            headers.push((name, value));
465        }
466    }
467
468    // Need the full body before dispatching.
469    if buf.len() < body_start + content_length {
470        return None;
471    }
472    let body = buf[body_start..body_start + content_length].to_vec();
473    Some((
474        ParsedReq {
475            method,
476            url,
477            http_version,
478            headers,
479            body,
480        },
481        body_start + content_length,
482    ))
483}
484
485fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
486    haystack.windows(needle.len()).position(|w| w == needle)
487}
488
489// ── IncomingMessage (req) ────────────────────────────────────────────────────
490
491fn build_incoming(req: &ParsedReq) -> Value {
492    let headers_obj = with_host(|h| {
493        let mut m = IndexMap::new();
494        for (k, v) in &req.headers {
495            m.insert(k.clone(), h.new_str(v.clone()));
496        }
497        h.new_object(m)
498    });
499    let mut extra = IndexMap::new();
500    extra.insert(
501        "method".into(),
502        with_host(|h| h.new_str(req.method.clone())),
503    );
504    extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
505    extra.insert(
506        "httpVersion".into(),
507        with_host(|h| h.new_str(req.http_version.clone())),
508    );
509    extra.insert("headers".into(), headers_obj);
510    super::net::new_emitter_object("IncomingMessage", extra)
511}
512
513/// The Readable surface of `req`. node-js reads the whole request body off the
514/// socket BEFORE invoking the listener and then delivers it as a single `data`
515/// emit followed by `end` — so there is no partially-consumed stream to pause,
516/// resume or unpipe, and these really are complete rather than stubbed.
517/// `setEncoding` is the exception: it changes what the `data` chunk IS, so it
518/// records the encoding and `emit_body` decodes with it.
519fn incoming_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
520    match method {
521        // Nothing is piped anywhere (node-js has no `pipe` on `req`), and the
522        // body is already buffered, so these are no-ops that return `this` —
523        // which is exactly Node's contract for an already-drained readable.
524        "pause" | "resume" | "unpipe" | "destroy" => Ok(recv.clone()),
525        "isPaused" => Ok(Value::Bool(false)),
526        // Subsequent `data` chunks arrive as strings in this encoding.
527        "setEncoding" => {
528            let enc = super::arg_str(&args, 0);
529            with_host(|h| {
530                let v = h.new_str(enc);
531                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
532                    p.insert("@@encoding".into(), v);
533                }
534            });
535            Ok(recv.clone())
536        }
537        _ => Err(crate::host::type_error(&format!(
538            "req.{method} is not a function"
539        ))),
540    }
541}
542
543// ── ServerResponse (res) ─────────────────────────────────────────────────────
544
545fn build_response(sock_id: u64) -> Value {
546    let resid = next_resid();
547    RESPONSES.with(|r| {
548        r.borrow_mut().insert(
549            resid,
550            ResState {
551                sock_id,
552                status: 200,
553                message: None,
554                headers: Vec::new(),
555                body: Vec::new(),
556            },
557        );
558    });
559    let mut extra = IndexMap::new();
560    extra.insert("@@resid".into(), Value::Float(resid as f64));
561    extra.insert("statusCode".into(), Value::Float(200.0));
562    super::net::new_emitter_object("ServerResponse", extra)
563}
564
565fn resid_of(res: &Value) -> Option<u64> {
566    with_host(|h| match h.get(res) {
567        Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
568        _ => None,
569    })
570}
571
572/// Instance dispatch for `IncomingMessage`/`ServerResponse` (EventEmitter methods
573/// handled by `net`'s shared emitter delegation via `stdlib::instance_call`).
574pub fn instance_call(
575    tag: &str,
576    recv: &Value,
577    method: &str,
578    args: Vec<Value>,
579) -> Result<Value, String> {
580    // EventEmitter surface is shared with `net` sockets, and the set of names is
581    // read from `events::METHODS` rather than re-listed: the copy that used to
582    // live here was missing `listeners`, `setMaxListeners` and `getMaxListeners`,
583    // so `unpipe` — reached from `body-parser` on every `express.json()` request
584    // — died on `req.listeners('data') is not a function`.
585    if super::events::METHODS.contains(&method) {
586        return super::events::instance_call(recv, method, args);
587    }
588    match tag {
589        "IncomingMessage" => incoming_call(recv, method, args),
590        "ServerResponse" => response_call(recv, method, args),
591        "ClientRequest" => client_request_call(recv, method, args),
592        // Minimal Agent: no live sockets to tear down.
593        "Agent" => match method {
594            "destroy" => Ok(Value::Undef),
595            "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
596            _ => Err(crate::host::type_error(&format!(
597                "agent.{method} is not a function"
598            ))),
599        },
600        _ => Err(crate::host::type_error(&format!(
601            "{method} is not a function"
602        ))),
603    }
604}
605
606fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
607    let Some(resid) = resid_of(res) else {
608        return Err(crate::host::type_error("invalid ServerResponse"));
609    };
610    match method {
611        "writeHead" => {
612            let status =
613                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
614            // Second arg is either the status message (string) or the headers obj.
615            let mut message: Option<String> = None;
616            let mut headers_arg: Option<Value> = None;
617            if let Some(a) = args.get(1) {
618                if with_host(|h| h.as_str(a)).is_some() {
619                    message = Some(with_host(|h| h.str_of(a)));
620                } else if !matches!(a, Value::Undef) {
621                    headers_arg = Some(a.clone());
622                }
623            }
624            if let Some(a) = args.get(2) {
625                if !matches!(a, Value::Undef) {
626                    headers_arg = Some(a.clone());
627                }
628            }
629            let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
630            RESPONSES.with(|r| {
631                if let Some(st) = r.borrow_mut().get_mut(&resid) {
632                    st.status = status;
633                    st.message = message;
634                    for (k, v) in header_pairs {
635                        upsert_header(&mut st.headers, &k, v);
636                    }
637                }
638            });
639            // Mirror onto the JS prop so `res.statusCode` reads reflect it.
640            set_res_prop(res, "statusCode", Value::Float(status as f64));
641            Ok(res.clone())
642        }
643        "setHeader" => {
644            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
645            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
646            RESPONSES.with(|r| {
647                if let Some(st) = r.borrow_mut().get_mut(&resid) {
648                    upsert_header(&mut st.headers, &k, v);
649                }
650            });
651            Ok(Value::Undef)
652        }
653        "getHeader" => {
654            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
655                .to_ascii_lowercase();
656            let val = RESPONSES.with(|r| {
657                r.borrow().get(&resid).and_then(|st| {
658                    st.headers
659                        .iter()
660                        .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
661                        .map(|(_, v)| v.clone())
662                })
663            });
664            Ok(val
665                .map(|v| with_host(|h| h.new_str(v)))
666                .unwrap_or(Value::Undef))
667        }
668        "removeHeader" => {
669            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
670            RESPONSES.with(|r| {
671                if let Some(st) = r.borrow_mut().get_mut(&resid) {
672                    st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
673                }
674            });
675            Ok(Value::Undef)
676        }
677        "write" => {
678            let bytes = value_bytes(args.first());
679            RESPONSES.with(|r| {
680                if let Some(st) = r.borrow_mut().get_mut(&resid) {
681                    st.body.extend_from_slice(&bytes);
682                }
683            });
684            Ok(Value::Bool(true))
685        }
686        "end" => {
687            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
688                let bytes = value_bytes(Some(chunk));
689                RESPONSES.with(|r| {
690                    if let Some(st) = r.borrow_mut().get_mut(&resid) {
691                        st.body.extend_from_slice(&bytes);
692                    }
693                });
694            }
695            finish_response(res, resid)?;
696            Ok(res.clone())
697        }
698        _ => Err(crate::host::type_error(&format!(
699            "res.{method} is not a function"
700        ))),
701    }
702}
703
704/// Serialize and write the response, then emit `finish`.
705fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
706    // Reconcile status with a possible `res.statusCode = n` assignment.
707    let js_status = with_host(|h| match h.get(res) {
708        Some(JsObj::Object(p)) => p.get("statusCode").map(|v| h.to_number(v) as u16),
709        _ => None,
710    });
711    let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
712    let Some(mut st) = st else { return Ok(()) };
713    if let Some(s) = js_status {
714        st.status = s;
715    }
716    let payload = serialize_response(&mut st);
717    super::net::socket_write_id(st.sock_id, &payload);
718    super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
719    Ok(())
720}
721
722/// Build the raw HTTP/1.1 response bytes: status line, headers (adding
723/// `Content-Length` and `Connection: keep-alive` if the user did not set framing
724/// headers), CRLFCRLF, then the body.
725fn serialize_response(st: &mut ResState) -> Vec<u8> {
726    let reason = st
727        .message
728        .clone()
729        .unwrap_or_else(|| status_text(st.status).to_string());
730    let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
731
732    let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
733    let chunked = st.headers.iter().any(|(k, v)| {
734        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
735    });
736
737    for (k, v) in &st.headers {
738        out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
739    }
740    if !chunked && !has("content-length") {
741        out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
742    }
743    if !has("connection") {
744        out.extend_from_slice(b"Connection: keep-alive\r\n");
745    }
746    out.extend_from_slice(b"\r\n");
747    out.extend_from_slice(&st.body);
748    out
749}
750
751// ── helpers ──────────────────────────────────────────────────────────────────
752
753/// Insert or replace a header (case-insensitive name match), preserving order.
754fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
755    if let Some(slot) = headers
756        .iter_mut()
757        .find(|(k, _)| k.eq_ignore_ascii_case(name))
758    {
759        slot.1 = value;
760    } else {
761        headers.push((name.to_string(), value));
762    }
763}
764
765/// Enumerable string key/value pairs of a plain object (for the `writeHead`
766/// headers argument).
767fn object_pairs(obj: &Value) -> Vec<(String, String)> {
768    with_host(|h| match h.get(obj) {
769        Some(JsObj::Object(p)) => p
770            .iter()
771            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
772            .map(|(k, v)| (k.clone(), h.str_of(v)))
773            .collect(),
774        _ => Vec::new(),
775    })
776}
777
778/// Set a JS-visible prop on the response object.
779fn set_res_prop(res: &Value, key: &str, val: Value) {
780    with_host(|h| {
781        if let Some(JsObj::Object(p)) = h.get_mut(res) {
782            p.insert(key.to_string(), val);
783        }
784    });
785}
786
787/// Raw bytes of a `write`/`end` argument: a Buffer's bytes, or a string's UTF-8.
788fn value_bytes(v: Option<&Value>) -> Vec<u8> {
789    let Some(v) = v else { return Vec::new() };
790    let is_buffer =
791        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
792    if is_buffer {
793        return with_host(|h| match h.get(v) {
794            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
795                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
796                _ => Vec::new(),
797            },
798            _ => Vec::new(),
799        });
800    }
801    with_host(|h| h.str_of(v)).into_bytes()
802}
803
804// ── client: http.request / http.get ──────────────────────────────────────────
805//
806// Mirrors the `https` client (https.rs), but over a plain `TcpStream` on port 80.
807// `.end()` (or `http.get` immediately) spawns a background thread that connects,
808// writes the request, reads the full response to EOF (the request forces
809// `Connection: close`), parses it, and posts an `IoTask` that emits `response`
810// with an `IncomingMessage` (then its body `data`/`end`) on the main thread.
811
812/// State of an in-flight client request until `.end()` dispatches it.
813struct ClientReq {
814    host: String,
815    port: u16,
816    method: String,
817    path: String,
818    headers: Vec<(String, String)>,
819    body: Vec<u8>,
820    /// The `req` object (a `ClientRequest` emitter) for `response`/`error` events.
821    request: Value,
822    sent: bool,
823}
824
825thread_local! {
826    static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
827        std::cell::RefCell::new(HashMap::new());
828    static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
829}
830
831fn next_reqid() -> u64 {
832    NEXT_REQID.with(|c| {
833        let id = c.get();
834        c.set(id + 1);
835        id
836    })
837}
838
839fn get_prop(recv: &Value, key: &str) -> Option<Value> {
840    with_host(|h| match h.get(recv) {
841        Some(JsObj::Object(p)) => p.get(key).cloned(),
842        _ => None,
843    })
844}
845
846fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
847    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
848}
849
850/// `http.request(options|url[, cb])` / `http.get(url|options[, cb])`. Returns a
851/// `ClientRequest` (a `ClientRequest` emitter). `get` auto-sends via `.end()`.
852pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
853    let mut host = "localhost".to_string();
854    let mut port: u16 = 80;
855    let mut path = "/".to_string();
856    let mut method = "GET".to_string();
857    let mut headers: Vec<(String, String)> = Vec::new();
858    let mut cb: Option<Value> = None;
859
860    for a in args {
861        if with_host(|h| crate::host::is_callable(h, a)) {
862            cb = Some(a.clone());
863        } else if with_host(|h| h.as_str(a)).is_some() {
864            let url = with_host(|h| h.str_of(a));
865            parse_url(&url, &mut host, &mut port, &mut path);
866        } else if matches!(a, Value::Obj(_)) {
867            for key in ["hostname", "host"] {
868                if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
869                    host = with_host(|h| h.str_of(&v));
870                }
871            }
872            if let Some(v) = get_prop(a, "port") {
873                let n = with_host(|h| h.to_number(&v));
874                if !n.is_nan() {
875                    port = n as u16;
876                }
877            }
878            if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
879                path = with_host(|h| h.str_of(&v));
880            }
881            if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
882            {
883                method = with_host(|h| h.str_of(&v));
884            }
885            if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
886                for (k, val) in object_pairs(&hv) {
887                    headers.push((k, val));
888                }
889            }
890        }
891    }
892    if is_get {
893        method = "GET".to_string();
894    }
895
896    let reqid = next_reqid();
897    let mut extra = IndexMap::new();
898    extra.insert("@@reqid".into(), Value::Float(reqid as f64));
899    extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
900    extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
901    let request = super::net::new_emitter_object("ClientRequest", extra);
902    // The `cb` is registered as the `response` listener (Node semantics).
903    if let Some(cb) = cb {
904        super::events::instance_call(
905            &request,
906            "on",
907            vec![with_host(|h| h.new_str("response")), cb],
908        )?;
909    }
910    CLIENT_REQS.with(|c| {
911        c.borrow_mut().insert(
912            reqid,
913            ClientReq {
914                host,
915                port,
916                method,
917                path,
918                headers,
919                body: Vec::new(),
920                request: request.clone(),
921                sent: false,
922            },
923        );
924    });
925    if is_get {
926        dispatch_request(reqid)?;
927    }
928    Ok(request)
929}
930
931/// Parse an `http://host[:port][/path]` URL into its parts (defaults port 80).
932fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
933    let rest = url.strip_prefix("http://").unwrap_or(url);
934    let (authority, p) = match rest.find('/') {
935        Some(i) => (&rest[..i], &rest[i..]),
936        None => (rest, "/"),
937    };
938    *path = if p.is_empty() {
939        "/".to_string()
940    } else {
941        p.to_string()
942    };
943    if let Some((h, port_str)) = authority.rsplit_once(':') {
944        *host = h.to_string();
945        if let Ok(n) = port_str.parse::<u16>() {
946            *port = n;
947        }
948    } else {
949        *host = authority.to_string();
950        *port = 80;
951    }
952}
953
954fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
955    let reqid = u64_prop(req, "@@reqid");
956    match method {
957        "write" => {
958            if let Some(id) = reqid {
959                let bytes = value_bytes(args.first());
960                CLIENT_REQS.with(|c| {
961                    if let Some(r) = c.borrow_mut().get_mut(&id) {
962                        r.body.extend_from_slice(&bytes);
963                    }
964                });
965            }
966            Ok(Value::Bool(true))
967        }
968        "end" => {
969            if let Some(id) = reqid {
970                if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
971                    let bytes = value_bytes(Some(chunk));
972                    CLIENT_REQS.with(|c| {
973                        if let Some(r) = c.borrow_mut().get_mut(&id) {
974                            r.body.extend_from_slice(&bytes);
975                        }
976                    });
977                }
978                dispatch_request(id)?;
979            }
980            Ok(req.clone())
981        }
982        "setHeader" => {
983            if let Some(id) = reqid {
984                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
985                let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
986                CLIENT_REQS.with(|c| {
987                    if let Some(r) = c.borrow_mut().get_mut(&id) {
988                        upsert_header(&mut r.headers, &k, v);
989                    }
990                });
991            }
992            Ok(Value::Undef)
993        }
994        "getHeader" => {
995            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
996                .to_ascii_lowercase();
997            let val = reqid.and_then(|id| {
998                CLIENT_REQS.with(|c| {
999                    c.borrow().get(&id).and_then(|r| {
1000                        r.headers
1001                            .iter()
1002                            .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
1003                            .map(|(_, v)| v.clone())
1004                    })
1005                })
1006            });
1007            Ok(val
1008                .map(|v| with_host(|h| h.new_str(v)))
1009                .unwrap_or(Value::Undef))
1010        }
1011        "removeHeader" => {
1012            if let Some(id) = reqid {
1013                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1014                CLIENT_REQS.with(|c| {
1015                    if let Some(r) = c.borrow_mut().get_mut(&id) {
1016                        r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
1017                    }
1018                });
1019            }
1020            Ok(Value::Undef)
1021        }
1022        "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
1023        _ => Err(crate::host::type_error(&format!(
1024            "req.{method} is not a function"
1025        ))),
1026    }
1027}
1028
1029/// Spawn the blocking HTTP exchange for a client request on a background thread;
1030/// the parsed response is posted back to the main thread.
1031fn dispatch_request(reqid: u64) -> Result<(), String> {
1032    let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
1033    if sent {
1034        return Ok(());
1035    }
1036    CLIENT_REQS.with(|c| {
1037        if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1038            r.sent = true;
1039        }
1040    });
1041
1042    let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1043        let b = c.borrow();
1044        let r = b.get(&reqid).unwrap();
1045        (
1046            r.host.clone(),
1047            r.port,
1048            r.method.clone(),
1049            r.path.clone(),
1050            r.headers.clone(),
1051            r.body.clone(),
1052        )
1053    });
1054
1055    let io_tx = with_host(|h| h.io_sender());
1056    with_host(|h| h.incr_handle());
1057
1058    // Build the request bytes (force `Connection: close` for read-to-EOF).
1059    let mut has_host = false;
1060    let mut has_len = false;
1061    let mut header_block = String::new();
1062    for (k, v) in &headers {
1063        if k.eq_ignore_ascii_case("host") {
1064            has_host = true;
1065        }
1066        if k.eq_ignore_ascii_case("content-length") {
1067            has_len = true;
1068        }
1069        if k.eq_ignore_ascii_case("connection") {
1070            continue;
1071        }
1072        header_block.push_str(&format!("{k}: {v}\r\n"));
1073    }
1074    let host_header = if port == 80 {
1075        host.clone()
1076    } else {
1077        format!("{host}:{port}")
1078    };
1079    let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1080    if !has_host {
1081        request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1082    }
1083    request_bytes.push_str(&header_block);
1084    if !has_len && !body.is_empty() {
1085        request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1086    }
1087    request_bytes.push_str("Connection: close\r\n\r\n");
1088    let mut wire = request_bytes.into_bytes();
1089    wire.extend_from_slice(&body);
1090
1091    std::thread::spawn(move || match exchange(&host, port, &wire) {
1092        Ok(raw) => {
1093            let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1094        }
1095        Err(msg) => {
1096            let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1097        }
1098    });
1099    Ok(())
1100}
1101
1102/// The blocking TCP round-trip: connect, write the request, read to EOF.
1103pub(crate) fn exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1104    let mut stream = TcpStream::connect((host, port))
1105        .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1106    stream
1107        .write_all(request)
1108        .map_err(|e| format!("Error: http write: {e}"))?;
1109    stream
1110        .flush()
1111        .map_err(|e| format!("Error: http flush: {e}"))?;
1112    let mut raw = Vec::new();
1113    let mut buf = [0u8; 16384];
1114    loop {
1115        match stream.read(&mut buf) {
1116            Ok(0) => break,
1117            Ok(n) => raw.extend_from_slice(&buf[..n]),
1118            Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1119            Err(e) => {
1120                if raw.is_empty() {
1121                    return Err(format!("Error: http read: {e}"));
1122                }
1123                break;
1124            }
1125        }
1126    }
1127    Ok(raw)
1128}
1129
1130/// Parse the raw response and emit `response` (with an `IncomingMessage`), then
1131/// the body `data`/`end`. Runs on the main thread.
1132fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1133    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1134    with_host(|h| h.decr_handle());
1135    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1136    let Some(entry) = entry else { return Ok(()) };
1137
1138    let (status, message, http_version, headers, body) = parse_raw_response(&raw);
1139
1140    let headers_obj = with_host(|h| {
1141        let mut m = IndexMap::new();
1142        for (k, v) in &headers {
1143            m.insert(k.clone(), h.new_str(v.clone()));
1144        }
1145        h.new_object(m)
1146    });
1147    let mut extra = IndexMap::new();
1148    extra.insert("statusCode".into(), Value::Float(status as f64));
1149    extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1150    extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1151    extra.insert("headers".into(), headers_obj);
1152    let res = super::net::new_emitter_object("IncomingMessage", extra);
1153
1154    super::events::instance_call(
1155        &entry.request,
1156        "emit",
1157        vec![with_host(|h| h.new_str("response")), res.clone()],
1158    )?;
1159    if !body.is_empty() {
1160        let chunk = super::buffer::from_bytes(&body);
1161        super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1162    }
1163    super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1164    Ok(())
1165}
1166
1167/// Emit `error` on the request when the exchange fails. Runs on the main thread.
1168fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1169    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1170    with_host(|h| h.decr_handle());
1171    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1172    if let Some(entry) = entry {
1173        let err = with_host(|h| {
1174            let mut m = IndexMap::new();
1175            m.insert("message".into(), h.new_str(msg.clone()));
1176            h.new_object(m)
1177        });
1178        super::events::instance_call(
1179            &entry.request,
1180            "emit",
1181            vec![with_host(|h| h.new_str("error")), err],
1182        )?;
1183    }
1184    Ok(())
1185}
1186
1187/// Parse a raw HTTP/1.1 response into `(status, message, version, headers, body)`.
1188/// Handles `Transfer-Encoding: chunked` and plain (Content-Length / to-EOF) bodies.
1189pub(crate) fn parse_raw_response(
1190    raw: &[u8],
1191) -> (u16, String, String, Vec<(String, String)>, Vec<u8>) {
1192    let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1193    let head = String::from_utf8_lossy(&raw[..head_end]);
1194    let body_start = (head_end + 4).min(raw.len());
1195    let mut lines = head.split("\r\n");
1196    let status_line = lines.next().unwrap_or("");
1197    let mut sp = status_line.splitn(3, ' ');
1198    let version = sp
1199        .next()
1200        .unwrap_or("HTTP/1.1")
1201        .strip_prefix("HTTP/")
1202        .unwrap_or("1.1")
1203        .to_string();
1204    let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1205    let message = sp.next().unwrap_or("").to_string();
1206
1207    let mut headers: Vec<(String, String)> = Vec::new();
1208    let mut chunked = false;
1209    for line in lines {
1210        if line.is_empty() {
1211            continue;
1212        }
1213        if let Some((k, v)) = line.split_once(':') {
1214            let name = k.trim().to_ascii_lowercase();
1215            let value = v.trim().to_string();
1216            if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1217                chunked = true;
1218            }
1219            headers.push((name, value));
1220        }
1221    }
1222    let raw_body = &raw[body_start..];
1223    let body = if chunked {
1224        decode_chunked(raw_body)
1225    } else {
1226        raw_body.to_vec()
1227    };
1228    (status, message, version, headers, body)
1229}
1230
1231/// Decode an HTTP/1.1 chunked body (best-effort; stops at the terminating
1232/// 0-chunk or when the input is exhausted).
1233fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1234    let mut out = Vec::new();
1235    while let Some(nl) = find_subslice(data, b"\r\n") {
1236        let size_line = String::from_utf8_lossy(&data[..nl]);
1237        let size_hex = size_line.split(';').next().unwrap_or("").trim();
1238        let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1239        if size == 0 {
1240            break;
1241        }
1242        let chunk_start = nl + 2;
1243        let chunk_end = (chunk_start + size).min(data.len());
1244        out.extend_from_slice(&data[chunk_start..chunk_end]);
1245        let next = chunk_end + 2;
1246        if next >= data.len() {
1247            break;
1248        }
1249        data = &data[next..];
1250    }
1251    out
1252}
1253
1254/// Standard reason phrase for common status codes (default `OK`).
1255fn status_text(code: u16) -> &'static str {
1256    match code {
1257        200 => "OK",
1258        201 => "Created",
1259        202 => "Accepted",
1260        204 => "No Content",
1261        301 => "Moved Permanently",
1262        302 => "Found",
1263        304 => "Not Modified",
1264        400 => "Bad Request",
1265        401 => "Unauthorized",
1266        403 => "Forbidden",
1267        404 => "Not Found",
1268        405 => "Method Not Allowed",
1269        409 => "Conflict",
1270        500 => "Internal Server Error",
1271        502 => "Bad Gateway",
1272        503 => "Service Unavailable",
1273        _ => "OK",
1274    }
1275}