Skip to main content

nodejs/stdlib/
http.rs

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