Skip to main content

nodejs/stdlib/
http2.rs

1//! Node `http2` module: a REAL, minimal HTTP/2 server over TLS+ALPN.
2//!
3//! This is a genuine HTTP/2 server (RFC 7540 framing + RFC 7541 HPACK via the
4//! `hpack` crate) layered on the same blocking-`rustls` model as `tls`/`https`.
5//! A real h2 client (`curl --http2`, `nghttp`, Node's `http2` client) can perform
6//! a GET and receive a response body from a server built with
7//! `http2.createSecureServer`.
8//!
9//! ── Threading model (identical discipline to `net`/`tls`) ────────────────────
10//! Background threads NEVER touch the JS heap (a main-thread `thread_local`). One
11//! thread per TCP connection owns the `rustls::StreamOwned`, performs the TLS
12//! handshake (negotiating ALPN `h2`), then runs the full HTTP/2 framing loop:
13//! reading/parsing frames and writing response frames. HPACK `Encoder`/`Decoder`
14//! (with their connection-scoped dynamic tables) live on that thread. Every
15//! JS-visible effect — building the `Http2Stream`/`Http2Session` objects, emitting
16//! `stream`/`session`/`request`, running listeners — happens on the main thread
17//! via posted `IoTask` closures. The stream objects talk back to their connection
18//! thread through an `mpsc` channel of `H2Cmd`s (`respond`/`write`/`end` enqueue a
19//! command; the owner thread encodes+writes the frame), so reads and writes stay
20//! on the single thread that owns the stateful TLS + HPACK cipher/table state.
21//!
22//! ── What IS implemented (server) ─────────────────────────────────────────────
23//!   * TLS handshake with ALPN offering only `h2`; non-`h2` clients are closed.
24//!   * Client connection preface check (`PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n`).
25//!   * SETTINGS: we send our (empty, valid) SETTINGS; we ACK the client's SETTINGS
26//!     and ignore incoming SETTINGS ACKs.
27//!   * HEADERS (type 1) inbound: PADDED + PRIORITY flags stripped, header block
28//!     HPACK-decoded into pseudo-headers (`:method`/`:path`/`:scheme`/`:authority`)
29//!     + regular headers. Fires `stream` (and compat `request`).
30//!   * DATA (type 0) inbound: emitted on the stream as `data`/`end` (request body).
31//!   * `Http2Stream.respond(headers)` → HPACK-encoded HEADERS with `:status`.
32//!   * `Http2Stream.write(data)` / `.end([data])` → DATA frames (END_STREAM on end),
33//!     chunked to <= 16384 (the default SETTINGS_MAX_FRAME_SIZE) per frame.
34//!   * PING (type 6): answered with a PING ACK echoing the 8-byte payload.
35//!   * WINDOW_UPDATE (type 8) / PRIORITY (type 2) / RST_STREAM (type 3): accepted
36//!     and ignored (large-enough default windows; see caveats).
37//!   * GOAWAY (type 7): sent on connection close; inbound GOAWAY ends the loop.
38//!
39//! ── Honest limitations (NOT implemented — deliberately, not faked) ────────────
40//!   * `http2.connect` (CLIENT) is NOT implemented — it throws a clear error.
41//!   * `http2.createServer` (cleartext h2c / prior-knowledge) is NOT implemented.
42//!   * CONTINUATION frames (type 9): a HEADERS frame WITHOUT END_HEADERS is not
43//!     reassembled — that stream is skipped. Real clients pack a single small GET
44//!     header block into one HEADERS frame, so this rarely triggers, but a very
45//!     large request header block would be dropped rather than mis-parsed.
46//!   * Flow control is minimal: we advertise/assume the default windows and IGNORE
47//!     inbound WINDOW_UPDATE for our own send side. A response body larger than the
48//!     peer's stream/connection window (65535 bytes by default) can STALL. Small
49//!     responses (the common case, and the test target) are unaffected.
50//!   * No server push, no trailers, no PRIORITY tree, no per-stream RST bookkeeping,
51//!     no ALTSVC/ORIGIN. These are absent, never stubbed to look present.
52//!
53//! ── Handler-fault isolation (why the server used to close right after HEADERS) ─
54//! The event loop treats an `Err` returned from a posted `IoTask` as FATAL
55//! (`host::drive_event_loop` runs `task()?`), which terminates the whole process
56//! and closes every live socket. `events::emit` propagates a listener's error
57//! (`invoke(&f, …)?`). So an exception thrown by the user's `stream`/`request`
58//! handler used to bubble out of the connection's `on_headers` IoTask and kill the
59//! server the instant the first request arrived — the client sees the TCP close as
60//! a "broken pipe" with no response. To prevent one faulty handler from taking
61//! down the server, the connection IoTasks (`on_headers`/`on_data`/`on_session`)
62//! CATCH handler errors, print them to stderr (like Node's uncaught-exception
63//! output, so the cause is visible), and return `Ok` — the loop and other
64//! connections survive. Set `HTTP2_DEBUG=1` to trace every frame in/out on stderr.
65//!
66//! ── Verification status ──────────────────────────────────────────────────────
67//! The framing (9-octet header layout, big-endian 24-bit length, frame type/flag
68//! values, HPACK usage) is written to RFC 7540/7541. It has NOT been compiled or
69//! run in this session (the parent owns the shared build), so the byte-level
70//! correctness is verified-by-construction against the RFCs, not by a live curl.
71//! See the final report for the exact `curl --http2` command to confirm it.
72
73use crate::host::{invoke, with_host, IoTask, JsObj};
74use fusevm::Value;
75use hpack::{Decoder, Encoder};
76use indexmap::IndexMap;
77use rustls::pki_types::{CertificateDer, PrivateKeyDer};
78use rustls::{ServerConfig, ServerConnection, StreamOwned};
79use std::collections::HashMap;
80use std::io::{Read, Write};
81use std::net::{TcpListener, TcpStream};
82use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
83use std::sync::mpsc::Sender;
84use std::sync::Arc;
85
86/// `http2` module functions routed through `stdlib::call`.
87pub const METHODS: &[&str] = &[
88    "createSecureServer",
89    "createServer",
90    "connect",
91    "getDefaultSettings",
92    "getPackedSettings",
93    "getUnpackedSettings",
94];
95
96/// Instance method names for the `@@native` tags this module owns (exposed to
97/// `stdlib::instance_has_method` so a method *read* yields a bound method).
98pub const SERVER_METHODS: &[&str] = &["listen", "close", "address", "setTimeout"];
99pub const STREAM_METHODS: &[&str] = &[
100    "respond",
101    "write",
102    "end",
103    "close",
104    "setEncoding",
105    "setTimeout",
106    "pause",
107    "resume",
108    // HTTP/1-compat surface (Http2ServerResponse), best-effort:
109    "writeHead",
110    "setHeader",
111    "getHeader",
112    "removeHeader",
113];
114pub const SESSION_METHODS: &[&str] = &[
115    "settings",
116    "ping",
117    "goaway",
118    "close",
119    "destroy",
120    "ref",
121    "unref",
122    "setTimeout",
123];
124
125// ── HTTP/2 constants (RFC 7540 §6, §11) ──────────────────────────────────────
126
127const PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
128
129// Frame types.
130const FT_DATA: u8 = 0x0;
131const FT_HEADERS: u8 = 0x1;
132const FT_PRIORITY: u8 = 0x2;
133const FT_RST_STREAM: u8 = 0x3;
134const FT_SETTINGS: u8 = 0x4;
135const FT_PING: u8 = 0x6;
136const FT_GOAWAY: u8 = 0x7;
137const FT_WINDOW_UPDATE: u8 = 0x8;
138const FT_CONTINUATION: u8 = 0x9;
139
140// Frame flags.
141const FL_END_STREAM: u8 = 0x1;
142const FL_ACK: u8 = 0x1; // SETTINGS/PING re-use bit 0 as ACK
143const FL_END_HEADERS: u8 = 0x4;
144const FL_PADDED: u8 = 0x8;
145const FL_PRIORITY: u8 = 0x20;
146
147/// Default SETTINGS_MAX_FRAME_SIZE — the largest DATA payload we emit per frame.
148const MAX_FRAME_SIZE: usize = 16384;
149
150// ── process-global id sources (ids minted on background threads) ─────────────
151
152static NEXT_SERVER_ID: AtomicU64 = AtomicU64::new(1);
153static NEXT_STREAM_KEY: AtomicU64 = AtomicU64::new(1);
154static NEXT_SESSION_KEY: AtomicU64 = AtomicU64::new(1);
155
156fn next_server_id() -> u64 {
157    NEXT_SERVER_ID.fetch_add(1, Ordering::Relaxed)
158}
159fn next_stream_key() -> u64 {
160    NEXT_STREAM_KEY.fetch_add(1, Ordering::Relaxed)
161}
162fn next_session_key() -> u64 {
163    NEXT_SESSION_KEY.fetch_add(1, Ordering::Relaxed)
164}
165
166// ── commands from the main thread to a connection's owner thread ─────────────
167
168/// A frame the owner thread should encode + write, produced by JS method calls.
169enum H2Cmd {
170    /// Send a HEADERS frame carrying the given (already ordered, `:status`-first)
171    /// header list; `end` sets END_STREAM (a header-only response).
172    Respond {
173        stream_id: u32,
174        headers: Vec<(String, String)>,
175        end: bool,
176    },
177    /// Send DATA (chunked to MAX_FRAME_SIZE); `end` sets END_STREAM on the last.
178    Data {
179        stream_id: u32,
180        data: Vec<u8>,
181        end: bool,
182    },
183    /// RST_STREAM with NO_ERROR.
184    Close { stream_id: u32 },
185    /// GOAWAY(NO_ERROR) then terminate the owner loop.
186    Goaway,
187}
188
189// ── main-thread state ────────────────────────────────────────────────────────
190
191struct H2ServerRec {
192    emitter: Value,
193    stop: Arc<AtomicBool>,
194}
195
196struct H2StreamRec {
197    emitter: Value,
198    tx: Sender<H2Cmd>,
199    stream_id: u32,
200    /// True once a HEADERS response frame has been sent (respond/writeHead).
201    responded: bool,
202}
203
204struct H2SessionRec {
205    #[allow(dead_code)]
206    emitter: Value,
207    tx: Sender<H2Cmd>,
208}
209
210#[derive(Default)]
211struct H2State {
212    servers: HashMap<u64, H2ServerRec>,
213    streams: HashMap<u64, H2StreamRec>,
214    sessions: HashMap<u64, H2SessionRec>,
215}
216
217thread_local! {
218    static H2: std::cell::RefCell<H2State> = std::cell::RefCell::new(H2State::default());
219    /// `ServerConfig`s built by `createSecureServer` before `listen` assigns an id
220    /// (mirrors `tls::PENDING_CONFIGS`).
221    static PENDING_CONFIGS: std::cell::RefCell<Vec<(Value, Arc<ServerConfig>)>> =
222        const { std::cell::RefCell::new(Vec::new()) };
223}
224
225// ── shared object / prop helpers (same shape as net/tls) ─────────────────────
226
227fn get_prop(recv: &Value, key: &str) -> Option<Value> {
228    with_host(|h| match h.get(recv) {
229        Some(JsObj::Object(p)) => p.get(key).cloned(),
230        _ => None,
231    })
232}
233
234fn set_prop(recv: &Value, key: &str, val: Value) {
235    with_host(|h| {
236        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
237            p.insert(key.to_string(), val);
238        }
239    });
240}
241
242fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
243    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
244}
245
246/// Delegate the EventEmitter surface to `events`; `None` for a non-emitter method.
247fn emitter_dispatch(recv: &Value, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
248    super::events::METHODS
249        .contains(&method)
250        .then(|| super::events::instance_call(recv, method, args.to_vec()))
251}
252
253/// Raw bytes of a `write`/`end`/`key`/`cert` argument (Buffer bytes or UTF-8).
254fn value_bytes(v: Option<&Value>) -> Vec<u8> {
255    let Some(v) = v else { return Vec::new() };
256    let is_buffer =
257        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
258    if is_buffer {
259        return with_host(|h| match h.get(v) {
260            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
261                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
262                _ => Vec::new(),
263            },
264            _ => Vec::new(),
265        });
266    }
267    with_host(|h| h.str_of(v)).into_bytes()
268}
269
270/// Enumerable string key/value pairs of a plain object (for a `respond`/`writeHead`
271/// headers argument). Hidden `@@`/`#` keys are skipped.
272fn object_pairs(obj: &Value) -> Vec<(String, String)> {
273    with_host(|h| match h.get(obj) {
274        Some(JsObj::Object(p)) => p
275            .iter()
276            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
277            .map(|(k, v)| (k.clone(), h.str_of(v)))
278            .collect(),
279        _ => Vec::new(),
280    })
281}
282
283// ── module entry ─────────────────────────────────────────────────────────────
284
285/// `stdlib::call` entry for `http2.<method>`.
286pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
287    Some(match method {
288        "createSecureServer" => create_secure_server(args),
289        "createServer" => Err(
290            "Error: http2.createServer (cleartext h2c / prior-knowledge) \
291            is not implemented in node-js; use http2.createSecureServer (h2 over TLS)"
292                .to_string(),
293        ),
294        "connect" => Err(
295            "Error: http2.connect (HTTP/2 client) is not implemented in node-js; \
296            only the HTTP/2 server (http2.createSecureServer) is implemented"
297                .to_string(),
298        ),
299        "getDefaultSettings" => Ok(default_settings_object()),
300        "getPackedSettings" => Ok(pack_settings(args)),
301        "getUnpackedSettings" => unpack_settings(args),
302        _ => return None,
303    })
304}
305
306/// `http2.constants` and other non-function properties, reachable via
307/// `namespace_property` → `stdlib::constant`.
308pub fn constant(name: &str) -> Option<Value> {
309    match name {
310        "constants" => Some(constants_object()),
311        // Expose ctor namespaces so `http2.Http2ServerRequest.prototype` etc. resolve
312        // to *something* (they are not separately constructable here).
313        "Http2ServerRequest" => Some(with_host(|h| {
314            h.alloc(JsObj::Builtin("Http2ServerRequest".into()))
315        })),
316        "Http2ServerResponse" => Some(with_host(|h| {
317            h.alloc(JsObj::Builtin("Http2ServerResponse".into()))
318        })),
319        _ => None,
320    }
321}
322
323/// The `http2.constants` object (a representative subset of RFC 7540 / nghttp2
324/// names — HTTP status codes, header-name constants, and error/settings codes).
325fn constants_object() -> Value {
326    with_host(|h| {
327        let mut m = IndexMap::new();
328        let put_i = |m: &mut IndexMap<String, Value>, k: &str, v: i64| {
329            m.insert(k.to_string(), Value::Float(v as f64));
330        };
331        // Common HTTP status constants.
332        put_i(&mut m, "HTTP_STATUS_OK", 200);
333        put_i(&mut m, "HTTP_STATUS_NO_CONTENT", 204);
334        put_i(&mut m, "HTTP_STATUS_MOVED_PERMANENTLY", 301);
335        put_i(&mut m, "HTTP_STATUS_FOUND", 302);
336        put_i(&mut m, "HTTP_STATUS_NOT_MODIFIED", 304);
337        put_i(&mut m, "HTTP_STATUS_BAD_REQUEST", 400);
338        put_i(&mut m, "HTTP_STATUS_UNAUTHORIZED", 401);
339        put_i(&mut m, "HTTP_STATUS_FORBIDDEN", 403);
340        put_i(&mut m, "HTTP_STATUS_NOT_FOUND", 404);
341        put_i(&mut m, "HTTP_STATUS_INTERNAL_SERVER_ERROR", 500);
342        // NGHTTP2 error codes (RFC 7540 §7).
343        put_i(&mut m, "NGHTTP2_NO_ERROR", 0x0);
344        put_i(&mut m, "NGHTTP2_PROTOCOL_ERROR", 0x1);
345        put_i(&mut m, "NGHTTP2_INTERNAL_ERROR", 0x2);
346        put_i(&mut m, "NGHTTP2_FLOW_CONTROL_ERROR", 0x3);
347        put_i(&mut m, "NGHTTP2_SETTINGS_TIMEOUT", 0x4);
348        put_i(&mut m, "NGHTTP2_STREAM_CLOSED", 0x5);
349        put_i(&mut m, "NGHTTP2_FRAME_SIZE_ERROR", 0x6);
350        put_i(&mut m, "NGHTTP2_REFUSED_STREAM", 0x7);
351        put_i(&mut m, "NGHTTP2_CANCEL", 0x8);
352        put_i(&mut m, "NGHTTP2_COMPRESSION_ERROR", 0x9);
353        put_i(&mut m, "NGHTTP2_ENHANCE_YOUR_CALM", 0xb);
354        // SETTINGS identifiers (RFC 7540 §6.5.2).
355        put_i(&mut m, "NGHTTP2_SETTINGS_HEADER_TABLE_SIZE", 0x1);
356        put_i(&mut m, "NGHTTP2_SETTINGS_ENABLE_PUSH", 0x2);
357        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS", 0x3);
358        put_i(&mut m, "NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE", 0x4);
359        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_FRAME_SIZE", 0x5);
360        put_i(&mut m, "NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE", 0x6);
361        // A representative subset of the HTTP2_HEADER_* string constants.
362        let hdr =
363            |m: &mut IndexMap<String, Value>, k: &str, v: &str, h: &mut crate::host::JsHost| {
364                let s = h.new_str(v);
365                m.insert(k.to_string(), s);
366            };
367        hdr(&mut m, "HTTP2_HEADER_STATUS", ":status", h);
368        hdr(&mut m, "HTTP2_HEADER_METHOD", ":method", h);
369        hdr(&mut m, "HTTP2_HEADER_AUTHORITY", ":authority", h);
370        hdr(&mut m, "HTTP2_HEADER_SCHEME", ":scheme", h);
371        hdr(&mut m, "HTTP2_HEADER_PATH", ":path", h);
372        hdr(&mut m, "HTTP2_HEADER_CONTENT_TYPE", "content-type", h);
373        hdr(&mut m, "HTTP2_HEADER_CONTENT_LENGTH", "content-length", h);
374        hdr(&mut m, "HTTP2_METHOD_GET", "GET", h);
375        hdr(&mut m, "HTTP2_METHOD_POST", "POST", h);
376        h.new_object(m)
377    })
378}
379
380/// A minimal default-settings object (Node's `http2.getDefaultSettings()` shape).
381fn default_settings_object() -> Value {
382    with_host(|h| {
383        let mut m = IndexMap::new();
384        m.insert("headerTableSize".into(), Value::Float(4096.0));
385        m.insert("enablePush".into(), Value::Bool(false));
386        m.insert("initialWindowSize".into(), Value::Float(65535.0));
387        m.insert("maxFrameSize".into(), Value::Float(MAX_FRAME_SIZE as f64));
388        m.insert("maxConcurrentStreams".into(), Value::Float(100.0));
389        h.new_object(m)
390    })
391}
392
393// ── getPackedSettings / getUnpackedSettings (RFC 7540 §6.5.1 wire form) ───────
394
395/// Append one SETTINGS entry (2-byte identifier + 4-byte value, big-endian).
396fn push_setting(out: &mut Vec<u8>, id: u16, val: u32) {
397    out.extend_from_slice(&id.to_be_bytes());
398    out.extend_from_slice(&val.to_be_bytes());
399}
400
401/// `http2.getPackedSettings(settings)` — serialize the SETTINGS values present on
402/// `settings` into the RFC 7540 §6.5.1 wire form (6 octets per entry: a 2-byte
403/// identifier followed by a 4-byte value, big-endian). Only keys actually present
404/// on the object are emitted, in the canonical identifier order (1..6) Node uses;
405/// this is the exact inverse of `getUnpackedSettings`.
406fn pack_settings(args: &[Value]) -> Value {
407    let settings = args.first().cloned().unwrap_or(Value::Undef);
408    let num = |key: &str| -> Option<u32> {
409        get_prop(&settings, key)
410            .filter(|v| !matches!(v, Value::Undef))
411            .map(|v| with_host(|h| h.to_number(&v)) as u32)
412    };
413    let mut out: Vec<u8> = Vec::new();
414    if let Some(v) = num("headerTableSize") {
415        push_setting(&mut out, 0x1, v);
416    }
417    if let Some(p) = get_prop(&settings, "enablePush").filter(|v| !matches!(v, Value::Undef)) {
418        let on = with_host(|h| h.truthy(&p));
419        push_setting(&mut out, 0x2, u32::from(on));
420    }
421    if let Some(v) = num("maxConcurrentStreams") {
422        push_setting(&mut out, 0x3, v);
423    }
424    if let Some(v) = num("initialWindowSize") {
425        push_setting(&mut out, 0x4, v);
426    }
427    if let Some(v) = num("maxFrameSize") {
428        push_setting(&mut out, 0x5, v);
429    }
430    if let Some(v) = num("maxHeaderListSize").or_else(|| num("maxHeaderSize")) {
431        push_setting(&mut out, 0x6, v);
432    }
433    super::buffer::from_bytes(&out)
434}
435
436/// `http2.getUnpackedSettings(buf)` — parse a packed SETTINGS payload (6 octets per
437/// entry: 2-byte identifier + 4-byte value, big-endian) back into a settings object
438/// (`{ headerTableSize, enablePush, maxConcurrentStreams, initialWindowSize,
439/// maxFrameSize, maxHeaderSize, maxHeaderListSize }`, only the keys present in the
440/// buffer). The inverse of `getPackedSettings`. Throws `ERR_HTTP2_INVALID_PACKED_
441/// SETTINGS_LENGTH` when the length is not a multiple of six.
442fn unpack_settings(args: &[Value]) -> Result<Value, String> {
443    let bytes = value_bytes(args.first());
444    if bytes.len() % 6 != 0 {
445        return Err("RangeError [ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH]: \
446                    Packed settings length must be a multiple of six"
447            .to_string());
448    }
449    let mut m = IndexMap::new();
450    for chunk in bytes.chunks_exact(6) {
451        let id = u16::from_be_bytes([chunk[0], chunk[1]]);
452        let val = u32::from_be_bytes([chunk[2], chunk[3], chunk[4], chunk[5]]);
453        match id {
454            0x1 => {
455                m.insert("headerTableSize".to_string(), Value::Float(val as f64));
456            }
457            0x2 => {
458                m.insert("enablePush".to_string(), Value::Bool(val != 0));
459            }
460            0x3 => {
461                m.insert("maxConcurrentStreams".to_string(), Value::Float(val as f64));
462            }
463            0x4 => {
464                m.insert("initialWindowSize".to_string(), Value::Float(val as f64));
465            }
466            0x5 => {
467                m.insert("maxFrameSize".to_string(), Value::Float(val as f64));
468            }
469            // Identifier 6 maps to BOTH `maxHeaderSize` and its `maxHeaderListSize`
470            // alias (Node emits both, in that order).
471            0x6 => {
472                m.insert("maxHeaderSize".to_string(), Value::Float(val as f64));
473                m.insert("maxHeaderListSize".to_string(), Value::Float(val as f64));
474            }
475            // Unknown identifiers are ignored per RFC 7540 §6.5.
476            _ => {}
477        }
478    }
479    Ok(with_host(|h| h.new_object(m)))
480}
481
482// ── http2.createSecureServer ─────────────────────────────────────────────────
483
484/// `http2.createSecureServer(options[, onRequestHandler])`. Parses `key`+`cert`
485/// into a `ServerConfig` with ALPN `h2` (a bad cert throws synchronously) and
486/// returns an `Http2Server` emitter. A supplied handler is registered as a compat
487/// `request` listener (Node semantics).
488fn create_secure_server(args: &[Value]) -> Result<Value, String> {
489    let mut options: Option<Value> = None;
490    let mut handler: Option<Value> = None;
491    for a in args {
492        if with_host(|h| crate::host::is_callable(h, a)) {
493            handler = Some(a.clone());
494        } else if matches!(a, Value::Obj(_)) {
495            options = Some(a.clone());
496        }
497    }
498    let opts = options.ok_or_else(|| {
499        crate::host::type_error("http2.createSecureServer requires options with `key` and `cert`")
500    })?;
501    let cert = value_bytes(get_prop(&opts, "cert").as_ref());
502    let key = value_bytes(get_prop(&opts, "key").as_ref());
503    if cert.is_empty() || key.is_empty() {
504        return Err(crate::host::type_error(
505            "http2.createSecureServer requires `key` and `cert`",
506        ));
507    }
508    let config = build_h2_server_config(&cert, &key)?;
509
510    let server = new_emitter_object("Http2Server", IndexMap::new());
511    if let Some(cb) = handler {
512        // Node registers the createSecureServer handler as a `request` listener.
513        super::events::instance_call(&server, "on", vec![with_host(|h| h.new_str("request")), cb])?;
514    }
515    PENDING_CONFIGS.with(|p| p.borrow_mut().push((server.clone(), config)));
516    Ok(server)
517}
518
519/// Build a `ServerConfig` from PEM `key`+`cert`, offering ALPN `h2` only.
520fn build_h2_server_config(cert_pem: &[u8], key_pem: &[u8]) -> Result<Arc<ServerConfig>, String> {
521    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut &cert_pem[..])
522        .collect::<Result<_, _>>()
523        .map_err(|e| format!("Error: http2: bad certificate PEM: {e}"))?;
524    if certs.is_empty() {
525        return Err("Error: http2: no certificates found in `cert`".to_string());
526    }
527    let key: PrivateKeyDer<'static> = rustls_pemfile::private_key(&mut &key_pem[..])
528        .map_err(|e| format!("Error: http2: bad private key PEM: {e}"))?
529        .ok_or_else(|| "Error: http2: no private key found in `key`".to_string())?;
530    let mut cfg = ServerConfig::builder()
531        .with_no_client_auth()
532        .with_single_cert(certs, key)
533        .map_err(|e| format!("Error: http2: invalid key/cert: {e}"))?;
534    // ALPN: offer only "h2" so the handshake negotiates HTTP/2.
535    cfg.alpn_protocols = vec![b"h2".to_vec()];
536    Ok(Arc::new(cfg))
537}
538
539fn take_pending_config(server: &Value) -> Option<Arc<ServerConfig>> {
540    PENDING_CONFIGS.with(|p| {
541        let mut p = p.borrow_mut();
542        p.iter()
543            .position(|(s, _)| s == server)
544            .map(|pos| p.remove(pos).1)
545    })
546}
547
548// ── instance dispatch (Http2Server / Http2Stream / Http2Session) ─────────────
549
550pub fn instance_call(
551    tag: &str,
552    recv: &Value,
553    method: &str,
554    args: Vec<Value>,
555) -> Result<Value, String> {
556    match tag {
557        "Http2Server" => server_call(recv, method, args),
558        "Http2Stream" => stream_call(recv, method, args),
559        "Http2Session" => session_call(recv, method, args),
560        _ => Err(crate::host::type_error(&format!(
561            "{method} is not a function"
562        ))),
563    }
564}
565
566fn server_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
567    if let Some(r) = emitter_dispatch(recv, method, &args) {
568        return r;
569    }
570    match method {
571        "listen" => server_listen(recv, &args),
572        "close" => server_close(recv, &args),
573        "address" => Ok(get_prop(recv, "@@address").unwrap_or(Value::Undef)),
574        "setTimeout" => Ok(recv.clone()),
575        _ => Err(crate::host::type_error(&format!(
576            "server.{method} is not a function"
577        ))),
578    }
579}
580
581/// `server.listen(port[, host][, callback])`. Binds on the main thread, spawns the
582/// accept loop, and fires `listening` + callback asynchronously.
583fn server_listen(recv: &Value, args: &[Value]) -> Result<Value, String> {
584    let port = with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(0.0)) as u16;
585    let mut host = "0.0.0.0".to_string();
586    let mut cb: Option<Value> = None;
587    for a in &args[1.min(args.len())..] {
588        if with_host(|h| h.as_str(a)).is_some() {
589            host = with_host(|h| h.str_of(a));
590        } else if with_host(|h| crate::host::is_callable(h, a)) {
591            cb = Some(a.clone());
592        }
593    }
594
595    let config = take_pending_config(recv)
596        .ok_or_else(|| crate::host::type_error("http2 server has no secure context"))?;
597    let listener = TcpListener::bind((host.as_str(), port))
598        .map_err(|e| format!("Error: listen EADDRINUSE: {e}"))?;
599    let local = listener.local_addr().ok();
600
601    let id = next_server_id();
602    set_prop(recv, "@@serverid", Value::Float(id as f64));
603    if let Some(addr) = local {
604        let mut a = IndexMap::new();
605        a.insert("port".into(), Value::Float(addr.port() as f64));
606        a.insert(
607            "address".into(),
608            with_host(|h| h.new_str(addr.ip().to_string())),
609        );
610        a.insert(
611            "family".into(),
612            with_host(|h| h.new_str(if addr.is_ipv6() { "IPv6" } else { "IPv4" })),
613        );
614        let addr_obj = with_host(|h| h.new_object(a));
615        set_prop(recv, "@@address", addr_obj);
616    }
617    let stop = Arc::new(AtomicBool::new(false));
618    H2.with(|s| {
619        s.borrow_mut().servers.insert(
620            id,
621            H2ServerRec {
622                emitter: recv.clone(),
623                stop: stop.clone(),
624            },
625        );
626    });
627    with_host(|h| h.incr_handle());
628
629    let io_tx = with_host(|h| h.io_sender());
630    listener.set_nonblocking(true).ok();
631    std::thread::spawn(move || loop {
632        if stop.load(Ordering::Acquire) {
633            break;
634        }
635        match listener.accept() {
636            Ok((stream, _addr)) => {
637                let cfg = config.clone();
638                let tx = io_tx.clone();
639                std::thread::spawn(move || serve_connection(id, stream, cfg, tx));
640            }
641            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
642                std::thread::sleep(std::time::Duration::from_millis(5));
643            }
644            Err(_) => break,
645        }
646    });
647
648    let server = recv.clone();
649    let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
650        super::events::instance_call(&server, "emit", vec![with_host(|h| h.new_str("listening"))])?;
651        if let Some(cb) = cb {
652            invoke(&cb, Vec::new(), None)?;
653        }
654        Ok(())
655    }));
656    Ok(recv.clone())
657}
658
659fn server_close(recv: &Value, args: &[Value]) -> Result<Value, String> {
660    if let Some(id) = u64_prop(recv, "@@serverid") {
661        let rec = H2.with(|s| s.borrow_mut().servers.remove(&id));
662        if let Some(rec) = rec {
663            rec.stop.store(true, Ordering::Release);
664            with_host(|h| h.decr_handle());
665            let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
666        }
667    }
668    if let Some(cb) = args
669        .first()
670        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
671    {
672        invoke(cb, Vec::new(), None)?;
673    }
674    super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
675    Ok(recv.clone())
676}
677
678// ── the per-connection owner thread: TLS + HTTP/2 framing ────────────────────
679
680/// One background thread per accepted TCP connection: complete the TLS handshake
681/// (negotiating ALPN `h2`), then run the HTTP/2 framing loop. Owns the
682/// `StreamOwned` and the HPACK `Encoder`/`Decoder` for the whole connection.
683fn serve_connection(
684    server_id: u64,
685    mut sock: TcpStream,
686    config: Arc<ServerConfig>,
687    io_tx: Sender<IoTask>,
688) {
689    // The socket is inherited non-blocking from the accept poll loop; put it in
690    // blocking mode so the TLS handshake and the initial SETTINGS write can't
691    // fail with WouldBlock. A per-read timeout (set later) gives the framing loop
692    // its read/write interleave without making writes non-blocking.
693    sock.set_nonblocking(false).ok();
694    let mut conn = match ServerConnection::new(config) {
695        Ok(c) => c,
696        Err(_) => return,
697    };
698    if conn.complete_io(&mut sock).is_err() {
699        return;
700    }
701    // ALPN must have negotiated "h2"; otherwise this is not an HTTP/2 connection.
702    let is_h2 = conn.alpn_protocol().map(|p| p == b"h2").unwrap_or(false);
703    let mut stream = StreamOwned::new(conn, sock);
704    if !is_h2 {
705        // Honest fallback: we only speak h2. Close the connection.
706        stream.conn.send_close_notify();
707        let _ = stream.flush();
708        let _ = stream.sock.shutdown(std::net::Shutdown::Both);
709        return;
710    }
711    // Command channel: main-thread stream/session methods post frames to write.
712    let (tx, rx) = std::sync::mpsc::channel::<H2Cmd>();
713
714    // Announce the session on the main thread.
715    let session_key = next_session_key();
716    {
717        let tx_sess = tx.clone();
718        let _ = io_tx.send(Box::new(move || {
719            on_session(server_id, session_key, tx_sess)
720        }));
721    }
722
723    if h2_debug() {
724        eprintln!("[http2] connection {session_key}: ALPN h2 negotiated, starting framing loop");
725    }
726
727    // Send our SETTINGS immediately (empty payload is valid). This runs in
728    // BLOCKING mode — the read timeout is set AFTERWARDS, because rustls's write
729    // internally completes pending handshake I/O (a read), and a read-timeout set
730    // beforehand makes that read WouldBlock → the write fails → the loop never
731    // starts (the connection-startup race).
732    let mut ok = true;
733    ok &= write_frame(&mut stream, FT_SETTINGS, 0, 0, &[]).is_ok();
734    ok &= stream.flush().is_ok();
735    // Now switch to a short read timeout so the loop interleaves reads with the
736    // outbound-command drain.
737    stream
738        .sock
739        .set_read_timeout(Some(std::time::Duration::from_millis(20)))
740        .ok();
741
742    let mut decoder = Decoder::new();
743    let mut encoder = Encoder::new();
744    let mut inbuf: Vec<u8> = Vec::new();
745    let mut got_preface = false;
746    let mut max_stream_id: u32 = 0;
747    // Map stream-id → main-thread stream key (for routing inbound DATA).
748    let mut id_to_key: HashMap<u32, u64> = HashMap::new();
749    let mut buf = [0u8; MAX_FRAME_SIZE];
750
751    'conn: loop {
752        // If the initial SETTINGS write failed the connection is already dead;
753        // fall through to the GOAWAY/close cleanup below. (`ok` is only ever set
754        // before the loop, so this is the entry gate, not a per-iteration test.)
755        if !ok {
756            break;
757        }
758        // 1) Drain queued outbound commands (respond/write/end frames enqueued by
759        //    the main thread). This runs on EVERY loop iteration — including after
760        //    a WouldBlock read — so async responses are written promptly.
761        loop {
762            match rx.try_recv() {
763                Ok(cmd) => {
764                    if h2_debug() {
765                        eprintln!(
766                            "[http2] connection {session_key}: draining {}",
767                            cmd_name(&cmd)
768                        );
769                    }
770                    if !apply_cmd(&mut stream, &mut encoder, max_stream_id, cmd) {
771                        if h2_debug() {
772                            eprintln!("[http2] connection {session_key}: write failed, closing");
773                        }
774                        break 'conn;
775                    }
776                }
777                Err(std::sync::mpsc::TryRecvError::Empty) => break,
778                Err(std::sync::mpsc::TryRecvError::Disconnected) => break,
779            }
780        }
781
782        // 2) Read whatever plaintext (HTTP/2 frames) is available.
783        match stream.read(&mut buf) {
784            Ok(0) => {
785                if h2_debug() {
786                    eprintln!("[http2] connection {session_key}: read EOF (Ok 0)");
787                }
788                break;
789            }
790            Ok(n) => inbuf.extend_from_slice(&buf[..n]),
791            Err(ref e)
792                if matches!(
793                    e.kind(),
794                    std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
795                ) =>
796            {
797                continue;
798            }
799            Err(e) => {
800                if h2_debug() {
801                    eprintln!(
802                        "[http2] connection {session_key}: read error {:?} ({e})",
803                        e.kind()
804                    );
805                }
806                break;
807            }
808        }
809
810        // 3) Consume the client connection preface (first 24 octets).
811        if !got_preface {
812            if inbuf.len() < PREFACE.len() {
813                continue;
814            }
815            if &inbuf[..PREFACE.len()] != PREFACE {
816                break; // not an HTTP/2 connection preface — bail.
817            }
818            inbuf.drain(..PREFACE.len());
819            got_preface = true;
820        }
821
822        // 4) Parse and dispatch every complete frame currently buffered.
823        loop {
824            if inbuf.len() < 9 {
825                break;
826            }
827            let len =
828                ((inbuf[0] as usize) << 16) | ((inbuf[1] as usize) << 8) | (inbuf[2] as usize);
829            if inbuf.len() < 9 + len {
830                break; // await the rest of the frame body.
831            }
832            let ftype = inbuf[3];
833            let flags = inbuf[4];
834            let stream_id =
835                u32::from_be_bytes([inbuf[5], inbuf[6], inbuf[7], inbuf[8]]) & 0x7fff_ffff;
836            let payload: Vec<u8> = inbuf[9..9 + len].to_vec();
837            inbuf.drain(..9 + len);
838            if h2_debug() {
839                eprintln!(
840                    "[http2] connection {session_key}: recv frame type={ftype} flags={flags:#04x} \
841                     stream={stream_id} len={len}"
842                );
843            }
844
845            match ftype {
846                FT_SETTINGS => {
847                    if flags & FL_ACK == 0 {
848                        // ACK the client's SETTINGS (empty payload, ACK flag).
849                        if write_frame(&mut stream, FT_SETTINGS, FL_ACK, 0, &[])
850                            .and_then(|_| stream.flush())
851                            .is_err()
852                        {
853                            break 'conn;
854                        }
855                    }
856                }
857                FT_PING => {
858                    if flags & FL_ACK == 0 {
859                        // Echo the 8-byte opaque data with the ACK flag set.
860                        if write_frame(&mut stream, FT_PING, FL_ACK, 0, &payload)
861                            .and_then(|_| stream.flush())
862                            .is_err()
863                        {
864                            break 'conn;
865                        }
866                    }
867                }
868                FT_HEADERS => {
869                    if stream_id > max_stream_id {
870                        max_stream_id = stream_id;
871                    }
872                    if flags & FL_END_HEADERS == 0 {
873                        // CONTINUATION reassembly is not implemented; skip this stream.
874                        continue;
875                    }
876                    let block = strip_headers_padding_priority(&payload, flags);
877                    let decoded = match decoder.decode(&block) {
878                        Ok(d) => d,
879                        Err(_) => continue,
880                    };
881                    let headers: Vec<(String, String)> = decoded
882                        .into_iter()
883                        .map(|(k, v)| {
884                            (
885                                String::from_utf8_lossy(&k).into_owned(),
886                                String::from_utf8_lossy(&v).into_owned(),
887                            )
888                        })
889                        .collect();
890                    let end_stream = flags & FL_END_STREAM != 0;
891                    let key = next_stream_key();
892                    id_to_key.insert(stream_id, key);
893                    let tx_stream = tx.clone();
894                    let _ = io_tx.send(Box::new(move || {
895                        on_headers(server_id, key, stream_id, headers, end_stream, tx_stream)
896                    }));
897                }
898                FT_DATA => {
899                    if let Some(&key) = id_to_key.get(&stream_id) {
900                        let data = strip_data_padding(&payload, flags);
901                        let end_stream = flags & FL_END_STREAM != 0;
902                        let _ = io_tx.send(Box::new(move || on_data(key, data, end_stream)));
903                    }
904                }
905                FT_GOAWAY => break 'conn,
906                // WINDOW_UPDATE / PRIORITY / RST_STREAM / CONTINUATION: accepted and
907                // ignored (large-enough default windows; see module limitations).
908                FT_WINDOW_UPDATE | FT_PRIORITY | FT_RST_STREAM | FT_CONTINUATION => {}
909                // Unknown/extension frame types: ignored per RFC 7540 §4.1.
910                _ => {}
911            }
912        }
913    }
914
915    // Best-effort GOAWAY then close.
916    if h2_debug() {
917        eprintln!(
918            "[http2] connection {session_key}: framing loop exited, sending GOAWAY + closing"
919        );
920    }
921    let mut goaway = Vec::with_capacity(8);
922    goaway.extend_from_slice(&(max_stream_id & 0x7fff_ffff).to_be_bytes());
923    goaway.extend_from_slice(&0u32.to_be_bytes()); // NO_ERROR
924    let _ = write_frame(&mut stream, FT_GOAWAY, 0, 0, &goaway);
925    let _ = stream.flush();
926    stream.conn.send_close_notify();
927    let _ = stream.flush();
928    let _ = stream.sock.shutdown(std::net::Shutdown::Both);
929
930    // Release the per-connection handle and drop this connection's records on the
931    // main thread (balances the `incr_handle` in `on_session`).
932    let stream_keys: Vec<u64> = id_to_key.values().copied().collect();
933    let _ = io_tx.send(Box::new(move || on_session_close(session_key, stream_keys)));
934}
935
936/// A short label for an `H2Cmd` (diagnostic tracing only).
937fn cmd_name(cmd: &H2Cmd) -> &'static str {
938    match cmd {
939        H2Cmd::Respond { .. } => "respond(HEADERS)",
940        H2Cmd::Data { .. } => "data(DATA)",
941        H2Cmd::Close { .. } => "close(RST_STREAM)",
942        H2Cmd::Goaway => "goaway(GOAWAY)",
943    }
944}
945
946/// Apply one outbound command by encoding + writing its frame(s). Returns false on
947/// a write error or an explicit GOAWAY (terminating the owner loop).
948fn apply_cmd(
949    stream: &mut StreamOwned<ServerConnection, TcpStream>,
950    encoder: &mut Encoder<'_>,
951    max_stream_id: u32,
952    cmd: H2Cmd,
953) -> bool {
954    match cmd {
955        H2Cmd::Respond {
956            stream_id,
957            headers,
958            end,
959        } => {
960            let block = encode_header_block(encoder, &headers);
961            if h2_debug() {
962                eprintln!(
963                    "[http2] write HEADERS stream={stream_id} end_stream={end} \
964                     hpack_len={} headers={headers:?}",
965                    block.len()
966                );
967            }
968            let flags = FL_END_HEADERS | if end { FL_END_STREAM } else { 0 };
969            write_frame(stream, FT_HEADERS, flags, stream_id, &block)
970                .and_then(|_| stream.flush())
971                .is_ok()
972        }
973        H2Cmd::Data {
974            stream_id,
975            data,
976            end,
977        } => {
978            if h2_debug() {
979                eprintln!(
980                    "[http2] write DATA stream={stream_id} len={} end_stream={end}",
981                    data.len()
982                );
983            }
984            send_data(stream, stream_id, &data, end)
985        }
986        H2Cmd::Close { stream_id } => {
987            // RST_STREAM(NO_ERROR).
988            write_frame(stream, FT_RST_STREAM, 0, stream_id, &0u32.to_be_bytes())
989                .and_then(|_| stream.flush())
990                .is_ok()
991        }
992        H2Cmd::Goaway => {
993            let mut g = Vec::with_capacity(8);
994            g.extend_from_slice(&(max_stream_id & 0x7fff_ffff).to_be_bytes());
995            g.extend_from_slice(&0u32.to_be_bytes());
996            let _ = write_frame(stream, FT_GOAWAY, 0, 0, &g);
997            let _ = stream.flush();
998            false
999        }
1000    }
1001}
1002
1003/// Send a body as DATA frames, chunked to MAX_FRAME_SIZE. END_STREAM is set on the
1004/// final frame when `end` is true (an empty body still emits one END_STREAM DATA).
1005fn send_data(
1006    stream: &mut StreamOwned<ServerConnection, TcpStream>,
1007    stream_id: u32,
1008    data: &[u8],
1009    end: bool,
1010) -> bool {
1011    if data.is_empty() {
1012        let flags = if end { FL_END_STREAM } else { 0 };
1013        return write_frame(stream, FT_DATA, flags, stream_id, &[])
1014            .and_then(|_| stream.flush())
1015            .is_ok();
1016    }
1017    let chunks: Vec<&[u8]> = data.chunks(MAX_FRAME_SIZE).collect();
1018    let last = chunks.len() - 1;
1019    for (i, chunk) in chunks.iter().enumerate() {
1020        let flags = if end && i == last { FL_END_STREAM } else { 0 };
1021        if write_frame(stream, FT_DATA, flags, stream_id, chunk).is_err() {
1022            return false;
1023        }
1024    }
1025    stream.flush().is_ok()
1026}
1027
1028/// Write one HTTP/2 frame: 9-octet header (24-bit length, 8-bit type, 8-bit flags,
1029/// 31-bit stream id) followed by the payload (RFC 7540 §4.1).
1030fn write_frame<W: Write>(
1031    w: &mut W,
1032    ftype: u8,
1033    flags: u8,
1034    stream_id: u32,
1035    payload: &[u8],
1036) -> std::io::Result<()> {
1037    let len = payload.len();
1038    let mut hdr = [0u8; 9];
1039    hdr[0] = (len >> 16) as u8;
1040    hdr[1] = (len >> 8) as u8;
1041    hdr[2] = len as u8;
1042    hdr[3] = ftype;
1043    hdr[4] = flags;
1044    hdr[5..9].copy_from_slice(&(stream_id & 0x7fff_ffff).to_be_bytes());
1045    w.write_all(&hdr)?;
1046    w.write_all(payload)?;
1047    Ok(())
1048}
1049
1050/// HPACK-encode a header list into a header block fragment.
1051fn encode_header_block(encoder: &mut Encoder<'_>, headers: &[(String, String)]) -> Vec<u8> {
1052    let owned: Vec<(Vec<u8>, Vec<u8>)> = headers
1053        .iter()
1054        .map(|(k, v)| (k.as_bytes().to_vec(), v.as_bytes().to_vec()))
1055        .collect();
1056    encoder.encode(owned.iter().map(|(k, v)| (k.as_slice(), v.as_slice())))
1057}
1058
1059/// Strip PADDED/PRIORITY prefixes/suffixes from a HEADERS payload, yielding the raw
1060/// header block fragment (RFC 7540 §6.2).
1061fn strip_headers_padding_priority(payload: &[u8], flags: u8) -> Vec<u8> {
1062    let mut start = 0usize;
1063    let mut pad_len = 0usize;
1064    if flags & FL_PADDED != 0 && !payload.is_empty() {
1065        pad_len = payload[0] as usize;
1066        start = 1;
1067    }
1068    if flags & FL_PRIORITY != 0 {
1069        start += 5; // 4-byte stream dependency + 1-byte weight
1070    }
1071    let end = payload.len().saturating_sub(pad_len);
1072    if start > end {
1073        return Vec::new();
1074    }
1075    payload[start..end].to_vec()
1076}
1077
1078/// Strip a PADDED prefix/suffix from a DATA payload (RFC 7540 §6.1).
1079fn strip_data_padding(payload: &[u8], flags: u8) -> Vec<u8> {
1080    if flags & FL_PADDED != 0 && !payload.is_empty() {
1081        let pad_len = payload[0] as usize;
1082        let end = payload.len().saturating_sub(pad_len);
1083        if 1 <= end {
1084            return payload[1..end].to_vec();
1085        }
1086        return Vec::new();
1087    }
1088    payload.to_vec()
1089}
1090
1091// ── main-thread event handlers (run from posted IoTasks) ─────────────────────
1092
1093/// Build the `Http2Session` object and emit `session` on the server. Also takes a
1094/// per-connection event-loop handle (released by `on_session_close`) so the loop
1095/// stays alive while this connection is served — mirroring `tls`'s per-socket
1096/// `incr_handle`.
1097fn on_session(server_id: u64, session_key: u64, tx: Sender<H2Cmd>) -> Result<(), String> {
1098    with_host(|h| h.incr_handle());
1099    let server = H2.with(|s| {
1100        s.borrow()
1101            .servers
1102            .get(&server_id)
1103            .map(|r| r.emitter.clone())
1104    });
1105    let Some(server) = server else { return Ok(()) };
1106    let mut extra = IndexMap::new();
1107    extra.insert("@@h2session".into(), Value::Float(session_key as f64));
1108    let session = new_emitter_object("Http2Session", extra);
1109    H2.with(|s| {
1110        s.borrow_mut().sessions.insert(
1111            session_key,
1112            H2SessionRec {
1113                emitter: session.clone(),
1114                tx,
1115            },
1116        );
1117    });
1118    if let Err(e) = super::events::instance_call(
1119        &server,
1120        "emit",
1121        vec![with_host(|h| h.new_str("session")), session],
1122    ) {
1123        report_handler_error("session", &e);
1124    }
1125    Ok(())
1126}
1127
1128/// Release the per-connection handle and drop the session + its stream records
1129/// (posted by the owner thread when its framing loop exits).
1130fn on_session_close(session_key: u64, stream_keys: Vec<u64>) -> Result<(), String> {
1131    H2.with(|s| {
1132        let mut st = s.borrow_mut();
1133        st.sessions.remove(&session_key);
1134        for k in &stream_keys {
1135            st.streams.remove(k);
1136        }
1137    });
1138    with_host(|h| h.decr_handle());
1139    // Wake the loop so a closed last handle lets it exit.
1140    let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
1141    Ok(())
1142}
1143
1144/// Handle a decoded HEADERS frame: build the `Http2Stream`, emit `stream`
1145/// (and compat `request`).
1146fn on_headers(
1147    server_id: u64,
1148    stream_key: u64,
1149    stream_id: u32,
1150    headers: Vec<(String, String)>,
1151    end_stream: bool,
1152    tx: Sender<H2Cmd>,
1153) -> Result<(), String> {
1154    let server = H2.with(|s| {
1155        s.borrow()
1156            .servers
1157            .get(&server_id)
1158            .map(|r| r.emitter.clone())
1159    });
1160    let Some(server) = server else { return Ok(()) };
1161
1162    // Build the header object exposed to JS (pseudo-headers kept as `:`-prefixed).
1163    let headers_obj = with_host(|h| {
1164        let mut m = IndexMap::new();
1165        for (k, v) in &headers {
1166            m.insert(k.clone(), h.new_str(v.clone()));
1167        }
1168        h.new_object(m)
1169    });
1170
1171    // The core Http2Stream object.
1172    let mut extra = IndexMap::new();
1173    extra.insert("@@h2key".into(), Value::Float(stream_key as f64));
1174    extra.insert("id".into(), Value::Float(stream_id as f64));
1175    let stream_obj = new_emitter_object("Http2Stream", extra);
1176    H2.with(|s| {
1177        s.borrow_mut().streams.insert(
1178            stream_key,
1179            H2StreamRec {
1180                emitter: stream_obj.clone(),
1181                tx,
1182                stream_id,
1183                responded: false,
1184            },
1185        );
1186    });
1187
1188    let method = header_value(&headers, ":method").unwrap_or_else(|| "GET".to_string());
1189    let path = header_value(&headers, ":path").unwrap_or_else(|| "/".to_string());
1190    if h2_debug() {
1191        eprintln!("[http2] dispatch stream={stream_id} {method} {path} (end_stream={end_stream})");
1192    }
1193
1194    // Emit the core `stream` event: (stream, headers). CRITICAL: a throwing user
1195    // handler must NOT propagate out of this IoTask — the event loop treats an
1196    // `Err` from a posted task as fatal (`drive_event_loop`: `task()?`) and would
1197    // tear the whole process down, closing every live connection (the classic
1198    // "server closed right after HEADERS → curl broken pipe"). So we catch the
1199    // handler error, surface it on stderr, and keep the loop (and other
1200    // connections) alive — mirroring how a server should isolate request faults.
1201    if let Err(e) = super::events::instance_call(
1202        &server,
1203        "emit",
1204        vec![
1205            with_host(|h| h.new_str("stream")),
1206            stream_obj.clone(),
1207            headers_obj.clone(),
1208        ],
1209    ) {
1210        report_handler_error("stream", &e);
1211        return Ok(());
1212    }
1213
1214    // Compat `request` event: (req, res). `req` is a lightweight emitter carrying
1215    // method/url/headers; `res` is the same Http2Stream (writeHead/end mapped).
1216    let req = super::events::new_emitter();
1217    set_prop(&req, "method", with_host(|h| h.new_str(method)));
1218    set_prop(&req, "url", with_host(|h| h.new_str(path)));
1219    set_prop(&req, "headers", headers_obj);
1220    if let Err(e) = super::events::instance_call(
1221        &server,
1222        "emit",
1223        vec![with_host(|h| h.new_str("request")), req, stream_obj.clone()],
1224    ) {
1225        report_handler_error("request", &e);
1226        return Ok(());
1227    }
1228
1229    // A GET (END_STREAM on HEADERS) has no body: signal `end` to the stream.
1230    if end_stream {
1231        if let Err(e) =
1232            super::events::instance_call(&stream_obj, "emit", vec![with_host(|h| h.new_str("end"))])
1233        {
1234            report_handler_error("stream.end", &e);
1235        }
1236    }
1237    Ok(())
1238}
1239
1240/// Report an uncaught error raised by a user request/stream handler. Printed to
1241/// stderr (a genuine program error, like Node's uncaught-exception output) so the
1242/// cause is visible instead of the process silently dying; never propagated, so
1243/// one faulty handler cannot kill the event loop / other in-flight connections.
1244fn report_handler_error(event: &str, err: &str) {
1245    eprintln!("http2: uncaught error in '{event}' handler: {err}");
1246}
1247
1248/// True when `HTTP2_DEBUG` is set in the environment — enables per-frame stderr
1249/// tracing for diagnosing the h2 framing path end-to-end.
1250fn h2_debug() -> bool {
1251    std::env::var_os("HTTP2_DEBUG").is_some()
1252}
1253
1254/// Emit an inbound request-body DATA chunk (and `end` on END_STREAM) on the stream.
1255fn on_data(stream_key: u64, data: Vec<u8>, end_stream: bool) -> Result<(), String> {
1256    let stream = H2.with(|s| {
1257        s.borrow()
1258            .streams
1259            .get(&stream_key)
1260            .map(|r| r.emitter.clone())
1261    });
1262    let Some(stream) = stream else { return Ok(()) };
1263    if !data.is_empty() {
1264        let chunk = super::buffer::from_bytes(&data);
1265        if let Err(e) = super::events::instance_call(
1266            &stream,
1267            "emit",
1268            vec![with_host(|h| h.new_str("data")), chunk],
1269        ) {
1270            report_handler_error("data", &e);
1271            return Ok(());
1272        }
1273    }
1274    if end_stream {
1275        if let Err(e) =
1276            super::events::instance_call(&stream, "emit", vec![with_host(|h| h.new_str("end"))])
1277        {
1278            report_handler_error("end", &e);
1279        }
1280    }
1281    Ok(())
1282}
1283
1284fn header_value(headers: &[(String, String)], name: &str) -> Option<String> {
1285    headers
1286        .iter()
1287        .find(|(k, _)| k == name)
1288        .map(|(_, v)| v.clone())
1289}
1290
1291// ── Http2Stream instance methods ─────────────────────────────────────────────
1292
1293fn stream_key_of(recv: &Value) -> Option<u64> {
1294    u64_prop(recv, "@@h2key")
1295}
1296
1297fn stream_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1298    if let Some(r) = emitter_dispatch(recv, method, &args) {
1299        return r;
1300    }
1301    match method {
1302        "respond" => {
1303            let hdrs = args.first().map(object_pairs).unwrap_or_default();
1304            // `options.endStream` (arg 1) → header-only response.
1305            let end = args
1306                .get(1)
1307                .and_then(|o| get_prop(o, "endStream"))
1308                .map(|v| with_host(|h| h.truthy(&v)))
1309                .unwrap_or(false);
1310            do_respond(recv, hdrs, end)?;
1311            Ok(recv.clone())
1312        }
1313        // HTTP/1-compat: writeHead(status[, headersObj]) → respond.
1314        "writeHead" => {
1315            let status =
1316                with_host(|h| args.first().map(|v| h.to_number(v)).unwrap_or(200.0)) as u32;
1317            let mut hdrs: Vec<(String, String)> = Vec::new();
1318            for a in args.iter().skip(1) {
1319                if matches!(a, Value::Obj(_)) {
1320                    hdrs = object_pairs(a);
1321                    break;
1322                }
1323            }
1324            // Inject the `:status` pseudo-header (writeHead's implicit status).
1325            hdrs.insert(0, (":status".to_string(), status.to_string()));
1326            do_respond(recv, hdrs, false)?;
1327            Ok(recv.clone())
1328        }
1329        "setHeader" => {
1330            // Stash pending compat headers until the response HEADERS are sent.
1331            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1332            let v = with_host(|h| h.str_of(&args.get(1).cloned().unwrap_or(Value::Undef)));
1333            let bag = pending_headers_obj(recv);
1334            set_prop(&bag, &k, with_host(|h| h.new_str(v)));
1335            Ok(Value::Undef)
1336        }
1337        "getHeader" => {
1338            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1339            Ok(get_prop(recv, "@@pendingHeaders")
1340                .and_then(|bag| get_prop(&bag, &k))
1341                .unwrap_or(Value::Undef))
1342        }
1343        "removeHeader" => {
1344            let k = with_host(|h| h.str_of(&args.first().cloned().unwrap_or(Value::Undef)));
1345            if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1346                with_host(|h| {
1347                    if let Some(JsObj::Object(p)) = h.get_mut(&bag) {
1348                        p.shift_remove(&k);
1349                    }
1350                });
1351            }
1352            Ok(Value::Undef)
1353        }
1354        "write" => {
1355            ensure_responded(recv)?;
1356            let bytes = value_bytes(args.first());
1357            send_stream_data(recv, bytes, false);
1358            Ok(Value::Bool(true))
1359        }
1360        "end" => {
1361            ensure_responded(recv)?;
1362            let bytes = args
1363                .first()
1364                .filter(|v| !matches!(v, Value::Undef))
1365                .map(|v| value_bytes(Some(v)))
1366                .unwrap_or_default();
1367            send_stream_data(recv, bytes, true);
1368            super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("finish"))])?;
1369            Ok(recv.clone())
1370        }
1371        "close" => {
1372            if let Some(key) = stream_key_of(recv) {
1373                let sent = H2.with(|s| {
1374                    s.borrow().streams.get(&key).map(|r| {
1375                        let _ = r.tx.send(H2Cmd::Close {
1376                            stream_id: r.stream_id,
1377                        });
1378                    })
1379                });
1380                let _ = sent;
1381            }
1382            Ok(recv.clone())
1383        }
1384        "setEncoding" | "setTimeout" | "pause" | "resume" => Ok(recv.clone()),
1385        _ => Err(crate::host::type_error(&format!(
1386            "stream.{method} is not a function"
1387        ))),
1388    }
1389}
1390
1391/// The lazily-created object holding compat `setHeader` values before the response.
1392fn pending_headers_obj(recv: &Value) -> Value {
1393    if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1394        return bag;
1395    }
1396    let bag = with_host(|h| h.new_object(IndexMap::new()));
1397    set_prop(recv, "@@pendingHeaders", bag.clone());
1398    bag
1399}
1400
1401/// Send a HEADERS response frame with the given headers (a `:status` is injected if
1402/// none present). Marks the stream responded.
1403fn do_respond(recv: &Value, mut headers: Vec<(String, String)>, end: bool) -> Result<(), String> {
1404    // Merge any compat setHeader() values collected before respond/writeHead.
1405    if let Some(bag) = get_prop(recv, "@@pendingHeaders") {
1406        for (k, v) in object_pairs(&bag) {
1407            if !headers.iter().any(|(hk, _)| hk.eq_ignore_ascii_case(&k)) {
1408                headers.push((k, v));
1409            }
1410        }
1411    }
1412    // Ensure a single `:status` pseudo-header appears first (HTTP/2 requires
1413    // pseudo-headers to precede regular ones).
1414    let status = headers
1415        .iter()
1416        .find(|(k, _)| k == ":status")
1417        .map(|(_, v)| v.clone())
1418        .unwrap_or_else(|| "200".to_string());
1419    let mut ordered: Vec<(String, String)> = vec![(":status".to_string(), status)];
1420    for (k, v) in headers.into_iter() {
1421        if k == ":status" {
1422            continue;
1423        }
1424        // HTTP/2 header names must be lowercase.
1425        ordered.push((k.to_ascii_lowercase(), v));
1426    }
1427
1428    let Some(key) = stream_key_of(recv) else {
1429        return Ok(());
1430    };
1431    H2.with(|s| {
1432        if let Some(r) = s.borrow_mut().streams.get_mut(&key) {
1433            if !r.responded {
1434                r.responded = true;
1435                let _ = r.tx.send(H2Cmd::Respond {
1436                    stream_id: r.stream_id,
1437                    headers: ordered,
1438                    end,
1439                });
1440            }
1441        }
1442    });
1443    Ok(())
1444}
1445
1446/// Ensure the response HEADERS have been sent (auto-`respond` with 200 otherwise).
1447fn ensure_responded(recv: &Value) -> Result<(), String> {
1448    let Some(key) = stream_key_of(recv) else {
1449        return Ok(());
1450    };
1451    let responded = H2.with(|s| {
1452        s.borrow()
1453            .streams
1454            .get(&key)
1455            .map(|r| r.responded)
1456            .unwrap_or(true)
1457    });
1458    if !responded {
1459        do_respond(recv, Vec::new(), false)?;
1460    }
1461    Ok(())
1462}
1463
1464/// Queue a DATA frame for this stream's connection thread.
1465fn send_stream_data(recv: &Value, data: Vec<u8>, end: bool) {
1466    if let Some(key) = stream_key_of(recv) {
1467        H2.with(|s| {
1468            if let Some(r) = s.borrow().streams.get(&key) {
1469                let _ = r.tx.send(H2Cmd::Data {
1470                    stream_id: r.stream_id,
1471                    data,
1472                    end,
1473                });
1474            }
1475        });
1476    }
1477}
1478
1479// ── Http2Session instance methods ────────────────────────────────────────────
1480
1481fn session_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
1482    if let Some(r) = emitter_dispatch(recv, method, &args) {
1483        return r;
1484    }
1485    match method {
1486        "close" | "destroy" | "goaway" => {
1487            if let Some(key) = u64_prop(recv, "@@h2session") {
1488                H2.with(|s| {
1489                    if let Some(r) = s.borrow().sessions.get(&key) {
1490                        let _ = r.tx.send(H2Cmd::Goaway);
1491                    }
1492                });
1493            }
1494            super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
1495            Ok(recv.clone())
1496        }
1497        "settings" | "ping" | "ref" | "unref" | "setTimeout" => Ok(recv.clone()),
1498        _ => Err(crate::host::type_error(&format!(
1499            "session.{method} is not a function"
1500        ))),
1501    }
1502}
1503
1504// ── shared emitter constructor (same shape as net/tls) ───────────────────────
1505
1506/// Build a native emitter object (`@@native` tag + `@@on`/`@@once` maps + extras),
1507/// sharing the EventEmitter shape with `events`/`net`/`tls`.
1508pub fn new_emitter_object(tag: &str, mut extra: IndexMap<String, Value>) -> Value {
1509    with_host(|h| {
1510        let on = h.new_object(IndexMap::new());
1511        let once = h.new_object(IndexMap::new());
1512        let mut m = IndexMap::new();
1513        m.insert("@@native".into(), h.new_str(tag));
1514        m.insert("@@on".into(), on);
1515        m.insert("@@once".into(), once);
1516        for (k, v) in extra.drain(..) {
1517            m.insert(k, v);
1518        }
1519        h.new_object(m)
1520    })
1521}