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    /// The request was a HEAD, so the response carries headers and no body
193    /// (RFC 9110 §9.3.2). The headers stay exactly as a GET would produce
194    /// them — `Content-Length` still describes the body that GET *would*
195    /// return — only the bytes after the blank line are suppressed.
196    head: bool,
197    /// Status code (`writeHead`/`res.statusCode`).
198    status: u16,
199    /// Optional custom status message.
200    message: Option<String>,
201    /// Response headers in insertion order; name kept as given, matched
202    /// case-insensitively.
203    headers: Vec<(String, String)>,
204    /// Accumulated body bytes (`write`/`end`).
205    body: Vec<u8>,
206}
207
208thread_local! {
209    static CONNS: std::cell::RefCell<HashMap<u64, HttpConn>> =
210        std::cell::RefCell::new(HashMap::new());
211    static RESPONSES: std::cell::RefCell<HashMap<u64, ResState>> =
212        std::cell::RefCell::new(HashMap::new());
213    static NEXT_RESID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
214}
215
216fn next_resid() -> u64 {
217    NEXT_RESID.with(|c| {
218        let id = c.get();
219        c.set(id + 1);
220        id
221    })
222}
223
224// ── module: http.createServer ────────────────────────────────────────────────
225
226/// `stdlib::call` entry for `http.<method>`.
227pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
228    match method {
229        "createServer" => Some(Ok(create_server(args.first().cloned()))),
230        "request" => Some(request(args, false)),
231        "get" => Some(request(args, true)),
232        "validateHeaderName" => Some(validate_header_name(args)),
233        "validateHeaderValue" => Some(validate_header_value(args)),
234        // No pooling/proxy substrate: accepted no-ops (match Node's `undefined`).
235        "setMaxIdleHTTPParsers" | "setGlobalProxyFromEnv" => Some(Ok(Value::Undef)),
236        _ => None,
237    }
238}
239
240/// `http.validateHeaderName(name[, label])` — throws `ERR_INVALID_HTTP_TOKEN`
241/// unless `name` is a valid RFC 7230 field-name token, else returns `undefined`.
242fn validate_header_name(args: &[Value]) -> Result<Value, String> {
243    let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
244    if is_valid_token(&name) {
245        Ok(Value::Undef)
246    } else {
247        Err(crate::host::coded_error(
248            "TypeError",
249            "ERR_INVALID_HTTP_TOKEN",
250            &format!("Header name must be a valid HTTP token [\"{name}\"]"),
251        ))
252    }
253}
254
255/// `http.validateHeaderValue(name, value)` — throws if `value` is `undefined`
256/// (`ERR_HTTP_INVALID_HEADER_VALUE`) or contains an invalid character
257/// (`ERR_INVALID_CHAR`), else returns `undefined`.
258fn validate_header_value(args: &[Value]) -> Result<Value, String> {
259    let name = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
260    let raw = args.get(1).cloned().unwrap_or(Value::Undef);
261    if matches!(raw, Value::Undef) {
262        return Err(crate::host::coded_error(
263            "TypeError",
264            "ERR_HTTP_INVALID_HEADER_VALUE",
265            &format!("Invalid value \"undefined\" for header \"{name}\""),
266        ));
267    }
268    let value = with_host(|h| h.str_of(&raw));
269    if value.bytes().any(|b| b != b'\t' && (b < 0x20 || b == 0x7f)) {
270        return Err(crate::host::coded_error(
271            "TypeError",
272            "ERR_INVALID_CHAR",
273            &format!("Invalid character in header content [\"{name}\"]"),
274        ));
275    }
276    Ok(Value::Undef)
277}
278
279/// RFC 7230 field-name token: one or more of the `tchar` set.
280fn is_valid_token(s: &str) -> bool {
281    !s.is_empty()
282        && s.bytes().all(|b| {
283            b.is_ascii_alphanumeric()
284                || matches!(
285                    b,
286                    b'!' | b'#'
287                        | b'$'
288                        | b'%'
289                        | b'&'
290                        | b'\''
291                        | b'*'
292                        | b'+'
293                        | b'-'
294                        | b'.'
295                        | b'^'
296                        | b'_'
297                        | b'`'
298                        | b'|'
299                        | b'~'
300                )
301        })
302}
303
304/// `new http.Agent([options])` — a minimal connection-pool holder. We open a
305/// fresh connection per request (no reuse), so the agent only carries its
306/// configuration for the JS side to read back.
307pub fn construct_agent(args: &[Value]) -> Value {
308    let pairs = args
309        .first()
310        .filter(|v| matches!(v, Value::Obj(_)))
311        .map(object_pairs)
312        .unwrap_or_default();
313    with_host(|h| {
314        let mut m = IndexMap::new();
315        m.insert("@@native".into(), h.new_str("Agent"));
316        m.insert("maxSockets".into(), Value::Float(f64::INFINITY));
317        m.insert("maxFreeSockets".into(), Value::Float(256.0));
318        m.insert("sockets".into(), h.new_object(IndexMap::new()));
319        m.insert("requests".into(), h.new_object(IndexMap::new()));
320        for (k, v) in pairs {
321            m.insert(k, h.new_str(v));
322        }
323        h.new_object(m)
324    })
325}
326
327/// Build an HTTP server: a `net.Server` whose per-connection hook wires up the
328/// request parser and remembers the `requestListener`.
329pub fn create_server(request_listener: Option<Value>) -> Value {
330    let server = super::net::create_server(None);
331    let listener = request_listener.unwrap_or(Value::Undef);
332    let hook = std::rc::Rc::new(
333        move |_server: &Value, socket: &Value| -> Result<(), String> {
334            let sock_id = socket_id_of(socket);
335            CONNS.with(|c| {
336                c.borrow_mut().insert(
337                    sock_id,
338                    HttpConn {
339                        socket: socket.clone(),
340                        listener: listener.clone(),
341                        buf: Vec::new(),
342                    },
343                );
344            });
345            Ok(())
346        },
347    );
348    super::net::set_conn_hook(&server, hook);
349    server
350}
351
352fn socket_id_of(socket: &Value) -> u64 {
353    with_host(|h| match h.get(socket) {
354        Some(JsObj::Object(p)) => p.get("@@netid").map(|v| h.to_number(v) as u64).unwrap_or(0),
355        _ => 0,
356    })
357}
358
359/// Discard an HTTP connection when its socket closes.
360pub fn drop_conn(sock_id: u64) {
361    CONNS.with(|c| {
362        c.borrow_mut().remove(&sock_id);
363    });
364}
365
366// ── request parsing ──────────────────────────────────────────────────────────
367
368/// Feed freshly received socket bytes into the HTTP parser. No-op for a socket
369/// that is not an HTTP connection (plain `net`). Runs on the main thread.
370pub fn feed(sock_id: u64, _socket: &Value, bytes: &[u8]) -> Result<(), String> {
371    let is_http = CONNS.with(|c| c.borrow().contains_key(&sock_id));
372    if !is_http {
373        return Ok(());
374    }
375    CONNS.with(|c| {
376        c.borrow_mut()
377            .get_mut(&sock_id)
378            .unwrap()
379            .buf
380            .extend_from_slice(bytes)
381    });
382
383    // Parse and dispatch every complete request currently buffered (pipelining).
384    loop {
385        let (listener, socket, parsed) = CONNS.with(|c| {
386            let mut c = c.borrow_mut();
387            let conn = c.get_mut(&sock_id).unwrap();
388            match parse_request(&conn.buf) {
389                Some((req, consumed)) => {
390                    conn.buf.drain(..consumed);
391                    (conn.listener.clone(), conn.socket.clone(), Some(req))
392                }
393                None => (Value::Undef, Value::Undef, None),
394            }
395        });
396        let Some(parsed) = parsed else { break };
397
398        let req = build_incoming(&parsed);
399        let res = build_response(sock_id, parsed.method.eq_ignore_ascii_case("HEAD"));
400        if with_host(|h| crate::host::is_callable(h, &listener)) {
401            invoke(&listener, vec![req.clone(), res], None)?;
402        }
403        // Body streaming: emit any request body then `end` for downstream readers.
404        // A listener that called `req.setEncoding(enc)` gets a decoded string
405        // chunk instead of a Buffer, matching Node's Readable.
406        if !parsed.body.is_empty() {
407            let encoding = with_host(|h| match h.get(&req) {
408                Some(JsObj::Object(p)) => p.get("@@encoding").map(|v| h.str_of(v)),
409                _ => None,
410            });
411            let chunk = match encoding {
412                Some(enc) => with_host(|h| {
413                    let s = super::buffer::encode_bytes(&parsed.body, &enc);
414                    h.new_str(s)
415                }),
416                None => super::buffer::from_bytes(&parsed.body),
417            };
418            super::events::instance_call(
419                &req,
420                "emit",
421                vec![with_host(|h| h.new_str("data")), chunk],
422            )?;
423        }
424        super::events::instance_call(&req, "emit", vec![with_host(|h| h.new_str("end"))])?;
425        let _ = socket; // (kept for future keep-alive bookkeeping)
426    }
427    Ok(())
428}
429
430/// A fully parsed HTTP request.
431struct ParsedReq {
432    method: String,
433    url: String,
434    http_version: String,
435    /// Lowercased header name → value (last wins, like Node for simple headers).
436    headers: Vec<(String, String)>,
437    /// The same headers in WIRE order and case, for `rawHeaders`. The lowered
438    /// map cannot express either, so a caller needing the original casing or a
439    /// repeated header has nothing to read without it.
440    raw_headers: Vec<(String, String)>,
441    body: Vec<u8>,
442}
443
444/// Try to parse one complete request from `buf`. Returns the request and the
445/// number of bytes consumed, or `None` if more bytes are needed.
446fn parse_request(buf: &[u8]) -> Option<(ParsedReq, usize)> {
447    // Header block ends at the first CRLFCRLF.
448    let head_end = find_subslice(buf, b"\r\n\r\n")?;
449    let head = &buf[..head_end];
450    let body_start = head_end + 4;
451
452    let head_str = String::from_utf8_lossy(head);
453    let mut lines = head_str.split("\r\n");
454    let request_line = lines.next()?;
455    let mut parts = request_line.split(' ');
456    let method = parts.next()?.to_string();
457    let url = parts.next()?.to_string();
458    let version = parts.next().unwrap_or("HTTP/1.1");
459    let http_version = version.strip_prefix("HTTP/").unwrap_or("1.1").to_string();
460
461    let mut headers: Vec<(String, String)> = Vec::new();
462    let mut raw_headers: Vec<(String, String)> = Vec::new();
463    let mut content_length = 0usize;
464    for line in lines {
465        if line.is_empty() {
466            continue;
467        }
468        if let Some((k, v)) = line.split_once(':') {
469            let name = k.trim().to_ascii_lowercase();
470            let value = v.trim().to_string();
471            if name == "content-length" {
472                content_length = value.parse().unwrap_or(0);
473            }
474            raw_headers.push((k.trim().to_string(), value.clone()));
475            headers.push((name, value));
476        }
477    }
478
479    // Need the full body before dispatching.
480    if buf.len() < body_start + content_length {
481        return None;
482    }
483    let body = buf[body_start..body_start + content_length].to_vec();
484    Some((
485        ParsedReq {
486            method,
487            url,
488            http_version,
489            headers,
490            raw_headers,
491            body,
492        },
493        body_start + content_length,
494    ))
495}
496
497fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
498    haystack.windows(needle.len()).position(|w| w == needle)
499}
500
501// ── IncomingMessage (req) ────────────────────────────────────────────────────
502
503/// `rawHeaders`: the flat name/value list in WIRE order and case. The `headers`
504/// object is lower-cased and one-value-per-name, so it can express neither.
505fn flat_headers(pairs: &[(String, String)]) -> Value {
506    with_host(|h| {
507        let flat: Vec<Value> = pairs
508            .iter()
509            .flat_map(|(k, v)| [h.new_str(k.clone()), h.new_str(v.clone())])
510            .collect();
511        h.new_array(flat)
512    })
513}
514
515fn build_incoming(req: &ParsedReq) -> Value {
516    let headers_obj = with_host(|h| {
517        let mut m = IndexMap::new();
518        for (k, v) in &req.headers {
519            m.insert(k.clone(), h.new_str(v.clone()));
520        }
521        h.new_object(m)
522    });
523    let mut extra = IndexMap::new();
524    extra.insert(
525        "method".into(),
526        with_host(|h| h.new_str(req.method.clone())),
527    );
528    extra.insert("url".into(), with_host(|h| h.new_str(req.url.clone())));
529    extra.insert(
530        "httpVersion".into(),
531        with_host(|h| h.new_str(req.http_version.clone())),
532    );
533    extra.insert("headers".into(), headers_obj);
534    extra.insert("rawHeaders".into(), flat_headers(&req.raw_headers));
535    super::net::new_emitter_object("IncomingMessage", extra)
536}
537
538/// The Readable surface of `req`. node-js reads the whole request body off the
539/// socket BEFORE invoking the listener and then delivers it as a single `data`
540/// emit followed by `end` — so there is no partially-consumed stream to pause,
541/// resume or unpipe, and these really are complete rather than stubbed.
542/// `setEncoding` is the exception: it changes what the `data` chunk IS, so it
543/// records the encoding and `emit_body` decodes with it.
544fn incoming_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
545    match method {
546        // Nothing is piped anywhere (node-js has no `pipe` on `req`), and the
547        // body is already buffered, so these are no-ops that return `this` —
548        // which is exactly Node's contract for an already-drained readable.
549        "pause" | "resume" | "unpipe" | "destroy" => Ok(recv.clone()),
550        "isPaused" => Ok(Value::Bool(false)),
551        // Subsequent `data` chunks arrive as strings in this encoding.
552        "setEncoding" => {
553            let enc = super::arg_str(&args, 0);
554            with_host(|h| {
555                let v = h.new_str(enc);
556                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
557                    p.insert("@@encoding".into(), v);
558                }
559            });
560            Ok(recv.clone())
561        }
562        _ => Err(crate::host::type_error(&format!(
563            "req.{method} is not a function"
564        ))),
565    }
566}
567
568// ── ServerResponse (res) ─────────────────────────────────────────────────────
569
570fn build_response(sock_id: u64, head: bool) -> Value {
571    let resid = next_resid();
572    RESPONSES.with(|r| {
573        r.borrow_mut().insert(
574            resid,
575            ResState {
576                sock_id,
577                head,
578                status: 200,
579                message: None,
580                headers: Vec::new(),
581                body: Vec::new(),
582            },
583        );
584    });
585    let mut extra = IndexMap::new();
586    extra.insert("@@resid".into(), Value::Float(resid as f64));
587    extra.insert("statusCode".into(), Value::Float(200.0));
588    super::net::new_emitter_object("ServerResponse", extra)
589}
590
591fn resid_of(res: &Value) -> Option<u64> {
592    with_host(|h| match h.get(res) {
593        Some(JsObj::Object(p)) => p.get("@@resid").map(|v| h.to_number(v) as u64),
594        _ => None,
595    })
596}
597
598/// Instance dispatch for `IncomingMessage`/`ServerResponse` (EventEmitter methods
599/// handled by `net`'s shared emitter delegation via `stdlib::instance_call`).
600pub fn instance_call(
601    tag: &str,
602    recv: &Value,
603    method: &str,
604    args: Vec<Value>,
605) -> Result<Value, String> {
606    // EventEmitter surface is shared with `net` sockets, and the set of names is
607    // read from `events::METHODS` rather than re-listed: the copy that used to
608    // live here was missing `listeners`, `setMaxListeners` and `getMaxListeners`,
609    // so `unpipe` — reached from `body-parser` on every `express.json()` request
610    // — died on `req.listeners('data') is not a function`.
611    if super::events::METHODS.contains(&method) {
612        return super::events::instance_call(recv, method, args);
613    }
614    match tag {
615        "IncomingMessage" => incoming_call(recv, method, args),
616        "ServerResponse" => response_call(recv, method, args),
617        "ClientRequest" => client_request_call(recv, method, args),
618        // Minimal Agent: no live sockets to tear down.
619        "Agent" => match method {
620            "destroy" => Ok(Value::Undef),
621            "getName" => Ok(with_host(|h| h.new_str("localhost:80:"))),
622            _ => Err(crate::host::type_error(&format!(
623                "agent.{method} is not a function"
624            ))),
625        },
626        _ => Err(crate::host::type_error(&format!(
627            "{method} is not a function"
628        ))),
629    }
630}
631
632fn response_call(res: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
633    let Some(resid) = resid_of(res) else {
634        return Err(crate::host::type_error("invalid ServerResponse"));
635    };
636    match method {
637        "writeHead" => {
638            let status =
639                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u16;
640            // Second arg is either the status message (string) or the headers obj.
641            let mut message: Option<String> = None;
642            let mut headers_arg: Option<Value> = None;
643            if let Some(a) = args.get(1) {
644                if with_host(|h| h.as_str(a)).is_some() {
645                    message = Some(with_host(|h| h.str_of(a)));
646                } else if !matches!(a, Value::Undef) {
647                    headers_arg = Some(a.clone());
648                }
649            }
650            if let Some(a) = args.get(2) {
651                if !matches!(a, Value::Undef) {
652                    headers_arg = Some(a.clone());
653                }
654            }
655            let header_pairs = headers_arg.map(|h| object_pairs(&h)).unwrap_or_default();
656            RESPONSES.with(|r| {
657                if let Some(st) = r.borrow_mut().get_mut(&resid) {
658                    st.status = status;
659                    st.message = message;
660                    for (k, v) in header_pairs {
661                        upsert_header(&mut st.headers, &k, v);
662                    }
663                }
664            });
665            // Mirror onto the JS prop so `res.statusCode` reads reflect it.
666            set_res_prop(res, "statusCode", Value::Float(status as f64));
667            Ok(res.clone())
668        }
669        "setHeader" => {
670            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
671            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
672            RESPONSES.with(|r| {
673                if let Some(st) = r.borrow_mut().get_mut(&resid) {
674                    upsert_header(&mut st.headers, &k, v);
675                }
676            });
677            Ok(Value::Undef)
678        }
679        "getHeader" => {
680            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
681                .to_ascii_lowercase();
682            let val = RESPONSES.with(|r| {
683                r.borrow().get(&resid).and_then(|st| {
684                    st.headers
685                        .iter()
686                        .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
687                        .map(|(_, v)| v.clone())
688                })
689            });
690            Ok(val
691                .map(|v| with_host(|h| h.new_str(v)))
692                .unwrap_or(Value::Undef))
693        }
694        "removeHeader" => {
695            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
696            RESPONSES.with(|r| {
697                if let Some(st) = r.borrow_mut().get_mut(&resid) {
698                    st.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
699                }
700            });
701            Ok(Value::Undef)
702        }
703        "write" => {
704            let bytes = value_bytes(args.first());
705            RESPONSES.with(|r| {
706                if let Some(st) = r.borrow_mut().get_mut(&resid) {
707                    st.body.extend_from_slice(&bytes);
708                }
709            });
710            Ok(Value::Bool(true))
711        }
712        "end" => {
713            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
714                let bytes = value_bytes(Some(chunk));
715                RESPONSES.with(|r| {
716                    if let Some(st) = r.borrow_mut().get_mut(&resid) {
717                        st.body.extend_from_slice(&bytes);
718                    }
719                });
720            }
721            finish_response(res, resid)?;
722            Ok(res.clone())
723        }
724        // Advertised in `stdlib::instance_methods` for ServerResponse and
725        // implemented in `https::response_call`, but missing here — so
726        // `res.flushHeaders()` threw "is not a function" over plaintext while
727        // working over TLS. A no-op is the honest implementation either way:
728        // `write` buffers into `ResState.body` and nothing reaches the socket
729        // until `end`, so there is no partial header block to flush.
730        // Advertised in `stdlib::instance_methods` for ServerResponse but never
731        // implemented in either module, so all three threw "is not a function"
732        // on both protocols despite feature-detection saying they exist.
733        // Node lowercases the names it reports, so these do too, while
734        // `setHeader` keeps the caller's spelling on the wire.
735        "hasHeader" => {
736            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
737            let found = RESPONSES.with(|r| {
738                r.borrow()
739                    .get(&resid)
740                    .is_some_and(|st| st.headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)))
741            });
742            Ok(Value::Bool(found))
743        }
744        "getHeaderNames" => {
745            let names = RESPONSES.with(|r| {
746                r.borrow()
747                    .get(&resid)
748                    .map(|st| {
749                        st.headers
750                            .iter()
751                            .map(|(k, _)| k.to_ascii_lowercase())
752                            .collect::<Vec<_>>()
753                    })
754                    .unwrap_or_default()
755            });
756            Ok(with_host(|h| {
757                let items = names.into_iter().map(|n| h.new_str(n)).collect::<Vec<_>>();
758                h.new_array(items)
759            }))
760        }
761        "getHeaders" => {
762            let pairs = RESPONSES.with(|r| {
763                r.borrow()
764                    .get(&resid)
765                    .map(|st| {
766                        st.headers
767                            .iter()
768                            .map(|(k, v)| (k.to_ascii_lowercase(), v.clone()))
769                            .collect::<Vec<_>>()
770                    })
771                    .unwrap_or_default()
772            });
773            Ok(with_host(|h| {
774                let mut m = IndexMap::new();
775                for (k, v) in pairs {
776                    m.insert(k, h.new_str(v));
777                }
778                h.new_object(m)
779            }))
780        }
781        "flushHeaders" => Ok(Value::Undef),
782        _ => Err(crate::host::type_error(&format!(
783            "res.{method} is not a function"
784        ))),
785    }
786}
787
788/// Serialize and write the response, then emit `finish`.
789fn finish_response(res: &Value, resid: u64) -> Result<(), String> {
790    // Reconcile status with a possible `res.statusCode = n` assignment, and the
791    // reason phrase with `res.statusMessage = '…'`. Only the code was being
792    // read back, so a custom message set that way was dropped and the default
793    // phrase for the code went out instead.
794    let (js_status, js_message) = with_host(|h| match h.get(res) {
795        Some(JsObj::Object(p)) => (
796            p.get("statusCode").map(|v| h.to_number(v) as u16),
797            p.get("statusMessage")
798                .filter(|v| !matches!(v, Value::Undef))
799                .map(|v| h.str_of(v)),
800        ),
801        _ => (None, None),
802    });
803    let st = RESPONSES.with(|r| r.borrow_mut().remove(&resid));
804    let Some(mut st) = st else { return Ok(()) };
805    if let Some(s) = js_status {
806        st.status = s;
807    }
808    if let Some(m) = js_message {
809        st.message = Some(m);
810    }
811    let payload = serialize_response(&mut st);
812    super::net::socket_write_id(st.sock_id, &payload);
813    super::events::instance_call(res, "emit", vec![with_host(|h| h.new_str("finish"))])?;
814    Ok(())
815}
816
817/// Build the raw HTTP/1.1 response bytes: status line, headers (adding
818/// `Content-Length` and `Connection: keep-alive` if the user did not set framing
819/// headers), CRLFCRLF, then the body.
820fn serialize_response(st: &mut ResState) -> Vec<u8> {
821    let reason = st
822        .message
823        .clone()
824        .unwrap_or_else(|| status_text(st.status).to_string());
825    let mut out = format!("HTTP/1.1 {} {}\r\n", st.status, reason).into_bytes();
826
827    let has = |name: &str| st.headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name));
828    let chunked = st.headers.iter().any(|(k, v)| {
829        k.eq_ignore_ascii_case("transfer-encoding") && v.to_ascii_lowercase().contains("chunked")
830    });
831
832    for (k, v) in &st.headers {
833        out.extend_from_slice(format!("{k}: {v}\r\n").as_bytes());
834    }
835    if !chunked && !has("content-length") {
836        out.extend_from_slice(format!("Content-Length: {}\r\n", st.body.len()).as_bytes());
837    }
838    if !has("connection") {
839        out.extend_from_slice(b"Connection: keep-alive\r\n");
840    }
841    out.extend_from_slice(b"\r\n");
842    // `Transfer-Encoding: chunked` is a promise about the BODY's framing, not
843    // just a header. Writing the bytes raw under it sent, for a handler that
844    // set the header and wrote "one" then "two":
845    //
846    //     Transfer-Encoding: chunked\r\n\r\nonetwo
847    //
848    // which any conforming client reads as a chunk-size line of "onetwo" — not
849    // hex, so the body is lost. Our own client only survived it by falling back
850    // to reading until EOF. `res.write` buffers into one `body` rather than
851    // streaming, so the boundaries between writes are already gone by here;
852    // one chunk carrying the whole body is the honest encoding of what we have,
853    // and chunk boundaries carry no meaning to the receiver.
854    if st.head {
855        // Headers only. `Content-Length`/`Transfer-Encoding` above already
856        // describe the GET body, which is what a HEAD response must advertise.
857    } else if chunked {
858        if !st.body.is_empty() {
859            out.extend_from_slice(format!("{:x}\r\n", st.body.len()).as_bytes());
860            out.extend_from_slice(&st.body);
861            out.extend_from_slice(b"\r\n");
862        }
863        out.extend_from_slice(b"0\r\n\r\n");
864    } else {
865        out.extend_from_slice(&st.body);
866    }
867    out
868}
869
870// ── helpers ──────────────────────────────────────────────────────────────────
871
872/// Insert or replace a header (case-insensitive name match), preserving order.
873fn upsert_header(headers: &mut Vec<(String, String)>, name: &str, value: String) {
874    if let Some(slot) = headers
875        .iter_mut()
876        .find(|(k, _)| k.eq_ignore_ascii_case(name))
877    {
878        slot.1 = value;
879    } else {
880        headers.push((name.to_string(), value));
881    }
882}
883
884/// Enumerable string key/value pairs of a plain object (for the `writeHead`
885/// headers argument).
886fn object_pairs(obj: &Value) -> Vec<(String, String)> {
887    with_host(|h| match h.get(obj) {
888        Some(JsObj::Object(p)) => p
889            .iter()
890            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
891            .map(|(k, v)| (k.clone(), h.str_of(v)))
892            .collect(),
893        _ => Vec::new(),
894    })
895}
896
897/// Set a JS-visible prop on the response object.
898fn set_res_prop(res: &Value, key: &str, val: Value) {
899    with_host(|h| {
900        if let Some(JsObj::Object(p)) = h.get_mut(res) {
901            p.insert(key.to_string(), val);
902        }
903    });
904}
905
906/// Raw bytes of a `write`/`end` argument: a Buffer's bytes, or a string's UTF-8.
907fn value_bytes(v: Option<&Value>) -> Vec<u8> {
908    let Some(v) = v else { return Vec::new() };
909    let is_buffer =
910        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
911    if is_buffer {
912        return with_host(|h| match h.get(v) {
913            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
914                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
915                _ => Vec::new(),
916            },
917            _ => Vec::new(),
918        });
919    }
920    with_host(|h| h.str_of(v)).into_bytes()
921}
922
923// ── client: http.request / http.get ──────────────────────────────────────────
924//
925// Mirrors the `https` client (https.rs), but over a plain `TcpStream` on port 80.
926// `.end()` (or `http.get` immediately) spawns a background thread that connects,
927// writes the request, reads the full response to EOF (the request forces
928// `Connection: close`), parses it, and posts an `IoTask` that emits `response`
929// with an `IncomingMessage` (then its body `data`/`end`) on the main thread.
930
931/// State of an in-flight client request until `.end()` dispatches it.
932struct ClientReq {
933    host: String,
934    port: u16,
935    method: String,
936    path: String,
937    headers: Vec<(String, String)>,
938    body: Vec<u8>,
939    /// The `req` object (a `ClientRequest` emitter) for `response`/`error` events.
940    request: Value,
941    sent: bool,
942}
943
944thread_local! {
945    static CLIENT_REQS: std::cell::RefCell<HashMap<u64, ClientReq>> =
946        std::cell::RefCell::new(HashMap::new());
947    static NEXT_REQID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
948}
949
950fn next_reqid() -> u64 {
951    NEXT_REQID.with(|c| {
952        let id = c.get();
953        c.set(id + 1);
954        id
955    })
956}
957
958fn get_prop(recv: &Value, key: &str) -> Option<Value> {
959    with_host(|h| match h.get(recv) {
960        Some(JsObj::Object(p)) => p.get(key).cloned(),
961        _ => None,
962    })
963}
964
965fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
966    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
967}
968
969/// `http.request(options|url[, cb])` / `http.get(url|options[, cb])`. Returns a
970/// `ClientRequest` (a `ClientRequest` emitter). `get` auto-sends via `.end()`.
971pub fn request(args: &[Value], is_get: bool) -> Result<Value, String> {
972    let mut host = "localhost".to_string();
973    let mut port: u16 = 80;
974    let mut path = "/".to_string();
975    let mut method = "GET".to_string();
976    let mut headers: Vec<(String, String)> = Vec::new();
977    let mut cb: Option<Value> = None;
978
979    for a in args {
980        if with_host(|h| crate::host::is_callable(h, a)) {
981            cb = Some(a.clone());
982        } else if with_host(|h| h.as_str(a)).is_some() {
983            let url = with_host(|h| h.str_of(a));
984            parse_url(&url, &mut host, &mut port, &mut path);
985        } else if matches!(a, Value::Obj(_)) {
986            for key in ["hostname", "host"] {
987                if let Some(v) = get_prop(a, key).filter(|v| with_host(|h| h.as_str(v)).is_some()) {
988                    host = with_host(|h| h.str_of(&v));
989                }
990            }
991            if let Some(v) = get_prop(a, "port") {
992                let n = with_host(|h| h.to_number(&v));
993                if !n.is_nan() {
994                    port = n as u16;
995                }
996            }
997            if let Some(v) = get_prop(a, "path").filter(|v| with_host(|h| h.as_str(v)).is_some()) {
998                path = with_host(|h| h.str_of(&v));
999            }
1000            if let Some(v) = get_prop(a, "method").filter(|v| with_host(|h| h.as_str(v)).is_some())
1001            {
1002                method = with_host(|h| h.str_of(&v));
1003            }
1004            if let Some(hv) = get_prop(a, "headers").filter(|v| matches!(v, Value::Obj(_))) {
1005                for (k, val) in object_pairs(&hv) {
1006                    headers.push((k, val));
1007                }
1008            }
1009        }
1010    }
1011    if is_get {
1012        method = "GET".to_string();
1013    }
1014
1015    let reqid = next_reqid();
1016    let mut extra = IndexMap::new();
1017    extra.insert("@@reqid".into(), Value::Float(reqid as f64));
1018    extra.insert("method".into(), with_host(|h| h.new_str(method.clone())));
1019    extra.insert("path".into(), with_host(|h| h.new_str(path.clone())));
1020    let request = super::net::new_emitter_object("ClientRequest", extra);
1021    // The `cb` is registered as the `response` listener (Node semantics).
1022    if let Some(cb) = cb {
1023        super::events::instance_call(
1024            &request,
1025            "on",
1026            vec![with_host(|h| h.new_str("response")), cb],
1027        )?;
1028    }
1029    CLIENT_REQS.with(|c| {
1030        c.borrow_mut().insert(
1031            reqid,
1032            ClientReq {
1033                host,
1034                port,
1035                method,
1036                path,
1037                headers,
1038                body: Vec::new(),
1039                request: request.clone(),
1040                sent: false,
1041            },
1042        );
1043    });
1044    if is_get {
1045        dispatch_request(reqid)?;
1046    }
1047    Ok(request)
1048}
1049
1050/// Parse an `http://host[:port][/path]` URL into its parts (defaults port 80).
1051fn parse_url(url: &str, host: &mut String, port: &mut u16, path: &mut String) {
1052    let rest = url.strip_prefix("http://").unwrap_or(url);
1053    let (authority, p) = match rest.find('/') {
1054        Some(i) => (&rest[..i], &rest[i..]),
1055        None => (rest, "/"),
1056    };
1057    *path = if p.is_empty() {
1058        "/".to_string()
1059    } else {
1060        p.to_string()
1061    };
1062    if let Some((h, port_str)) = authority.rsplit_once(':') {
1063        *host = h.to_string();
1064        if let Ok(n) = port_str.parse::<u16>() {
1065            *port = n;
1066        }
1067    } else {
1068        *host = authority.to_string();
1069        *port = 80;
1070    }
1071}
1072
1073fn client_request_call(req: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1074    let reqid = u64_prop(req, "@@reqid");
1075    match method {
1076        "write" => {
1077            if let Some(id) = reqid {
1078                let bytes = value_bytes(args.first());
1079                CLIENT_REQS.with(|c| {
1080                    if let Some(r) = c.borrow_mut().get_mut(&id) {
1081                        r.body.extend_from_slice(&bytes);
1082                    }
1083                });
1084            }
1085            Ok(Value::Bool(true))
1086        }
1087        "end" => {
1088            if let Some(id) = reqid {
1089                if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
1090                    let bytes = value_bytes(Some(chunk));
1091                    CLIENT_REQS.with(|c| {
1092                        if let Some(r) = c.borrow_mut().get_mut(&id) {
1093                            r.body.extend_from_slice(&bytes);
1094                        }
1095                    });
1096                }
1097                dispatch_request(id)?;
1098            }
1099            Ok(req.clone())
1100        }
1101        "setHeader" => {
1102            if let Some(id) = reqid {
1103                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1104                let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1105                CLIENT_REQS.with(|c| {
1106                    if let Some(r) = c.borrow_mut().get_mut(&id) {
1107                        upsert_header(&mut r.headers, &k, v);
1108                    }
1109                });
1110            }
1111            Ok(Value::Undef)
1112        }
1113        "getHeader" => {
1114            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)))
1115                .to_ascii_lowercase();
1116            let val = reqid.and_then(|id| {
1117                CLIENT_REQS.with(|c| {
1118                    c.borrow().get(&id).and_then(|r| {
1119                        r.headers
1120                            .iter()
1121                            .find(|(hk, _)| hk.eq_ignore_ascii_case(&k))
1122                            .map(|(_, v)| v.clone())
1123                    })
1124                })
1125            });
1126            Ok(val
1127                .map(|v| with_host(|h| h.new_str(v)))
1128                .unwrap_or(Value::Undef))
1129        }
1130        "removeHeader" => {
1131            if let Some(id) = reqid {
1132                let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1133                CLIENT_REQS.with(|c| {
1134                    if let Some(r) = c.borrow_mut().get_mut(&id) {
1135                        r.headers.retain(|(hk, _)| !hk.eq_ignore_ascii_case(&k));
1136                    }
1137                });
1138            }
1139            Ok(Value::Undef)
1140        }
1141        "abort" | "destroy" | "setTimeout" | "flushHeaders" => Ok(req.clone()),
1142        _ => Err(crate::host::type_error(&format!(
1143            "req.{method} is not a function"
1144        ))),
1145    }
1146}
1147
1148/// Spawn the blocking HTTP exchange for a client request on a background thread;
1149/// the parsed response is posted back to the main thread.
1150fn dispatch_request(reqid: u64) -> Result<(), String> {
1151    let sent = CLIENT_REQS.with(|c| c.borrow().get(&reqid).map(|r| r.sent).unwrap_or(true));
1152    if sent {
1153        return Ok(());
1154    }
1155    CLIENT_REQS.with(|c| {
1156        if let Some(r) = c.borrow_mut().get_mut(&reqid) {
1157            r.sent = true;
1158        }
1159    });
1160
1161    let (host, port, method, path, headers, body) = CLIENT_REQS.with(|c| {
1162        let b = c.borrow();
1163        let r = b.get(&reqid).unwrap();
1164        (
1165            r.host.clone(),
1166            r.port,
1167            r.method.clone(),
1168            r.path.clone(),
1169            r.headers.clone(),
1170            r.body.clone(),
1171        )
1172    });
1173
1174    let io_tx = with_host(|h| h.io_sender());
1175    with_host(|h| h.incr_handle());
1176
1177    // Build the request bytes (force `Connection: close` for read-to-EOF).
1178    let mut has_host = false;
1179    let mut has_len = false;
1180    let mut header_block = String::new();
1181    for (k, v) in &headers {
1182        if k.eq_ignore_ascii_case("host") {
1183            has_host = true;
1184        }
1185        if k.eq_ignore_ascii_case("content-length") {
1186            has_len = true;
1187        }
1188        if k.eq_ignore_ascii_case("connection") {
1189            continue;
1190        }
1191        header_block.push_str(&format!("{k}: {v}\r\n"));
1192    }
1193    let host_header = if port == 80 {
1194        host.clone()
1195    } else {
1196        format!("{host}:{port}")
1197    };
1198    let mut request_bytes = format!("{method} {path} HTTP/1.1\r\n");
1199    if !has_host {
1200        request_bytes.push_str(&format!("Host: {host_header}\r\n"));
1201    }
1202    request_bytes.push_str(&header_block);
1203    if !has_len && !body.is_empty() {
1204        request_bytes.push_str(&format!("Content-Length: {}\r\n", body.len()));
1205    }
1206    request_bytes.push_str("Connection: close\r\n\r\n");
1207    let mut wire = request_bytes.into_bytes();
1208    wire.extend_from_slice(&body);
1209
1210    std::thread::spawn(move || match exchange(&host, port, &wire) {
1211        Ok(raw) => {
1212            let _ = io_tx.send(Box::new(move || deliver_response(reqid, raw)));
1213        }
1214        Err(msg) => {
1215            let _ = io_tx.send(Box::new(move || deliver_error(reqid, msg)));
1216        }
1217    });
1218    Ok(())
1219}
1220
1221/// The blocking TCP round-trip: connect, write the request, read until the
1222/// response message is COMPLETE — by its own framing when it has one, and only
1223/// then to EOF.
1224///
1225/// Reading unconditionally to EOF made the client depend on the server hanging
1226/// up. Against `http.createServer` it does not: the response carries
1227/// `Content-Length` and `Connection: keep-alive`, and nothing closes the
1228/// connection, so a request served by this runtime's own server deadlocked —
1229/// the caller waiting for a FIN that the server, waiting for the caller, would
1230/// never send. Node's client stops at the end of the message, and so does this
1231/// one now; the EOF path survives only for a response with no framing at all
1232/// (HTTP/1.0 style), which is the one case where EOF *is* the framing.
1233pub(crate) fn exchange(host: &str, port: u16, request: &[u8]) -> Result<Vec<u8>, String> {
1234    let mut stream = TcpStream::connect((host, port))
1235        .map_err(|e| format!("Error: connect ECONNREFUSED {host}:{port}: {e}"))?;
1236    stream
1237        .write_all(request)
1238        .map_err(|e| format!("Error: http write: {e}"))?;
1239    stream
1240        .flush()
1241        .map_err(|e| format!("Error: http flush: {e}"))?;
1242    let head_request = request
1243        .split(|b| *b == b' ')
1244        .next()
1245        .is_some_and(|m| m.eq_ignore_ascii_case(b"HEAD"));
1246    let mut raw = Vec::new();
1247    let mut buf = [0u8; 16384];
1248    loop {
1249        if response_is_complete(&raw, head_request) {
1250            break;
1251        }
1252        match stream.read(&mut buf) {
1253            Ok(0) => break,
1254            Ok(n) => raw.extend_from_slice(&buf[..n]),
1255            Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1256            Err(e) => {
1257                if raw.is_empty() {
1258                    return Err(format!("Error: http read: {e}"));
1259                }
1260                break;
1261            }
1262        }
1263    }
1264    Ok(raw)
1265}
1266
1267/// Whether `raw` already holds a whole HTTP/1.1 response, per RFC 9112 §6.3.
1268///
1269/// `false` for anything unframed — a response with neither `Transfer-Encoding:
1270/// chunked` nor `Content-Length`, where the end of the body IS the end of the
1271/// connection — so the caller keeps reading to EOF exactly as it used to.
1272pub(crate) fn response_is_complete(raw: &[u8], head_request: bool) -> bool {
1273    let Some(head_end) = find_subslice(raw, b"\r\n\r\n") else {
1274        return false;
1275    };
1276    let head = String::from_utf8_lossy(&raw[..head_end]);
1277    let mut lines = head.split("\r\n");
1278    let status = lines
1279        .next()
1280        .and_then(|l| l.split(' ').nth(1).and_then(|s| s.parse::<u16>().ok()))
1281        .unwrap_or(0);
1282    // A 1xx is an INTERIM response: the real one follows on the same
1283    // connection, so the message is not finished and reading continues.
1284    if (100..200).contains(&status) {
1285        return false;
1286    }
1287    // These carry no body however they are framed (RFC 9112 §6.3, cases 1-2).
1288    if head_request || status == 204 || status == 304 {
1289        return true;
1290    }
1291    let mut chunked = false;
1292    let mut content_length: Option<usize> = None;
1293    for line in lines {
1294        let Some((k, v)) = line.split_once(':') else {
1295            continue;
1296        };
1297        let name = k.trim().to_ascii_lowercase();
1298        let value = v.trim();
1299        if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1300            chunked = true;
1301        } else if name == "content-length" {
1302            content_length = value.parse().ok();
1303        }
1304    }
1305    let body = &raw[head_end + 4..];
1306    // Chunked wins over Content-Length when both are present (RFC 9112 §6.3).
1307    if chunked {
1308        return chunked_body_is_terminated(body);
1309    }
1310    match content_length {
1311        Some(n) => body.len() >= n,
1312        None => false,
1313    }
1314}
1315
1316/// Whether a chunked body has reached its terminating zero-size chunk and the
1317/// trailer section's closing CRLF.
1318fn chunked_body_is_terminated(mut data: &[u8]) -> bool {
1319    loop {
1320        let Some(nl) = find_subslice(data, b"\r\n") else {
1321            return false;
1322        };
1323        let size_line = String::from_utf8_lossy(&data[..nl]);
1324        let Ok(size) = usize::from_str_radix(size_line.split(';').next().unwrap_or("").trim(), 16)
1325        else {
1326            return false;
1327        };
1328        if size == 0 {
1329            // Trailer section (possibly empty) ends at the next blank line.
1330            return find_subslice(&data[nl + 2..], b"\r\n").is_some();
1331        }
1332        // chunk-data + its trailing CRLF.
1333        let consumed = nl + 2 + size + 2;
1334        if data.len() < consumed {
1335            return false;
1336        }
1337        data = &data[consumed..];
1338    }
1339}
1340
1341/// Parse the raw response and emit `response` (with an `IncomingMessage`), then
1342/// the body `data`/`end`. Runs on the main thread.
1343fn deliver_response(reqid: u64, raw: Vec<u8>) -> Result<(), String> {
1344    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1345    with_host(|h| h.decr_handle());
1346    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1347    let Some(entry) = entry else { return Ok(()) };
1348
1349    let ParsedRes {
1350        status,
1351        message,
1352        http_version,
1353        headers,
1354        raw_headers: raw_header_pairs,
1355        body,
1356    } = parse_raw_response(&raw);
1357
1358    let headers_obj = with_host(|h| {
1359        let mut m = IndexMap::new();
1360        for (k, v) in &headers {
1361            m.insert(k.clone(), h.new_str(v.clone()));
1362        }
1363        h.new_object(m)
1364    });
1365    // `rawHeaders` is the flat name/value list in WIRE order and case, which
1366    // `headers` (lower-cased and de-duplicated into an object) cannot express.
1367    // It was absent entirely, so a caller needing the original casing or a
1368    // repeated header had nothing to read.
1369    let raw_headers = flat_headers(&raw_header_pairs);
1370    let mut extra = IndexMap::new();
1371    extra.insert("rawHeaders".into(), raw_headers);
1372    extra.insert("statusCode".into(), Value::Float(status as f64));
1373    extra.insert("statusMessage".into(), with_host(|h| h.new_str(message)));
1374    extra.insert("httpVersion".into(), with_host(|h| h.new_str(http_version)));
1375    extra.insert("headers".into(), headers_obj);
1376    let res = super::net::new_emitter_object("IncomingMessage", extra);
1377
1378    super::events::instance_call(
1379        &entry.request,
1380        "emit",
1381        vec![with_host(|h| h.new_str("response")), res.clone()],
1382    )?;
1383    if !body.is_empty() {
1384        let chunk = super::buffer::from_bytes(&body);
1385        super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("data")), chunk])?;
1386    }
1387    super::events::instance_call(&res, "emit", vec![with_host(|h| h.new_str("end"))])?;
1388    Ok(())
1389}
1390
1391/// Emit `error` on the request when the exchange fails. Runs on the main thread.
1392fn deliver_error(reqid: u64, msg: String) -> Result<(), String> {
1393    let entry = CLIENT_REQS.with(|c| c.borrow_mut().remove(&reqid));
1394    with_host(|h| h.decr_handle());
1395    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1396    if let Some(entry) = entry {
1397        let err = with_host(|h| {
1398            let mut m = IndexMap::new();
1399            m.insert("message".into(), h.new_str(msg.clone()));
1400            h.new_object(m)
1401        });
1402        super::events::instance_call(
1403            &entry.request,
1404            "emit",
1405            vec![with_host(|h| h.new_str("error")), err],
1406        )?;
1407    }
1408    Ok(())
1409}
1410
1411/// Parse a raw HTTP/1.1 response into `(status, message, version, headers, body)`.
1412/// Handles `Transfer-Encoding: chunked` and plain (Content-Length / to-EOF) bodies.
1413/// One parsed HTTP response.
1414pub(crate) struct ParsedRes {
1415    pub status: u16,
1416    pub message: String,
1417    pub http_version: String,
1418    /// Lower-cased name → value, for the `headers` object.
1419    pub headers: Vec<(String, String)>,
1420    /// The same headers in wire order and case, for `rawHeaders`.
1421    pub raw_headers: Vec<(String, String)>,
1422    pub body: Vec<u8>,
1423}
1424
1425pub(crate) fn parse_raw_response(raw: &[u8]) -> ParsedRes {
1426    let head_end = find_subslice(raw, b"\r\n\r\n").unwrap_or(raw.len());
1427    let head = String::from_utf8_lossy(&raw[..head_end]);
1428    let body_start = (head_end + 4).min(raw.len());
1429    let mut lines = head.split("\r\n");
1430    let status_line = lines.next().unwrap_or("");
1431    let mut sp = status_line.splitn(3, ' ');
1432    let version = sp
1433        .next()
1434        .unwrap_or("HTTP/1.1")
1435        .strip_prefix("HTTP/")
1436        .unwrap_or("1.1")
1437        .to_string();
1438    let status = sp.next().and_then(|s| s.parse::<u16>().ok()).unwrap_or(0);
1439    let message = sp.next().unwrap_or("").to_string();
1440
1441    let mut headers: Vec<(String, String)> = Vec::new();
1442    let mut raw_headers: Vec<(String, String)> = Vec::new();
1443    let mut chunked = false;
1444    for line in lines {
1445        if line.is_empty() {
1446            continue;
1447        }
1448        if let Some((k, v)) = line.split_once(':') {
1449            let name = k.trim().to_ascii_lowercase();
1450            let value = v.trim().to_string();
1451            if name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") {
1452                chunked = true;
1453            }
1454            raw_headers.push((k.trim().to_string(), value.clone()));
1455            headers.push((name, value));
1456        }
1457    }
1458    let raw_body = &raw[body_start..];
1459    let body = if chunked {
1460        decode_chunked(raw_body)
1461    } else {
1462        raw_body.to_vec()
1463    };
1464    ParsedRes {
1465        status,
1466        message,
1467        http_version: version,
1468        headers,
1469        raw_headers,
1470        body,
1471    }
1472}
1473
1474/// Decode an HTTP/1.1 chunked body (best-effort; stops at the terminating
1475/// 0-chunk or when the input is exhausted).
1476fn decode_chunked(mut data: &[u8]) -> Vec<u8> {
1477    let mut out = Vec::new();
1478    while let Some(nl) = find_subslice(data, b"\r\n") {
1479        let size_line = String::from_utf8_lossy(&data[..nl]);
1480        let size_hex = size_line.split(';').next().unwrap_or("").trim();
1481        let size = usize::from_str_radix(size_hex, 16).unwrap_or(0);
1482        if size == 0 {
1483            break;
1484        }
1485        let chunk_start = nl + 2;
1486        let chunk_end = (chunk_start + size).min(data.len());
1487        out.extend_from_slice(&data[chunk_start..chunk_end]);
1488        let next = chunk_end + 2;
1489        if next >= data.len() {
1490            break;
1491        }
1492        data = &data[next..];
1493    }
1494    out
1495}
1496
1497/// Standard reason phrase for common status codes (default `OK`).
1498fn status_text(code: u16) -> &'static str {
1499    match code {
1500        200 => "OK",
1501        201 => "Created",
1502        202 => "Accepted",
1503        204 => "No Content",
1504        301 => "Moved Permanently",
1505        302 => "Found",
1506        304 => "Not Modified",
1507        400 => "Bad Request",
1508        401 => "Unauthorized",
1509        403 => "Forbidden",
1510        404 => "Not Found",
1511        405 => "Method Not Allowed",
1512        409 => "Conflict",
1513        500 => "Internal Server Error",
1514        502 => "Bad Gateway",
1515        503 => "Service Unavailable",
1516        _ => "OK",
1517    }
1518}
1519
1520#[cfg(test)]
1521mod framing_tests {
1522    use super::{response_is_complete, serialize_response, ResState};
1523
1524    /// The shape `http.createServer` writes: `Content-Length` plus
1525    /// `Connection: keep-alive`, and no FIN. Reading to EOF deadlocked here.
1526    #[test]
1527    fn a_keep_alive_content_length_response_is_complete_at_its_last_body_byte() {
1528        let head = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\
1529                     Content-Length: 12\r\nConnection: keep-alive\r\n\r\n";
1530        let mut raw = head.to_vec();
1531        assert!(!response_is_complete(&raw, false), "no body yet");
1532        raw.extend_from_slice(b"hello GET /");
1533        assert!(!response_is_complete(&raw, false), "11 of 12 bytes");
1534        raw.push(b'p');
1535        assert!(response_is_complete(&raw, false), "12 of 12 bytes");
1536    }
1537
1538    /// The encoder's own output must satisfy the decoder. `serialize_response`
1539    /// frames a chunked body and `response_is_complete` decides when one has
1540    /// arrived; if they disagree the client hangs waiting for a terminator the
1541    /// server never wrote, which is what happened when the body went out raw
1542    /// under a `Transfer-Encoding: chunked` header.
1543    #[test]
1544    fn a_chunked_response_this_server_wrote_is_complete_to_this_client() {
1545        let mut st = ResState {
1546            sock_id: 0,
1547            head: false,
1548            status: 200,
1549            message: None,
1550            headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1551            body: b"onetwo".to_vec(),
1552        };
1553        let wire = serialize_response(&mut st);
1554        assert!(
1555            wire.ends_with(b"\r\n\r\n6\r\nonetwo\r\n0\r\n\r\n"),
1556            "unexpected framing: {}",
1557            String::from_utf8_lossy(&wire)
1558        );
1559        assert!(response_is_complete(&wire, false));
1560        // Every prefix short of the terminator must still read as incomplete,
1561        // so the client keeps reading rather than truncating the body.
1562        for cut in 1..wire.len() {
1563            assert!(
1564                !response_is_complete(&wire[..cut], false),
1565                "a {cut}-byte prefix was reported complete"
1566            );
1567        }
1568    }
1569
1570    /// An empty chunked body is still a terminator, not nothing.
1571    #[test]
1572    fn an_empty_chunked_response_is_just_the_zero_chunk() {
1573        let mut st = ResState {
1574            sock_id: 0,
1575            head: false,
1576            status: 200,
1577            message: None,
1578            headers: vec![("Transfer-Encoding".into(), "chunked".into())],
1579            body: Vec::new(),
1580        };
1581        let wire = serialize_response(&mut st);
1582        assert!(
1583            wire.ends_with(b"\r\n\r\n0\r\n\r\n"),
1584            "{}",
1585            String::from_utf8_lossy(&wire)
1586        );
1587        assert!(response_is_complete(&wire, false));
1588    }
1589
1590    /// A HEAD response keeps the headers a GET would send and drops the body.
1591    #[test]
1592    fn a_head_response_advertises_the_get_length_and_sends_no_body() {
1593        let mk = |head: bool| {
1594            let mut st = ResState {
1595                sock_id: 0,
1596                head,
1597                status: 200,
1598                message: None,
1599                headers: vec![("Content-Type".into(), "text/plain".into())],
1600                body: b"body-here".to_vec(),
1601            };
1602            serialize_response(&mut st)
1603        };
1604        let head = mk(true);
1605        let get = mk(false);
1606        let text = String::from_utf8_lossy(&head).into_owned();
1607        assert!(text.contains("Content-Length: 9"), "{text}");
1608        assert!(
1609            text.ends_with("\r\n\r\n"),
1610            "HEAD must stop at the blank line: {text}"
1611        );
1612        assert!(String::from_utf8_lossy(&get).ends_with("body-here"));
1613        // Identical headers, differing only in the body that follows.
1614        let split = |v: &[u8]| {
1615            String::from_utf8_lossy(v)
1616                .split("\r\n\r\n")
1617                .next()
1618                .unwrap()
1619                .to_string()
1620        };
1621        assert_eq!(split(&head), split(&get));
1622    }
1623
1624    #[test]
1625    fn a_partial_header_block_is_never_complete() {
1626        assert!(!response_is_complete(b"", false));
1627        assert!(!response_is_complete(b"HTTP/1.1 200 OK\r\n", false));
1628        assert!(!response_is_complete(
1629            b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n",
1630            false
1631        ));
1632    }
1633
1634    /// No `Content-Length` and no `Transfer-Encoding`: the connection close IS
1635    /// the framing, so the caller must keep reading to EOF.
1636    #[test]
1637    fn an_unframed_response_is_never_reported_complete() {
1638        let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbody bytes";
1639        assert!(!response_is_complete(raw, false));
1640    }
1641
1642    #[test]
1643    fn a_chunked_body_completes_only_at_its_zero_chunk() {
1644        let head = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
1645        let mut raw = head.to_vec();
1646        assert!(!response_is_complete(&raw, false));
1647        raw.extend_from_slice(b"5\r\nhello\r\n");
1648        assert!(
1649            !response_is_complete(&raw, false),
1650            "one chunk, no terminator"
1651        );
1652        raw.extend_from_slice(b"0\r\n");
1653        assert!(
1654            !response_is_complete(&raw, false),
1655            "trailer CRLF still missing"
1656        );
1657        raw.extend_from_slice(b"\r\n");
1658        assert!(response_is_complete(&raw, false));
1659    }
1660
1661    /// A chunk size line may carry extensions, and `Transfer-Encoding` wins
1662    /// over a `Content-Length` that arrives with it (RFC 9112 §6.3).
1663    #[test]
1664    fn chunk_extensions_parse_and_chunked_outranks_content_length() {
1665        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 99\r\nTransfer-Encoding: chunked\r\n\r\n\
1666                    3;name=v\r\nabc\r\n0\r\n\r\n";
1667        assert!(response_is_complete(raw, false));
1668        let short =
1669            b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\nabc";
1670        assert!(
1671            !response_is_complete(short, false),
1672            "Content-Length must not settle a chunked response"
1673        );
1674    }
1675
1676    /// 204 and 304 carry no body whatever the headers claim, and neither does
1677    /// any response to a HEAD.
1678    #[test]
1679    fn bodiless_statuses_and_head_finish_at_the_header_block() {
1680        assert!(response_is_complete(
1681            b"HTTP/1.1 204 No Content\r\nContent-Length: 7\r\n\r\n",
1682            false
1683        ));
1684        assert!(response_is_complete(
1685            b"HTTP/1.1 304 Not Modified\r\nContent-Length: 7\r\n\r\n",
1686            false
1687        ));
1688        assert!(response_is_complete(
1689            b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n",
1690            true
1691        ));
1692        assert!(
1693            !response_is_complete(b"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\n", false),
1694            "the same response to a GET still needs its body"
1695        );
1696    }
1697
1698    /// A 1xx is interim: the real response follows on the same connection.
1699    #[test]
1700    fn an_interim_response_does_not_end_the_read() {
1701        assert!(!response_is_complete(
1702            b"HTTP/1.1 100 Continue\r\n\r\n",
1703            false
1704        ));
1705        assert!(!response_is_complete(
1706            b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n",
1707            false
1708        ));
1709    }
1710}