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