From 252884b328562b658c03976520f6d80306bbd767 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Fri, 24 Jul 2026 12:23:37 +0200 Subject: [PATCH 1/7] refactor(http): merge server implementation into HTTP extension --- .github/workflows/test.yml | 2 +- Cargo.lock | 15 +--- Cargo.toml | 9 ++- changelog.d/6826-http-server-consolidation.md | 1 + crates/perry-codegen/src/ext_registry.rs | 6 +- .../src/lower_call/native_table/http_http2.rs | 2 +- .../src/lower_call/native_table/net_events.rs | 2 +- .../src/runtime_decls/stdlib_ffi/net_http.rs | 2 +- crates/perry-ext-fastify/Cargo.toml | 12 +-- crates/perry-ext-fastify/src/cluster_bind.rs | 8 +- crates/perry-ext-fastify/src/context.rs | 2 +- crates/perry-ext-fastify/src/server.rs | 18 ++--- crates/perry-ext-fastify/src/upgrade.rs | 4 +- crates/perry-ext-http-server/Cargo.toml | 65 --------------- .../src/test_async_shims.rs | 57 ------------- crates/perry-ext-http/Cargo.toml | 20 +++-- crates/perry-ext-http/src/client_overload.rs | 2 +- crates/perry-ext-http/src/force_link.rs | 10 +-- crates/perry-ext-http/src/lib.rs | 19 ++--- .../src/server}/cluster_bind.rs | 0 .../src/server}/dispatch_ext.rs | 54 +++++++------ .../src/server}/handle_dispatch.rs | 48 +++++------ .../src/server}/http2_server.rs | 28 ++++--- .../src/server}/http2_server/controls.rs | 2 +- .../src/server}/http2_server/dispatch.rs | 8 +- .../src/server}/http2_server/pump.rs | 18 +++-- .../src/server}/http2_server/session.rs | 6 +- .../src/server}/http2_session_settings.rs | 0 .../src/server}/http2_settings.rs | 0 .../src/server}/http2_stream_props.rs | 6 +- .../src/server}/https_server.rs | 38 ++++----- .../src/server/mod.rs} | 69 ++++++++-------- .../src/server}/raw_upgrade.rs | 6 +- .../src/server}/request.rs | 28 +++---- .../src/server}/response.rs | 16 ++-- .../src/server}/response_fast.rs | 0 .../src/server}/response_tests.rs | 0 .../src/server}/server.rs | 81 ++++++++++--------- .../src/server}/server/in_flight.rs | 14 ++-- .../src => perry-ext-http/src/server}/tls.rs | 0 .../src/server}/types.rs | 0 .../src/server}/upgrade.rs | 10 ++- crates/perry-ext-http/src/test_async_shims.rs | 7 +- crates/perry-ext-net/Cargo.toml | 4 +- crates/perry-ext-net/src/adopt.rs | 2 +- crates/perry-ext-net/src/handle_ids.rs | 2 +- crates/perry-ext-net/src/jsvalue.rs | 2 +- crates/perry-ext-net/src/lib.rs | 4 +- crates/perry-ext-ws/Cargo.toml | 4 +- crates/perry-ext-ws/src/lib.rs | 6 +- crates/perry-ffi/src/error.rs | 2 +- crates/perry-ffi/src/handle.rs | 10 +-- crates/perry-hir/src/lower/expr_member.rs | 4 +- crates/perry-runtime/Cargo.toml | 4 +- crates/perry-runtime/src/buffer/query.rs | 4 +- .../src/closure/dynamic_props.rs | 2 +- crates/perry-runtime/src/cluster.rs | 4 +- crates/perry-runtime/src/cluster_sched.rs | 5 +- crates/perry-runtime/src/error.rs | 2 +- crates/perry-runtime/src/lib.rs | 2 +- .../native_module_dispatch/dispatch_q_u.rs | 2 +- crates/perry-runtime/src/symbol/properties.rs | 2 +- crates/perry-runtime/src/value/handle.rs | 2 +- crates/perry-runtime/src/value/tags.rs | 2 +- crates/perry-stdlib/Cargo.toml | 16 ++-- .../perry-stdlib/src/common/async_bridge.rs | 2 +- .../perry-stdlib/src/common/dispatch/init.rs | 4 +- .../src/common/dispatch/method_dispatch.rs | 2 +- crates/perry-stdlib/src/tls.rs | 2 +- crates/perry-stdlib/src/ws.rs | 2 +- .../commands/compile/optimized_libs/driver.rs | 10 +-- crates/perry/src/commands/stdlib_features.rs | 5 +- crates/perry/well_known_bindings.toml | 5 +- docs/src/contributing/crate-policy.md | 2 +- .../release_sweep_tiers/tier12_link_smoke.sh | 2 +- .../test_issue_1124_http_buffer_body.ts | 2 +- ...st_issue_2533_aliased_http_createserver.ts | 2 +- test-files/test_node_http_basic.ts | 2 +- workspace-architecture.json | 8 +- 79 files changed, 359 insertions(+), 473 deletions(-) create mode 100644 changelog.d/6826-http-server-consolidation.md delete mode 100644 crates/perry-ext-http-server/Cargo.toml delete mode 100644 crates/perry-ext-http-server/src/test_async_shims.rs rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/cluster_bind.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/dispatch_ext.rs (86%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/handle_dispatch.rs (96%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_server.rs (96%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_server/controls.rs (98%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_server/dispatch.rs (98%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_server/pump.rs (96%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_server/session.rs (99%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_session_settings.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_settings.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/http2_stream_props.rs (95%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/https_server.rs (96%) rename crates/{perry-ext-http-server/src/lib.rs => perry-ext-http/src/server/mod.rs} (88%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/raw_upgrade.rs (98%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/request.rs (97%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/response.rs (99%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/response_fast.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/response_tests.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/server.rs (96%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/server/in_flight.rs (95%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/tls.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/types.rs (100%) rename crates/{perry-ext-http-server/src => perry-ext-http/src/server}/upgrade.rs (94%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b2405ffaa..2831adbfe9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1913,7 +1913,7 @@ jobs: # stdlib/http/snippets.ts excluded since v0.5.886: it links # against js_axios_response_data_parsed + # js_node_http2_create_secure_server which live in - # perry-ext-axios / perry-ext-http-server. v0.5.885's + # perry-ext-axios / perry-ext-http. v0.5.885's # PERRY_NO_AUTO_OPTIMIZE skips the well-known-binding probe # that would route those .a files into the link surface. # Proper fix: hoist well-known-binding lookup out of diff --git a/Cargo.lock b/Cargo.lock index 3b865a620d..b2d88edcf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5836,20 +5836,6 @@ dependencies = [ [[package]] name = "perry-ext-http" version = "0.5.1264" -dependencies = [ - "bytes", - "lazy_static", - "perry-ext-http-server", - "perry-ffi", - "perry-runtime", - "reqwest", - "serde_json", - "tokio", -] - -[[package]] -name = "perry-ext-http-server" -version = "0.5.1264" dependencies = [ "bytes", "h2", @@ -5861,6 +5847,7 @@ dependencies = [ "perry-ext-ws", "perry-ffi", "perry-runtime", + "reqwest", "rustls", "rustls-pemfile", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 2c33e6f60e..44c01cb5b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ members = [ "crates/perry-ext-ws", "crates/perry-ext-net", "crates/perry-ext-http", - "crates/perry-ext-http-server", "crates/perry-ext-streams", "crates/perry-ext-fastify", "crates/perry-ext-pdf", @@ -371,6 +370,13 @@ similar = "2.4" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "multipart", "blocking"] } tokio = { version = "1", features = ["full"] } tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] } +hyper = "1.4" +hyper-util = "0.1" +http-body-util = "0.1" +tokio-rustls = "0.26" +rustls = "0.23" +rustls-pemfile = "2" +socket2 = "0.6" futures-util = "0.3" url = "2" dirs = "6" @@ -447,7 +453,6 @@ perry-ext-mongodb = { path = "crates/perry-ext-mongodb" } perry-ext-ws = { path = "crates/perry-ext-ws" } perry-ext-net = { path = "crates/perry-ext-net" } perry-ext-http = { path = "crates/perry-ext-http" } -perry-ext-http-server = { path = "crates/perry-ext-http-server" } perry-ext-streams = { path = "crates/perry-ext-streams" } perry-ext-fastify = { path = "crates/perry-ext-fastify" } perry-ext-pdf = { path = "crates/perry-ext-pdf" } diff --git a/changelog.d/6826-http-server-consolidation.md b/changelog.d/6826-http-server-consolidation.md new file mode 100644 index 0000000000..60dc1bcd4b --- /dev/null +++ b/changelog.d/6826-http-server-consolidation.md @@ -0,0 +1 @@ +refactor(http): merge the HTTP/1.1, HTTPS, HTTP/2, WebSocket upgrade, and event-loop pump implementation into `perry-ext-http` while preserving `libperry_ext_http.a` and its native ABI. diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index 517da4e9bc..0a1a091c4e 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -240,8 +240,8 @@ const FFI_REGISTRY: &[(&str, OwnerKind)] = &[ ("js_http_client_request_socket", OwnerKind::WellKnown("http")), // ── #846: node:http server ─────────────────────────────────────── - // `perry-ext-http-server` defines `js_node_http_*`. It's pulled in - // transitively via `perry-ext-http` (rlib dep), and the well-known + // `perry-ext-http` defines `js_node_http_*` in its internal server + // module, and the well-known // table already has `[bindings.http]` / `[bindings.https]` / // `[bindings.http2]` → `perry-ext-http`. So tagging these as // `WellKnown("http")` makes the existing flip do the right thing: @@ -834,7 +834,7 @@ mod tests { } /// #3954 regression: HTTP-suite native-table rows can emit newer - /// `perry-ext-http`, `perry-ext-http-server`, or `perry-ext-net` + /// `perry-ext-http` or `perry-ext-net` /// symbols without the module-import path being visible to collection. /// Each emitted external symbol must independently flip its well-known /// owner so the wrapper joins the link line. diff --git a/crates/perry-codegen/src/lower_call/native_table/http_http2.rs b/crates/perry-codegen/src/lower_call/native_table/http_http2.rs index 6a0c9ee04f..78265e64dc 100644 --- a/crates/perry-codegen/src/lower_call/native_table/http_http2.rs +++ b/crates/perry-codegen/src/lower_call/native_table/http_http2.rs @@ -68,7 +68,7 @@ pub(super) const HTTP_HTTP2_ROWS: &[NativeModSig] = &[ ret: NR_OBJ_FROM_JSON_STR, }, // ========== node:http2 settings helpers (issue #3168) ========== - // Pure pack/unpack functions implemented in perry-ext-http-server (so + // Pure pack/unpack functions implemented in perry-ext-http (so // their Buffer alloc/recognition shares the program's runtime copy). NativeModSig { module: "http2", diff --git a/crates/perry-codegen/src/lower_call/native_table/net_events.rs b/crates/perry-codegen/src/lower_call/native_table/net_events.rs index 07c81353bf..23aeb993c4 100644 --- a/crates/perry-codegen/src/lower_call/native_table/net_events.rs +++ b/crates/perry-codegen/src/lower_call/native_table/net_events.rs @@ -758,7 +758,7 @@ pub(super) const NET_EVENTS_ROWS: &[NativeModSig] = &[ // perry-codegen/src/expr.rs (not this table); the instance methods // dispatch here once the let-binding gets registered as // `("net", "Server")` in HIR lowering. Shape mirrors - // `js_node_http_server_*` from perry-ext-http-server (signatures + // `js_node_http_server_*` from perry-ext-http (signatures // are deliberately parallel so the codegen side reads the same). NativeModSig { module: "net", diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs index 8af1330270..b522d44a79 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs @@ -182,7 +182,7 @@ pub(crate) fn declare_net_http(module: &mut LlModule) { module.declare_function("js_https_request", I64, &[DOUBLE, I64]); // ========== node:http / node:https / node:http2 SERVER (issue #577) ========== - // perry-ext-http-server — handler-push HTTP/1.1 + HTTP/2 + TLS via rustls. + // perry-ext-http — handler-push HTTP/1.1 + HTTP/2 + TLS via rustls. // Symbols are linked through perry-ext-http (rlib dep), so the // existing `bindings.http` / `bindings.https` / `bindings.http2` // entries in well_known_bindings.toml route imports here. diff --git a/crates/perry-ext-fastify/Cargo.toml b/crates/perry-ext-fastify/Cargo.toml index 0729cbfcbf..e49da023b1 100644 --- a/crates/perry-ext-fastify/Cargo.toml +++ b/crates/perry-ext-fastify/Cargo.toml @@ -18,11 +18,11 @@ perry-ffi.workspace = true # it from upgrade::handle_fastify_websocket_upgrade so the fastify # `app.server.on("upgrade", …)` path produces a ws_id usable through # the rest of the perry-ext-ws FFI surface. Mirrors the proven -# perry-ext-http-server (#577 Phase 4) dependency. +# perry-ext-http (#577 Phase 4) dependency. perry-ext-ws = { path = "../perry-ext-ws" } -hyper = { version = "1.4", features = ["server", "http1", "http2"] } -hyper-util = { version = "0.1", features = ["server", "server-auto", "tokio"] } -http-body-util = "0.1" +hyper = { workspace = true, features = ["server", "http1", "http2"] } +hyper-util = { workspace = true, features = ["server", "server-auto", "tokio"] } +http-body-util.workspace = true bytes.workspace = true tokio = { workspace = true } tokio-tungstenite = { workspace = true } @@ -30,12 +30,12 @@ serde_json.workspace = true lazy_static.workspace = true # #cluster — SO_REUSEPORT bind for `cluster.fork()` workers (unix only), -# mirroring perry-ext-http-server's cluster_bind. The +# mirroring perry-ext-http's cluster_bind. The # `perry_cluster_worker_listening` symbol it reports to resolves at final link # (defined in perry-runtime), like perry-ffi's runtime helpers — no Cargo dep on # perry-runtime is needed. [target.'cfg(unix)'.dependencies] -socket2 = "0.6" +socket2.workspace = true [dev-dependencies] perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-fastify/src/cluster_bind.rs b/crates/perry-ext-fastify/src/cluster_bind.rs index 5384e29be0..3ca8725fba 100644 --- a/crates/perry-ext-fastify/src/cluster_bind.rs +++ b/crates/perry-ext-fastify/src/cluster_bind.rs @@ -9,8 +9,8 @@ //! //! This wires Fastify into the cluster machinery that already exists in //! perry-runtime (`worker_reuseport_bind`, `perry_cluster_worker_listening`) -//! and is used by `net` and perry-ext-http-server. It mirrors the SO_REUSEPORT -//! (`SCHED_NONE`) path of perry-ext-http-server's HTTP/2 & HTTPS listen sites. +//! and is used by `net` and perry-ext-http. It mirrors the SO_REUSEPORT +//! (`SCHED_NONE`) path of perry-ext-http's HTTP/2 & HTTPS listen sites. //! Round-robin fd-passing (`SCHED_RR`) and the shared ephemeral port for //! `listen(0)` (#4962) are a follow-up here, exactly as for those sites today. @@ -18,7 +18,7 @@ use std::net::{SocketAddr, TcpListener}; /// True when this process is a `cluster.fork()`ed worker (non-empty /// `NODE_UNIQUE_ID` in the environment — the same check the runtime and -/// perry-ext-http-server use). +/// perry-ext-http use). pub(crate) fn is_cluster_worker() -> bool { std::env::var("NODE_UNIQUE_ID") .map(|s| !s.is_empty()) @@ -62,7 +62,7 @@ pub(crate) fn bind_listener(addr: SocketAddr, reuse_port: bool) -> std::io::Resu extern "C" { // Defined in perry-runtime's cluster module. This crate has no Cargo dep on // perry-runtime (dev-dep only); the symbol resolves at final link, the same - // way perry-ffi's runtime helpers do — matching perry-ext-http-server's + // way perry-ffi's runtime helpers do — matching perry-ext-http's // `cluster_bind`. fn perry_cluster_worker_listening( addr_ptr: *const u8, diff --git a/crates/perry-ext-fastify/src/context.rs b/crates/perry-ext-fastify/src/context.rs index 12be662a47..8aada9ef3e 100644 --- a/crates/perry-ext-fastify/src/context.rs +++ b/crates/perry-ext-fastify/src/context.rs @@ -47,7 +47,7 @@ extern "C" { /// `BufferHeader` registered with the runtime's BUFFER_REGISTRY. /// Used to distinguish `Buffer` / `Uint8Array` payloads from /// `StringHeader`-shaped objects when building response bodies. - /// Same C-exposed extern perry-ext-http-server uses (see + /// Same C-exposed extern perry-ext-http uses (see /// `crates/perry-runtime/src/buffer.rs:601`). fn js_buffer_is_buffer(ptr: i64) -> i32; } diff --git a/crates/perry-ext-fastify/src/server.rs b/crates/perry-ext-fastify/src/server.rs index a0dad46d4d..5237c85dfe 100644 --- a/crates/perry-ext-fastify/src/server.rs +++ b/crates/perry-ext-fastify/src/server.rs @@ -154,7 +154,7 @@ pub struct ErrorHeader { /// user code never resumed and any subsequent code (an in-process /// `fetch` against the same process, `app.close()`, etc.) never ran — /// the compat-sweep fixture timed out at gtimeout(30s). The fix -/// mirrors what perry-ext-http-server did in #604: `listen()` returns +/// mirrors what perry-ext-http did in #604: `listen()` returns /// immediately after spawning the accept loop, and a new /// `js_fastify_process_pending` extern wired into perry-stdlib's main /// pump drains the per-server mpsc each tick. The receiver lives @@ -184,7 +184,7 @@ pub struct FastifyServerHandle { /// `app.server.on("upgrade", …)` handlers. Sent by the hyper accept /// task after `hyper::upgrade::on` resolves and the upgraded stream /// has been registered with `perry_ext_ws::register_external_ws_stream`. -/// Mirror of perry-ext-http-server's `HttpPendingUpgrade`. +/// Mirror of perry-ext-http's `HttpPendingUpgrade`. pub struct FastifyPendingUpgrade { pub app_handle: Handle, pub method: String, @@ -257,7 +257,7 @@ pub unsafe extern "C" fn js_fastify_listen(app_handle: Handle, opts: f64, callba let (request_tx, request_rx) = mpsc::channel::(1024); // #1113 — separate channel for WebSocket upgrade events so a busy - // request stream can't starve them (mirror of perry-ext-http-server). + // request stream can't starve them (mirror of perry-ext-http). let (upgrade_tx, upgrade_rx) = mpsc::channel::(256); let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>(); let request_tx = Arc::new(request_tx); @@ -296,7 +296,7 @@ pub unsafe extern "C" fn js_fastify_listen(app_handle: Handle, opts: f64, callba // hit). `spawn_blocking_with_reactor` runs the closure inside a worker // task (`runtime().spawn(async { … })`), so `tokio::spawn`-ing the // accept loop drives it and its fan-out serve tasks on the worker pool — - // mirroring perry-ext-http-server / -net / -ws. (A bare + // mirroring perry-ext-http / -net / -ws. (A bare // `Handle::current().block_on` here would panic "Cannot start a runtime // from within a runtime" inside the worker task; spawn instead.) perry_ffi::spawn_blocking_with_reactor(move || { @@ -585,7 +585,7 @@ pub extern "C" fn js_fastify_process_pending() -> i32 { None => continue, }; // #1113 — drain WebSocket upgrades FIRST so a busy request - // stream can't starve them (mirror of perry-ext-http-server's + // stream can't starve them (mirror of perry-ext-http's // `js_node_http_server_process_pending`). count += drain_server_upgrades(h); while let Some(pending) = try_recv_pending_request(h) { @@ -630,7 +630,7 @@ pub extern "C" fn js_fastify_process_pending() -> i32 { } /// #1113 — non-blocking try_recv for a pending WebSocket upgrade. -/// Mirror of perry-ext-http-server's `try_recv_upgrade`. +/// Mirror of perry-ext-http's `try_recv_upgrade`. fn try_recv_fastify_upgrade(server_handle: Handle) -> Option { if let Some(s) = get_handle::(server_handle) { let mut guard = s.upgrade_rx.lock().unwrap(); @@ -657,7 +657,7 @@ pub extern "C" fn js_fastify_has_active() -> i32 { // Even after close(), the upgrade channel may still hold // queued items the pump needs to drain on a later tick // before the program can exit cleanly (mirror of - // perry-ext-http-server's `server_is_active`). + // perry-ext-http's `server_is_active`). if let Ok(guard) = s.upgrade_rx.lock() { if let Some(rx) = guard.as_ref() { if !rx.is_closed() && !rx.is_empty() { @@ -712,7 +712,7 @@ async fn handle_request( // tungstenite server handshake, registers the WebSocketStream // with perry-ext-ws, and queues a `FastifyPendingUpgrade` for the // main-thread pump to fire the registered handlers. Mirror of - // perry-ext-http-server's #577 Phase 4 path. + // perry-ext-http's #577 Phase 4 path. if crate::upgrade::is_websocket_upgrade(&req) { return handle_fastify_websocket_upgrade( app_handle, req, method, path, headers, upgrade_tx, @@ -834,7 +834,7 @@ async fn handle_request( } } -/// #1113 — WebSocket upgrade dispatch (mirror of perry-ext-http-server's +/// #1113 — WebSocket upgrade dispatch (mirror of perry-ext-http's /// `handle_websocket_upgrade`, issue #577 Phase 4). /// /// Synchronously builds the 101 response (so hyper drives the protocol diff --git a/crates/perry-ext-fastify/src/upgrade.rs b/crates/perry-ext-fastify/src/upgrade.rs index e5cdf4219d..454f469090 100644 --- a/crates/perry-ext-fastify/src/upgrade.rs +++ b/crates/perry-ext-fastify/src/upgrade.rs @@ -1,7 +1,7 @@ //! #1113 — `app.server.on("upgrade", (req, socket, head) => …)` for //! HTTP Upgrade requests (WebSocket handshakes) on a fastify app. //! -//! Mirrors perry-ext-http-server's `upgrade.rs` (issue #577 Phase 4), +//! Mirrors perry-ext-http's `upgrade.rs` (issue #577 Phase 4), //! the proven template for bidirectional WebSocket upgrade dispatch. //! //! # Design @@ -46,7 +46,7 @@ extern "C" { /// `Connection: Upgrade` (case-insensitive contains) and /// `Upgrade: websocket` (case-insensitive). Hyper's `headers()` /// already lowercases names, so we only normalize values. Identical -/// to perry-ext-http-server's `is_websocket_upgrade`. +/// to perry-ext-http's `is_websocket_upgrade`. pub(crate) fn is_websocket_upgrade(req: &hyper::Request) -> bool { let h = req.headers(); let connection_ok = h diff --git a/crates/perry-ext-http-server/Cargo.toml b/crates/perry-ext-http-server/Cargo.toml deleted file mode 100644 index 5ccc3d0479..0000000000 --- a/crates/perry-ext-http-server/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "perry-ext-http-server" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Native bindings for Node.js's `node:http`, `node:https`, and `node:http2` server modules — hyper-based HTTP/1.1 + HTTP/2 + TLS via rustls. Issue #577." - -[lints] -workspace = true - -[lib] -crate-type = ["staticlib", "rlib"] - -[dependencies] -perry-ffi.workspace = true -# Phase 4: cross-crate WS upgrade handoff. perry-ext-ws exposes -# `register_external_ws_stream(WebSocketStream) -> i64`; we -# call it from upgrade::handle_websocket_upgrade so 'upgrade' event -# listeners receive a ws_id usable through the rest of the -# perry-ext-ws FFI surface (`js_ws_send` / `js_ws_close` / `js_ws_on`). -perry-ext-ws = { path = "../perry-ext-ws" } -# #4973: raw-socket `'upgrade'` handoff. perry-ext-net exposes -# `adopt_upgraded_tcp_stream(TcpStream) -> i64` so keyless Upgrade requests -# reach JS as a standard net.Socket with nothing written to the wire -# (Node semantics) — see raw_upgrade.rs. -perry-ext-net = { path = "../perry-ext-net" } -hyper = { version = "1.4", features = ["server", "http1", "http2"] } -hyper-util = { version = "0.1", features = ["server", "server-auto", "tokio"] } -h2 = "0.4" -http-body-util = "0.1" -bytes.workspace = true -tokio = { workspace = true } -tokio-rustls = "0.26" -rustls = { version = "0.23", default-features = false, features = ["std", "ring", "tls12"] } -rustls-pemfile = "2" -serde_json.workspace = true -lazy_static.workspace = true -tokio-tungstenite = { workspace = true } - -# #4914: SO_REUSEPORT binds for cluster-worker port sharing (unix-only API). -[target.'cfg(unix)'.dependencies] -socket2 = "0.6" - -[dev-dependencies] -perry-ffi = { workspace = true, features = ["runtime-link"] } -# #6303: perry-runtime MUST be built here with the same feature set the shipped -# `libperry_runtime.a` / `libperry_stdlib.a` carry (i.e. its `default`). This crate -# is a `staticlib`, so it BUNDLES the perry-runtime rlib objects into -# `libperry_ext_*.a` — and perry links the ext archives BEFORE stdlib/runtime -# (`prefer_well_known_before_stdlib`), so those bundled objects WIN the link for -# every symbol they define. The workspace dep is `default-features = false`, so -# without `"default"` here a per-crate `cargo build -p perry-ext-` (exactly what -# release-packages.yml does in its per-crate loop) bundles a runtime with -# `regex-engine`/`temporal`/... compiled OUT. The dispatchers those features gate -# are exported UNCONDITIONALLY (`js_string_replace_search_dyn`, -# `js_native_call_method`, ...) with the feature-gated logic `#[cfg]`-ed out of the -# BODY — so the degraded copy silently ToString-coerces a RegExp argument and -# searches for it literally instead of matching it (str.replace(re, fn) never fires -# its callback). Keep `"default"` in lock-step with perry-runtime's default feature -# list; the `ext_crates_bundle_a_full_featured_perry_runtime` test (well_known.rs) guards it. -# #6314: `stdlib` drops the bundled no-op `stdlib_stubs` (js_stdlib_init_dispatch, -# ...) from this staticlib's perry-runtime copy. Linked before stdlib, the no-op -# `js_stdlib_init_dispatch` otherwise wins first-definition and never registers -# the tokio reactor — every node:http server dies on its first accept. -perry-runtime = { workspace = true, features = ["default", "external-ws-symbols", "stdlib"] } diff --git a/crates/perry-ext-http-server/src/test_async_shims.rs b/crates/perry-ext-http-server/src/test_async_shims.rs deleted file mode 100644 index 0446606a10..0000000000 --- a/crates/perry-ext-http-server/src/test_async_shims.rs +++ /dev/null @@ -1,57 +0,0 @@ -use perry_ffi::Promise; -use std::ffi::c_void; - -// Unit-test binaries do not link the host stdlib/runtime archive that normally -// provides the perry_ffi async bridge. Keep these synchronous shims test-only. - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_new() -> *mut Promise { - perry_runtime::promise::js_promise_new() as *mut Promise -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_resolve_bits(promise: *mut Promise, bits: u64) { - perry_runtime::promise::js_promise_resolve( - promise as *mut perry_runtime::Promise, - f64::from_bits(bits), - ); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_reject_bits(promise: *mut Promise, bits: u64) { - perry_runtime::promise::js_promise_reject( - promise as *mut perry_runtime::Promise, - f64::from_bits(bits), - ); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_promise_resolve_deferred( - promise: *mut Promise, - ctx: *mut c_void, - invoke: extern "C" fn(*mut c_void) -> u64, -) { - perry_ffi_promise_resolve_bits(promise, invoke(ctx)); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_spawn_blocking(ctx: *mut c_void, invoke: extern "C" fn(*mut c_void)) { - invoke(ctx); -} - -#[no_mangle] -pub extern "C" fn perry_ffi_spawn_blocking_with_reactor( - ctx: *mut c_void, - invoke: extern "C" fn(*mut c_void), -) { - invoke(ctx); -} - -// `perry_ffi_spawn_async` is declared extern in perry-ffi's async runtime and -// normally provided by perry-stdlib / the prebuilt static archive at the final -// link. The unit-test binary pulls it in transitively via the perry-ext-net -// rlib but has no perry-stdlib edge in a fresh checkout, so it fails to link on -// this one symbol. None of this crate's unit tests touch the async runtime, so -// a no-op stub lets the test binary link. (Test-only; never shipped.) -#[no_mangle] -pub extern "C" fn perry_ffi_spawn_async(_ctx: *mut c_void) {} diff --git a/crates/perry-ext-http/Cargo.toml b/crates/perry-ext-http/Cargo.toml index a3fee1e391..8d8442ec68 100644 --- a/crates/perry-ext-http/Cargo.toml +++ b/crates/perry-ext-http/Cargo.toml @@ -13,12 +13,17 @@ crate-type = ["staticlib", "rlib"] [dependencies] perry-ffi.workspace = true -# Server-side surface (issue #577) lives in perry-ext-http-server. Pulling -# it in as an rlib dep makes the `js_node_http_*` / `js_node_https_*` / -# `js_node_http2_*` symbols flow through into libperry_ext_http.a so a -# single staticlib registration in well_known_bindings.toml covers -# both client and server. The crate split keeps the source layered. -perry-ext-http-server.workspace = true +# Server-side HTTP/1.1, HTTPS, HTTP/2, and WebSocket upgrade support. +perry-ext-ws.workspace = true +perry-ext-net.workspace = true +hyper = { workspace = true, features = ["server", "http1", "http2"] } +hyper-util = { workspace = true, features = ["server", "server-auto", "tokio"] } +h2 = "0.4" +http-body-util.workspace = true +tokio-rustls.workspace = true +rustls = { workspace = true, features = ["std", "ring", "tls12"] } +rustls-pemfile.workspace = true +tokio-tungstenite = { workspace = true } # #2154: Agent argument validation throws `RangeError [ERR_OUT_OF_RANGE]` # via `js_throw` + `register_error_code_pub`, which are perry-runtime's # Rust-ABI helpers (not perry-ffi's). Stays consistent with perry-stdlib's @@ -57,5 +62,8 @@ bytes.workspace = true serde_json.workspace = true lazy_static.workspace = true +[target.'cfg(unix)'.dependencies] +socket2.workspace = true + [dev-dependencies] perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-http/src/client_overload.rs b/crates/perry-ext-http/src/client_overload.rs index 5ce39b0e3a..b331842aae 100644 --- a/crates/perry-ext-http/src/client_overload.rs +++ b/crates/perry-ext-http/src/client_overload.rs @@ -22,7 +22,7 @@ use super::{ /// Resolved positional arguments for `request()` / `get()` after Node's /// type-directed overload handling. Mirrors `parse_listen_args` in -/// `perry-ext-http-server`: codegen packs every user argument into a +/// `perry-ext-http`: codegen packs every user argument into a /// single JS array (`NA_VARARGS`) and we resolve each slot by value /// type so the callback is picked up wherever it floats. pub(crate) struct ClientArgs { diff --git a/crates/perry-ext-http/src/force_link.rs b/crates/perry-ext-http/src/force_link.rs index 25a7f14eb7..77365beb80 100644 --- a/crates/perry-ext-http/src/force_link.rs +++ b/crates/perry-ext-http/src/force_link.rs @@ -1,11 +1,9 @@ -//! Linker-retention anchors for perry-ext-http-server's `#[no_mangle]` FFI +//! Linker-retention anchors for perry-ext-http server module's `#[no_mangle]` FFI //! symbols. Split out of lib.rs (#4975) to stay under the 2000-line CI cap; //! see the `#[used] FORCE_LINK_HTTP_SERVER` table below for the mechanism. -// #1652: force the linker to retain perry-ext-http-server's `#[no_mangle]` -// FFI symbols. The `extern crate perry_ext_http_server as _server_link` -// at the top of this file pulls the rlib into the dependency graph, but -// the server functions are referenced only by codegen-generated callsites +// #1652: force the linker to retain perry-ext-http server module's `#[no_mangle]` +// FFI symbols. The server functions are referenced only by codegen-generated callsites // in the *user* program — never by this crate's Rust. Under LTO / staticlib // emission they can therefore be dead-stripped, and the final link then // fails with `Undefined symbols: _js_node_http_create_server` for any @@ -28,7 +26,7 @@ // so retention there is unaffected. Nothing cargo-depends on this crate, so // gating on `test` is sufficient. #[cfg(not(test))] -#[allow(dead_code)] +#[allow(dead_code, clashing_extern_declarations)] mod force_link_http_server { extern "C" { // http server + IncomingMessage + ServerResponse entry points. diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index e44d62ca28..62c535924d 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -7,13 +7,10 @@ //! //! # Server-side surface (issue #577) //! -//! `perry-ext-http-server` ships the server-side counterpart — +//! The internal `server` module ships the server-side counterpart — //! `http.createServer`, `https.createServer`, `http2.createSecureServer`. -//! It's pulled in here as an rlib dep so its `js_node_http_*` / -//! `js_node_https_*` / `js_node_http2_*` symbols flow into -//! `libperry_ext_http.a`. Don't remove the `extern crate` declaration -//! after this docblock — it keeps the linker from dead-stripping the -//! server symbols when no client-side code happens to reference them. +//! Its `js_node_http_*` / `js_node_https_*` / `js_node_http2_*` symbols +//! are exported from `libperry_ext_http.a` alongside the client surface. //! //! # Architecture (mirrors perry-ext-cron + perry-stdlib's http.rs) //! @@ -42,11 +39,11 @@ //! a v0.6.0 followup that needs a cooperative `spawn_async` surface //! on perry-ffi (today's surface is sync-via-blocking-pool only). -extern crate perry_ext_http_server as _server_link; - mod agent; pub use agent::*; +pub(crate) mod server; + // Client factory overload normalization (#3226 / #3227 / #3228) — // extracted from this file to stay under the 2000-line lint cap. mod client_overload; @@ -1874,8 +1871,7 @@ pub unsafe extern "C" fn js_http_process_pending() -> i32 { #[cfg(test)] mod tests; // Test-only `perry_ffi_*` async-bridge shims so the lib test links without the -// host stdlib archive (mirrors perry-ext-net / perry-ext-http-server). -#[cfg(test)] +// host stdlib archive (mirrors perry-ext-net / the HTTP server module). #[cfg(test)] mod test_async_shims; @@ -1885,6 +1881,5 @@ fn _force_link() -> Option<*mut ArrayHeader> { None } -// #1652 / #4975: linker-retention anchors for the server FFI symbols live -// in force_link.rs (extracted to keep this file under the 2000-line cap). +// Retain server exports through release LTO/staticlib emission. mod force_link; diff --git a/crates/perry-ext-http-server/src/cluster_bind.rs b/crates/perry-ext-http/src/server/cluster_bind.rs similarity index 100% rename from crates/perry-ext-http-server/src/cluster_bind.rs rename to crates/perry-ext-http/src/server/cluster_bind.rs diff --git a/crates/perry-ext-http-server/src/dispatch_ext.rs b/crates/perry-ext-http/src/server/dispatch_ext.rs similarity index 86% rename from crates/perry-ext-http-server/src/dispatch_ext.rs rename to crates/perry-ext-http/src/server/dispatch_ext.rs index cb60837477..e445f075d9 100644 --- a/crates/perry-ext-http-server/src/dispatch_ext.rs +++ b/crates/perry-ext-http/src/server/dispatch_ext.rs @@ -332,40 +332,42 @@ unsafe extern "C" fn http_server_method_dispatch_ext( return 0; } let value = if is_http_server_method(name) - && crate::handle_dispatch::js_ext_http_server_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_server_is_handle(handle) != 0 { - Some(crate::handle_dispatch::js_ext_http_server_dispatch_method( - handle, method_ptr, method_len, args_ptr, args_len, - )) + Some( + crate::server::handle_dispatch::js_ext_http_server_dispatch_method( + handle, method_ptr, method_len, args_ptr, args_len, + ), + ) } else if is_incoming_message_member(name) - && crate::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 { Some( - crate::handle_dispatch::js_ext_http_incoming_message_dispatch_method( + crate::server::handle_dispatch::js_ext_http_incoming_message_dispatch_method( handle, method_ptr, method_len, args_ptr, args_len, ), ) } else if is_server_response_member(name) - && crate::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 { Some( - crate::handle_dispatch::js_ext_http_server_response_dispatch_method( + crate::server::handle_dispatch::js_ext_http_server_response_dispatch_method( handle, method_ptr, method_len, args_ptr, args_len, ), ) } else if is_h2_session_member(name) - && crate::http2_server::dispatch::js_ext_http2_session_is_handle(handle) != 0 + && crate::server::http2_server::dispatch::js_ext_http2_session_is_handle(handle) != 0 { Some( - crate::http2_server::dispatch::js_ext_http2_session_dispatch_method( + crate::server::http2_server::dispatch::js_ext_http2_session_dispatch_method( handle, method_ptr, method_len, args_ptr, args_len, ), ) } else if is_h2_stream_member(name) - && crate::http2_server::dispatch::js_ext_http2_stream_is_handle(handle) != 0 + && crate::server::http2_server::dispatch::js_ext_http2_stream_is_handle(handle) != 0 { Some( - crate::http2_server::dispatch::js_ext_http2_stream_dispatch_method( + crate::server::http2_server::dispatch::js_ext_http2_stream_dispatch_method( handle, method_ptr, method_len, args_ptr, args_len, ), ) @@ -395,50 +397,50 @@ unsafe extern "C" fn http_server_property_dispatch_ext( return 0; } let value = if is_http_server_property(name) - && crate::handle_dispatch::js_ext_http_server_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_server_is_handle(handle) != 0 { Some( - crate::handle_dispatch::js_ext_http_server_dispatch_property( + crate::server::handle_dispatch::js_ext_http_server_dispatch_property( handle, property_ptr, property_len, ), ) } else if is_incoming_message_member(name) - && crate::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 { Some( - crate::handle_dispatch::js_ext_http_incoming_message_dispatch_property( + crate::server::handle_dispatch::js_ext_http_incoming_message_dispatch_property( handle, property_ptr, property_len, ), ) } else if is_server_response_member(name) - && crate::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 { Some( - crate::handle_dispatch::js_ext_http_server_response_dispatch_property( + crate::server::handle_dispatch::js_ext_http_server_response_dispatch_property( handle, property_ptr, property_len, ), ) } else if is_h2_session_member(name) - && crate::http2_server::dispatch::js_ext_http2_session_is_handle(handle) != 0 + && crate::server::http2_server::dispatch::js_ext_http2_session_is_handle(handle) != 0 { Some( - crate::http2_server::dispatch::js_ext_http2_session_dispatch_property( + crate::server::http2_server::dispatch::js_ext_http2_session_dispatch_property( handle, property_ptr, property_len, ), ) } else if is_h2_stream_member(name) - && crate::http2_server::dispatch::js_ext_http2_stream_is_handle(handle) != 0 + && crate::server::http2_server::dispatch::js_ext_http2_stream_is_handle(handle) != 0 { Some( - crate::http2_stream_props::js_ext_http2_stream_dispatch_property( + crate::server::http2_stream_props::js_ext_http2_stream_dispatch_property( handle, property_ptr, property_len, @@ -483,9 +485,9 @@ unsafe extern "C" fn http_server_property_set_dispatch_ext( | "strictContentLength" | "socket" | "connection" - ) && crate::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 + ) && crate::server::handle_dispatch::js_ext_http_server_response_is_handle(handle) != 0 { - return crate::handle_dispatch::js_ext_http_server_response_dispatch_property_set( + return crate::server::handle_dispatch::js_ext_http_server_response_dispatch_property_set( handle, property_ptr, property_len, @@ -493,9 +495,9 @@ unsafe extern "C" fn http_server_property_set_dispatch_ext( ); } if matches!(name, "socket" | "connection") - && crate::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 + && crate::server::handle_dispatch::js_ext_http_incoming_message_is_handle(handle) != 0 { - return crate::handle_dispatch::js_ext_http_incoming_message_dispatch_property_set( + return crate::server::handle_dispatch::js_ext_http_incoming_message_dispatch_property_set( handle, property_ptr, property_len, diff --git a/crates/perry-ext-http-server/src/handle_dispatch.rs b/crates/perry-ext-http/src/server/handle_dispatch.rs similarity index 96% rename from crates/perry-ext-http-server/src/handle_dispatch.rs rename to crates/perry-ext-http/src/server/handle_dispatch.rs index 4ac6ed1fc7..4221717753 100644 --- a/crates/perry-ext-http-server/src/handle_dispatch.rs +++ b/crates/perry-ext-http/src/server/handle_dispatch.rs @@ -26,12 +26,12 @@ use perry_ffi::{ alloc_string, get_handle, get_handle_mut, js_object_alloc_with_shape, JsValue, StringHeader, }; -use crate::http2_server::Http2SecureServer; -use crate::https_server::HttpsServer; -use crate::request::IncomingMessage; -use crate::response::ServerResponse; -use crate::server::HttpServer; -use crate::types::{read_string_header, POINTER_TAG, PTR_MASK, TAG_NULL, TAG_UNDEFINED}; +use crate::server::http2_server::Http2SecureServer; +use crate::server::https_server::HttpsServer; +use crate::server::request::IncomingMessage; +use crate::server::response::ServerResponse; +use crate::server::server::HttpServer; +use crate::server::types::{read_string_header, POINTER_TAG, PTR_MASK, TAG_NULL, TAG_UNDEFINED}; #[repr(C)] struct ErrorHeader { @@ -98,8 +98,8 @@ extern "C" { key: *const StringHeader, value: f64, ); - fn js_promise_rejected(reason: f64) -> *mut crate::types::Promise; - fn js_promise_resolved(value: f64) -> *mut crate::types::Promise; + fn js_promise_rejected(reason: f64) -> *mut crate::server::types::Promise; + fn js_promise_resolved(value: f64) -> *mut crate::server::types::Promise; fn js_node_http_im_method(handle: i64) -> *mut StringHeader; fn js_node_http_im_url(handle: i64) -> *mut StringHeader; @@ -361,7 +361,7 @@ pub unsafe extern "C" fn js_ext_http_server_dispatch_method( js_node_http_server_address_json(handle) }; if s.is_null() { - f64::from_bits(crate::types::TAG_NULL) + f64::from_bits(crate::server::types::TAG_NULL) } else { f64::from_bits(js_json_parse(s)) } @@ -597,10 +597,10 @@ pub unsafe extern "C" fn js_ext_http_incoming_message_dispatch_method( string_ptr_value(js_node_http_im_http_version(handle)) } "httpVersionMajor" | "__get_httpVersionMajor" => { - crate::request::incoming_http_version_part(handle, false) + crate::server::request::incoming_http_version_part(handle, false) } "httpVersionMinor" | "__get_httpVersionMinor" => { - crate::request::incoming_http_version_part(handle, true) + crate::server::request::incoming_http_version_part(handle, true) } "__get_complete" => bool_value(js_node_http_im_complete(handle) != 0), "__get_aborted" => bool_value(js_node_http_im_aborted(handle) != 0), @@ -622,10 +622,10 @@ pub unsafe extern "C" fn js_ext_http_incoming_message_dispatch_method( json_string_value_empty_object(js_node_http_im_trailers_distinct_json(handle)) } "__get_socket" | "socket" | "__get_connection" | "connection" => { - crate::request::incoming_socket_override(handle).unwrap_or(self_ref) + crate::server::request::incoming_socket_override(handle).unwrap_or(self_ref) } "__set_socket" | "__set_connection" if !args.is_empty() => { - crate::request::incoming_socket_assign(handle, args[0]); + crate::server::request::incoming_socket_assign(handle, args[0]); undef } "_addHeaderLine" if args.len() >= 3 => { @@ -727,7 +727,9 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_method( .map(|a| closure_arg(Some(*a))) .find(|c| *c != 0) .unwrap_or(0); - bool_value(crate::response::js_node_http_res_write_with_cb(handle, args[0], cb) != 0) + bool_value( + crate::server::response::js_node_http_res_write_with_cb(handle, args[0], cb) != 0, + ) } "addTrailers" if !args.is_empty() => { js_node_http_res_add_trailers(handle, args[0]); @@ -751,7 +753,7 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_method( .unwrap_or(0); (first, cb) }; - crate::response::js_node_http_res_end_with_cb(handle, chunk, cb); + crate::server::response::js_node_http_res_end_with_cb(handle, chunk, cb); self_ref } "flushHeaders" => { @@ -801,11 +803,11 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_method( self_ref } "assignSocket" if !args.is_empty() => { - crate::response::js_node_http_res_assign_socket(handle, args[0]); + crate::server::response::js_node_http_res_assign_socket(handle, args[0]); undef } "detachSocket" => { - crate::response::js_node_http_res_detach_socket( + crate::server::response::js_node_http_res_detach_socket( handle, args.first().copied().unwrap_or(undef), ); @@ -896,8 +898,8 @@ pub unsafe extern "C" fn js_ext_http_incoming_message_dispatch_property( "method" => string_ptr_value(js_node_http_im_method(handle)), "url" => string_ptr_value(js_node_http_im_url(handle)), "httpVersion" => string_ptr_value(js_node_http_im_http_version(handle)), - "httpVersionMajor" => crate::request::incoming_http_version_part(handle, false), - "httpVersionMinor" => crate::request::incoming_http_version_part(handle, true), + "httpVersionMajor" => crate::server::request::incoming_http_version_part(handle, false), + "httpVersionMinor" => crate::server::request::incoming_http_version_part(handle, true), "headers" => json_string_value(js_node_http_im_headers_json(handle)), "rawHeaders" => json_string_value(js_node_http_im_raw_headers_json(handle)), "headersDistinct" => json_string_value(js_node_http_im_headers_distinct_json(handle)), @@ -932,7 +934,7 @@ pub unsafe extern "C" fn js_ext_http_incoming_message_dispatch_property( "writable" => bool_value( js_node_http_im_destroyed(handle) == 0 && js_node_http_im_aborted(handle) == 0, ), - "socket" | "connection" => crate::request::incoming_socket_override(handle) + "socket" | "connection" => crate::server::request::incoming_socket_override(handle) .unwrap_or_else(|| handle_to_pointer_f64(handle)), "signal" => js_node_http_im_signal(handle), "remoteAddress" => string_ptr_value(js_node_http_im_remote_address(handle)), @@ -1066,7 +1068,7 @@ pub unsafe extern "C" fn js_ext_http_incoming_message_dispatch_property_set( // Node's `connection` accessor writes `this.socket`; both aliases // land on the same slot. "socket" | "connection" => { - if crate::request::incoming_socket_assign(handle, value) { + if crate::server::request::incoming_socket_assign(handle, value) { 1 } else { 0 @@ -1189,7 +1191,7 @@ fn string_value_arg(value: f64) -> *const StringHeader { if v.is_string() { return v.as_string_ptr(); } - match crate::types::jsvalue_to_owned_string(value) { + match crate::server::types::jsvalue_to_owned_string(value) { Some(s) => alloc_string(&s).as_raw(), None => std::ptr::null(), } @@ -1293,7 +1295,7 @@ fn closure_arg(value: Option) -> i64 { // #4909 — a Buffer chunk is POINTER_TAG too; `end(buf, cb)` used to // treat the buffer as the `end(cb)` callback form, drop the chunk, and // then call the buffer ("TypeError: value is not a function"). - if unsafe { crate::types::js_value_is_closure(bits as i64) } == 0 { + if unsafe { crate::server::types::js_value_is_closure(bits as i64) } == 0 { return 0; } (bits & PTR_MASK) as i64 diff --git a/crates/perry-ext-http-server/src/http2_server.rs b/crates/perry-ext-http/src/server/http2_server.rs similarity index 96% rename from crates/perry-ext-http-server/src/http2_server.rs rename to crates/perry-ext-http/src/server/http2_server.rs index d22a6a6a8c..6453edd7a8 100644 --- a/crates/perry-ext-http-server/src/http2_server.rs +++ b/crates/perry-ext-http/src/server/http2_server.rs @@ -36,19 +36,23 @@ use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot}; use tokio_rustls::TlsAcceptor; -use crate::ensure_gc_scanner_registered; -use crate::http2_session_settings::Http2SettingsState; -use crate::request::{ +use crate::server::ensure_gc_scanner_registered; +use crate::server::http2_session_settings::Http2SettingsState; +use crate::server::request::{ alloc_incoming_message, emit_no_arg_to_listeners, handle_to_pointer_f64, with_implicit_this, IncomingMessage, }; -use crate::response::{alloc_server_response_for_request, HyperResponseShape, ResponseBody}; -use crate::server::{synthesize_default_response_if_needed, HttpPendingRequest, HttpServer}; -use crate::tls::{ +use crate::server::response::{ + alloc_server_response_for_request, HyperResponseShape, ResponseBody, +}; +use crate::server::server::{ + synthesize_default_response_if_needed, HttpPendingRequest, HttpServer, +}; +use crate::server::tls::{ build_server_config, has_pem_material, json_value_to_pem_bytes, parse_cert_chain, parse_private_key, }; -use crate::types::{ +use crate::server::types::{ extract_host, extract_port, js_promise_run_microtasks, js_value_is_closure, jsvalue_to_body_bytes, jsvalue_to_owned_string, read_string_header, POINTER_TAG, PTR_MASK, STRING_TAG, TAG_NULL, TAG_UNDEFINED, @@ -438,7 +442,7 @@ pub unsafe extern "C" fn js_node_http2_create_server(first_arg: f64, second_arg: #[no_mangle] pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_array: i64) -> i64 { // Returns `server_handle` for chainability (#2129). - let parsed = crate::types::parse_listen_args(args_array); + let parsed = crate::server::types::parse_listen_args(args_array); let opts_f64 = parsed.opts; let port = extract_port(opts_f64, 443); let host = parsed @@ -458,7 +462,7 @@ pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_ar Err(_) => SocketAddr::from(([0, 0, 0, 0], port)), }; // #4914 — SO_REUSEPORT in cluster workers; plain bind otherwise. - let std_listener = match crate::cluster_bind::bind_listener(addr) { + let std_listener = match crate::server::cluster_bind::bind_listener(addr) { Ok(l) => l, Err(e) => { eprintln!("[node:http2] bind {}:{} failed: {}", host, port, e); @@ -470,7 +474,7 @@ pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_ar eprintln!("[node:http2] set_nonblocking failed: {}", e); return server_handle; } - crate::cluster_bind::notify_listening(&host, actual_port); + crate::server::cluster_bind::notify_listening(&host, actual_port); // Capture `noDelay` (default true) under the same handle lock as the TLS // config so the accept loop can apply it per connection. Mirrors the HTTP/1 @@ -543,7 +547,7 @@ pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_ar // server's `noDelay` on the raw TCP socket here, // before the TLS or h2c branch — the option // persists through any wrapping. - crate::server::apply_accept_no_delay(&stream, no_delay); + crate::server::server::apply_accept_no_delay(&stream, no_delay); let acceptor = acceptor.clone(); let request_tx = request_tx_for_spawn.clone(); tokio::spawn(async move { @@ -607,7 +611,7 @@ pub unsafe extern "C" fn js_node_http2_server_listen(server_handle: i64, args_ar // assigned. The pump binds `this` to the server when it fires them // (#2132). See `server::drain_deferred_listen_for`. if let Some(s) = get_handle_mut::(server_handle) { - crate::server::queue_deferred_listening_emit(&mut s.base, callback); + crate::server::server::queue_deferred_listening_emit(&mut s.base, callback); } // Closes #604 — `listen()` is now non-blocking; the unified diff --git a/crates/perry-ext-http-server/src/http2_server/controls.rs b/crates/perry-ext-http/src/server/http2_server/controls.rs similarity index 98% rename from crates/perry-ext-http-server/src/http2_server/controls.rs rename to crates/perry-ext-http/src/server/http2_server/controls.rs index 28f3e5c73b..90c0adc6d2 100644 --- a/crates/perry-ext-http-server/src/http2_server/controls.rs +++ b/crates/perry-ext-http/src/server/http2_server/controls.rs @@ -4,7 +4,7 @@ use super::*; use perry_ffi::{get_handle, get_handle_mut, iter_handle_ids_of, JsValue}; -use crate::types::{jsvalue_to_body_bytes, TAG_UNDEFINED}; +use crate::server::types::{jsvalue_to_body_bytes, TAG_UNDEFINED}; pub(crate) fn numeric_value(value: f64) -> Option { let v = JsValue::from_bits(value.to_bits()); diff --git a/crates/perry-ext-http-server/src/http2_server/dispatch.rs b/crates/perry-ext-http/src/server/http2_server/dispatch.rs similarity index 98% rename from crates/perry-ext-http-server/src/http2_server/dispatch.rs rename to crates/perry-ext-http/src/server/http2_server/dispatch.rs index cea140203b..af32338665 100644 --- a/crates/perry-ext-http-server/src/http2_server/dispatch.rs +++ b/crates/perry-ext-http/src/server/http2_server/dispatch.rs @@ -9,9 +9,9 @@ use perry_ffi::{ }; use std::collections::HashMap; -use crate::request::{emit_no_arg_to_listeners, handle_to_pointer_f64}; -use crate::response::HyperResponseShape; -use crate::types::{ +use crate::server::request::{emit_no_arg_to_listeners, handle_to_pointer_f64}; +use crate::server::response::HyperResponseShape; +use crate::server::types::{ jsvalue_to_body_bytes, jsvalue_to_owned_string, read_string_header, POINTER_TAG, PTR_MASK, TAG_UNDEFINED, }; @@ -323,7 +323,7 @@ fn end_server_h2_stream(handle: i64, body: Vec) { status_message: None, headers, trailers: Vec::new(), - body: crate::response::ShapeBody::Full(body), + body: crate::server::response::ShapeBody::Full(body), }; if let Some(tx) = stream.response_tx.take() { let _ = tx.send(shape); diff --git a/crates/perry-ext-http-server/src/http2_server/pump.rs b/crates/perry-ext-http/src/server/http2_server/pump.rs similarity index 96% rename from crates/perry-ext-http-server/src/http2_server/pump.rs rename to crates/perry-ext-http/src/server/http2_server/pump.rs index f00220aa5a..12871a667e 100644 --- a/crates/perry-ext-http-server/src/http2_server/pump.rs +++ b/crates/perry-ext-http/src/server/http2_server/pump.rs @@ -16,12 +16,14 @@ use perry_ffi::{ }; use tokio::sync::{mpsc, oneshot}; -use crate::request::{ +use crate::server::request::{ alloc_incoming_message, handle_to_pointer_f64, with_implicit_this, IncomingMessage, }; -use crate::response::{alloc_server_response_for_request, HyperResponseShape, ResponseBody}; -use crate::server::{synthesize_default_response_if_needed, HttpPendingRequest}; -use crate::types::{js_promise_run_microtasks, POINTER_TAG, PTR_MASK, TAG_UNDEFINED}; +use crate::server::response::{ + alloc_server_response_for_request, HyperResponseShape, ResponseBody, +}; +use crate::server::server::{synthesize_default_response_if_needed, HttpPendingRequest}; +use crate::server::types::{js_promise_run_microtasks, POINTER_TAG, PTR_MASK, TAG_UNDEFINED}; pub(crate) async fn handle_h2_request( server_handle: i64, @@ -162,9 +164,9 @@ pub(crate) fn process_pending_h2(pending: HttpPendingRequest) { // The HTTP/2 stream handle is recycled from the same pool, so clear it too // (no-op when `h2_stream_handle == 0`). unsafe { - crate::types::js_handle_clear_side_tables(pending.request_handle); - crate::types::js_handle_clear_side_tables(pending.response_handle); - crate::types::js_handle_clear_side_tables(pending.h2_stream_handle); + crate::server::types::js_handle_clear_side_tables(pending.request_handle); + crate::server::types::js_handle_clear_side_tables(pending.response_handle); + crate::server::types::js_handle_clear_side_tables(pending.h2_stream_handle); } // #4903 — Node invokes `'request'` listeners (and the `createServer` // handler, which is one) with `this` bound to the server. @@ -244,7 +246,7 @@ fn synthesize_default_h2_stream_response(stream_handle: i64) { status_message: None, headers, trailers: Vec::new(), - body: crate::response::ShapeBody::Full(Vec::new()), + body: crate::server::response::ShapeBody::Full(Vec::new()), }; if let Some(tx) = stream.response_tx.take() { let _ = tx.send(shape); diff --git a/crates/perry-ext-http-server/src/http2_server/session.rs b/crates/perry-ext-http/src/server/http2_server/session.rs similarity index 99% rename from crates/perry-ext-http-server/src/http2_server/session.rs rename to crates/perry-ext-http/src/server/http2_server/session.rs index 92210318d8..06db65fcd3 100644 --- a/crates/perry-ext-http-server/src/http2_server/session.rs +++ b/crates/perry-ext-http/src/server/http2_server/session.rs @@ -16,9 +16,9 @@ use perry_ffi::{ register_handle, JsValue, }; -use crate::ensure_gc_scanner_registered; -use crate::http2_session_settings::Http2SettingsState; -use crate::types::jsvalue_to_owned_string; +use crate::server::ensure_gc_scanner_registered; +use crate::server::http2_session_settings::Http2SettingsState; +use crate::server::types::jsvalue_to_owned_string; pub(crate) fn register_server_session(server_handle: i64, peer_addr: SocketAddr) -> i64 { let session_handle = register_handle(Http2SessionHandle { diff --git a/crates/perry-ext-http-server/src/http2_session_settings.rs b/crates/perry-ext-http/src/server/http2_session_settings.rs similarity index 100% rename from crates/perry-ext-http-server/src/http2_session_settings.rs rename to crates/perry-ext-http/src/server/http2_session_settings.rs diff --git a/crates/perry-ext-http-server/src/http2_settings.rs b/crates/perry-ext-http/src/server/http2_settings.rs similarity index 100% rename from crates/perry-ext-http-server/src/http2_settings.rs rename to crates/perry-ext-http/src/server/http2_settings.rs diff --git a/crates/perry-ext-http-server/src/http2_stream_props.rs b/crates/perry-ext-http/src/server/http2_stream_props.rs similarity index 95% rename from crates/perry-ext-http-server/src/http2_stream_props.rs rename to crates/perry-ext-http/src/server/http2_stream_props.rs index aceec49c60..479958302b 100644 --- a/crates/perry-ext-http-server/src/http2_stream_props.rs +++ b/crates/perry-ext-http/src/server/http2_stream_props.rs @@ -1,10 +1,10 @@ use perry_ffi::get_handle; -use crate::http2_server::{ +use crate::server::http2_server::{ bind_handle_method, bool_value, empty_object_value, pairs_to_js_object, Http2StreamHandle, }; -use crate::request::handle_to_pointer_f64; -use crate::types::TAG_UNDEFINED; +use crate::server::request::handle_to_pointer_f64; +use crate::server::types::TAG_UNDEFINED; #[no_mangle] pub unsafe extern "C" fn js_ext_http2_stream_dispatch_property( diff --git a/crates/perry-ext-http-server/src/https_server.rs b/crates/perry-ext-http/src/server/https_server.rs similarity index 96% rename from crates/perry-ext-http-server/src/https_server.rs rename to crates/perry-ext-http/src/server/https_server.rs index 6eed3dc29c..ae3706442f 100644 --- a/crates/perry-ext-http-server/src/https_server.rs +++ b/crates/perry-ext-http/src/server/https_server.rs @@ -23,17 +23,19 @@ use tokio::net::TcpListener; use tokio::sync::{mpsc, oneshot}; use tokio_rustls::TlsAcceptor; -use crate::ensure_gc_scanner_registered; -use crate::request::{ +use crate::server::ensure_gc_scanner_registered; +use crate::server::request::{ alloc_incoming_message, emit_no_arg_to_listeners, handle_to_pointer_f64, with_implicit_this, IncomingMessage, }; -use crate::response::{alloc_server_response_for_request, HyperResponseShape, ResponseBody}; -use crate::server::{ +use crate::server::response::{ + alloc_server_response_for_request, HyperResponseShape, ResponseBody, +}; +use crate::server::server::{ sanitize_request_timeout, signal_connections_close, HttpPendingRequest, HttpServer, ReadActivity, TrackedConnection, CONNECTIONS, NEXT_CONNECTION_ID, PENDING_CONNECTION_EVENTS, }; -use crate::tls::{ +use crate::server::tls::{ build_certless_server_config, build_server_config, has_pem_material, json_value_to_pem_bytes, parse_cert_chain, parse_private_key, }; @@ -74,7 +76,7 @@ unsafe fn parse_https_opts(opts_f64: f64) -> (Vec, Vec, bool) { .unwrap_or(false); (key_pem, cert_pem, enable_h2) } -use crate::types::{ +use crate::server::types::{ extract_host, extract_port, js_promise_run_microtasks, read_string_header, POINTER_TAG, PTR_MASK, }; @@ -93,7 +95,7 @@ pub unsafe extern "C" fn js_node_https_create_server(opts_f64: f64, handler: i64 let (key_pem, cert_pem, enable_http2_alpn) = parse_https_opts(opts_f64); let mut base = HttpServer::with_handler(handler); - crate::server::apply_server_options(&mut base, opts_f64); + crate::server::server::apply_server_options(&mut base, opts_f64); let cert_chain = parse_cert_chain(&cert_pem); let has_tls_material = has_pem_material(&key_pem, &cert_pem); @@ -153,7 +155,7 @@ pub struct HttpsServer { #[no_mangle] pub unsafe extern "C" fn js_node_https_server_listen(server_handle: i64, args_array: i64) -> i64 { // Returns `server_handle` for chainability (#2129). - let parsed = crate::types::parse_listen_args(args_array); + let parsed = crate::server::types::parse_listen_args(args_array); let opts_f64 = parsed.opts; let port = extract_port(opts_f64, 443); let host = parsed @@ -174,7 +176,7 @@ pub unsafe extern "C" fn js_node_https_server_listen(server_handle: i64, args_ar Err(_) => SocketAddr::from(([0, 0, 0, 0], port)), }; // #4914 — SO_REUSEPORT in cluster workers; plain bind otherwise. - let std_listener = match crate::cluster_bind::bind_listener(addr) { + let std_listener = match crate::server::cluster_bind::bind_listener(addr) { Ok(l) => l, Err(e) => { eprintln!("[node:https] bind {}:{} failed: {}", host, port, e); @@ -186,7 +188,7 @@ pub unsafe extern "C" fn js_node_https_server_listen(server_handle: i64, args_ar eprintln!("[node:https] set_nonblocking failed: {}", e); return server_handle; } - crate::cluster_bind::notify_listening(&host, actual_port); + crate::server::cluster_bind::notify_listening(&host, actual_port); // Capture `noDelay` (default true) under the same handle lock as the TLS // config, so the accept loop can apply it per connection without re-locking @@ -237,7 +239,7 @@ pub unsafe extern "C" fn js_node_https_server_listen(server_handle: i64, args_ar // default. Honor the server's `noDelay` option // (default true) on the raw TCP socket before the // TLS handshake; the option persists through rustls. - crate::server::apply_accept_no_delay(&stream, no_delay); + crate::server::server::apply_accept_no_delay(&stream, no_delay); let acceptor = acceptor.clone(); let request_tx = request_tx_for_spawn.clone(); // #4905/#4971 — register the connection so @@ -325,7 +327,7 @@ pub unsafe extern "C" fn js_node_https_server_listen(server_handle: i64, args_ar // assigned. The pump binds `this` to the server when it fires them // (#2132). See `server::drain_deferred_listen_for`. if let Some(s) = get_handle_mut::(server_handle) { - crate::server::queue_deferred_listening_emit(&mut s.base, callback); + crate::server::server::queue_deferred_listening_emit(&mut s.base, callback); } // Closes #604 — `listen()` is now non-blocking. Pending requests @@ -451,8 +453,8 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { // #6710 — clear a possibly-recycled handle id's per-handle JS side tables // before the handler observes req/res (see process_pending in server.rs). unsafe { - crate::types::js_handle_clear_side_tables(pending.request_handle); - crate::types::js_handle_clear_side_tables(pending.response_handle); + crate::server::types::js_handle_clear_side_tables(pending.request_handle); + crate::server::types::js_handle_clear_side_tables(pending.response_handle); } // #4903 — Node invokes `'request'` listeners (and the `createServer` // handler, which is one) with `this` bound to the server. @@ -475,7 +477,7 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { js_promise_run_microtasks(); } } - crate::server::finalize_or_park_request(&pending); + crate::server::server::finalize_or_park_request(&pending); return; } for cb in &pending.request_listeners { @@ -510,7 +512,7 @@ pub(crate) fn process_pending_https(pending: HttpPendingRequest) { // is already flushed, otherwise park it for the reaper instead of // synthesizing a premature empty response and freeing the handles out // from under the pending work. - crate::server::finalize_or_park_request(&pending); + crate::server::server::finalize_or_park_request(&pending); } /// `httpsServer.address()` mirroring `http.Server.address()`. @@ -587,7 +589,7 @@ pub extern "C" fn js_node_https_server_close_all_connections(handle: i64) { // registries are shared and keyed by server handle, and the HTTP // path also finalizes parked async requests whose connection task // just died. - crate::server::js_node_http_server_close_all_connections(handle); + crate::server::server::js_node_http_server_close_all_connections(handle); } /// `httpsServer.closeIdleConnections()` — destroy connections with no @@ -731,7 +733,7 @@ mod nodelay_tests { //! `lib.rs` (`http_server_seeds_node_timeout_defaults`), so a default //! server continues to get `TCP_NODELAY` on. - use crate::server::apply_accept_no_delay; + use crate::server::server::apply_accept_no_delay; use tokio::net::{TcpListener, TcpStream}; /// Accept a loopback connection and return the SERVER-side stream — the diff --git a/crates/perry-ext-http-server/src/lib.rs b/crates/perry-ext-http/src/server/mod.rs similarity index 88% rename from crates/perry-ext-http-server/src/lib.rs rename to crates/perry-ext-http/src/server/mod.rs index 4ebfe4858f..402616dba6 100644 --- a/crates/perry-ext-http-server/src/lib.rs +++ b/crates/perry-ext-http/src/server/mod.rs @@ -54,10 +54,6 @@ use perry_ffi::{gc_register_mutable_root_scanner_named, iter_handles_of_mut, GcR mod cluster_bind; mod dispatch_ext; -// Unit-test binaries do not link the host stdlib/runtime archive that -// provides the perry_ffi async bridge; without these the test link is at the -// mercy of --gc-sections keeping/dropping the perry-ffi references pulled in -// via the perry-ext-net rlib (same shims as perry-ext-net / perry-ext-fetch). mod handle_dispatch; mod http2_server; mod http2_session_settings; @@ -69,9 +65,6 @@ mod request; mod response; mod response_fast; mod server; -#[cfg(test)] -#[cfg(test)] -mod test_async_shims; mod tls; mod types; mod upgrade; @@ -99,7 +92,7 @@ static GC_REGISTERED: Once = Once::new(); /// same root cause as issue #35 for net.Socket listeners. pub(crate) fn ensure_gc_scanner_registered() { GC_REGISTERED.call_once(|| { - gc_register_mutable_root_scanner_named("perry-ext-http-server", scan_http_server_roots); + gc_register_mutable_root_scanner_named("perry-ext-http", scan_http_server_roots); // #2532 — register the server pump + has-active with perry-runtime // directly. In a workspace build perry-stdlib drains these via its // `external-http-server-pump` arm, but an out-of-tree install links @@ -112,8 +105,8 @@ pub(crate) fn ensure_gc_scanner_registered() { fn js_register_aux_has_active(f: extern "C" fn() -> i32); } unsafe { - js_register_aux_pump(crate::server::js_node_http_server_process_pending); - js_register_aux_has_active(crate::server::js_node_http_server_has_active); + js_register_aux_pump(crate::server::server::js_node_http_server_process_pending); + js_register_aux_has_active(crate::server::server::js_node_http_server_has_active); } // Wall 10 — register the handle property/method/property-set dispatch // extensions so erased-receiver `req.url` / `res.end(...)` etc. route to @@ -121,7 +114,7 @@ pub(crate) fn ensure_gc_scanner_registered() { // `external-http-server-pump` (the prebuilt `full` stdlib used by // out-of-tree installs and `PERRY_NO_AUTO_OPTIMIZE=1`). See // `dispatch_ext.rs`. - crate::dispatch_ext::ensure_dispatch_extensions_registered(); + crate::server::dispatch_ext::ensure_dispatch_extensions_registered(); }); } @@ -231,7 +224,8 @@ mod tests { fn young_gc_value() -> f64 { f64::from_bits( - crate::types::POINTER_TAG | (young_gc_root() as u64 & crate::types::PTR_MASK), + crate::server::types::POINTER_TAG + | (young_gc_root() as u64 & crate::server::types::PTR_MASK), ) } @@ -242,7 +236,7 @@ mod tests { fn assert_nanbox_rewritten(before: f64, after: f64) { assert_ne!(after.to_bits(), before.to_bits()); - let ptr = after.to_bits() & crate::types::PTR_MASK; + let ptr = after.to_bits() & crate::server::types::PTR_MASK; assert!(perry_runtime::arena::pointer_in_nursery(ptr as usize)); } @@ -267,7 +261,10 @@ mod tests { assert_eq!(s.keep_alive_timeout_buffer, 1_000.0); assert_eq!(s.request_timeout, 300_000.0); assert_eq!(s.idle_timeout, 0.0); - assert_eq!(s.max_headers_count.to_bits(), crate::types::TAG_NULL); + assert_eq!( + s.max_headers_count.to_bits(), + crate::server::types::TAG_NULL + ); assert_eq!(s.max_requests_per_socket, 0.0); assert!(s.no_delay); assert!(!s.keep_alive); @@ -282,41 +279,41 @@ mod tests { let handle = register_handle(HttpServer::with_handler(0)); // Sanity: defaults visible through the FFI getter. assert_eq!( - crate::server::js_node_http_server_headers_timeout(handle), + crate::server::server::js_node_http_server_headers_timeout(handle), 60_000.0 ); assert_eq!( - crate::server::js_node_http_server_keep_alive_timeout_buffer(handle), + crate::server::server::js_node_http_server_keep_alive_timeout_buffer(handle), 1_000.0 ); // Set then read back. - crate::server::js_node_http_server_set_headers_timeout(handle, 0.0); - crate::server::js_node_http_server_set_keep_alive_timeout_buffer(handle, 250.0); - crate::server::js_node_http_server_set_idle_timeout(handle, 45_000.0); - crate::server::js_node_http_server_set_max_requests_per_socket(handle, 100.0); + crate::server::server::js_node_http_server_set_headers_timeout(handle, 0.0); + crate::server::server::js_node_http_server_set_keep_alive_timeout_buffer(handle, 250.0); + crate::server::server::js_node_http_server_set_idle_timeout(handle, 45_000.0); + crate::server::server::js_node_http_server_set_max_requests_per_socket(handle, 100.0); assert_eq!( - crate::server::js_node_http_server_headers_timeout(handle), + crate::server::server::js_node_http_server_headers_timeout(handle), 0.0 ); assert_eq!( - crate::server::js_node_http_server_keep_alive_timeout_buffer(handle), + crate::server::server::js_node_http_server_keep_alive_timeout_buffer(handle), 250.0 ); assert_eq!( - crate::server::js_node_http_server_idle_timeout(handle), + crate::server::server::js_node_http_server_idle_timeout(handle), 45_000.0 ); assert_eq!( - crate::server::js_node_http_server_max_requests_per_socket(handle), + crate::server::server::js_node_http_server_max_requests_per_socket(handle), 100.0, ); // `setTimeout(ms, cb)` updates the idle timeout and registers // the cb as a `'timeout'` listener — returns the handle for chaining. let chained = - crate::server::js_node_http_server_set_timeout_method(handle, 9_999.0, 0xCAFE); + crate::server::server::js_node_http_server_set_timeout_method(handle, 9_999.0, 0xCAFE); assert_eq!(chained, handle); assert_eq!( - crate::server::js_node_http_server_idle_timeout(handle), + crate::server::server::js_node_http_server_idle_timeout(handle), 9_999.0 ); let listener_count = get_handle::(handle) @@ -337,7 +334,7 @@ mod tests { perry_runtime::gc::js_shadow_slot_set(0, options.bits()); let mut server = HttpServer::with_handler(0); - crate::server::apply_server_options(&mut server, f64::from_bits(options.bits())); + crate::server::server::apply_server_options(&mut server, f64::from_bits(options.bits())); assert_eq!(server.headers_timeout, 111.0); assert_eq!(server.keep_alive_timeout, 222.0); @@ -348,10 +345,7 @@ mod tests { #[test] fn gc_mutable_scanner_rewrites_server_wrapper_and_request_response_roots() { let _guard = GcTestGuard::new(); - perry_ffi::gc_register_mutable_root_scanner_named( - "perry-ext-http-server", - scan_http_server_roots, - ); + perry_ffi::gc_register_mutable_root_scanner_named("perry-ext-http", scan_http_server_roots); let http_handler = young_gc_root(); let http_listener = young_gc_root(); @@ -451,7 +445,7 @@ mod tests { /// lacking a throw path here we coerce to the nearest in-range value). #[test] fn request_timeout_is_sanitized_to_a_u64_safe_ms_count() { - use crate::server::sanitize_request_timeout; + use crate::server::server::sanitize_request_timeout; // Non-finite falls back to Node's 300s default rather than // saturating the cast. @@ -492,17 +486,18 @@ mod tests { fn request_timeout_setter_paths_store_sanitized_values() { // Property-setter path: `Infinity` previously stored verbatim. let handle = register_handle(HttpServer::with_handler(0)); - let ret = crate::server::js_node_http_server_set_request_timeout(handle, f64::INFINITY); + let ret = + crate::server::server::js_node_http_server_set_request_timeout(handle, f64::INFINITY); // The setter returns the assigned value (JS `a = b` evaluates to `b`)… assert!(ret.is_infinite()); // …but the *stored* field is sanitized to the safe default. assert_eq!( - crate::server::js_node_http_server_request_timeout(handle), + crate::server::server::js_node_http_server_request_timeout(handle), 300_000.0 ); - crate::server::js_node_http_server_set_request_timeout(handle, 7_500.0); + crate::server::server::js_node_http_server_set_request_timeout(handle, 7_500.0); assert_eq!( - crate::server::js_node_http_server_request_timeout(handle), + crate::server::server::js_node_http_server_request_timeout(handle), 7_500.0 ); drop_handle(handle); @@ -516,7 +511,7 @@ mod tests { perry_runtime::gc::js_shadow_slot_set(0, options.bits()); let mut server = HttpServer::with_handler(0); - crate::server::apply_server_options(&mut server, f64::from_bits(options.bits())); + crate::server::server::apply_server_options(&mut server, f64::from_bits(options.bits())); // Oversized `requestTimeout` clamped; unrelated knob untouched. assert_eq!(server.request_timeout, 9_007_199_254_740_991.0); assert_eq!(server.headers_timeout, 222.0); diff --git a/crates/perry-ext-http-server/src/raw_upgrade.rs b/crates/perry-ext-http/src/server/raw_upgrade.rs similarity index 98% rename from crates/perry-ext-http-server/src/raw_upgrade.rs rename to crates/perry-ext-http/src/server/raw_upgrade.rs index 907ec4706b..b1f4c34ee0 100644 --- a/crates/perry-ext-http-server/src/raw_upgrade.rs +++ b/crates/perry-ext-http/src/server/raw_upgrade.rs @@ -36,9 +36,9 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf}; use tokio::net::TcpStream; use tokio::sync::mpsc; -use crate::request::alloc_incoming_message; -use crate::request::IncomingMessage; -use crate::server::HttpPendingUpgrade; +use crate::server::request::alloc_incoming_message; +use crate::server::request::IncomingMessage; +use crate::server::server::HttpPendingUpgrade; /// Replays an already-read prefix before the live stream. Write side passes /// straight through. diff --git a/crates/perry-ext-http-server/src/request.rs b/crates/perry-ext-http/src/server/request.rs similarity index 97% rename from crates/perry-ext-http-server/src/request.rs rename to crates/perry-ext-http/src/server/request.rs index 6bc5b89ecb..d9261c3910 100644 --- a/crates/perry-ext-http-server/src/request.rs +++ b/crates/perry-ext-http/src/server/request.rs @@ -25,7 +25,7 @@ use perry_ffi::{ RawClosureHeader, StringHeader, }; -use crate::types::{ +use crate::server::types::{ jsvalue_to_owned_string, read_string_header, POINTER_TAG, PTR_MASK, STRING_TAG, }; @@ -122,11 +122,11 @@ impl IncomingMessage { paused: false, encoding: None, trailers: HashMap::new(), - signal_controller: f64::from_bits(crate::types::TAG_UNDEFINED), - signal: f64::from_bits(crate::types::TAG_UNDEFINED), + signal_controller: f64::from_bits(crate::server::types::TAG_UNDEFINED), + signal: f64::from_bits(crate::server::types::TAG_UNDEFINED), close_emitted: false, standalone: false, - socket_value: f64::from_bits(crate::types::TAG_UNDEFINED), + socket_value: f64::from_bits(crate::server::types::TAG_UNDEFINED), socket_overridden: false, } } @@ -146,7 +146,7 @@ fn is_undefined(value: f64) -> bool { fn object_value(ptr: *mut T) -> f64 { if ptr.is_null() { - f64::from_bits(crate::types::TAG_UNDEFINED) + f64::from_bits(crate::server::types::TAG_UNDEFINED) } else { f64::from_bits(JsValue::from_object_ptr(ptr).bits()) } @@ -472,7 +472,7 @@ pub extern "C" fn js_node_http_im_raw_body(handle: i64) -> f64 { .unwrap_or_default(); let buf = alloc_buffer(&bytes); if buf.is_null() { - f64::from_bits(crate::types::TAG_UNDEFINED) + f64::from_bits(crate::server::types::TAG_UNDEFINED) } else { f64::from_bits(POINTER_TAG | (buf as u64 & PTR_MASK)) } @@ -483,7 +483,7 @@ pub extern "C" fn js_node_http_im_raw_body(handle: i64) -> f64 { pub extern "C" fn js_node_http_im_signal(handle: i64) -> f64 { get_handle_mut::(handle) .map(ensure_signal) - .unwrap_or_else(|| f64::from_bits(crate::types::TAG_UNDEFINED)) + .unwrap_or_else(|| f64::from_bits(crate::server::types::TAG_UNDEFINED)) } /// `req.socket.remoteAddress` — peer IP as a dotted string. @@ -610,7 +610,7 @@ pub unsafe extern "C" fn js_node_http_im_on( // Issue #1124 followup — the same `("http", // "IncomingMessage", "on")` dispatch row services // BOTH the server-side IncomingMessage (registered - // here in perry-ext-http-server) and the client-side + // here in perry-ext-http) and the client-side // IncomingMessage that `http.get(url, (res) => …)`'s // callback receives from perry-ext-http. The // codegen can't distinguish them at compile time @@ -719,15 +719,15 @@ pub extern "C" fn js_node_http_im_read(handle: i64) -> f64 { let (bytes, encoding) = match get_handle_mut::(handle) { Some(im) => { if im.data_emitted { - return f64::from_bits(crate::types::TAG_NULL); + return f64::from_bits(crate::server::types::TAG_NULL); } im.data_emitted = true; (im.body_bytes.clone(), im.encoding.clone()) } - None => return f64::from_bits(crate::types::TAG_NULL), + None => return f64::from_bits(crate::server::types::TAG_NULL), }; if bytes.is_empty() { - return f64::from_bits(crate::types::TAG_NULL); + return f64::from_bits(crate::server::types::TAG_NULL); } // #5437 POST-body: Node's `req.read()` returns a Buffer by default and a // string only after `setEncoding()`. `Readable.toWeb(req)` (Next.js App @@ -744,7 +744,7 @@ pub extern "C" fn js_node_http_im_read(handle: i64) -> f64 { None => { let buf = alloc_buffer(&bytes); if buf.is_null() { - return f64::from_bits(crate::types::TAG_NULL); + return f64::from_bits(crate::server::types::TAG_NULL); } f64::from_bits(POINTER_TAG | (buf as u64 & PTR_MASK)) } @@ -874,7 +874,7 @@ pub(crate) fn with_implicit_this(this_val: f64, f: impl FnOnce() -> R) -> R { /// fields empty. #[no_mangle] pub extern "C" fn js_node_http_incoming_message_standalone_new(socket: f64) -> i64 { - crate::ensure_gc_scanner_registered(); + crate::server::ensure_gc_scanner_registered(); let mut im = IncomingMessage::new( String::new(), String::new(), @@ -1160,7 +1160,7 @@ mod add_header_line_tests { // `new http.IncomingMessage()` then `req.connection = v` must read // back through both `socket` and `connection` (#4904). let handle = js_node_http_incoming_message_standalone_new(f64::from_bits( - crate::types::TAG_UNDEFINED, + crate::server::types::TAG_UNDEFINED, )); assert!(incoming_socket_override(handle).is_some()); let marker = 1234.5_f64; diff --git a/crates/perry-ext-http-server/src/response.rs b/crates/perry-ext-http/src/server/response.rs similarity index 99% rename from crates/perry-ext-http-server/src/response.rs rename to crates/perry-ext-http/src/server/response.rs index f737feff2c..e412e52b15 100644 --- a/crates/perry-ext-http-server/src/response.rs +++ b/crates/perry-ext-http/src/server/response.rs @@ -19,8 +19,8 @@ use std::pin::Pin; use std::task::{Context, Poll}; use tokio::sync::oneshot; -use crate::request::{emit_no_arg_to_listeners, handle_to_pointer_f64}; -use crate::types::{ +use crate::server::request::{emit_no_arg_to_listeners, handle_to_pointer_f64}; +use crate::server::types::{ js_json_stringify, js_node_setheaders_entries_json, js_value_is_closure, jsvalue_to_body_bytes, jsvalue_to_owned_string, read_string_header, PTR_MASK, STRING_TAG, TAG_FALSE, TAG_NULL, TAG_TRUE, TAG_UNDEFINED, @@ -280,7 +280,7 @@ impl HyperResponseShape { // `StatusCode` constant, skipping `from_u16`'s numeric range-check // + `unwrap_or` on every response. Uncommon / custom codes keep the // parsing path, so the resulting status is identical for every code. - let status = crate::response_fast::status_code_const(self.status) + let status = crate::server::response_fast::status_code_const(self.status) .unwrap_or_else(|| StatusCode::from_u16(self.status).unwrap_or(StatusCode::OK)); let mut builder = Response::builder().status(status); // `res.statusMessage = 'Custom Message'` must reach the HTTP/1 @@ -377,7 +377,7 @@ impl HyperResponseShape { // timeouts servers commonly run with (Node's 5 s default, etc.), so // the per-response `format!` only fires for an unusual timeout. The // interned string equals `format!("timeout={}", secs)` exactly. - let value = crate::response_fast::keep_alive_header_value(secs) + let value = crate::server::response_fast::keep_alive_header_value(secs) .map(str::to_string) .unwrap_or_else(|| format!("timeout={}", secs)); self.headers.push(("Keep-Alive".to_string(), value)); @@ -1697,7 +1697,7 @@ pub extern "C" fn js_node_http_outgoing_message_new() -> i64 { /// flushes through the socket assigned via `res.assignSocket(socket)`. #[no_mangle] pub unsafe extern "C" fn js_node_http_server_response_standalone_new(req: f64) -> i64 { - crate::ensure_gc_scanner_registered(); + crate::server::ensure_gc_scanner_registered(); let (tx, _rx) = oneshot::channel::(); let mut sr = ServerResponse::new(tx); sr.standalone = true; @@ -1767,7 +1767,7 @@ pub extern "C" fn js_node_http_res_write_with_cb(handle: i64, chunk: f64, callba "ERR_STREAM_DESTROYED", perry_ffi::ErrorKind::Error, ); - crate::http2_server::call1(callback, f64::from_bits(err.bits())); + crate::server::http2_server::call1(callback, f64::from_bits(err.bits())); } return 0; } @@ -1886,7 +1886,9 @@ unsafe fn standalone_end(handle: i64, chunk: f64, callback: i64) { // message, or an uncommon code, falls back so its reason still reaches // the wire byte-for-byte. let mut head = match sr.status_message.as_deref() { - None => crate::response_fast::status_line_bytes(sr.status_code).map(str::to_string), + None => { + crate::server::response_fast::status_line_bytes(sr.status_code).map(str::to_string) + } Some(_) => None, } .unwrap_or_else(|| { diff --git a/crates/perry-ext-http-server/src/response_fast.rs b/crates/perry-ext-http/src/server/response_fast.rs similarity index 100% rename from crates/perry-ext-http-server/src/response_fast.rs rename to crates/perry-ext-http/src/server/response_fast.rs diff --git a/crates/perry-ext-http-server/src/response_tests.rs b/crates/perry-ext-http/src/server/response_tests.rs similarity index 100% rename from crates/perry-ext-http-server/src/response_tests.rs rename to crates/perry-ext-http/src/server/response_tests.rs diff --git a/crates/perry-ext-http-server/src/server.rs b/crates/perry-ext-http/src/server/server.rs similarity index 96% rename from crates/perry-ext-http-server/src/server.rs rename to crates/perry-ext-http/src/server/server.rs index 98d49e871f..1159a4186e 100644 --- a/crates/perry-ext-http-server/src/server.rs +++ b/crates/perry-ext-http/src/server/server.rs @@ -29,15 +29,15 @@ use perry_ffi::{ RawClosureHeader, StringHeader, }; -use crate::ensure_gc_scanner_registered; -use crate::request::{ +use crate::server::ensure_gc_scanner_registered; +use crate::server::request::{ alloc_incoming_message, emit_no_arg_to_listeners, handle_to_pointer_f64, with_implicit_this, IncomingMessage, }; -use crate::response::{ +use crate::server::response::{ alloc_server_response_for_request, HyperResponseShape, ResponseBody, ServerResponse, }; -use crate::types::{ +use crate::server::types::{ extract_host, extract_port, js_handle_clear_side_tables, js_promise_run_microtasks, js_promise_state, js_value_is_closure, jsvalue_to_owned_string, read_string_header, Promise, POINTER_TAG, PTR_MASK, TAG_NULL, TAG_UNDEFINED, @@ -649,7 +649,7 @@ fn serve_http_connection( }) .unwrap_or(false); let stream = if has_upgrade_listeners { - match crate::raw_upgrade::peek_and_maybe_dispatch_raw_upgrade( + match crate::server::raw_upgrade::peek_and_maybe_dispatch_raw_upgrade( server_handle, peer, stream, @@ -657,14 +657,14 @@ fn serve_http_connection( ) .await { - crate::raw_upgrade::PeekResult::Handled => { + crate::server::raw_upgrade::PeekResult::Handled => { CONNECTIONS.lock().unwrap().remove(&conn_id); return; } - crate::raw_upgrade::PeekResult::Passthrough(s) => s, + crate::server::raw_upgrade::PeekResult::Passthrough(s) => s, } } else { - crate::raw_upgrade::PrefixedStream::empty(stream) + crate::server::raw_upgrade::PrefixedStream::empty(stream) }; let io = TokioIo::new(ReadActivity::new(stream, read_active_for_io)); let service = service_fn(move |req: Request| { @@ -723,7 +723,7 @@ fn spawn_rr_inject_loop( // the channel closes → fd < 0). The thread parks on `recv_fd` for the // server's lifetime; it ends when the IPC channel closes. std::thread::spawn(move || loop { - let fd = crate::cluster_bind::recv_fd(key_id); + let fd = crate::server::cluster_bind::recv_fd(key_id); if fd < 0 || fd_tx.blocking_send(fd).is_err() { break; } @@ -779,7 +779,7 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr // Returns `server_handle` so `createServer(...).listen(...).on(...)` chains // correctly. Pre-#2129 this was `-> ()` and chained sites broke at runtime // with `undefined.on is not a function`. - let parsed = crate::types::parse_listen_args(args_array); + let parsed = crate::server::types::parse_listen_args(args_array); let opts_f64 = parsed.opts; let port = extract_port(opts_f64, 3000); let host = parsed @@ -809,10 +809,10 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr // worker binds the primary-resolved port itself with SO_REUSEPORT so // `listen(0)` still shares one ephemeral port (#4914). let address_type: i32 = if host.contains(':') { 6 } else { 4 }; - let is_worker = crate::cluster_bind::is_cluster_worker(); - let rr = is_worker && crate::cluster_bind::worker_sched_is_rr(); + let is_worker = crate::server::cluster_bind::is_cluster_worker(); + let rr = is_worker && crate::server::cluster_bind::worker_sched_is_rr(); let resolved = if is_worker { - crate::cluster_bind::worker_query_listen(&host, port as i32, address_type, rr) + crate::server::cluster_bind::worker_query_listen(&host, port as i32, address_type, rr) } else { None }; @@ -830,7 +830,7 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr #[cfg(unix)] { let actual_port = resolved.unwrap(); - crate::cluster_bind::notify_listening(&host, actual_port); + crate::server::cluster_bind::notify_listening(&host, actual_port); let no_delay; if let Some(s) = get_handle_mut::(server_handle) { s.bound_port = actual_port; @@ -843,7 +843,8 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr } else { return server_handle; } - let key_id = crate::cluster_bind::compute_key_id(&host, actual_port, address_type); + let key_id = + crate::server::cluster_bind::compute_key_id(&host, actual_port, address_type); spawn_rr_inject_loop( server_handle, key_id, @@ -864,7 +865,7 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr }; // #4914 — cluster workers bind with SO_REUSEPORT so N workers share // the port; `bind_listener` falls through to a plain bind otherwise. - let std_listener = match crate::cluster_bind::bind_listener(addr) { + let std_listener = match crate::server::cluster_bind::bind_listener(addr) { Ok(l) => l, Err(e) => { eprintln!("[node:http] bind {}:{} failed: {}", host, bind_port, e); @@ -876,7 +877,7 @@ pub unsafe extern "C" fn js_node_http_server_listen(server_handle: i64, args_arr eprintln!("[node:http] set_nonblocking failed: {}", e); return server_handle; } - crate::cluster_bind::notify_listening(&host, actual_port); + crate::server::cluster_bind::notify_listening(&host, actual_port); // Node applies `noDelay` (default true) to every accepted connection. // Capture it before the accept loop spawns so the option can be set on @@ -1194,7 +1195,7 @@ async fn handle_request( // tungstenite path — keyless Upgrade requests are served Node-style by // the raw peek path in raw_upgrade.rs and only reach hyper when no // listener was attached at accept time. - if crate::upgrade::is_websocket_upgrade(&req) { + if crate::server::upgrade::is_websocket_upgrade(&req) { let has_upgrade_listeners = get_handle::(server_handle) .map(|s| { s.listeners @@ -1421,7 +1422,7 @@ pub extern "C" fn js_node_http_server_has_active() -> i32 { if active != 0 { return 1; } - iter_handles_of::(|s| { + iter_handles_of::(|s| { if server_is_active(&s.base) { active = 1; } @@ -1429,12 +1430,12 @@ pub extern "C" fn js_node_http_server_has_active() -> i32 { if active != 0 { return 1; } - iter_handles_of::(|s| { + iter_handles_of::(|s| { if server_is_active(&s.base) { active = 1; } }); - if active == 0 && crate::http2_server::has_active_h2_clients() { + if active == 0 && crate::server::http2_server::has_active_h2_clients() { active = 1; } // #4728 — a request parked awaiting an async handler keeps the loop @@ -1576,7 +1577,7 @@ pub extern "C" fn js_node_http_server_process_pending() -> i32 { let listeners = get_handle::(server_handle) .and_then(|s| s.listeners.get("connection").cloned()) .or_else(|| { - get_handle::(server_handle) + get_handle::(server_handle) .and_then(|s| s.base.listeners.get("connection").cloned()) }) .unwrap_or_default(); @@ -1616,14 +1617,14 @@ pub extern "C" fn js_node_http_server_process_pending() -> i32 { // dispatch extensions + GC scanner are registered on the // main thread before user code touches the socket. perry_ext_net::ensure_adopted_socket_dispatch(); - crate::upgrade::fire_upgrade_listeners( + crate::server::upgrade::fire_upgrade_listeners( up.server_handle, up.request_handle, up.raw_socket_id, up.head, ); } else { - crate::upgrade::fire_upgrade_listeners( + crate::server::upgrade::fire_upgrade_listeners( up.server_handle, up.request_handle, up.ws_id, @@ -1639,34 +1640,36 @@ pub extern "C" fn js_node_http_server_process_pending() -> i32 { } let mut https_handles: Vec = Vec::new(); - perry_ffi::iter_handle_ids_of::(|id| { + perry_ffi::iter_handle_ids_of::(|id| { https_handles.push(id) }); for h in https_handles { - count += - drain_deferred_listen_for::(h, |s| &mut s.base); - while let Some(p) = crate::https_server::try_recv_pending_https_nonblocking(h) { - crate::https_server::process_pending_https(p); + count += drain_deferred_listen_for::(h, |s| { + &mut s.base + }); + while let Some(p) = crate::server::https_server::try_recv_pending_https_nonblocking(h) { + crate::server::https_server::process_pending_https(p); count += 1; } } let mut h2_handles: Vec = Vec::new(); - perry_ffi::iter_handle_ids_of::(|id| { + perry_ffi::iter_handle_ids_of::(|id| { h2_handles.push(id) }); for h in h2_handles { - count += drain_deferred_listen_for::(h, |s| { - &mut s.base - }); - count += crate::http2_server::process_pending_h2_events(); - while let Some(p) = crate::http2_server::try_recv_pending_h2_nonblocking(h) { - crate::http2_server::process_pending_h2(p); + count += drain_deferred_listen_for::( + h, + |s| &mut s.base, + ); + count += crate::server::http2_server::process_pending_h2_events(); + while let Some(p) = crate::server::http2_server::try_recv_pending_h2_nonblocking(h) { + crate::server::http2_server::process_pending_h2(p); count += 1; - count += crate::http2_server::process_pending_h2_events(); + count += crate::server::http2_server::process_pending_h2_events(); } } - count += crate::http2_server::process_pending_h2_events(); + count += crate::server::http2_server::process_pending_h2_events(); // #5010 — drain perry-ext-net's own pending-event queue. A raw // `'upgrade'` (#4973) hands the listener a real `net.Socket` adopted into @@ -1882,7 +1885,7 @@ pub(crate) fn synthesize_default_response_if_needed(response_handle: i64) { status_message: sr.status_message.clone(), headers, trailers: Vec::new(), - body: crate::response::ShapeBody::Full(body), + body: crate::server::response::ShapeBody::Full(body), }; if let Some(tx) = sr.response_tx.take() { let _ = tx.send(shape); diff --git a/crates/perry-ext-http-server/src/server/in_flight.rs b/crates/perry-ext-http/src/server/server/in_flight.rs similarity index 95% rename from crates/perry-ext-http-server/src/server/in_flight.rs rename to crates/perry-ext-http/src/server/server/in_flight.rs index b568b6810b..04865d7752 100644 --- a/crates/perry-ext-http-server/src/server/in_flight.rs +++ b/crates/perry-ext-http/src/server/server/in_flight.rs @@ -25,9 +25,11 @@ use std::time::{Duration, Instant}; use perry_ffi::get_handle; -use crate::request::close_incoming_message; -use crate::response::ServerResponse; -use crate::server::{synthesize_default_response_if_needed, HttpPendingRequest, HttpServer}; +use crate::server::request::close_incoming_message; +use crate::server::response::ServerResponse; +use crate::server::server::{ + synthesize_default_response_if_needed, HttpPendingRequest, HttpServer, +}; /// A request whose handler returned before finishing the response. pub(crate) struct InFlightRequest { @@ -113,7 +115,7 @@ pub(crate) fn reap_in_flight_requests() { if !ended { // Streaming backpressure cleared — fire `'drain'` (outside // the lock) so `res.on('drain')` producer loops resume. - let ls = crate::response::take_drain_listeners_if_ready(e.response_handle); + let ls = crate::server::response::take_drain_listeners_if_ready(e.response_handle); if !ls.is_empty() { drain_listeners.push(ls); } @@ -127,7 +129,7 @@ pub(crate) fn reap_in_flight_requests() { .and_then(|sr| sr.response_tx.as_ref()) .map(|tx| tx.is_closed()) .unwrap_or(false) - || crate::response::stream_receiver_gone(e.response_handle); + || crate::server::response::stream_receiver_gone(e.response_handle); let expired = now >= e.deadline; if ended || expired || peer_gone { // Only synthesize when we're giving up on a handler @@ -160,7 +162,7 @@ pub(crate) fn reap_in_flight_requests() { }); } for ls in drain_listeners { - crate::request::emit_no_arg_to_listeners(&ls); + crate::server::request::emit_no_arg_to_listeners(&ls); } // Finalize outside the lock — `synthesize_default_response_if_needed` // and `drop_handle` don't touch `IN_FLIGHT`, but keeping them off the diff --git a/crates/perry-ext-http-server/src/tls.rs b/crates/perry-ext-http/src/server/tls.rs similarity index 100% rename from crates/perry-ext-http-server/src/tls.rs rename to crates/perry-ext-http/src/server/tls.rs diff --git a/crates/perry-ext-http-server/src/types.rs b/crates/perry-ext-http/src/server/types.rs similarity index 100% rename from crates/perry-ext-http-server/src/types.rs rename to crates/perry-ext-http/src/server/types.rs diff --git a/crates/perry-ext-http-server/src/upgrade.rs b/crates/perry-ext-http/src/server/upgrade.rs similarity index 94% rename from crates/perry-ext-http-server/src/upgrade.rs rename to crates/perry-ext-http/src/server/upgrade.rs index 6b356e16b0..861dd34b50 100644 --- a/crates/perry-ext-http-server/src/upgrade.rs +++ b/crates/perry-ext-http/src/server/upgrade.rs @@ -4,7 +4,7 @@ //! # Design //! //! When a hyper service fn sees a request with `Connection: Upgrade` -//! + `Upgrade: websocket`, perry-ext-http-server diverges from the +//! + `Upgrade: websocket`, perry-ext-http diverges from the //! Phase 1 (req, res) flow. Instead: //! //! 1. The accepting tokio task awaits `hyper::upgrade::on(&mut req)`, @@ -37,9 +37,11 @@ use perry_ffi::{alloc_string, get_handle_mut, JsClosure, RawClosureHeader}; -use crate::request::handle_to_pointer_f64; -use crate::server::HttpServer; -use crate::types::{js_promise_run_microtasks, POINTER_TAG, PTR_MASK, STRING_TAG, TAG_UNDEFINED}; +use crate::server::request::handle_to_pointer_f64; +use crate::server::server::HttpServer; +use crate::server::types::{ + js_promise_run_microtasks, POINTER_TAG, PTR_MASK, STRING_TAG, TAG_UNDEFINED, +}; /// Test whether a request looks like a WebSocket upgrade — checks /// `Connection: Upgrade` (case-insensitive contains) and diff --git a/crates/perry-ext-http/src/test_async_shims.rs b/crates/perry-ext-http/src/test_async_shims.rs index db1a482367..e4b5407bbe 100644 --- a/crates/perry-ext-http/src/test_async_shims.rs +++ b/crates/perry-ext-http/src/test_async_shims.rs @@ -3,7 +3,7 @@ // live in `perry-stdlib::perry_ffi_async`, only linked into the final user // program). Provide synchronous, test-only shims for the `perry_ffi_*` async // externs the crate references so `cargo test -p perry-ext-http` links — same -// pattern as `perry-ext-net` / `perry-ext-http-server`. +// pattern as `perry-ext-net` / `perry-ext-fetch`. use perry_ffi::Promise; use std::ffi::c_void; @@ -50,3 +50,8 @@ pub extern "C" fn perry_ffi_spawn_blocking_with_reactor( ) { invoke(ctx); } + +// Pulled in transitively through perry-ext-net but normally supplied by the +// host stdlib archive, which unit-test binaries do not link. +#[no_mangle] +pub extern "C" fn perry_ffi_spawn_async(_ctx: *mut c_void) {} diff --git a/crates/perry-ext-net/Cargo.toml b/crates/perry-ext-net/Cargo.toml index f0795aedf3..249b5a7351 100644 --- a/crates/perry-ext-net/Cargo.toml +++ b/crates/perry-ext-net/Cargo.toml @@ -20,8 +20,8 @@ tokio = { workspace = true } # `&[u8]`). Drops the per-read `Vec` alloc + memcpy. Already in the # lockfile via tokio/rustls, so declaring it pulls nothing new. bytes.workspace = true -tokio-rustls = "0.26" -rustls = "0.23" +tokio-rustls.workspace = true +rustls.workspace = true rustls-native-certs = "0.8" [dev-dependencies] diff --git a/crates/perry-ext-net/src/adopt.rs b/crates/perry-ext-net/src/adopt.rs index 10560d9eaf..8199990225 100644 --- a/crates/perry-ext-net/src/adopt.rs +++ b/crates/perry-ext-net/src/adopt.rs @@ -12,7 +12,7 @@ use tokio::sync::mpsc; /// Adopt an already-connected TCP stream as a `net.Socket` handle. /// -/// perry-ext-http-server's raw `'upgrade'` path (#4973) calls this: Node +/// perry-ext-http's raw `'upgrade'` path (#4973) calls this: Node /// hands the `'upgrade'` listener the raw connection socket with nothing /// written to it, so the HTTP accept task peels the request head off the /// stream and passes the live stream here. The returned id drives the diff --git a/crates/perry-ext-net/src/handle_ids.rs b/crates/perry-ext-net/src/handle_ids.rs index 8ddcd483ae..05a3115b83 100644 --- a/crates/perry-ext-net/src/handle_ids.rs +++ b/crates/perry-ext-net/src/handle_ids.rs @@ -12,7 +12,7 @@ /// dispatch (`class_handles.rs::composite_handle_method_dispatch`) asks each /// registered extension "is this handle yours?" — so the FIRST extension whose /// private counter reached that number claims the call. Next.js's HTTP server -/// (perry-ext-http-server handle 1, via `register_handle`) therefore claimed +/// (perry-ext-http handle 1, via `register_handle`) therefore claimed /// `socket.on('data', …)` on this crate's socket 1: the listener landed on the /// HTTP server, the reader delivered the MySQL greeting to an empty listener /// list, and mysql2's handshake hung to ETIMEDOUT. diff --git a/crates/perry-ext-net/src/jsvalue.rs b/crates/perry-ext-net/src/jsvalue.rs index d9d949e847..90072cb243 100644 --- a/crates/perry-ext-net/src/jsvalue.rs +++ b/crates/perry-ext-net/src/jsvalue.rs @@ -56,7 +56,7 @@ extern "C" { /// Issue #1131 — read a NaN-boxed JS value as the raw bytes to put on /// the wire for `socket.write(chunk)`. Outbound mirror of -/// `perry-ext-http-server`'s `jsvalue_to_body_bytes` (#1124): a JS +/// `perry-ext-http`'s `jsvalue_to_body_bytes` (#1124): a JS /// string and a `Buffer` have *different* memory layouts /// (`StringHeader` is 20 bytes, `{ utf16_len, byte_len, capacity, /// refcount, flags }`; `BufferHeader` is 8 bytes, `{ length, capacity diff --git a/crates/perry-ext-net/src/lib.rs b/crates/perry-ext-net/src/lib.rs index c5170279fe..05b0ac97e5 100644 --- a/crates/perry-ext-net/src/lib.rs +++ b/crates/perry-ext-net/src/lib.rs @@ -248,7 +248,7 @@ pub(crate) mod statics { } /// Backing state for an `net.Server` handle (`net.createServer(...)`). -/// Mirrors `perry-ext-http-server::HttpServer` in shape but stripped to +/// Mirrors `perry-ext-http::HttpServer` in shape but stripped to /// the raw-TCP surface — no hyper, no request/response channels, just /// the accept loop's shutdown sender + bound address. Per-server event /// listeners (`'connection'`, `'listening'`, `'close'`, `'error'`) live @@ -954,7 +954,7 @@ pub unsafe extern "C" fn js_net_server_close(handle: i64, callback_i64: i64) { /// `server.address()` — returns a JSON string the TS-side wrapper can /// `JSON.parse` into `{ port, address, family }`. Matches the -/// perry-ext-http-server contract (`js_node_http_server_address_json`). +/// perry-ext-http contract (`js_node_http_server_address_json`). /// /// Returns `null` (as a JS string) for an unlistening server. /// diff --git a/crates/perry-ext-ws/Cargo.toml b/crates/perry-ext-ws/Cargo.toml index bf478d836e..3088f267e5 100644 --- a/crates/perry-ext-ws/Cargo.toml +++ b/crates/perry-ext-ws/Cargo.toml @@ -14,7 +14,7 @@ crate-type = ["staticlib", "rlib"] [dependencies] perry-ffi.workspace = true tokio = { workspace = true } -# Use the workspace pin so perry-ext-http-server (issue #577) and +# Use the workspace pin so perry-ext-http (issue #577) and # perry-ext-ws agree on the WebSocketStream type for the upgrade # handoff; mismatched versions split type identity and the # `register_external_ws_stream` re-export silently fails to compile. @@ -24,7 +24,7 @@ tokio-tungstenite = { workspace = true } # CryptoProvider (feature unification enables both `ring` and `aws-lc-rs` in # the final link, and rustls panics on the first wss:// handshake unless one # is installed). Same pattern as perry-ext-net. -rustls = "0.23" +rustls.workspace = true futures-util = "0.3" lazy_static.workspace = true diff --git a/crates/perry-ext-ws/src/lib.rs b/crates/perry-ext-ws/src/lib.rs index 3e69713e63..b026b98563 100644 --- a/crates/perry-ext-ws/src/lib.rs +++ b/crates/perry-ext-ws/src/lib.rs @@ -53,7 +53,7 @@ const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; /// #6117 — rustls panics resolving the process-level CryptoProvider on the /// first `wss://` handshake when both `ring` and `aws-lc-rs` end up -/// feature-unified into the final link (perry-ext-http-server brings ring; +/// feature-unified into the final link (perry-ext-http brings ring; /// perry-ext-net brings aws-lc-rs). Install one explicitly before /// connecting. Idempotent — `install_default` errors (ignored) if a /// provider is already set. Same pattern as perry-ext-net's tls module. @@ -992,7 +992,7 @@ pub extern "C" fn js_ws_server_close(handle: i64) { } /// Register an externally-provided WebSocket stream as a perry-ext-ws -/// connection — used by perry-ext-http-server's upgrade path so that +/// connection — used by perry-ext-http's upgrade path so that /// `Server.on('upgrade', ...)` integration flows through the same /// per-client IO loop and listener registry as standalone /// `WebSocketServer({port})` connections (issue #577 Phase 4). @@ -1037,7 +1037,7 @@ where /// /// The handshake + per-client IO loop already happened: the host /// server's accept task (fastify's `handle_fastify_websocket_upgrade` -/// or perry-ext-http-server's `handle_websocket_upgrade`) drove +/// or perry-ext-http's `handle_websocket_upgrade`) drove /// `hyper::upgrade::on`, completed the tungstenite server handshake, /// and called `register_external_ws_stream` (which spawned /// `drive_server_client_io`). By the time the user's `'upgrade'` diff --git a/crates/perry-ffi/src/error.rs b/crates/perry-ffi/src/error.rs index f33ac37d42..3e0222a0bb 100644 --- a/crates/perry-ffi/src/error.rs +++ b/crates/perry-ffi/src/error.rs @@ -4,7 +4,7 @@ //! //! # Why //! -//! Wrappers compiled into their own staticlib (e.g. `perry-ext-http-server`) +//! Wrappers compiled into their own staticlib (e.g. `perry-ext-http`) //! cannot depend on `perry-runtime`'s Rust API and must not touch the //! runtime's thread-local registries directly: a direct //! `is_registered_buffer` / `register_error_code` call from the wrapper's diff --git a/crates/perry-ffi/src/handle.rs b/crates/perry-ffi/src/handle.rs index 087cab8f21..db52bf00d3 100644 --- a/crates/perry-ffi/src/handle.rs +++ b/crates/perry-ffi/src/handle.rs @@ -94,7 +94,7 @@ static NEXT_HANDLE: AtomicI64 = AtomicI64::new(FFI_HANDLE_ID_START); /// /// Without this, [`register_handle`] only ever bumps [`NEXT_HANDLE`], so a /// long-lived process that allocates a handle per unit of work — e.g. -/// `perry-ext-http-server`, which registers a request + response handle per +/// `perry-ext-http`, which registers a request + response handle per /// request and `drop_handle`s both once the response flushes — burns through /// the visible id band (`1 .. 0x40000`) and eventually panics in /// [`next_fresh_handle_id`], even though only a handful of handles are live at @@ -126,7 +126,7 @@ const FREE_HANDLES_CAP: usize = 64 * 1024; /// is fixed and published — a generation cannot be packed into the id). A /// consumer that resolves an object purely by id therefore cannot distinguish /// "the object I was given" from "a *different* object that happens to occupy -/// the same recycled id now." `perry-ext-http-server` hits this: a request +/// the same recycled id now." `perry-ext-http` hits this: a request /// handler can return before `res.end()`, leaving a stale JS-side `res` value /// (a bare tagged id) outstanding; once that request is finalized its id is /// freed. If the id were recycled *immediately*, the very next @@ -732,7 +732,7 @@ where /// pattern is to snapshot ids into a `Vec` first, then act on each /// id outside the iteration. /// -/// perry-ext-http-server's main-thread pump walks every registered +/// perry-ext-http's main-thread pump walks every registered /// HttpServer / HttpsServer / Http2SecureServer handle each tick to /// drain pending requests. pub fn iter_handle_ids_of(mut f: F) @@ -831,7 +831,7 @@ pub fn gc_register_mutable_root_scanner(scanner: GcMutableRootScanner) { /// Register a source-attributed mutable GC root scanner with Perry's runtime. /// /// `source` should be a short, stable package or subsystem name such as -/// `perry-ext-http-server`. It is copied into runtime GC diagnostics and +/// `perry-ext-http`. It is copied into runtime GC diagnostics and /// verifier errors so native roots do not collapse behind `perry-ffi`'s shared /// dispatcher. pub fn gc_register_mutable_root_scanner_named(source: &'static str, scanner: GcMutableRootScanner) { @@ -1078,7 +1078,7 @@ mod tests { fn freed_id_is_not_reusable_until_drained_no_cross_request_bleed() { // The ABA / use-after-recycle regression. Models the HTTP cross-request // body bleed in handle-registry terms (the layer where the hazard lives, - // independent of perry-ext-http-server's reaper plumbing): a response R1 + // independent of perry-ext-http's reaper plumbing): a response R1 // is registered under id `h`; the handler returns before `res.end()` and // the request is later finalized, freeing `h`; within the SAME tick a new // request registers its response R2; then a stale `res.write`/`res.end` diff --git a/crates/perry-hir/src/lower/expr_member.rs b/crates/perry-hir/src/lower/expr_member.rs index 99fb3395cb..e08a253708 100644 --- a/crates/perry-hir/src/lower/expr_member.rs +++ b/crates/perry-hir/src/lower/expr_member.rs @@ -1815,7 +1815,7 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re // with class_filter = Some("IncomingMessage" | // "ServerResponse"). Mapping table is the set of // properties exposed via per-class FFI getters in - // perry-ext-http-server. Anything not in the set + // perry-ext-http. Anything not in the set // falls back to the existing bare-method-name // dispatch (covers `request.headers` on fastify // and similar). @@ -1864,7 +1864,7 @@ fn lower_member_inner(ctx: &mut LoweringContext, member: &ast::MemberExpr) -> Re // Issue #2210 — `server.headersTimeout` etc. // get rewritten to `__get_` so the read // dispatches through the per-prop FFI in - // perry-ext-http-server (Phase 1 returns the + // perry-ext-http (Phase 1 returns the // stored numeric default; Phase 2 will reflect // the live hyper accept-loop state). | ("HttpServer", "listening") diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index 2de1e644b5..32afde13aa 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -42,7 +42,7 @@ mod-dgram = [] # that never imports `http2` links none of these tables and macOS # `-dead_strip` recovers the space. Pure cfg gate, no extra deps. This gates # only the constant tables — the HTTP/2 server surface (`js_node_http2_*`) is -# unaffected (it lives in perry-ext-http-server, routed via `http-client`). +# unaffected (it lives in perry-ext-http, routed via `http-client`). mod-http2-constants = [] # Cold-path diagnostic JSON serializers (~67 KB of code + the `serde_json` # pulled only by them, which dead-strips when unreferenced): GC cycle telemetry @@ -223,7 +223,7 @@ resolv-conf = "0.7" # + the common multicast/broadcast/TTL options; socket2 borrows the same fd # (SockRef) for the few std lacks — set_multicast_if and source-specific # membership — so those are real rather than silent no-ops. -socket2 = { version = "0.6", features = ["all"] } +socket2 = { workspace = true, features = ["all"] } # mimalloc is declared 64-bit-only below (see the target_pointer_width section) — # on arm64_32/ILP32 it is neither used nor compiled. diff --git a/crates/perry-runtime/src/buffer/query.rs b/crates/perry-runtime/src/buffer/query.rs index 5551611f46..70874c0090 100644 --- a/crates/perry-runtime/src/buffer/query.rs +++ b/crates/perry-runtime/src/buffer/query.rs @@ -5,7 +5,7 @@ use super::*; /// length through `out_len`. Returns null (and sets `*out_len = 0`) for any /// value that is neither a Buffer nor a TypedArray. /// -/// Used by out-of-crate FFI callers (`perry-ext-http-server`'s +/// Used by out-of-crate FFI callers (`perry-ext-http`'s /// `getUnpackedSettings`) that must read a *program-allocated* Buffer's /// bytes. Going through this extern symbol ensures the Buffer-registry /// lookup runs in the same runtime copy that allocated the Buffer (via the @@ -60,7 +60,7 @@ pub unsafe extern "C" fn js_value_buffer_or_typedarray_data( std::ptr::null() } -// Referenced only from the prebuilt `perry-ext-http-server` archive, so the +// Referenced only from the prebuilt `perry-ext-http` archive, so the // auto-optimize LTO pass would otherwise dead-strip it. Pin it. #[used] static KEEP_JS_VALUE_BUFFER_OR_TYPEDARRAY_DATA: unsafe extern "C" fn(f64, *mut u32) -> *const u8 = diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 08331a3f8d..20c9989832 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -422,7 +422,7 @@ pub fn is_closure_ptr(ptr: usize) -> bool { /// raw bits) is a closure/function — a `POINTER_TAG` value whose pointee /// carries `CLOSURE_MAGIC` — and 0 for objects, arrays, strings, numbers, and /// everything else. Exposed for external wrapper crates that link the runtime -/// only by C ABI (e.g. perry-ext-http-server's `parse_listen_args`, #2041), +/// only by C ABI (e.g. perry-ext-http's `parse_listen_args`, #2041), /// which need to tell a callback argument apart from an options-object /// argument without a Cargo dependency on perry-runtime. #[no_mangle] diff --git a/crates/perry-runtime/src/cluster.rs b/crates/perry-runtime/src/cluster.rs index ad308a6b6c..120dca8f65 100644 --- a/crates/perry-runtime/src/cluster.rs +++ b/crates/perry-runtime/src/cluster.rs @@ -116,8 +116,8 @@ pub fn worker_shared_reuseport_bind( /// over the fork IPC channel so it can emit `cluster.on('listening')` /// (#4914). No-op outside cluster workers or when the channel is gone. /// `#[no_mangle]` because the HTTP/HTTPS/HTTP2 listen sites live in -/// `perry-ext-http-server`, which has no Cargo dep on perry-runtime — the -/// symbol resolves at final link (same pattern as perry-ffi's helpers). +/// `perry-ext-http`. The C ABI remains the stable interface used by the +/// HTTP listen sites and perry-ffi helpers. #[no_mangle] pub extern "C" fn perry_cluster_worker_listening( addr_ptr: *const u8, diff --git a/crates/perry-runtime/src/cluster_sched.rs b/crates/perry-runtime/src/cluster_sched.rs index 8cd8f33de1..274fd664e0 100644 --- a/crates/perry-runtime/src/cluster_sched.rs +++ b/crates/perry-runtime/src/cluster_sched.rs @@ -618,9 +618,8 @@ pub fn tcp_stream_from_fd(fd: RawFd) -> std::net::TcpStream { } // --------------------------------------------------------------------------- -// C ABI surface for the out-of-crate listen sites (perry-ext-http-server has no -// Cargo dep on perry-runtime; these resolve at final link like perry-ffi's -// helpers and `perry_cluster_worker_listening`). +// C ABI surface for the HTTP listen sites. These remain aligned with +// perry-ffi's helpers and `perry_cluster_worker_listening`. // --------------------------------------------------------------------------- #[cfg(unix)] diff --git a/crates/perry-runtime/src/error.rs b/crates/perry-runtime/src/error.rs index 0366565c4c..cbcfd7bb1d 100644 --- a/crates/perry-runtime/src/error.rs +++ b/crates/perry-runtime/src/error.rs @@ -370,7 +370,7 @@ pub unsafe extern "C" fn js_error_value_with_code( } /// Generic "throw a JS Error subclass carrying a Node `.code`" FFI entry -/// point for out-of-crate callers (e.g. `perry-ext-http-server`'s http2 +/// point for out-of-crate callers (e.g. `perry-ext-http`'s http2 /// settings helpers) that have no direct access to `perry-runtime`'s Rust /// API. Diverges via `js_throw`. /// diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index d0c6f360a1..4f6907e128 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -335,7 +335,7 @@ mod stdlib_pump { // // perry-stdlib owns the single `STDLIB_PUMP_FN` slot above and drains // every in-tree module's pending queue from there. But the - // `perry-ext-*` wrapper crates (perry-ext-http-server's request queue, + // `perry-ext-*` wrapper crates (perry-ext-http's request queue, // perry-ext-http's client response queue, …) are normally drained by // `js_stdlib_process_pending`'s `#[cfg(feature = "external-*-pump")]` // arms — which are only compiled in when the *workspace* auto-optimize diff --git a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs index 8aa43b075e..24aeacc186 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch/dispatch_q_u.rs @@ -564,7 +564,7 @@ pub(crate) unsafe fn nm_dispatch_tls(ctx: &NmCtx, module_name: &str, method_name // method-call form (`http.createServer(...)`) already lowers through a // dedicated codegen NATIVE_MODULE_TABLE path; the value-read form yields // a bound-method closure (see `is_native_module_callable_export`) that - // lands here when invoked. The impls live in perry-ext-http-server, so + // lands here when invoked. The impls live in perry-ext-http, so // route through the dispatcher perry-stdlib registers at startup under // `external-http-server-pump` (enabled whenever http/https/http2 is // imported). Null when the http ext crate isn't linked → undefined. The diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs index 2d7487d75f..b083f09ccd 100644 --- a/crates/perry-runtime/src/symbol/properties.rs +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -134,7 +134,7 @@ pub(crate) fn clear_all_symbol_properties_for_object(obj_key: usize) { /// #6710: clear every per-handle JS-property side table for a recycled handle /// id (the string expando table AND the symbol tables). Called on the MAIN -/// (JS-owning) thread from perry-ext-http-server just before a recycled +/// (JS-owning) thread from perry-ext-http just before a recycled /// `IncomingMessage`/`ServerResponse` id is handed to a new request's handler, /// so no request inherits a prior request's `req.__rid` / `isRSCRequest` / /// `NextInternalRequestMeta`. The `handle` id equals `obj_key_from_f64` of the diff --git a/crates/perry-runtime/src/value/handle.rs b/crates/perry-runtime/src/value/handle.rs index 1d099bd569..5b6ed43b60 100644 --- a/crates/perry-runtime/src/value/handle.rs +++ b/crates/perry-runtime/src/value/handle.rs @@ -94,7 +94,7 @@ pub extern "C" fn js_set_native_tls_dispatch(func: JsNativeTlsDispatchFn) { /// Set the node:http/https/http2 server-factory dispatcher. Registered by /// perry-stdlib at startup (under `external-http-server-pump`) so a captured / -/// aliased `createServer` reaches the perry-ext-http-server impls, which this +/// aliased `createServer` reaches the perry-ext-http impls, which this /// crate can't call directly. Stays null when the http ext crate isn't linked. (#2533) #[no_mangle] pub extern "C" fn js_set_native_http_dispatch(func: JsNativeHttpDispatchFn) { diff --git a/crates/perry-runtime/src/value/tags.rs b/crates/perry-runtime/src/value/tags.rs index aded82b68f..c2629b98ca 100644 --- a/crates/perry-runtime/src/value/tags.rs +++ b/crates/perry-runtime/src/value/tags.rs @@ -158,7 +158,7 @@ pub(crate) type JsNativeTlsDispatchFn = /// enabled whenever a program imports one of those modules). Lets a captured / /// aliased `createServer` (`const cs = createServer; cs(handler)`, or /// `@hono/node-server`'s `const createServer = options.createServer || -/// createServerHTTP`) reach the perry-ext-http-server impls. Unlike crypto/zlib +/// createServerHTTP`) reach the perry-ext-http impls. Unlike crypto/zlib /// it also takes the module name so one callback can route http vs https vs /// http2. Stays null when the http ext crate isn't linked. (#2533) pub(crate) type JsNativeHttpDispatchFn = diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 8ba441db37..79f6c4844b 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -168,9 +168,9 @@ external-zlib-pump = ["async-runtime"] # Activated by `optimized_libs::build_optimized_libs` (v0.5.714) when # the well-known flip routes `node:http` / `node:https` / `node:http2` -# to perry-ext-http (which bundles perry-ext-http-server). Tells +# to perry-ext-http (which contains the server implementation). Tells # perry-stdlib's `js_stdlib_process_pending` and -# `js_stdlib_has_active_handles` to call into perry-ext-http-server's +# `js_stdlib_has_active_handles` to call into perry-ext-http's # `js_node_http_server_process_pending` / `js_node_http_server_has_active` # externs each tick. Without this, the http server's accept-loop # tokio task pushes pending requests that nobody drains and the @@ -360,9 +360,9 @@ rand = "0.8" # Required by lodash (core module) tokio = { version = "1", features = ["full"], optional = true } # HTTP Server -hyper = { version = "1.4", features = ["server", "http1", "http2"], optional = true } -hyper-util = { version = "0.1", features = ["server", "server-auto", "tokio"], optional = true } -http-body-util = { version = "0.1", optional = true } +hyper = { workspace = true, features = ["server", "http1", "http2"], optional = true } +hyper-util = { workspace = true, features = ["server", "server-auto", "tokio"], optional = true } +http-body-util = { workspace = true, optional = true } bytes = { workspace = true, optional = true } # DashMap is used by the always-on handle registry (`common/handle.rs`), # so it must NOT be optional — every minimal-stdlib build needs it. @@ -376,10 +376,10 @@ tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"], futures-util = { version = "0.3", optional = true } # TLS (for net.Socket.upgradeToTLS and tls.connect) — rustls-only, no OpenSSL. -tokio-rustls = { version = "0.26", optional = true } -rustls = { version = "0.23", optional = true } +tokio-rustls = { workspace = true, optional = true } +rustls = { workspace = true, optional = true } rustls-native-certs = { version = "0.8", optional = true } -rustls-pemfile = { version = "2", optional = true } +rustls-pemfile = { workspace = true, optional = true } # Database sqlx = { version = "0.9.0", default-features = false, features = ["runtime-tokio", "mysql", "postgres", "chrono"], optional = true } diff --git a/crates/perry-stdlib/src/common/async_bridge.rs b/crates/perry-stdlib/src/common/async_bridge.rs index 6a0b2e8342..8449c41029 100644 --- a/crates/perry-stdlib/src/common/async_bridge.rs +++ b/crates/perry-stdlib/src/common/async_bridge.rs @@ -609,7 +609,7 @@ pub extern "C" fn js_stdlib_process_pending() -> i32 { count += unsafe { crate::tls::js_tls_process_pending() }; } - // Process pending HTTP server requests + WS upgrades (perry-ext-http-server). + // Process pending HTTP server requests + WS upgrades (perry-ext-http). // Closes #604 — pre-fix `js_node_http_server_listen` blocked the // main TS thread inside an inner event_loop, so axios.get/etc. // after a `server.listen(port, () => resolve())` callback never diff --git a/crates/perry-stdlib/src/common/dispatch/init.rs b/crates/perry-stdlib/src/common/dispatch/init.rs index 5686a5d4d3..969bbfc787 100644 --- a/crates/perry-stdlib/src/common/dispatch/init.rs +++ b/crates/perry-stdlib/src/common/dispatch/init.rs @@ -180,7 +180,7 @@ pub unsafe extern "C" fn js_handle_prototype_dispatch(handle: i64) -> f64 { /// #2533: route a captured / aliased `http`/`https`/`http2` `createServer` /// (or the `Server` / `createSecureServer` aliases) back to the -/// perry-ext-http-server factories. Registered with the runtime via +/// perry-ext-http factories. Registered with the runtime via /// `js_set_native_http_dispatch` under `external-http-server-pump` (enabled /// whenever the program imports one of those modules), so we can safely /// `extern "C"`-reference the ext-crate symbols — they're guaranteed linked. @@ -670,7 +670,7 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { perry_runtime::js_set_native_tls_dispatch(crate::tls::js_tls_native_dispatch); // #2533: route captured / aliased http/https/http2 `createServer` back to - // the perry-ext-http-server factories. Only registered when the http ext + // the perry-ext-http factories. Only registered when the http ext // crate is linked (its symbols are referenced by the dispatcher), so the // runtime arm stays null-and-undefined for non-http programs. #[cfg(feature = "external-http-server-pump")] diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index d78df68326..fb8ec2e65f 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -507,7 +507,7 @@ pub unsafe extern "C" fn js_handle_method_dispatch( } // External http-server path (#2153): when `node:http` / `node:https` / - // `node:http2` routes through perry-ext-http-server, the HttpServer handle + // `node:http2` routes through perry-ext-http, the HttpServer handle // returned by `http.createServer(...)` reaches `js_native_call_method` via // the small-handle range check above whenever the receiver's static type // is `any` (e.g. `const s: any = http.createServer(...); s.listen(0)` or diff --git a/crates/perry-stdlib/src/tls.rs b/crates/perry-stdlib/src/tls.rs index f81f02f7eb..1951fc57d5 100644 --- a/crates/perry-stdlib/src/tls.rs +++ b/crates/perry-stdlib/src/tls.rs @@ -756,7 +756,7 @@ unsafe fn build_server_config_from_options( // the X.509 v1 certs in Node's test fixtures (`UnsupportedCertVersion`). // Node serves whatever cert/key the user supplies; load the signing // key directly and install a fixed-cert resolver. (Mirrors - // `perry-ext-http-server::tls::build_server_config`.) + // `perry-ext-http::tls::build_server_config`.) let signing_key = rustls::crypto::ring::default_provider() .key_provider .load_private_key(key) diff --git a/crates/perry-stdlib/src/ws.rs b/crates/perry-stdlib/src/ws.rs index 9583125bec..5c699cf7eb 100644 --- a/crates/perry-stdlib/src/ws.rs +++ b/crates/perry-stdlib/src/ws.rs @@ -22,7 +22,7 @@ use crate::common::{for_each_handle_mut_of, get_handle_mut, register_handle, Han /// #6117 — rustls panics resolving the process-level CryptoProvider on the /// first `wss://` handshake when both `ring` and `aws-lc-rs` end up -/// feature-unified into the final link (perry-ext-http-server brings ring; +/// feature-unified into the final link (perry-ext-http brings ring; /// net/tls bring aws-lc-rs). Install one explicitly before connecting. /// Idempotent — `install_default` errors (ignored) if a provider is already /// set. Mirrors `net::mod` / `tls` (#4971) and `perry-ext-net`. diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index fcb13e09e5..bfc93dc634 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -266,7 +266,7 @@ pub(crate) fn build_optimized_libs( // = ["web-fetch"]`, strip the umbrella and re-assert // `web-fetch`: fetch.rs/fetch_blob.rs stay, // http.rs/axios.rs go. The well-known staticlib - // (perry-ext-http / perry-ext-http-server) is still + // (perry-ext-http) is still // added for the actual node:http surface. if *feat == "http-client" && ctx.uses_fetch { features.remove("http-client"); @@ -377,10 +377,10 @@ pub(crate) fn build_optimized_libs( features.insert("external-fastify-pump"); } // Closes #604 — when the well-known flip routes `node:http` / - // `node:https` / `node:http2` to perry-ext-http (which bundles - // perry-ext-http-server), activate `external-http-server-pump` + // `node:https` / `node:http2` to perry-ext-http, activate + // `external-http-server-pump` // so perry-stdlib's main-thread pump and active-handles gate - // call into perry-ext-http-server's queue each tick. Without + // call into perry-ext-http's queue each tick. Without // this, the http server's accept-loop tokio task pushes // requests that nobody drains, and the program hangs (pre-#604 // listen() blocked the main thread; post-#604 listen() is @@ -498,7 +498,7 @@ pub(crate) fn build_optimized_libs( // so the link uses the prebuilt full `libperry_stdlib.a`. // That full stdlib does NOT carry the `perry-ext-*` host // functions — `node:http`'s server lives in perry-ext-http / - // perry-ext-http-server, which aren't perry-stdlib deps — so + // perry-ext-http, which aren't perry-stdlib deps — so // an out-of-box `node:http` server otherwise fails to link // with `Undefined symbols: _js_node_http_create_server…`. // Resolve the well-known ext staticlibs the program needs diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index 2ec1539366..df7222684e 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -36,11 +36,10 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { // ── HTTP client (reqwest) ───────────────────────────────────── // `http` / `https` / `http2` join the `http-client` umbrella since // they bottom out in reqwest just like axios + node-fetch — and - // perry-ext-http-server (issue #577) needs the same async-runtime + // perry-ext-http (issue #577) needs the same async-runtime // bridge for `perry_ffi_spawn_blocking_with_reactor`. The // well-known flip swaps perry-stdlib's http.rs for perry-ext-http - // (v0.5.571); `http2` flips to the same staticlib via the rlib - // dep on perry-ext-http-server. Programs that import `streams` + // (v0.5.571); `http2` flips to the same staticlib. Programs that import `streams` // should NOT also use the well-known flip — streams stays in // perry-stdlib until its own port lands. "axios" | "node-fetch" | "http" | "https" | "http2" => &["http-client"], diff --git a/crates/perry/well_known_bindings.toml b/crates/perry/well_known_bindings.toml index 5ef4487d14..9c83b201ed 100644 --- a/crates/perry/well_known_bindings.toml +++ b/crates/perry/well_known_bindings.toml @@ -243,9 +243,8 @@ crate = "perry-ext-http" lib = "perry_ext_http" tracking = "#466" -# Server-side `node:http2` lives in perry-ext-http-server, but its -# symbols are pulled in transitively through perry-ext-http (rlib -# dep), so the staticlib binding stays uniform across http/https/http2. +# Server-side `node:http2` lives in perry-ext-http's internal server +# module, so the staticlib binding stays uniform across http/https/http2. [bindings.http2] crate = "perry-ext-http" lib = "perry_ext_http" diff --git a/docs/src/contributing/crate-policy.md b/docs/src/contributing/crate-policy.md index 539fdc47b5..89c4f7612f 100644 --- a/docs/src/contributing/crate-policy.md +++ b/docs/src/contributing/crate-policy.md @@ -111,7 +111,7 @@ views reproduce each crate's category, decision, source path, Rust LOC, production dependencies, internal consumers, default membership, and workspace lint inheritance. LOC is reported live and is deliberately not committed. -The committed baseline records only structural signals: the 78 reviewed member +The committed baseline records only structural signals: the 76 reviewed member decisions, the default dependency closure, the `perry` CLI closure, and decision counts. Any structural change must update the policy intentionally; ordinary Rust source edits do not churn the baseline. diff --git a/scripts/release_sweep_tiers/tier12_link_smoke.sh b/scripts/release_sweep_tiers/tier12_link_smoke.sh index a91b5eb2c0..dbfa615e52 100755 --- a/scripts/release_sweep_tiers/tier12_link_smoke.sh +++ b/scripts/release_sweep_tiers/tier12_link_smoke.sh @@ -174,7 +174,7 @@ for entry in "${TARGETS[@]}"; do done # --- #1652 / #589: node:http + Web Fetch link+run smoke (host only) --- -# Guards against perry-ext-http-server's `js_node_http_*` FFI symbols, or the +# Guards against perry-ext-http's `js_node_http_*` FFI symbols, or the # Web Fetch Headers/Request/Response constructors, silently dropping out of # the default link. node:http server only links on the host (perry-ext-http # isn't cross-compiled to the mobile targets), so this is host-only. The diff --git a/test-files/test_issue_1124_http_buffer_body.ts b/test-files/test_issue_1124_http_buffer_body.ts index bbafc1b3be..f15db1a03c 100644 --- a/test-files/test_issue_1124_http_buffer_body.ts +++ b/test-files/test_issue_1124_http_buffer_body.ts @@ -2,7 +2,7 @@ // `res.write(buf)` / `res.end(buf)` and emitted the correct `Content-Length`, // but the wire body was zeroed. // -// Root cause: `crates/perry-ext-http-server/src/types.rs::jsvalue_to_body_bytes` +// Root cause: `crates/perry-ext-http/src/server/types.rs::jsvalue_to_body_bytes` // (lines 121–160) cast every POINTER_TAG pointer straight to // `*mut StringHeader` and read `byte_len` + `data_after_StringHeader` from it. // But `BufferHeader` is `{ length: u32, capacity: u32 }` (8 bytes, data diff --git a/test-files/test_issue_2533_aliased_http_createserver.ts b/test-files/test_issue_2533_aliased_http_createserver.ts index 572527c1f1..c5b0265a50 100644 --- a/test-files/test_issue_2533_aliased_http_createserver.ts +++ b/test-files/test_issue_2533_aliased_http_createserver.ts @@ -8,7 +8,7 @@ // `is_native_module_callable_export` so the value-read yields a bound-method // closure, and routes the closure's invocation through a new // `JS_NATIVE_HTTP_DISPATCH` hook (registered by perry-stdlib under -// `external-http-server-pump`) to the perry-ext-http-server factories. +// `external-http-server-pump`) to the perry-ext-http factories. // // Scope mirrors #2153 (HttpServer dynamic dispatch): this verifies the bind // path — `createServer` no longer throws and the returned server supports diff --git a/test-files/test_node_http_basic.ts b/test-files/test_node_http_basic.ts index d594fd6c4f..84427b1fa1 100644 --- a/test-files/test_node_http_basic.ts +++ b/test-files/test_node_http_basic.ts @@ -3,7 +3,7 @@ // property-style `req.method` / `req.url`, property-set // `res.statusCode = N`, method-call `res.setHeader(...)` / // `res.end(...)`. End-to-end smoke for the HIR/codegen plumbing -// through to perry-ext-http-server's hyper accept loop. +// through to perry-ext-http's hyper accept loop. import { createServer } from "node:http"; diff --git a/workspace-architecture.json b/workspace-architecture.json index c42e06af64..f85ce7d398 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -23,7 +23,7 @@ ] }, "baseline": { - "workspace_members": 77, + "workspace_members": 76, "default_dependency_closure": [ "perry", "perry-api-manifest", @@ -65,7 +65,7 @@ "decision_counts": { "externalize": 29, "keep": 42, - "merge": 2, + "merge": 1, "remove": 1, "review": 3 } @@ -203,10 +203,6 @@ "category": "binding", "decision": "keep" }, - "perry-ext-http-server": { - "category": "binding", - "decision": "merge" - }, "perry-ext-ioredis": { "category": "binding", "decision": "externalize" From aa7dd7ae385ecdb3a7042af45a4bbb1fef55f919 Mon Sep 17 00:00:00 2001 From: Sergi Gonzalez <31130069+TheHypnoo@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:07:42 +0200 Subject: [PATCH 2/7] refactor(http): route HTTP bindings through perry-ffi (#6834) * refactor(http): route HTTP bindings through perry-ffi * chore(changelog): add HTTP FFI boundary entry --- changelog.d/6834-http-ffi-boundary.md | 1 + crates/perry-ext-http/Cargo.toml | 29 +------- crates/perry-ext-http/src/agent.rs | 73 ++++++------------- .../src/client_request_surface.rs | 61 +++++++--------- crates/perry-ext-http/src/lib.rs | 22 ++---- crates/perry-ext-http/src/response_headers.rs | 13 ++-- crates/perry-ext-http/src/server/mod.rs | 21 ++---- crates/perry-ext-http/src/tls_client.rs | 26 +------ crates/perry-ext-http/src/validation.rs | 25 ++++--- crates/perry-ffi/src/closure.rs | 42 +++++++++++ crates/perry-ffi/src/error.rs | 30 +++++++- crates/perry-ffi/src/event_pump.rs | 11 +++ crates/perry-ffi/src/jsvalue.rs | 63 +++++++++++++++- crates/perry-ffi/src/lib.rs | 15 ++-- docs/src/contributing/crate-policy.md | 2 - workspace-architecture.json | 4 +- 16 files changed, 244 insertions(+), 194 deletions(-) create mode 100644 changelog.d/6834-http-ffi-boundary.md diff --git a/changelog.d/6834-http-ffi-boundary.md b/changelog.d/6834-http-ffi-boundary.md new file mode 100644 index 0000000000..e199d43940 --- /dev/null +++ b/changelog.d/6834-http-ffi-boundary.md @@ -0,0 +1 @@ +refactor(http): route production HTTP bindings through `perry-ffi` and remove the normal `perry-ext-http` dependency on `perry-runtime` while preserving the selected HTTP archive ABI. diff --git a/crates/perry-ext-http/Cargo.toml b/crates/perry-ext-http/Cargo.toml index 8d8442ec68..288dbecf44 100644 --- a/crates/perry-ext-http/Cargo.toml +++ b/crates/perry-ext-http/Cargo.toml @@ -24,33 +24,6 @@ tokio-rustls.workspace = true rustls = { workspace = true, features = ["std", "ring", "tls12"] } rustls-pemfile.workspace = true tokio-tungstenite = { workspace = true } -# #2154: Agent argument validation throws `RangeError [ERR_OUT_OF_RANGE]` -# via `js_throw` + `register_error_code_pub`, which are perry-runtime's -# Rust-ABI helpers (not perry-ffi's). Stays consistent with perry-stdlib's -# parallel `http.rs` AgentHandle, which already calls into perry-runtime -# directly. Cargo feature unification keeps the stdlib feature on when -# both stdlib and this crate link the same perry-runtime — no duplicate -# symbols, no behaviour change for the default `full` build. -# #6303: perry-runtime MUST be built here with the same feature set the shipped -# `libperry_runtime.a` / `libperry_stdlib.a` carry (i.e. its `default`). This crate -# is a `staticlib`, so it BUNDLES the perry-runtime rlib objects into -# `libperry_ext_*.a` — and perry links the ext archives BEFORE stdlib/runtime -# (`prefer_well_known_before_stdlib`), so those bundled objects WIN the link for -# every symbol they define. The workspace dep is `default-features = false`, so -# without `"default"` here a per-crate `cargo build -p perry-ext-` (exactly what -# release-packages.yml does in its per-crate loop) bundles a runtime with -# `regex-engine`/`temporal`/... compiled OUT. The dispatchers those features gate -# are exported UNCONDITIONALLY (`js_string_replace_search_dyn`, -# `js_native_call_method`, ...) with the feature-gated logic `#[cfg]`-ed out of the -# BODY — so the degraded copy silently ToString-coerces a RegExp argument and -# searches for it literally instead of matching it (str.replace(re, fn) never fires -# its callback). Keep `"default"` in lock-step with perry-runtime's default feature -# list; the `ext_crates_bundle_a_full_featured_perry_runtime` test (well_known.rs) guards it. -# #6314: `stdlib` drops the bundled no-op `stdlib_stubs` (js_stdlib_init_dispatch, -# ...) from this staticlib's perry-runtime copy. Linked before stdlib, the no-op -# `js_stdlib_init_dispatch` otherwise wins first-definition and never registers -# the tokio reactor — every node:http server dies on its first accept. -perry-runtime = { workspace = true, features = ["default", "external-ws-symbols", "stdlib"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "http2"], default-features = false } tokio = { workspace = true } # Zero-copy body chunks: reqwest::Response::chunk() yields a refcounted @@ -66,4 +39,6 @@ lazy_static.workspace = true socket2.workspace = true [dev-dependencies] +# GC and async test shims call runtime internals; production code uses only perry-ffi. +perry-runtime.workspace = true perry-ffi = { workspace = true, features = ["runtime-link"] } diff --git a/crates/perry-ext-http/src/agent.rs b/crates/perry-ext-http/src/agent.rs index 7dedb717e7..78d1ba4f40 100644 --- a/crates/perry-ext-http/src/agent.rs +++ b/crates/perry-ext-http/src/agent.rs @@ -43,8 +43,8 @@ use crate::ensure_gc_scanner_registered; use lazy_static::lazy_static; use perry_ffi::{ - alloc_string, get_handle, get_handle_mut, iter_handles_of_mut, register_handle, GcRootVisitor, - Handle, JsClosure, JsString, JsValue, RawClosureHeader, StringHeader, + alloc_string, get_handle, get_handle_mut, iter_handles_of_mut, register_handle, ErrorKind, + GcRootVisitor, Handle, JsClosure, JsString, JsValue, RawClosureHeader, StringHeader, }; use std::collections::HashMap; use std::sync::Mutex; @@ -253,10 +253,7 @@ fn throw_out_of_range(name: &str, bound: &str, received: f64) -> ! { "The value of \"{}\" is out of range. It must be {}. Received {}", name, bound, received_str ); - let msg_ptr = perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32); - perry_runtime::node_submodules::register_error_code_pub(msg_ptr, "ERR_OUT_OF_RANGE"); - let err = perry_runtime::error::js_rangeerror_new(msg_ptr); - perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64)) + perry_ffi::throw_with_code(&message, "ERR_OUT_OF_RANGE", ErrorKind::RangeError) } fn format_received_number(n: f64) -> String { @@ -301,44 +298,25 @@ fn validate_positive(name: &str, value: f64) { /// universe so we can't mix them on the `js_object_get_field_by_name` /// boundary. unsafe fn read_field_bits(obj_f64: f64, field: &str) -> Option { - let bits = obj_f64.to_bits(); - let upper = bits >> 48; - let obj_ptr: *const perry_runtime::ObjectHeader = if upper >= 0x7FF8 { - (bits & PTR_MASK) as *const perry_runtime::ObjectHeader - } else if upper == 0 && bits >= 0x10000 { - bits as *const perry_runtime::ObjectHeader - } else { - return None; - }; - if obj_ptr.is_null() { - return None; - } - let key = perry_runtime::js_string_from_bytes(field.as_ptr(), field.len() as u32); - let val = perry_runtime::js_object_get_field_by_name(obj_ptr, key); - if val.is_undefined() || val.is_null() { + let value = perry_ffi::object_field_by_name(JsValue::from_bits(obj_f64.to_bits()), field); + if value.is_undefined() || value.is_null() { None } else { - Some(val.bits()) + Some(value.bits()) } } -unsafe fn raw_object_ptr_is_null(val_f64: f64) -> bool { - let bits = val_f64.to_bits(); - let upper = bits >> 48; - if upper >= 0x7FF8 { - (bits & PTR_MASK) == 0 - } else { - !(upper == 0 && bits >= 0x10000) - } +unsafe fn raw_object_ptr_is_null(value: f64) -> bool { + !JsValue::from_bits(value.to_bits()).is_pointer_or_raw() } unsafe fn read_number_field(obj_f64: f64, field: &str) -> Option { let bits = read_field_bits(obj_f64, field)?; - let val = perry_runtime::JSValue::from_bits(bits); + let val = JsValue::from_bits(bits); if val.is_number() { Some(val.to_number()) } else if val.is_int32() { - Some(val.as_int32() as f64) + Some(val.to_int32() as f64) } else { None } @@ -346,9 +324,9 @@ unsafe fn read_number_field(obj_f64: f64, field: &str) -> Option { unsafe fn read_bool_field(obj_f64: f64, field: &str) -> Option { let bits = read_field_bits(obj_f64, field)?; - let val = perry_runtime::JSValue::from_bits(bits); + let val = JsValue::from_bits(bits); if val.is_bool() { - Some(val.as_bool()) + Some(val.to_bool()) } else { None } @@ -356,11 +334,11 @@ unsafe fn read_bool_field(obj_f64: f64, field: &str) -> Option { unsafe fn read_string_field(obj_f64: f64, field: &str) -> Option { let bits = read_field_bits(obj_f64, field)?; - let val = perry_runtime::JSValue::from_bits(bits); + let val = JsValue::from_bits(bits); if !val.is_string() { return None; } - let ptr = val.as_string_ptr() as *mut perry_ffi::StringHeader; + let ptr = val.as_string_ptr(); if ptr.is_null() { return None; } @@ -617,16 +595,7 @@ unsafe fn agent_new_with_protocol(options_f64: f64, default_protocol: &str) -> H "The argument 'scheduling' must be one of: 'fifo', 'lifo'. Received {:?}", s ); - let msg_ptr = - perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32); - perry_runtime::node_submodules::register_error_code_pub( - msg_ptr, - "ERR_INVALID_ARG_VALUE", - ); - let err = perry_runtime::error::js_typeerror_new(msg_ptr); - perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer( - err as i64, - )) + perry_ffi::throw_with_code(&message, "ERR_INVALID_ARG_VALUE", ErrorKind::TypeError) } agent.scheduling = s; } @@ -857,11 +826,13 @@ fn json_value_to_string(v: &serde_json::Value) -> String { #[no_mangle] pub extern "C" fn js_http_agent_noop_self(handle: Handle) -> Handle { - perry_runtime::stub_diag::perry_stub_warn( - "http.Agent keepSocketAlive/reuseSocket", - "reqwest owns the keep-alive pool; per-socket hooks are no-ops", - Some("#4917"), - ); + unsafe { + perry_ffi::warn_stub( + c"http.Agent keepSocketAlive/reuseSocket", + c"reqwest owns the keep-alive pool; per-socket hooks are no-ops", + Some(c"#4917"), + ) + }; handle } diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index 2ce0e6bb64..9c548ae138 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -29,12 +29,11 @@ fn null_value() -> f64 { } fn bool_value(value: bool) -> f64 { - f64::from_bits(perry_runtime::JSValue::bool(value).bits()) + f64::from_bits(JsValue::from_bool(value).bits()) } fn string_value(value: &str) -> f64 { - let ptr = perry_runtime::js_string_from_bytes(value.as_ptr(), value.len() as u32); - f64::from_bits(perry_runtime::JSValue::string_ptr(ptr).bits()) + f64::from_bits(JsValue::from_string_ptr(alloc_string(value).as_raw()).bits()) } fn handle_value(handle: Handle) -> f64 { @@ -117,12 +116,16 @@ fn remove_header_by_name(handle: Handle, name: &str) { fn headers_array(handle: Handle, raw: bool) -> f64 { let names = header_names(handle, raw); - let mut arr = perry_runtime::js_array_alloc(names.len() as u32); + let mut array = unsafe { perry_ffi::js_array_alloc(names.len() as u32) }; for name in names { - let ptr = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32); - arr = perry_runtime::js_array_push(arr, perry_runtime::JSValue::string_ptr(ptr)); + array = unsafe { + perry_ffi::js_array_push( + array, + JsValue::from_string_ptr(alloc_string(&name).as_raw()), + ) + }; } - f64::from_bits(perry_runtime::JSValue::array_ptr(arr).bits()) + f64::from_bits(JsValue::from_object_ptr(array).bits()) } fn headers_object(handle: Handle) -> f64 { @@ -136,35 +139,29 @@ fn headers_object(handle: Handle) -> f64 { .unwrap_or_default(); entries.sort_by(|a, b| a.0.cmp(&b.0)); entries.dedup_by(|a, b| a.0 == b.0); - - let obj = perry_runtime::js_object_alloc_null_proto(0, entries.len() as u32); - let mut keys = perry_runtime::js_array_alloc(entries.len() as u32); - for (index, (key, value)) in entries.iter().enumerate() { - let key_ptr = perry_runtime::js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value_ptr = perry_runtime::js_string_from_bytes(value.as_ptr(), value.len() as u32); - perry_runtime::js_object_set_field( - obj, - index as u32, - perry_runtime::JSValue::string_ptr(value_ptr), - ); - keys = perry_runtime::js_array_push(keys, perry_runtime::JSValue::string_ptr(key_ptr)); - } - perry_runtime::js_object_set_keys(obj, keys); - f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits()) + let fields: Vec<(&str, JsValue)> = entries + .iter() + .map(|(key, value)| { + ( + key.as_str(), + JsValue::from_string_ptr(alloc_string(value).as_raw()), + ) + }) + .collect(); + f64::from_bits(perry_ffi::alloc_null_proto_object(&fields).bits()) } /// `{ name: }` — stands in for `.constructor` so /// `out.constructor.name` discriminates ClientRequest/ServerResponse the /// way the corpus outgoing-message tests expect (#4909). pub(crate) fn constructor_object(name: &str) -> f64 { - let obj = perry_runtime::js_object_alloc_null_proto(0, 1); - let key_ptr = perry_runtime::js_string_from_bytes("name".as_ptr(), 4); - let value_ptr = perry_runtime::js_string_from_bytes(name.as_ptr(), name.len() as u32); - perry_runtime::js_object_set_field(obj, 0, perry_runtime::JSValue::string_ptr(value_ptr)); - let mut keys = perry_runtime::js_array_alloc(1); - keys = perry_runtime::js_array_push(keys, perry_runtime::JSValue::string_ptr(key_ptr)); - perry_runtime::js_object_set_keys(obj, keys); - f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits()) + f64::from_bits( + perry_ffi::alloc_null_proto_object(&[( + "name", + JsValue::from_string_ptr(alloc_string(name).as_raw()), + )]) + .bits(), + ) } fn socket_value(handle: Handle) -> f64 { @@ -173,9 +170,7 @@ fn socket_value(handle: Handle) -> f64 { } with_state_mut(handle, |state| { if state.socket == 0.0 { - let obj = perry_runtime::js_object_alloc(0, 0); - state.socket = - f64::from_bits(perry_runtime::JSValue::object_ptr(obj as *mut u8).bits()); + state.socket = f64::from_bits(perry_ffi::alloc_object().bits()); } state.socket }) diff --git a/crates/perry-ext-http/src/lib.rs b/crates/perry-ext-http/src/lib.rs index 62c535924d..56de802405 100644 --- a/crates/perry-ext-http/src/lib.rs +++ b/crates/perry-ext-http/src/lib.rs @@ -851,10 +851,10 @@ unsafe fn invoke_create_socket( // of reading an uninitialized register for the second parameter. static REGISTER_ARITY: Once = Once::new(); REGISTER_ARITY.call_once(|| { - perry_runtime::closure::js_register_closure_arity(http_create_socket_cb as *const u8, 2); + perry_ffi::register_closure_arity(http_create_socket_cb as *const u8, 2); }); - let cb = perry_runtime::closure::js_closure_alloc(http_create_socket_cb as *const u8, 1); + let cb = perry_ffi::alloc_closure(http_create_socket_cb as *const u8, 1); if cb.is_null() { return; } @@ -862,7 +862,7 @@ unsafe fn invoke_create_socket( // (still-stored) method/url/headers/body and resume dispatch. Stored as an // f64 (a small registry id, not a heap pointer) — pointer-free, so it // needs no GC layout fixup, matching `sqlite_tx_wrapper`'s db-handle slot. - perry_runtime::closure::js_closure_set_capture_f64(cb, 0, request_handle as f64); + perry_ffi::set_closure_capture_f64(cb, 0, request_handle as f64); let cb_val = f64::from_bits(POINTER_TAG | (cb as usize as u64 & PTR_MASK)); let req_val = f64::from_bits(POINTER_TAG | (request_handle as u64 & PTR_MASK)); @@ -879,12 +879,11 @@ unsafe fn invoke_create_socket( /// the override hands back a `net.Socket` (POINTER_TAG-boxed handle, or a bare /// small handle on some codegen paths). unsafe extern "C" fn http_create_socket_cb( - closure: *const perry_runtime::ClosureHeader, + closure: *const RawClosureHeader, err: f64, socket: f64, ) -> f64 { - let request_handle = - perry_runtime::closure::js_closure_get_capture_f64(closure, 0) as i64 as Handle; + let request_handle = perry_ffi::closure_capture_f64(closure, 0) as i64 as Handle; // Node calls `cb(err)` on failure, `cb(null, socket)` on success. let err_bits = err.to_bits(); @@ -1468,16 +1467,7 @@ unsafe fn emit_socket_timeout_overflow_warning(ms: f64) { "{value_text} does not fit into a 32-bit signed integer.\n\ Timer duration was truncated to 2147483647." ); - let msg_ptr = perry_runtime::js_string_from_bytes(message.as_ptr(), message.len() as u32); - let label = "TimeoutOverflowWarning"; - let label_ptr = perry_runtime::js_string_from_bytes(label.as_ptr(), label.len() as u32); - let msg_value = f64::from_bits(perry_runtime::JSValue::string_ptr(msg_ptr).bits()); - let label_value = f64::from_bits(perry_runtime::JSValue::string_ptr(label_ptr).bits()); - perry_runtime::process::js_process_emit_warning( - msg_value, - label_value, - f64::from_bits(TAG_UNDEFINED), - ); + perry_ffi::emit_warning(&message, "TimeoutOverflowWarning"); } /// `IncomingMessage.setEncoding(encoding)` for client responses. The same diff --git a/crates/perry-ext-http/src/response_headers.rs b/crates/perry-ext-http/src/response_headers.rs index 0468936456..f6ca382f34 100644 --- a/crates/perry-ext-http/src/response_headers.rs +++ b/crates/perry-ext-http/src/response_headers.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; -use perry_ffi::{alloc_string, JsValue, ObjectHeader}; +use perry_ffi::{alloc_string, js_array_alloc, js_array_push, JsValue, ObjectHeader}; const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; @@ -95,14 +95,13 @@ pub(crate) fn build_response_headers_object(raw: &[(String, String)]) -> f64 { if !obj.is_null() { for (i, key) in order.iter().enumerate() { let v = if key == "set-cookie" { - let mut arr = perry_runtime::js_array_alloc(set_cookie.len() as u32); + let mut arr = unsafe { js_array_alloc(set_cookie.len() as u32) }; for cookie in &set_cookie { - let ptr = - perry_runtime::js_string_from_bytes(cookie.as_ptr(), cookie.len() as u32); - arr = - perry_runtime::js_array_push(arr, perry_runtime::JSValue::string_ptr(ptr)); + arr = unsafe { + js_array_push(arr, JsValue::from_string_ptr(alloc_string(cookie).as_raw())) + }; } - JsValue::from_bits(perry_runtime::JSValue::array_ptr(arr).bits()) + JsValue::from_object_ptr(arr) } else if let Some(val) = combined.get(key) { let s = alloc_string(val); JsValue::from_string_ptr(s.as_raw()) diff --git a/crates/perry-ext-http/src/server/mod.rs b/crates/perry-ext-http/src/server/mod.rs index 402616dba6..4e193713e0 100644 --- a/crates/perry-ext-http/src/server/mod.rs +++ b/crates/perry-ext-http/src/server/mod.rs @@ -93,21 +93,12 @@ static GC_REGISTERED: Once = Once::new(); pub(crate) fn ensure_gc_scanner_registered() { GC_REGISTERED.call_once(|| { gc_register_mutable_root_scanner_named("perry-ext-http", scan_http_server_roots); - // #2532 — register the server pump + has-active with perry-runtime - // directly. In a workspace build perry-stdlib drains these via its - // `external-http-server-pump` arm, but an out-of-tree install links - // the prebuilt full stdlib with that arm compiled OUT — so without - // this the accepted requests would never be dispatched and the - // program would hang. Registration is idempotent on the runtime - // side, so the in-tree double-drain is a harmless no-op. - extern "C" { - fn js_register_aux_pump(f: extern "C" fn() -> i32); - fn js_register_aux_has_active(f: extern "C" fn() -> i32); - } - unsafe { - js_register_aux_pump(crate::server::server::js_node_http_server_process_pending); - js_register_aux_has_active(crate::server::server::js_node_http_server_has_active); - } + // Register the extension pump for out-of-tree links where stdlib does + // not compile its HTTP pump arm. Runtime registration is idempotent. + perry_ffi::register_aux_event_pump( + crate::server::server::js_node_http_server_process_pending, + crate::server::server::js_node_http_server_has_active, + ); // Wall 10 — register the handle property/method/property-set dispatch // extensions so erased-receiver `req.url` / `res.end(...)` etc. route to // our handles even when the linked perry-stdlib was built WITHOUT diff --git a/crates/perry-ext-http/src/tls_client.rs b/crates/perry-ext-http/src/tls_client.rs index f857d9294c..7d687d836b 100644 --- a/crates/perry-ext-http/src/tls_client.rs +++ b/crates/perry-ext-http/src/tls_client.rs @@ -36,8 +36,6 @@ //! tests need `rejectUnauthorized:false`. The `ca` trust anchors are //! still wired up so properly-SAN'd certs verify. -use super::PTR_MASK; - /// Parsed client-side TLS options. `Default` is "no TLS customization", /// in which case the caller keeps using the pooled default client. #[derive(Clone, Default, Debug)] @@ -285,26 +283,6 @@ mod tests { /// detect `checkServerIdentity` without a JSON round-trip (which drops /// functions). Mirrors the raw NaN-boxed field read in `agent.rs`. unsafe fn has_function_field(obj_f64: f64, field: &str) -> bool { - let bits = obj_f64.to_bits(); - let upper = bits >> 48; - let obj_ptr: *const perry_runtime::ObjectHeader = if upper >= 0x7FF8 { - (bits & PTR_MASK) as *const perry_runtime::ObjectHeader - } else if upper == 0 && bits >= 0x10000 { - bits as *const perry_runtime::ObjectHeader - } else { - return false; - }; - if obj_ptr.is_null() { - return false; - } - let key = perry_runtime::js_string_from_bytes(field.as_ptr(), field.len() as u32); - let val = perry_runtime::js_object_get_field_by_name(obj_ptr, key); - if val.is_undefined() || val.is_null() { - return false; - } - // Closures are NaN-boxed with POINTER_TAG (0x7FFD); a bare raw pointer - // (codegen sometimes hands these back) is also accepted. - let vbits = val.bits(); - let vupper = vbits >> 48; - vupper == 0x7FFD || (vupper == 0 && vbits >= 0x10000) + perry_ffi::object_field_by_name(perry_ffi::JsValue::from_bits(obj_f64.to_bits()), field) + .is_pointer_or_raw() } diff --git a/crates/perry-ext-http/src/validation.rs b/crates/perry-ext-http/src/validation.rs index 8f7af103c3..84dfa2e2bd 100644 --- a/crates/perry-ext-http/src/validation.rs +++ b/crates/perry-ext-http/src/validation.rs @@ -10,7 +10,7 @@ //! `ERR_OUT_OF_RANGE`). Throwing unwinds through the codegen call site back //! to the JS `try` / `assert.throws` frame. -use perry_runtime::fs::validate::throw_type_error_with_code; +use perry_ffi::{throw_with_code, ErrorKind}; /// Node HTTP token bytes (RFC 7230 `tchar`, mirrored from /// `lib/_http_common.js` `tokenRegExp`). Used for both method names and @@ -38,7 +38,7 @@ pub(crate) fn validate_client_url_string(raw: &str) { Err(_) => true, }; if invalid { - throw_type_error_with_code("Invalid URL", "ERR_INVALID_URL"); + throw_with_code("Invalid URL", "ERR_INVALID_URL", ErrorKind::TypeError); } } @@ -55,9 +55,10 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol // `validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser')`). if let Some(v) = obj.get("insecureHTTPParser") { if !v.is_boolean() && !v.is_null() { - throw_type_error_with_code( + throw_with_code( "The \"options.insecureHTTPParser\" property must be of type boolean.", "ERR_INVALID_ARG_TYPE", + ErrorKind::TypeError, ); } } @@ -66,9 +67,10 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol // `validateNumber(timeout, 'timeout')`). `timeout: null` throws. if let Some(v) = obj.get("timeout") { if !v.is_number() { - throw_type_error_with_code( + throw_with_code( "The \"timeout\" argument must be of type number.", "ERR_INVALID_ARG_TYPE", + ErrorKind::TypeError, ); } } @@ -81,9 +83,10 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol let normalized = format!("{}:", proto.trim_end_matches(':')); let expected = format!("{default_protocol}:"); if normalized != expected { - throw_type_error_with_code( + throw_with_code( &format!("Protocol \"{normalized}\" not supported. Expected \"{expected}\""), "ERR_INVALID_PROTOCOL", + ErrorKind::TypeError, ); } } @@ -96,9 +99,10 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol // default instead of throwing (#4970). if let Some(method) = obj.get("method").and_then(|v| v.as_str()) { if !method.is_empty() && !is_valid_token(method) { - throw_type_error_with_code( + throw_with_code( &format!("Method must be a valid HTTP token [\"{method}\"]"), "ERR_INVALID_HTTP_TOKEN", + ErrorKind::TypeError, ); } } @@ -110,9 +114,10 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol let cp = c as u32; !(0x21..=0xff).contains(&cp) }) { - throw_type_error_with_code( + throw_with_code( "Request path contains unescaped characters", "ERR_UNESCAPED_CHARACTERS", + ErrorKind::TypeError, ); } } @@ -122,15 +127,17 @@ pub(crate) fn validate_client_options(opts: &serde_json::Value, default_protocol if let Some(headers) = obj.get("headers").and_then(|v| v.as_object()) { for (name, value) in headers { if name.eq_ignore_ascii_case("host") && value.is_array() { - throw_type_error_with_code( + throw_with_code( "The \"options.headers.host\" property must be of type string.", "ERR_INVALID_ARG_TYPE", + ErrorKind::TypeError, ); } if !is_valid_token(name) { - throw_type_error_with_code( + throw_with_code( &format!("Header name must be a valid HTTP token [\"{name}\"]"), "ERR_INVALID_HTTP_TOKEN", + ErrorKind::TypeError, ); } } diff --git a/crates/perry-ffi/src/closure.rs b/crates/perry-ffi/src/closure.rs index 6fd0127e0d..06e4b450f8 100644 --- a/crates/perry-ffi/src/closure.rs +++ b/crates/perry-ffi/src/closure.rs @@ -53,6 +53,36 @@ extern "C" { arg2: f64, arg3: f64, ) -> f64; + fn js_closure_alloc(func_ptr: *const u8, capture_count: u32) -> *mut ClosureHeader; + fn js_register_closure_arity(func_ptr: *const u8, arity: u32); + fn js_closure_get_capture_f64(closure: *const ClosureHeader, index: u32) -> f64; + fn js_closure_set_capture_f64(closure: *mut ClosureHeader, index: u32, value: f64); +} + +/// Register the arity the runtime uses when dispatching a native closure. +pub fn register_closure_arity(func: *const u8, arity: u32) { + unsafe { js_register_closure_arity(func, arity) } +} + +/// Allocate a native closure with `capture_count` f64 capture slots. +pub fn alloc_closure(func: *const u8, capture_count: u32) -> *mut ClosureHeader { + unsafe { js_closure_alloc(func, capture_count) } +} + +/// Read an f64 capture slot from a native closure. +/// +/// # Safety +/// `closure` must point to a live closure with an allocated `index` slot. +pub unsafe fn closure_capture_f64(closure: *const ClosureHeader, index: u32) -> f64 { + js_closure_get_capture_f64(closure, index) +} + +/// Write an f64 capture slot in a native closure. +/// +/// # Safety +/// `closure` must point to a live closure with an allocated `index` slot. +pub unsafe fn set_closure_capture_f64(closure: *mut ClosureHeader, index: u32, value: f64) { + js_closure_set_capture_f64(closure, index, value) } /// Opaque handle to a JS closure (a `*const ClosureHeader`). @@ -142,4 +172,16 @@ mod tests { assert!(null.is_null()); assert!(null.as_raw().is_null()); } + + #[cfg(feature = "runtime-link")] + #[test] + fn native_closure_retains_capture() { + unsafe extern "C" fn callback(_: *const ClosureHeader) -> f64 { + 0.0 + } + register_closure_arity(callback as *const u8, 0); + let closure = alloc_closure(callback as *const u8, 1); + unsafe { set_closure_capture_f64(closure, 0, 42.0) }; + assert_eq!(unsafe { closure_capture_f64(closure, 0) }, 42.0); + } } diff --git a/crates/perry-ffi/src/error.rs b/crates/perry-ffi/src/error.rs index 3e0222a0bb..910fa5de2e 100644 --- a/crates/perry-ffi/src/error.rs +++ b/crates/perry-ffi/src/error.rs @@ -13,7 +13,8 @@ //! through these single extern symbols keeps the registry/throw logic in //! the one runtime copy the dispatch path resolves to. -use crate::JsValue; +use crate::{alloc_string, JsValue}; +use std::ffi::{c_char, CStr}; extern "C" { /// Runtime entry: build an Error subclass with a `.code`. @@ -51,6 +52,8 @@ extern "C" { syscall_len: usize, errno: f64, ) -> f64; + fn js_process_emit_warning(warning: f64, type_name: f64, code: f64); + fn perry_stub_warn_ffi(name: *const c_char, reason: *const c_char, issue: *const c_char); } /// Which JS Error subclass [`throw_with_code`] raises. @@ -125,6 +128,31 @@ pub fn system_error_value(msg: &str, code: &str, syscall: &str, errno: i64) -> J JsValue::from_bits(value.to_bits()) } +/// Queue a Node process warning with the given message and type name. +pub fn emit_warning(message: &str, type_name: &str) { + let message = JsValue::from_string_ptr(alloc_string(message).as_raw()); + let type_name = JsValue::from_string_ptr(alloc_string(type_name).as_raw()); + unsafe { + js_process_emit_warning( + f64::from_bits(message.bits()), + f64::from_bits(type_name.bits()), + f64::from_bits(JsValue::UNDEFINED.bits()), + ) + } +} + +/// Emit the runtime's once-per-symbol no-op warning. +/// +/// # Safety +/// All strings must live for the process lifetime. Use C string literals. +pub unsafe fn warn_stub(name: &'static CStr, reason: &'static CStr, issue: Option<&'static CStr>) { + perry_stub_warn_ffi( + name.as_ptr(), + reason.as_ptr(), + issue.map_or(std::ptr::null(), CStr::as_ptr), + ) +} + /// Borrow the raw bytes of a `Buffer` or `TypedArray` value. Returns /// `None` for any value that is neither (the caller should raise a /// `TypeError` in that case). The borrow is valid for the duration of the diff --git a/crates/perry-ffi/src/event_pump.rs b/crates/perry-ffi/src/event_pump.rs index 6ac75953d2..25bbf246cb 100644 --- a/crates/perry-ffi/src/event_pump.rs +++ b/crates/perry-ffi/src/event_pump.rs @@ -45,6 +45,17 @@ extern "C" { /// one wake — the main-loop tick drains every queue each pass /// regardless. fn js_notify_main_thread(); + fn js_register_aux_pump(f: extern "C" fn() -> i32); + fn js_register_aux_has_active(f: extern "C" fn() -> i32); +} + +/// Register an extension event pump and activity probe with the runtime. +/// Registration is idempotent for each function pointer. +pub fn register_aux_event_pump(pump: extern "C" fn() -> i32, has_active: extern "C" fn() -> i32) { + unsafe { + js_register_aux_pump(pump); + js_register_aux_has_active(has_active); + } } /// Wake the main thread so it picks up a pending event the calling diff --git a/crates/perry-ffi/src/jsvalue.rs b/crates/perry-ffi/src/jsvalue.rs index 50668fe0b3..b36e2cce20 100644 --- a/crates/perry-ffi/src/jsvalue.rs +++ b/crates/perry-ffi/src/jsvalue.rs @@ -32,7 +32,7 @@ //! These tag values are part of perry-ffi's stable API — a //! perry-runtime renumbering bumps perry-ffi major. -use crate::{ArrayHeader, ObjectHeader, StringHeader}; +use crate::{alloc_string, ArrayHeader, ObjectHeader, StringHeader}; const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; @@ -259,6 +259,12 @@ impl JsValue { std::ptr::null_mut() } } + + /// True when this is a tagged heap pointer or a legacy bare pointer. + #[inline] + pub const fn is_pointer_or_raw(self) -> bool { + self.is_pointer() || (self.0 >> 48 == 0 && self.0 >= 0x10000) + } } impl std::fmt::Debug for JsValue { @@ -322,6 +328,11 @@ extern "C" { /// Write the field at `field_index`. pub fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, value: JsValue); + + fn js_object_alloc(class_id: u32, field_count: u32) -> *mut ObjectHeader; + fn js_object_alloc_null_proto(class_id: u32, field_count: u32) -> *mut ObjectHeader; + fn js_object_set_keys(obj: *mut ObjectHeader, keys_array: *mut ArrayHeader); + fn js_object_get_field_by_name(obj: *const ObjectHeader, key: *const StringHeader) -> JsValue; } /// Compute `(packed_keys_bytes, shape_id)` for use with @@ -336,6 +347,56 @@ extern "C" { /// the same key list, which improves shape sharing across /// crates. `0x4646_0000` ("FF" prefix) namespaces perry-ffi-built /// shapes from perry-stdlib's hand-rolled ones. +/// Allocate an empty ordinary object. +pub fn alloc_object() -> JsValue { + let object = unsafe { js_object_alloc(0, 0) }; + if object.is_null() { + JsValue::UNDEFINED + } else { + JsValue::from_object_ptr(object) + } +} + +/// Allocate a null-prototype object with the given fields. +/// +/// Use this for Node objects such as request headers where inherited keys must +/// not be visible. Field order is preserved in the runtime keys array. +pub fn alloc_null_proto_object(fields: &[(&str, JsValue)]) -> JsValue { + let obj = unsafe { js_object_alloc_null_proto(0, fields.len() as u32) }; + if obj.is_null() { + return JsValue::UNDEFINED; + } + let mut keys = unsafe { js_array_alloc(fields.len() as u32) }; + for (index, (key, value)) in fields.iter().enumerate() { + unsafe { js_object_set_field(obj, index as u32, *value) }; + keys = unsafe { js_array_push(keys, JsValue::from_string_ptr(alloc_string(key).as_raw())) }; + } + unsafe { js_object_set_keys(obj, keys) }; + JsValue::from_object_ptr(obj) +} + +/// Read an own or inherited named field from an object value. +/// +/// Untagged legacy pointers are accepted because older generated call paths +/// can still pass them. Non-object values return `undefined`. +pub fn object_field_by_name(value: JsValue, key: &str) -> JsValue { + let bits = value.bits(); + let obj = if value.is_pointer() { + value.as_pointer::() + } else if bits >> 48 == 0 && bits >= 0x10000 { + bits as *mut ObjectHeader + } else { + std::ptr::null_mut() + }; + if obj.is_null() { + return JsValue::UNDEFINED; + } + let key = alloc_string(key); + unsafe { js_object_get_field_by_name(obj, key.as_raw()) } +} + +/// Compute `(packed_keys_bytes, shape_id)` for use with +/// [`js_object_alloc_with_shape`]. pub fn build_object_shape(keys: &[&str]) -> (Vec, u32) { let mut packed: Vec = Vec::new(); let mut shape_id: u32 = 0x4646_0000; diff --git a/crates/perry-ffi/src/lib.rs b/crates/perry-ffi/src/lib.rs index ae69827f26..959d6d5d97 100644 --- a/crates/perry-ffi/src/lib.rs +++ b/crates/perry-ffi/src/lib.rs @@ -72,12 +72,16 @@ pub use handle::{ mod jsvalue; pub use jsvalue::{ - build_object_shape, js_array_alloc, js_array_get, js_array_length, js_array_push, js_array_set, - js_object_alloc_with_shape, js_object_get_field, js_object_set_field, JsValue, + alloc_null_proto_object, alloc_object, build_object_shape, js_array_alloc, js_array_get, + js_array_length, js_array_push, js_array_set, js_object_alloc_with_shape, js_object_get_field, + js_object_set_field, object_field_by_name, JsValue, }; mod closure; -pub use closure::{JsClosure, RawClosureHeader}; +pub use closure::{ + alloc_closure, closure_capture_f64, register_closure_arity, set_closure_capture_f64, JsClosure, + RawClosureHeader, +}; mod bigint; pub use bigint::{alloc_bigint_from_str, read_bigint_limbs}; @@ -90,11 +94,12 @@ pub use json::json_stringify; mod error; pub use error::{ - error_value_with_code, system_error_value, throw_with_code, value_byte_slice, ErrorKind, + emit_warning, error_value_with_code, system_error_value, throw_with_code, value_byte_slice, + warn_stub, ErrorKind, }; mod event_pump; -pub use event_pump::notify_main_thread; +pub use event_pump::{notify_main_thread, register_aux_event_pump}; mod raw_net; pub use raw_net::{raw_net, register_raw_net, RawNetVtable}; diff --git a/docs/src/contributing/crate-policy.md b/docs/src/contributing/crate-policy.md index 89c4f7612f..dbfdffdce5 100644 --- a/docs/src/contributing/crate-policy.md +++ b/docs/src/contributing/crate-policy.md @@ -63,8 +63,6 @@ reason to split or merge it. - Native bindings use `perry-ffi` as their production interface to Perry. - A production dependency from `perry-ext-*` to `perry-runtime` is forbidden. - `perry-ext-http` is the sole recorded migration debt while its missing FFI - capabilities are introduced. - Test binaries may enable `perry-ffi/runtime-link`; that edge provides runtime symbols for tests and is not part of the binding's distributed contract. - Runtime and stdlib functionality must have one production implementation. diff --git a/workspace-architecture.json b/workspace-architecture.json index f85ce7d398..4fa21ef747 100644 --- a/workspace-architecture.json +++ b/workspace-architecture.json @@ -3,9 +3,7 @@ "expected_default_members": [ "perry" ], - "allowed_binding_runtime_dependencies": [ - "perry-ext-http" - ], + "allowed_binding_runtime_dependencies": [], "ci": { "linux_host_excluded_members": [ "perry-ui-android", From d77062e1dd60a0d00549284634af00e6f5e1f9d8 Mon Sep 17 00:00:00 2001 From: Sergi Gonzalez <31130069+TheHypnoo@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:07:51 +0200 Subject: [PATCH 3/7] refactor(http): remove bundled Node HTTP client (#6835) * refactor(http): route HTTP bindings through perry-ffi * chore(changelog): add HTTP FFI boundary entry * refactor(http): remove bundled Node HTTP client --- changelog.d/6835-remove-stdlib-http-client.md | 1 + crates/perry-stdlib/Cargo.toml | 24 +- .../perry-stdlib/src/common/async_bridge.rs | 7 - .../perry-stdlib/src/common/dispatch/init.rs | 8 +- .../src/common/dispatch/method_dispatch.rs | 10 - .../src/common/dispatch/property_dispatch.rs | 10 - crates/perry-stdlib/src/http.rs | 1999 ----------------- .../perry-stdlib/src/http/agent_dispatch.rs | 164 -- .../src/http/client_request_surface.rs | 409 ---- .../src/http/external_client_request.rs | 62 - crates/perry-stdlib/src/jsonwebtoken.rs | 2 +- crates/perry-stdlib/src/lib.rs | 22 +- .../commands/compile/optimized_libs/driver.rs | 44 +- crates/perry/src/commands/stdlib_features.rs | 41 +- 14 files changed, 34 insertions(+), 2769 deletions(-) create mode 100644 changelog.d/6835-remove-stdlib-http-client.md delete mode 100644 crates/perry-stdlib/src/http.rs delete mode 100644 crates/perry-stdlib/src/http/agent_dispatch.rs delete mode 100644 crates/perry-stdlib/src/http/client_request_surface.rs delete mode 100644 crates/perry-stdlib/src/http/external_client_request.rs diff --git a/changelog.d/6835-remove-stdlib-http-client.md b/changelog.d/6835-remove-stdlib-http-client.md new file mode 100644 index 0000000000..5309f9d83e --- /dev/null +++ b/changelog.d/6835-remove-stdlib-http-client.md @@ -0,0 +1 @@ +refactor(http): remove the duplicate bundled Node HTTP client and keep Node HTTP on `perry-ext-http`. diff --git a/crates/perry-stdlib/Cargo.toml b/crates/perry-stdlib/Cargo.toml index 79f6c4844b..a21f929790 100644 --- a/crates/perry-stdlib/Cargo.toml +++ b/crates/perry-stdlib/Cargo.toml @@ -79,12 +79,8 @@ bundled-commander = [] # perry-stdlib's per-tick bridge into it lives behind `external-fastify-pump`. http-server = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:bytes", "async-runtime"] -# HTTP client (node-fetch, axios) — single umbrella for both. The -# well-known flip strips this when the user imports either module -# (both perry-ext-fetch and perry-ext-axios cover the symbol surface -# under the umbrella). Per-binding splits could land later if a -# program needs e.g. axios-only without fetch, but in practice they -# share the reqwest dep so splitting saves no binary size. +# Web Fetch and Axios compatibility surface. The well-known flip can +# strip this when the external binding owns the imported surface. # `bundled-streams` rides under the umbrella for backwards-compat # with v0.5.571's `--features http-client` callers (which got # `pub mod streams` transitively). The well-known flip strips @@ -92,19 +88,9 @@ http-server = ["dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:bytes", # axios / node-fetch / http / https is imported through the # well-known table — both ends move in lockstep. # -# #5174: `http-client` decomposes into `web-fetch` (the Web Fetch API — -# `fetch()`, `Headers`, `Request`, `Response`, `Blob`/`File`, in -# `src/fetch/` + `src/fetch_blob.rs`) plus the bundled node:http client -# (`src/http.rs` + `src/axios.rs`). The halves share the -# reqwest/async-runtime/streams deps but are otherwise independent. -# Keeping them separable lets the well-known flip strip *only* the -# bundled node:http client when `node:http` routes to perry-ext-http, -# while preserving the Web Fetch FFIs a program needs for a bare -# `new Headers()` / `new Response()`. Linking both the bundled client -# and perry-ext-http previously produced duplicate -# `js_http_process_pending` (et al.) symbols, and perry-ext-http's -# aux-pump call bound to perry-stdlib's empty-queue copy — wedging the -# in-process response pump (#5174). +# #5174: `http-client` decomposes into `web-fetch` (the Web Fetch API) +# plus the Axios compatibility module. Node HTTP is provided only by +# perry-ext-http. web-fetch = ["dep:reqwest", "async-runtime", "bundled-streams"] http-client = ["web-fetch"] diff --git a/crates/perry-stdlib/src/common/async_bridge.rs b/crates/perry-stdlib/src/common/async_bridge.rs index 8449c41029..56ae4dcf41 100644 --- a/crates/perry-stdlib/src/common/async_bridge.rs +++ b/crates/perry-stdlib/src/common/async_bridge.rs @@ -570,13 +570,6 @@ pub extern "C" fn js_stdlib_process_pending() -> i32 { count += ws_count; } - // Process pending HTTP events (http/https client callbacks) - #[cfg(feature = "http-client")] - { - let http_count = unsafe { crate::http::js_http_process_pending() }; - count += http_count; - } - // Process pending raw TCP socket events (net.Socket). // v0.5.579 — gate now fires for `bundled-net` (perry-stdlib's // own implementation) AND `external-net-pump` (which the diff --git a/crates/perry-stdlib/src/common/dispatch/init.rs b/crates/perry-stdlib/src/common/dispatch/init.rs index 969bbfc787..09bce746c7 100644 --- a/crates/perry-stdlib/src/common/dispatch/init.rs +++ b/crates/perry-stdlib/src/common/dispatch/init.rs @@ -83,10 +83,6 @@ pub unsafe extern "C" fn js_handle_property_set_dispatch( // #4904: Agent tunables (`agent.maxSockets = 4`) and the // `agent.createConnection = fn` monkeypatch pattern Node's tests use. - #[cfg(feature = "http-client")] - if crate::http::dispatch_agent_property_set(handle, property_name, value) { - return; - } #[cfg(feature = "external-http-client-pump")] if matches!( property_name, @@ -487,7 +483,7 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { #[cfg(feature = "web-fetch")] fn js_register_global_fetch_body_init_ptr(f: extern "C" fn(f64) -> i64); // #4965: Headers → `res.setHeaders` entries-JSON producer. - #[cfg(feature = "http-client")] + #[cfg(feature = "web-fetch")] fn js_register_global_headers_entries_json( f: extern "C" fn(f64) -> *mut perry_runtime::StringHeader, ); @@ -532,7 +528,7 @@ pub unsafe extern "C" fn js_stdlib_init_dispatch() { ); #[cfg(feature = "web-fetch")] js_register_global_fetch_body_init_ptr(crate::fetch::js_response_body_init_ptr); - #[cfg(feature = "http-client")] + #[cfg(feature = "web-fetch")] js_register_global_headers_entries_json(crate::fetch::js_headers_setheaders_entries_json); #[cfg(feature = "web-fetch")] js_register_global_headers_object_json(crate::fetch::js_headers_fetch_object_json); diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index fb8ec2e65f..04ebcaed5a 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -55,11 +55,6 @@ pub unsafe extern "C" fn js_handle_method_dispatch( return value; } - #[cfg(feature = "http-client")] - if let Some(value) = unsafe { crate::http::dispatch_agent_method(handle, method_name, &args) } { - return value; - } - #[cfg(feature = "external-http-client-pump")] { extern "C" { @@ -93,11 +88,6 @@ pub unsafe extern "C" fn js_handle_method_dispatch( } } - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_client_request_method(handle, method_name, &args) { - return value; - } - // node:sqlite DatabaseSync handle. Keep this before the better-sqlite3 // SQLite fallbacks because method names like prepare/exec/close overlap // but the lifecycle/error semantics are intentionally different. diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs index 4f3c5944d7..3f893dec17 100644 --- a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -36,16 +36,6 @@ pub unsafe extern "C" fn js_handle_property_dispatch( return value; } - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_agent_property(handle, property_name) { - return value; - } - - #[cfg(feature = "http-client")] - if let Some(value) = crate::http::dispatch_client_request_property(handle, property_name) { - return value; - } - #[cfg(all(feature = "tls", not(target_os = "ios"), not(target_os = "android")))] if let Some(value) = crate::tls::dispatch_tls_property(handle, property_name) { return value; diff --git a/crates/perry-stdlib/src/http.rs b/crates/perry-stdlib/src/http.rs deleted file mode 100644 index cb5e65023e..0000000000 --- a/crates/perry-stdlib/src/http.rs +++ /dev/null @@ -1,1999 +0,0 @@ -//! HTTP/HTTPS client module (Node.js http/https compatible) -//! -//! Native implementation of Node.js http.request(), http.get(), https.request(), https.get() -//! using reqwest. Provides callback-based API matching the Node.js pattern used by SDKs -//! like twitter-api-v2, rss-parser, web-push, etc. -//! -//! Both http and https share this implementation — reqwest handles TLS based on URL scheme. - -use perry_runtime::{ - js_array_get_jsvalue, js_array_length, js_closure_call0, js_closure_call1, - js_object_get_field_by_name, js_object_keys, js_string_from_bytes, ArrayHeader, ClosureHeader, - JSValue, StringHeader, -}; -use std::collections::HashMap; -use std::sync::Mutex; - -use crate::common::async_bridge::spawn; -use crate::common::{for_each_handle_mut_of, get_handle_mut, register_handle, Handle}; - -mod client_request_surface; -pub(crate) use client_request_surface::{ - dispatch_client_request_method, dispatch_client_request_property, -}; -mod agent_dispatch; -pub(crate) use agent_dispatch::{ - dispatch_agent_method, dispatch_agent_property, dispatch_agent_property_set, -}; -#[cfg(feature = "external-http-client-pump")] -mod external_client_request; - -extern "C" { - fn js_value_is_closure(value_bits: i64) -> i32; - fn js_class_method_bind( - instance: f64, - method_name_ptr: *const u8, - method_name_len: usize, - ) -> f64; -} - -const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; -const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - -/// Pending HTTP events to be processed on the main thread -static HTTP_PENDING_EVENTS: once_cell::sync::Lazy>> = - once_cell::sync::Lazy::new(|| Mutex::new(Vec::new())); - -/// Push an HTTP event and wake the main thread (issue #84). -/// Every producer is inside an `async move { ... }` running on a tokio -/// worker — without the notify the event waits for the next event-loop -/// timeout to be picked up. -fn push_http_event(ev: PendingHttpEvent) { - HTTP_PENDING_EVENTS.lock().unwrap().push(ev); - perry_runtime::event_pump::js_notify_main_thread(); -} - -static HTTP_GC_REGISTERED: std::sync::Once = std::sync::Once::new(); - -/// Register the http GC root scanner exactly once. User closures passed -/// to `http.request(options, cb)` or `req.on('error', cb)` / `res.on(...)` -/// are stored inside ClientRequestHandle / IncomingMessageHandle values -/// in the handle registry and would otherwise not be marked by GC — -/// issue #35 pattern, same root cause as net.Socket listeners. -fn ensure_gc_scanner_registered() { - HTTP_GC_REGISTERED.call_once(|| { - perry_runtime::gc::gc_register_mutable_root_scanner_named( - "stdlib:http", - scan_http_roots_mut, - ); - }); -} - -/// GC root scanner for HTTP callback closures. Walks every -/// ClientRequestHandle (response callback + 'error' listeners) and -/// IncomingMessageHandle ('data' / 'end' / 'error' listeners) in the -/// handle registry. -#[allow(dead_code)] -fn scan_http_roots(mark: &mut dyn FnMut(f64)) { - let mut visitor = perry_runtime::gc::RuntimeRootVisitor::for_copy(mark); - scan_http_roots_mut(&mut visitor); -} - -fn scan_http_roots_mut(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { - for_each_handle_mut_of::(|req| { - visitor.visit_i64_slot(&mut req.response_callback); - for cb_vec in req.listeners.values_mut() { - for cb in cb_vec.iter_mut() { - visitor.visit_i64_slot(cb); - } - } - }); - - for_each_handle_mut_of::(|msg| { - for cb_vec in msg.listeners.values_mut() { - for cb in cb_vec.iter_mut() { - visitor.visit_i64_slot(cb); - } - } - }); - - // #2154: stored `agent.createConnection` / `.createSocket` closure - // pointers. Skip the 0-slot to avoid emitting an invalid root for - // agents that haven't had an override assigned. - for_each_handle_mut_of::(|agent| { - if agent.create_connection != 0 { - visitor.visit_i64_slot(&mut agent.create_connection); - } - if agent.create_socket != 0 { - visitor.visit_i64_slot(&mut agent.create_socket); - } - }); - - client_request_surface::scan_roots(visitor); -} - -/// Events that fire on the main thread via js_http_process_pending -enum PendingHttpEvent { - /// Response received: (request_handle, status, status_message, headers, body) - Response { - request_handle: Handle, - status: u16, - status_message: String, - headers: Vec<(String, String)>, - body: Vec, - }, - /// Error on request: (request_handle, error_message) - Error { - request_handle: Handle, - error_message: String, - }, -} - -/// ClientRequest handle — accumulates request options before sending -pub struct ClientRequestHandle { - /// HTTP method - method: String, - /// Full URL to request - url: String, - /// Request headers - headers: HashMap, - /// Request body (accumulated via write()) - body: Vec, - /// Response callback closure pointer (receives IncomingMessage handle) - response_callback: i64, - /// Event listeners: 'error' callbacks - listeners: HashMap>, - /// Timeout in milliseconds - timeout_ms: Option, - /// Whether end() has been called (prevents double-send) - ended: bool, - /// `options.agent` handle (#2154). When non-zero, dispatch reads the - /// Agent's `keepAlive` / `maxFreeSockets` / `keepAliveMsecs` and - /// folds them into the per-request reqwest::ClientBuilder config so - /// pool-related Agent options are honored instead of ignored. - agent_handle: Handle, -} - -/// Agent handle — Node's `http.Agent` / `https.Agent`. Perry's -/// `http.request` honors the Agent for its connection-pool config -/// (#2154); the rest of the fields are still pure metadata mirrored -/// from Node's defaults so `getName(options)` and the property -/// accessors agree byte-for-byte with Node's `lib/_http_agent.js`. -/// -/// Trackers: #2129 (initial constructor + getName), #2154 (validation -/// + per-agent client + socket-counter accessors + setters). -pub struct AgentHandle { - /// `https.Agent` defaults to `"https:"`, `http.Agent` to `"http:"`. - /// `null` is a legitimate value (some tests set it explicitly). - pub protocol: Option, - pub keep_alive: bool, - pub keep_alive_msecs: f64, - pub max_sockets: f64, - pub max_total_sockets: f64, - pub max_free_sockets: f64, - pub scheduling: String, - pub timeout_ms: Option, - /// `agent.destroy()` flips this so the `destroyed` accessor mirrors - /// Node's getter (#2154). - pub destroyed: bool, - /// User-supplied `createConnection` override closure pointer (#2154). - /// Storage + GC-rooting only today — full happy-path invocation - /// needs net.Socket-shaped JS objects and is tracked separately. - pub create_connection: i64, - pub create_socket: i64, -} - -impl Default for AgentHandle { - fn default() -> Self { - AgentHandle { - protocol: Some("http:".to_string()), - keep_alive: false, - keep_alive_msecs: 1000.0, - max_sockets: f64::INFINITY, - max_total_sockets: f64::INFINITY, - max_free_sockets: 256.0, - scheduling: "lifo".to_string(), - timeout_ms: None, - destroyed: false, - create_connection: 0, - create_socket: 0, - } - } -} - -/// IncomingMessage handle — represents an HTTP response -pub struct IncomingMessageHandle { - /// HTTP status code - pub status_code: u16, - /// HTTP status message - pub status_message: String, - /// Response headers - pub headers: HashMap, - /// Response body - pub body: Vec, - /// Event listeners: 'data', 'end', 'error' callbacks - pub listeners: HashMap>, - /// Encoding requested through `res.setEncoding(enc)`. - pub encoding: Option, -} - -/// Helper to extract string from StringHeader pointer -unsafe fn string_from_header(ptr: *const StringHeader) -> Option { - if ptr.is_null() { - return None; - } - let len = (*ptr).byte_len as usize; - let data_ptr = (ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data_ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) -} - -/// Helper to extract a string field from a NaN-boxed JS object -unsafe fn get_object_string_field(obj_f64: f64, field_name: &str) -> Option { - let obj_bits = obj_f64.to_bits(); - let upper = obj_bits >> 48; - // Must be a pointer-like value (POINTER_TAG 0x7FFD or raw pointer) - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - return None; - }; - if obj_ptr.is_null() { - return None; - } - - let key_str = js_string_from_bytes(field_name.as_ptr(), field_name.len() as u32); - let field_val = js_object_get_field_by_name(obj_ptr, key_str); - - if field_val.is_undefined() || field_val.is_null() { - return None; - } - - if field_val.is_string() { - let str_ptr = field_val.as_string_ptr(); - if !str_ptr.is_null() { - return string_from_header(str_ptr); - } - } - - // Try to extract from a number (port is often a number) - if field_val.is_number() { - return Some(format!("{}", field_val.as_number() as i64)); - } - - None -} - -/// Helper to extract a number field from a NaN-boxed JS object -unsafe fn get_object_number_field(obj_f64: f64, field_name: &str) -> Option { - let obj_bits = obj_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - return None; - }; - if obj_ptr.is_null() { - return None; - } - - let key_str = js_string_from_bytes(field_name.as_ptr(), field_name.len() as u32); - let field_val = js_object_get_field_by_name(obj_ptr, key_str); - - if field_val.is_undefined() || field_val.is_null() { - return None; - } - - if field_val.is_number() { - return Some(field_val.as_number()); - } - - None -} - -/// Helper to fetch a raw NaN-boxed field value from a JS object by name. -/// Returns None when the receiver is not a pointer-like value; returns -/// `Some(JSValue::undefined())` when the field is absent (matches the -/// underlying `js_object_get_field_by_name` behavior for `obj.missing`). -unsafe fn get_object_field_raw(obj_f64: f64, field_name: &str) -> Option { - let obj_bits = obj_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - return None; - }; - if obj_ptr.is_null() { - return None; - } - let key_str = js_string_from_bytes(field_name.as_ptr(), field_name.len() as u32); - Some(js_object_get_field_by_name(obj_ptr, key_str)) -} - -/// Returns true iff the JS value is "truthy" enough that Node's -/// `name += options.field` branch fires (i.e. `if (options.field)`): -/// not undefined, not null, not the empty string, not 0, not false. -fn jsvalue_is_truthy(v: JSValue) -> bool { - if v.is_undefined() || v.is_null() { - return false; - } - if v.is_bool() { - return v.as_bool(); - } - if v.is_int32() { - return v.as_int32() != 0; - } - if v.is_number() { - let n = v.as_number(); - return n != 0.0 && !n.is_nan(); - } - if v.is_string() || v.is_short_string() { - let s_ptr = perry_runtime::value::js_get_string_pointer_unified(f64::from_bits(v.bits())); - if s_ptr == 0 { - return false; - } - let header = s_ptr as *const StringHeader; - unsafe { (*header).byte_len > 0 } - } else { - // Other pointer values (objects, arrays, buffers) are always truthy - // in JS. - true - } -} - -/// Coerce a JS value to its string representation, matching how -/// `name += options.field` does ToString in JS. Strings/numbers/bools -/// flow through directly; arrays comma-join; buffers stringify their -/// content; objects fall back to "[object Object]". -unsafe fn jsvalue_to_string(v: JSValue) -> String { - let header = perry_runtime::value::js_jsvalue_to_string(f64::from_bits(v.bits())); - string_from_header(header).unwrap_or_default() -} - -/// `JSON.stringify(v)` as a Rust `String`. Used by https.Agent.getName -/// for the `sigalgs` field, which Node serializes as JSON. -unsafe fn jsvalue_to_json_string(v: JSValue) -> String { - let header = perry_runtime::json::js_json_stringify(f64::from_bits(v.bits()), 0); - string_from_header(header).unwrap_or_default() -} - -/// Helper to extract headers from a NaN-boxed JS headers object -unsafe fn extract_headers_from_object(obj_f64: f64) -> HashMap { - let mut result = HashMap::new(); - - let obj_bits = obj_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *mut perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *mut perry_runtime::ObjectHeader - } else { - return result; - }; - if obj_ptr.is_null() { - return result; - } - - // Get the keys array - let keys_ptr = js_object_keys(obj_ptr); - if keys_ptr.is_null() { - return result; - } - let len = js_array_length(keys_ptr); - - for i in 0..len { - let key_bits = js_array_get_jsvalue(keys_ptr, i); - let key_val = JSValue::from_bits(key_bits); - if key_val.is_string() { - let key_str_ptr = key_val.as_string_ptr(); - if !key_str_ptr.is_null() { - if let Some(key) = string_from_header(key_str_ptr) { - // Get value for this key - let val = js_object_get_field_by_name( - obj_ptr as *const perry_runtime::ObjectHeader, - key_str_ptr, - ); - if val.is_string() { - let val_ptr = val.as_string_ptr(); - if !val_ptr.is_null() { - if let Some(value) = string_from_header(val_ptr) { - result.insert(key, value); - } - } - } - } - } - } - } - - result -} - -/// Build URL from Node.js http.request options object -/// Options can have: hostname, host, port, path, protocol -unsafe fn build_url_from_options(options_f64: f64, default_protocol: &str) -> String { - let protocol = get_object_string_field(options_f64, "protocol") - .unwrap_or_else(|| format!("{}:", default_protocol)); - let protocol = protocol.trim_end_matches(':'); - - let hostname = get_object_string_field(options_f64, "hostname") - .or_else(|| get_object_string_field(options_f64, "host")) - .unwrap_or_else(|| "localhost".to_string()); - - // Remove port from hostname if present (host can be "hostname:port") - let hostname = hostname.split(':').next().unwrap_or("localhost"); - - let port = get_object_string_field(options_f64, "port") - .or_else(|| get_object_number_field(options_f64, "port").map(|n| format!("{}", n as u16))); - - let path = get_object_string_field(options_f64, "path").unwrap_or_else(|| "/".to_string()); - - match port { - Some(p) => format!("{}://{}:{}{}", protocol, hostname, p, path), - None => format!("{}://{}{}", protocol, hostname, path), - } -} - -/// Check if a f64 value is a NaN-boxed string pointer -fn is_string_value(val: f64) -> bool { - let bits = val.to_bits(); - let upper = bits >> 48; - upper == 0x7FFF // STRING_TAG -} - -/// Extract string from a NaN-boxed string value -unsafe fn extract_string_value(val: f64) -> Option { - let bits = val.to_bits(); - let upper = bits >> 48; - let ptr = if upper == 0x7FFF { - // STRING_TAG - (bits & 0x0000_FFFF_FFFF_FFFF) as *const StringHeader - } else if upper == 0x7FFD { - // POINTER_TAG (sometimes strings use this) - (bits & 0x0000_FFFF_FFFF_FFFF) as *const StringHeader - } else if upper == 0 && bits >= 0x10000 { - bits as *const StringHeader - } else { - return None; - }; - if ptr.is_null() { - return None; - } - string_from_header(ptr) -} - -// ======================================================================== -// Agent extraction (used by http.request / https.request / http.get) -// ======================================================================== - -/// Extract `options.agent` from a NaN-boxed options object. Returns 0 -/// when the field is missing, not a pointer, or doesn't resolve to an -/// AgentHandle. #2154. -unsafe fn extract_agent_handle(options_f64: f64) -> Handle { - let obj_bits = options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - return 0; - }; - if obj_ptr.is_null() { - return 0; - } - let key = js_string_from_bytes("agent".as_ptr(), 5); - let val = js_object_get_field_by_name(obj_ptr, key); - if !val.is_pointer() { - return 0; - } - let candidate = (val.bits() & 0x0000_FFFF_FFFF_FFFF) as Handle; - if get_handle_mut::(candidate).is_some() { - candidate - } else { - 0 - } -} - -fn normalize_url(raw: String, default_protocol: &str) -> String { - if raw.starts_with("http://") || raw.starts_with("https://") { - raw - } else if raw.is_empty() { - String::new() - } else { - format!("{}://{}", default_protocol, raw) - } -} - -unsafe fn url_from_js_value(value: f64, default_protocol: &str) -> String { - if is_string_value(value) { - return normalize_url( - extract_string_value(value).unwrap_or_default(), - default_protocol, - ); - } - if let Some(href) = get_object_string_field(value, "href") { - return normalize_url(href, default_protocol); - } - build_url_from_options(value, default_protocol) -} - -#[derive(Default)] -struct RequestOverload { - primary: Option, - options: Option, - callback: i64, -} - -unsafe fn parse_request_overload(args_array: i64) -> RequestOverload { - let mut out = RequestOverload::default(); - let arr_ptr = args_array as *const ArrayHeader; - if arr_ptr.is_null() || (args_array as u64) >> 48 != 0 { - return out; - } - let len = (*arr_ptr).length as usize; - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const u64; - for i in 0..len { - let bits = *elements.add(i); - if js_value_is_closure(bits as i64) != 0 { - out.callback = (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - continue; - } - let value = f64::from_bits(bits); - if out.primary.is_none() { - out.primary = Some(value); - } else if out.options.is_none() { - out.options = Some(value); - } - } - out -} - -unsafe fn request_parts_from_options( - primary: f64, - options: f64, - default_protocol: &str, - _auto_end: bool, -) -> (String, String, HashMap, Option, Handle) { - let method = get_object_string_field(options, "method") - .unwrap_or_else(|| "GET".to_string()) - .to_uppercase(); - let url = url_from_js_value(primary, default_protocol); - let mut headers = HashMap::new(); - if let Some(headers_val) = get_object_field_raw(options, "headers") { - if !headers_val.is_undefined() && !headers_val.is_null() { - headers = extract_headers_from_object(f64::from_bits(headers_val.bits())); - } - } - let timeout_ms = get_object_number_field(options, "timeout").map(|n| n as u64); - let agent_handle = extract_agent_handle(options); - (method, url, headers, timeout_ms, agent_handle) -} - -unsafe fn build_request_from_overload( - overload: RequestOverload, - default_protocol: &str, - force_get: bool, -) -> Handle { - ensure_gc_scanner_registered(); - let undefined = f64::from_bits(JSValue::undefined().bits()); - let primary = overload.primary.unwrap_or(undefined); - let options = overload.options.unwrap_or(primary); - let (method, url, headers, timeout_ms, agent_handle) = - request_parts_from_options(primary, options, default_protocol, force_get); - let handle = register_handle(ClientRequestHandle { - method, - url, - headers, - body: Vec::new(), - response_callback: overload.callback, - listeners: HashMap::new(), - timeout_ms, - ended: false, - agent_handle, - }); - if force_get { - js_http_client_request_end(handle, undefined); - } - handle -} - -// ======================================================================== -// FFI Functions -// ======================================================================== - -/// http.request(options, callback) -> ClientRequest handle -/// -/// options: NaN-boxed JS object with hostname, port, path, method, headers -/// callback: closure pointer for response callback (receives IncomingMessage handle) -/// -/// Returns a ClientRequest handle (i64) -#[no_mangle] -pub unsafe extern "C" fn js_http_request(options_f64: f64, callback_i64: i64) -> Handle { - ensure_gc_scanner_registered(); - let method = get_object_string_field(options_f64, "method") - .unwrap_or_else(|| "GET".to_string()) - .to_uppercase(); - - let url = build_url_from_options(options_f64, "http"); - - let mut headers = HashMap::new(); - - // Extract headers sub-object - let obj_bits = options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - std::ptr::null() - }; - - if !obj_ptr.is_null() { - let headers_key = js_string_from_bytes("headers".as_ptr(), 7); - let headers_val = js_object_get_field_by_name(obj_ptr, headers_key); - if !headers_val.is_undefined() && !headers_val.is_null() { - let headers_f64 = f64::from_bits(headers_val.bits()); - headers = extract_headers_from_object(headers_f64); - } - } - - let timeout_ms = get_object_number_field(options_f64, "timeout").map(|n| n as u64); - let agent_handle = extract_agent_handle(options_f64); - - register_handle(ClientRequestHandle { - method, - url, - headers, - body: Vec::new(), - response_callback: callback_i64, - listeners: HashMap::new(), - timeout_ms, - ended: false, - agent_handle, - }) -} - -/// `new http.ClientRequest(options)` (#4904). Perry's client model defers -/// the actual send to `.end()`, so constructing is exactly `http.request` -/// without a response callback. Node coerces a falsy `options.method` to -/// `GET` — mirror that here (`http.request` keeps whatever string it got). -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_standalone_new(options_f64: f64) -> Handle { - let handle = js_http_request(options_f64, 0); - if let Some(req) = get_handle_mut::(handle) { - if req.method.is_empty() { - req.method = "GET".to_string(); - } - } - handle -} - -/// https.request(options, callback) -> ClientRequest handle -/// Same as http.request but defaults to https protocol -#[no_mangle] -pub unsafe extern "C" fn js_https_request(options_f64: f64, callback_i64: i64) -> Handle { - ensure_gc_scanner_registered(); - let method = get_object_string_field(options_f64, "method") - .unwrap_or_else(|| "GET".to_string()) - .to_uppercase(); - - let url = build_url_from_options(options_f64, "https"); - - let mut headers = HashMap::new(); - - let obj_bits = options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - std::ptr::null() - }; - - if !obj_ptr.is_null() { - let headers_key = js_string_from_bytes("headers".as_ptr(), 7); - let headers_val = js_object_get_field_by_name(obj_ptr, headers_key); - if !headers_val.is_undefined() && !headers_val.is_null() { - let headers_f64 = f64::from_bits(headers_val.bits()); - headers = extract_headers_from_object(headers_f64); - } - } - - let timeout_ms = get_object_number_field(options_f64, "timeout").map(|n| n as u64); - let agent_handle = extract_agent_handle(options_f64); - - register_handle(ClientRequestHandle { - method, - url, - headers, - body: Vec::new(), - response_callback: callback_i64, - listeners: HashMap::new(), - timeout_ms, - ended: false, - agent_handle, - }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_https_request_variadic(args_array: i64) -> Handle { - build_request_from_overload(parse_request_overload(args_array), "https", false) -} - -/// http.get(url_or_options, callback) -> ClientRequest handle -/// Convenience method: sets method to GET and auto-calls end() -/// -/// First arg can be a string URL or an options object -#[no_mangle] -pub unsafe extern "C" fn js_http_get(url_or_options_f64: f64, callback_i64: i64) -> Handle { - ensure_gc_scanner_registered(); - let (url, headers, timeout_ms, agent_handle) = if is_string_value(url_or_options_f64) { - let url = extract_string_value(url_or_options_f64).unwrap_or_default(); - (url, HashMap::new(), None, 0) - } else { - // Options object - let url = build_url_from_options(url_or_options_f64, "http"); - let mut headers = HashMap::new(); - - let obj_bits = url_or_options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - std::ptr::null() - }; - - if !obj_ptr.is_null() { - let headers_key = js_string_from_bytes("headers".as_ptr(), 7); - let headers_val = js_object_get_field_by_name(obj_ptr, headers_key); - if !headers_val.is_undefined() && !headers_val.is_null() { - let headers_f64 = f64::from_bits(headers_val.bits()); - headers = extract_headers_from_object(headers_f64); - } - } - - let timeout_ms = get_object_number_field(url_or_options_f64, "timeout").map(|n| n as u64); - let agent_handle = extract_agent_handle(url_or_options_f64); - - (url, headers, timeout_ms, agent_handle) - }; - - let handle = register_handle(ClientRequestHandle { - method: "GET".to_string(), - url, - headers, - body: Vec::new(), - response_callback: callback_i64, - listeners: HashMap::new(), - timeout_ms, - ended: false, - agent_handle, - }); - - // GET auto-calls end() - js_http_client_request_end(handle, f64::from_bits(JSValue::undefined().bits())); - - handle -} - -/// https.get(url_or_options, callback) -> ClientRequest handle -/// Same as http.get but defaults to https -#[no_mangle] -pub unsafe extern "C" fn js_https_get(url_or_options_f64: f64, callback_i64: i64) -> Handle { - ensure_gc_scanner_registered(); - let (url, headers, timeout_ms, agent_handle) = if is_string_value(url_or_options_f64) { - let url = extract_string_value(url_or_options_f64).unwrap_or_default(); - // If URL doesn't start with https://, prepend it - let url = if url.starts_with("http://") || url.starts_with("https://") { - url - } else { - format!("https://{}", url) - }; - (url, HashMap::new(), None, 0) - } else { - let url = build_url_from_options(url_or_options_f64, "https"); - let mut headers = HashMap::new(); - - let obj_bits = url_or_options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - std::ptr::null() - }; - - if !obj_ptr.is_null() { - let headers_key = js_string_from_bytes("headers".as_ptr(), 7); - let headers_val = js_object_get_field_by_name(obj_ptr, headers_key); - if !headers_val.is_undefined() && !headers_val.is_null() { - let headers_f64 = f64::from_bits(headers_val.bits()); - headers = extract_headers_from_object(headers_f64); - } - } - - let timeout_ms = get_object_number_field(url_or_options_f64, "timeout").map(|n| n as u64); - let agent_handle = extract_agent_handle(url_or_options_f64); - - (url, headers, timeout_ms, agent_handle) - }; - - let handle = register_handle(ClientRequestHandle { - method: "GET".to_string(), - url, - headers, - body: Vec::new(), - response_callback: callback_i64, - listeners: HashMap::new(), - timeout_ms, - ended: false, - agent_handle, - }); - - // GET auto-calls end() - js_http_client_request_end(handle, f64::from_bits(JSValue::undefined().bits())); - - handle -} - -#[no_mangle] -pub unsafe extern "C" fn js_https_get_variadic(args_array: i64) -> Handle { - build_request_from_overload(parse_request_overload(args_array), "https", true) -} - -/// ClientRequest.write(body) — append data to request body -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_write(handle: Handle, body_f64: f64) -> Handle { - if let Some(req) = get_handle_mut::(handle) { - if let Some(body_str) = extract_string_value(body_f64) { - req.body.extend_from_slice(body_str.as_bytes()); - } - return handle; - } - - #[cfg(feature = "external-http-client-pump")] - { - let _ = unsafe { external_client_request::dispatch_method(handle, "write", &[body_f64]) }; - } - handle -} - -/// ClientRequest.end(body?) — finalize request and send it -/// Optional body parameter is appended before sending. -/// Spawns async reqwest request and queues response for main thread processing. -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_end(handle: Handle, body_f64: f64) -> Handle { - // Append optional body - if let Some(body_str) = extract_string_value(body_f64) { - if let Some(req) = get_handle_mut::(handle) { - req.body.extend_from_slice(body_str.as_bytes()); - } - } - - // Extract request data for async task - let (method, url, headers, body, timeout_ms, agent_pool) = { - let req = match get_handle_mut::(handle) { - Some(r) => r, - None => { - #[cfg(feature = "external-http-client-pump")] - { - let _ = unsafe { - external_client_request::dispatch_method(handle, "end", &[body_f64]) - }; - } - return handle; - } - }; - if req.ended { - return handle; // Already sent - } - req.ended = true; - // #2154: pull the Agent's pool config out NOW (still on the main - // thread; tokio worker can't safely touch the handle registry). - // `(keep_alive, max_free_sockets, keep_alive_msecs)` — None when - // the caller didn't pass `options.agent`, in which case we - // build a vanilla reqwest::Client below. - let agent_pool = if req.agent_handle != 0 { - get_handle_mut::(req.agent_handle) - .map(|a| (a.keep_alive, a.max_free_sockets, a.keep_alive_msecs)) - } else { - None - }; - ( - req.method.clone(), - req.url.clone(), - req.headers.clone(), - req.body.clone(), - req.timeout_ms, - agent_pool, - ) - }; - - // Spawn async HTTP request - let req_handle = handle; - spawn(async move { - let mut builder = reqwest::Client::builder(); - // Node's http client never follows redirects; disable reqwest's default (rationale: `apply_node_proxy_policy` in `perry-ext-http`). - builder = builder.redirect(reqwest::redirect::Policy::none()); - builder = if let Some(timeout) = timeout_ms { - builder.timeout(std::time::Duration::from_millis(timeout)) - } else { - builder.timeout(std::time::Duration::from_secs(30)) - }; - // #2154: honor Agent pool config when one is supplied. Without - // an Agent we keep the prior vanilla builder (no idle pool - // override) — Perry's stdlib http path historically created a - // fresh Client per request and we don't want to silently - // change that for code that doesn't opt in via options.agent. - if let Some((keep_alive, max_free_sockets, keep_alive_msecs)) = agent_pool { - let pool_max_idle = if keep_alive { - if !max_free_sockets.is_finite() || max_free_sockets > usize::MAX as f64 { - 256 - } else { - max_free_sockets.max(1.0) as usize - } - } else { - 0 - }; - let idle_timeout = if keep_alive { - let ms = if keep_alive_msecs.is_finite() && keep_alive_msecs > 0.0 { - keep_alive_msecs - } else { - 1000.0 - }; - std::time::Duration::from_millis(ms as u64) - } else { - std::time::Duration::from_millis(0) - }; - builder = builder - .pool_max_idle_per_host(pool_max_idle) - .pool_idle_timeout(idle_timeout); - } - let client = match builder.build() { - Ok(c) => c, - Err(e) => { - push_http_event(PendingHttpEvent::Error { - request_handle: req_handle, - error_message: format!("Failed to create HTTP client: {}", e), - }); - return; - } - }; - - let mut request = match method.as_str() { - "POST" => client.post(&url), - "PUT" => client.put(&url), - "DELETE" => client.delete(&url), - "PATCH" => client.patch(&url), - "HEAD" => client.head(&url), - "OPTIONS" => client.request(reqwest::Method::OPTIONS, &url), - _ => client.get(&url), - }; - - // Add headers - for (key, value) in &headers { - request = request.header(key.as_str(), value.as_str()); - } - - // Add body if non-empty - if !body.is_empty() { - request = request.body(body); - } - - match request.send().await { - Ok(response) => { - let status = response.status().as_u16(); - let status_message = response - .status() - .canonical_reason() - .unwrap_or("") - .to_string(); - - let mut resp_headers = Vec::new(); - for (key, value) in response.headers() { - if let Ok(v) = value.to_str() { - resp_headers.push((key.to_string(), v.to_string())); - } - } - - let body = response.bytes().await.unwrap_or_default().to_vec(); - - push_http_event(PendingHttpEvent::Response { - request_handle: req_handle, - status, - status_message, - headers: resp_headers, - body, - }); - } - Err(e) => { - push_http_event(PendingHttpEvent::Error { - request_handle: req_handle, - error_message: format!("{}", e), - }); - } - } - }); - - handle -} - -/// ClientRequest/IncomingMessage .on(event, callback) — register event listener -/// Works for both ClientRequest ('error') and IncomingMessage ('data', 'end', 'error') -#[no_mangle] -pub unsafe extern "C" fn js_http_on( - handle: Handle, - event_name_ptr: *const StringHeader, - callback_ptr: i64, -) -> Handle { - ensure_gc_scanner_registered(); - let event_name = match string_from_header(event_name_ptr) { - Some(name) => name, - None => return handle, - }; - - if callback_ptr == 0 { - return handle; - } - - // Try ClientRequest first - if let Some(req) = get_handle_mut::(handle) { - req.listeners - .entry(event_name) - .or_insert_with(Vec::new) - .push(callback_ptr); - return handle; - } - - // Try IncomingMessage - if let Some(res) = get_handle_mut::(handle) { - res.listeners - .entry(event_name) - .or_insert_with(Vec::new) - .push(callback_ptr); - return handle; - } - - handle -} - -/// ClientRequest.setHeader(name, value) — set a request header -#[no_mangle] -pub unsafe extern "C" fn js_http_set_header( - handle: Handle, - name_ptr: *const StringHeader, - value_ptr: *const StringHeader, -) -> Handle { - let name = match string_from_header(name_ptr) { - Some(n) => n, - None => return handle, - }; - let value = match string_from_header(value_ptr) { - Some(v) => v, - None => return handle, - }; - - if client_request_surface::is_client_request_handle(handle) { - client_request_surface::set_header(handle, &name, value); - return handle; - } - - #[cfg(feature = "external-http-client-pump")] - { - let name_value = f64::from_bits(0x7FFF_0000_0000_0000u64 | (name_ptr as u64 & PTR_MASK)); - let value_value = f64::from_bits(0x7FFF_0000_0000_0000u64 | (value_ptr as u64 & PTR_MASK)); - let _ = unsafe { - external_client_request::dispatch_method( - handle, - "setHeader", - &[name_value, value_value], - ) - }; - } - - handle -} - -/// ClientRequest.setTimeout(ms) — set request timeout -#[no_mangle] -pub unsafe extern "C" fn js_http_set_timeout(handle: Handle, ms: f64) -> Handle { - if let Some(req) = get_handle_mut::(handle) { - req.timeout_ms = Some(ms as u64); - return handle; - } - - #[cfg(feature = "external-http-client-pump")] - { - let _ = unsafe { external_client_request::dispatch_method(handle, "setTimeout", &[ms]) }; - } - handle -} - -/// IncomingMessage.setEncoding(encoding) — store the requested text encoding -/// for response data events and return the receiver for chaining. -#[no_mangle] -pub unsafe extern "C" fn js_http_incoming_message_set_encoding( - handle: Handle, - encoding_ptr: *const StringHeader, -) -> Handle { - let encoding = string_from_header(encoding_ptr).unwrap_or_else(|| "utf8".to_string()); - let mut matched = false; - if let Some(res) = get_handle_mut::(handle) { - res.encoding = Some(encoding); - matched = true; - } - if matched { - return handle; - } - - #[cfg(feature = "external-http-client-pump")] - { - extern "C" { - fn js_ext_http_client_incoming_message_is_handle(handle: i64) -> i32; - fn js_ext_http_client_incoming_message_set_encoding( - handle: i64, - encoding_ptr: *const StringHeader, - ) -> i64; - } - if js_ext_http_client_incoming_message_is_handle(handle) != 0 { - js_ext_http_client_incoming_message_set_encoding(handle, encoding_ptr); - return handle; - } - } - - #[cfg(feature = "external-http-server-pump")] - { - extern "C" { - fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; - fn js_node_http_im_set_encoding(handle: i64, encoding_ptr: *const StringHeader) -> i64; - } - if js_ext_http_incoming_message_is_handle(handle) != 0 { - js_node_http_im_set_encoding(handle, encoding_ptr); - } - } - handle -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_method(handle: Handle) -> *mut StringHeader { - let method = match get_handle_mut::(handle) { - Some(req) => req.method.clone(), - None => { - #[cfg(feature = "external-http-client-pump")] - if let Some(ptr) = unsafe { external_client_request::string_property(handle, "method") } - { - return ptr; - } - String::new() - } - }; - unsafe { js_string_from_bytes(method.as_ptr(), method.len() as u32) } -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_protocol(handle: Handle) -> *mut StringHeader { - let protocol = match get_handle_mut::(handle) { - Some(req) => reqwest::Url::parse(&req.url) - .map(|u| format!("{}:", u.scheme())) - .unwrap_or_default(), - None => { - #[cfg(feature = "external-http-client-pump")] - if let Some(ptr) = - unsafe { external_client_request::string_property(handle, "protocol") } - { - return ptr; - } - String::new() - } - }; - unsafe { js_string_from_bytes(protocol.as_ptr(), protocol.len() as u32) } -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_host(handle: Handle) -> *mut StringHeader { - let host = match get_handle_mut::(handle) { - Some(req) => reqwest::Url::parse(&req.url) - .ok() - .and_then(|u| u.host_str().map(|s| s.to_string())) - .unwrap_or_default(), - None => { - #[cfg(feature = "external-http-client-pump")] - if let Some(ptr) = unsafe { external_client_request::string_property(handle, "host") } { - return ptr; - } - String::new() - } - }; - unsafe { js_string_from_bytes(host.as_ptr(), host.len() as u32) } -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_path(handle: Handle) -> *mut StringHeader { - let path = match get_handle_mut::(handle) { - Some(req) => reqwest::Url::parse(&req.url) - .map(|u| { - let mut path = u.path().to_string(); - if path.is_empty() { - path.push('/'); - } - if let Some(q) = u.query() { - path.push('?'); - path.push_str(q); - } - path - }) - .unwrap_or_default(), - None => { - #[cfg(feature = "external-http-client-pump")] - if let Some(ptr) = unsafe { external_client_request::string_property(handle, "path") } { - return ptr; - } - String::new() - } - }; - unsafe { js_string_from_bytes(path.as_ptr(), path.len() as u32) } -} - -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_listener_count( - handle: Handle, - event_ptr: *const StringHeader, -) -> f64 { - let event = match string_from_header(event_ptr) { - Some(e) => e, - None => return 0.0, - }; - match get_handle_mut::(handle) { - Some(req) => { - let explicit = req.listeners.get(&event).map(|v| v.len()).unwrap_or(0); - let implicit_response = if event == "response" && req.response_callback != 0 { - 1 - } else { - 0 - }; - (explicit + implicit_response) as f64 - } - None => { - #[cfg(feature = "external-http-client-pump")] - { - let event_value = - f64::from_bits(0x7FFF_0000_0000_0000u64 | (event_ptr as u64 & PTR_MASK)); - if let Some(value) = unsafe { - external_client_request::dispatch_method( - handle, - "listenerCount", - &[event_value], - ) - } { - return value; - } - } - 0.0 - } - } -} - -/// IncomingMessage.statusCode — get response status code -#[no_mangle] -pub extern "C" fn js_http_status_code(handle: Handle) -> f64 { - if let Some(res) = get_handle_mut::(handle) { - return res.status_code as f64; - } - 0.0 -} - -/// IncomingMessage.statusMessage — get response status message -#[no_mangle] -pub extern "C" fn js_http_status_message(handle: Handle) -> *mut StringHeader { - if let Some(res) = get_handle_mut::(handle) { - return js_string_from_bytes(res.status_message.as_ptr(), res.status_message.len() as u32); - } - js_string_from_bytes("".as_ptr(), 0) -} - -/// IncomingMessage.headers — get response headers as a JS object -/// Returns a NaN-boxed object pointer (f64) -#[no_mangle] -pub unsafe extern "C" fn js_http_response_headers(handle: Handle) -> f64 { - if let Some(res) = get_handle_mut::(handle) { - // Build a JS object with the headers - let obj = perry_runtime::js_object_alloc(0, res.headers.len() as u32); - let keys_arr = perry_runtime::js_array_alloc(res.headers.len() as u32); - - for (idx, (key, val)) in res.headers.iter().enumerate() { - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - perry_runtime::js_array_push(keys_arr, JSValue::string_ptr(key_ptr)); - let val_ptr = js_string_from_bytes(val.as_ptr(), val.len() as u32); - perry_runtime::js_object_set_field(obj, idx as u32, JSValue::string_ptr(val_ptr)); - } - perry_runtime::js_object_set_keys(obj, keys_arr); - - return f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()); - } - - #[cfg(feature = "external-http-server-pump")] - { - extern "C" { - fn js_ext_http_incoming_message_is_handle(handle: i64) -> i32; - fn js_ext_http_incoming_message_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - } - if js_ext_http_incoming_message_is_handle(handle) != 0 { - return js_ext_http_incoming_message_dispatch_property(handle, b"headers".as_ptr(), 7); - } - } - - f64::from_bits(JSValue::undefined().bits()) -} - -/// Process pending HTTP events on the main thread. -/// Called from js_stdlib_process_pending(). -/// Returns number of events processed. -/// -/// #1114 followup: same per-tick scratch-Vec discipline as the fastify -/// (e538caa7), net, and ws pumps. Called every event-loop iteration + -/// every inline `await` poll iteration; the original -/// `Vec::drain(..).collect()` was a per-call heap alloc that contributed -/// to the GC `madvise` churn under sustained HTTP client traffic. -#[no_mangle] -pub unsafe extern "C" fn js_http_process_pending() -> i32 { - thread_local! { - static SCRATCH: std::cell::RefCell> = - const { std::cell::RefCell::new(Vec::new()) }; - } - let mut events = SCRATCH.with(|s| std::mem::take(&mut *s.borrow_mut())); - events.clear(); - { - let mut guard = HTTP_PENDING_EVENTS.lock().unwrap(); - events.append(&mut *guard); - } - - let count = events.len() as i32; - - for event in events.drain(..) { - match event { - PendingHttpEvent::Response { - request_handle, - status, - status_message, - headers, - body, - } => { - // Get the response callback and error listeners from the ClientRequest - let (response_callback, _error_listeners) = { - match get_handle_mut::(request_handle) { - Some(req) => ( - req.response_callback, - req.listeners.get("error").cloned().unwrap_or_default(), - ), - None => continue, - } - }; - - // Create IncomingMessage handle - let mut headers_map = HashMap::new(); - for (k, v) in headers { - headers_map.insert(k, v); - } - - let body_clone = body.clone(); - - let incoming_handle = register_handle(IncomingMessageHandle { - status_code: status, - status_message, - headers: headers_map, - body, - listeners: HashMap::new(), - encoding: None, - }); - - // Call the response callback with the IncomingMessage handle - // The handle must be NaN-boxed with POINTER_TAG so the closure - // parameter extraction (js_nanbox_get_pointer) can extract it - if response_callback != 0 { - let closure_ptr = response_callback as *const ClosureHeader; - let handle_f64 = f64::from_bits( - 0x7FFD_0000_0000_0000u64 | (incoming_handle as u64 & 0x0000_FFFF_FFFF_FFFF), - ); - js_closure_call1(closure_ptr, handle_f64); - } - - // After the response callback has returned, data/end listeners - // should be registered on the IncomingMessage. Fire them now. - - // Fire 'data' event with the full body as a single chunk - let data_listeners: Vec = { - match get_handle_mut::(incoming_handle) { - Some(res) => res.listeners.get("data").cloned().unwrap_or_default(), - None => Vec::new(), - } - }; - - if !data_listeners.is_empty() && !body_clone.is_empty() { - // Create a NaN-boxed string from the body - let body_str = - js_string_from_bytes(body_clone.as_ptr(), body_clone.len() as u32); - let body_f64 = f64::from_bits( - 0x7FFF_0000_0000_0000u64 | (body_str as u64 & 0x0000_FFFF_FFFF_FFFF), - ); - - for cb in data_listeners { - if cb != 0 { - let closure = cb as *const ClosureHeader; - js_closure_call1(closure, body_f64); - } - } - } - - // Fire 'end' event - let end_listeners: Vec = { - match get_handle_mut::(incoming_handle) { - Some(res) => res.listeners.get("end").cloned().unwrap_or_default(), - None => Vec::new(), - } - }; - - for cb in end_listeners { - if cb != 0 { - let closure = cb as *const ClosureHeader; - js_closure_call0(closure); - } - } - } - - PendingHttpEvent::Error { - request_handle, - error_message, - } => { - // Get 'error' listeners from the ClientRequest - let error_listeners: Vec = { - match get_handle_mut::(request_handle) { - Some(req) => req.listeners.get("error").cloned().unwrap_or_default(), - None => Vec::new(), - } - }; - - if !error_listeners.is_empty() { - // Create error string as NaN-boxed value - let err_str = - js_string_from_bytes(error_message.as_ptr(), error_message.len() as u32); - let err_f64 = f64::from_bits( - 0x7FFF_0000_0000_0000u64 | (err_str as u64 & 0x0000_FFFF_FFFF_FFFF), - ); - - for cb in error_listeners { - if cb != 0 { - let closure = cb as *const ClosureHeader; - js_closure_call1(closure, err_f64); - } - } - } - } - } - } - - // Restore the (capacity-retaining) buffer to the thread-local so the - // next tick reuses it. A re-entrant pump call during dispatch may - // have left a grown buffer in the slot — keep whichever is larger. - SCRATCH.with(|s| { - let mut slot = s.borrow_mut(); - if events.capacity() >= slot.capacity() { - *slot = events; - } - }); - - count -} - -// ======================================================================== -// http.Agent / https.Agent (#2129) -// ======================================================================== - -/// `new http.Agent(options?)` — register a fresh AgentHandle. `options` is -/// either undefined or a NaN-boxed object whose recognized fields override -/// the defaults; unknown fields are ignored (Node behavior). -/// -/// Mirrors Node's argument validation for the small set of options whose -/// rejection is observable (`maxTotalSockets` and `maxSockets`: number, -/// finite, > 0). Other options are no-op overrides today because Perry -/// does not pool sockets. -#[no_mangle] -pub unsafe extern "C" fn js_http_agent_new(options_f64: f64) -> Handle { - js_http_agent_new_with_protocol(options_f64, b"http:".as_ptr(), 5) -} - -#[no_mangle] -pub unsafe extern "C" fn js_https_agent_new(options_f64: f64) -> Handle { - js_http_agent_new_with_protocol(options_f64, b"https:".as_ptr(), 6) -} - -/// #2154: throw `RangeError [ERR_OUT_OF_RANGE]` with Node's exact -/// message shape — `The value of "" is out of range. It must be -/// . Received `. The `assert.throws(..., { code: ... })` -/// path in test-http-agent-maxtotalsockets.js (and adjacent tests) -/// reads the `code` property so we need both the RangeError class and -/// the side-table code registration. -fn throw_agent_out_of_range(name: &str, bound: &str, received: f64) -> ! { - let received_str = if received.is_nan() { - "NaN".to_string() - } else if received.is_infinite() { - if received.is_sign_negative() { - "-Infinity".to_string() - } else { - "Infinity".to_string() - } - } else if received.fract() == 0.0 && received.abs() < 1e21 { - format!("{}", received as i64) - } else { - format!("{}", received) - }; - let message = format!( - "The value of \"{}\" is out of range. It must be {}. Received {}", - name, bound, received_str - ); - let msg_ptr = unsafe { js_string_from_bytes(message.as_ptr(), message.len() as u32) }; - perry_runtime::node_submodules::register_error_code_pub(msg_ptr, "ERR_OUT_OF_RANGE"); - let err = perry_runtime::error::js_rangeerror_new(msg_ptr); - perry_runtime::exception::js_throw(perry_runtime::value::js_nanbox_pointer(err as i64)) -} - -fn validate_agent_positive(name: &str, v: f64) { - // `+Infinity` is the Node default for maxSockets/maxTotalSockets, so - // accept it explicitly even though `v > 0.0` would also pass — keep - // the symmetry clear with the ext-http mirror. - if v.is_infinite() && v.is_sign_positive() { - return; - } - if v.is_nan() || v <= 0.0 { - throw_agent_out_of_range(name, "> 0", v); - } -} - -unsafe fn js_http_agent_new_with_protocol( - options_f64: f64, - default_protocol_ptr: *const u8, - default_protocol_len: usize, -) -> Handle { - let default_protocol = std::str::from_utf8(std::slice::from_raw_parts( - default_protocol_ptr, - default_protocol_len, - )) - .unwrap_or("http:") - .to_string(); - - let mut agent = AgentHandle { - protocol: Some(default_protocol), - ..AgentHandle::default() - }; - - let opts_bits = options_f64.to_bits(); - let opts_undef = - opts_bits == JSValue::undefined().bits() || opts_bits == JSValue::null().bits(); - - if !opts_undef { - if let Some(v) = get_object_number_field(options_f64, "keepAliveMsecs") { - if v.is_nan() || v < 0.0 { - throw_agent_out_of_range("keepAliveMsecs", ">= 0", v); - } - agent.keep_alive_msecs = v; - } - if let Some(v) = get_object_number_field(options_f64, "maxSockets") { - validate_agent_positive("maxSockets", v); - agent.max_sockets = v; - } - if let Some(v) = get_object_number_field(options_f64, "maxFreeSockets") { - validate_agent_positive("maxFreeSockets", v); - agent.max_free_sockets = v; - } - if let Some(v) = get_object_number_field(options_f64, "maxTotalSockets") { - validate_agent_positive("maxTotalSockets", v); - agent.max_total_sockets = v; - } - if let Some(v) = get_object_number_field(options_f64, "timeout") { - agent.timeout_ms = Some(v); - } - if let Some(s) = get_object_string_field(options_f64, "scheduling") { - agent.scheduling = s; - } - // `keepAlive` is a boolean; reuse the object header reader. - let obj_bits = options_f64.to_bits(); - let upper = obj_bits >> 48; - let obj_ptr = if upper >= 0x7FF8 { - (obj_bits & 0x0000_FFFF_FFFF_FFFF) as *const perry_runtime::ObjectHeader - } else if upper == 0 && obj_bits >= 0x10000 { - obj_bits as *const perry_runtime::ObjectHeader - } else { - std::ptr::null() - }; - if !obj_ptr.is_null() { - let key = js_string_from_bytes("keepAlive".as_ptr(), 9); - let val = js_object_get_field_by_name(obj_ptr, key); - if val.is_bool() { - agent.keep_alive = val.as_bool(); - } - // #2154: storage for createConnection / createSocket - // overrides. GC-rooted via `scan_http_roots_mut` below. - for (slot_field, slot) in [ - ("createConnection", &mut agent.create_connection), - ("createSocket", &mut agent.create_socket), - ] { - let key = js_string_from_bytes(slot_field.as_ptr(), slot_field.len() as u32); - let val = js_object_get_field_by_name(obj_ptr, key); - if val.is_pointer() { - *slot = (val.bits() & 0x0000_FFFF_FFFF_FFFF) as i64; - } - } - } - } - - register_handle(agent) -} - -/// `agent.getName([options])` — Node's canonical key under which sockets are -/// pooled. The base shape is `${host}:${port}:${localAddress}` with optional -/// `:${family}` and `:${socketPath}` appended. For https.Agent instances -/// 20 extra fields are appended (ca, cert, ciphers, key, …) per Node's -/// `lib/https.js`. Tests assert exact strings; see -/// `test/parallel/test-http-agent-getname.js` and -/// `test/parallel/test-https-agent-getname.js`. -#[no_mangle] -pub unsafe extern "C" fn js_http_agent_get_name( - handle: Handle, - options_f64: f64, -) -> *mut StringHeader { - let is_https = get_handle_mut::(handle) - .and_then(|a| a.protocol.as_deref().map(|p| p == "https:")) - .unwrap_or(false); - - let mut name = build_http_agent_name(options_f64); - if is_https { - append_https_agent_name_fields(&mut name, options_f64); - } - js_string_from_bytes(name.as_ptr(), name.len() as u32) -} - -/// Compute the http.Agent.getName portion of the pool key. -unsafe fn build_http_agent_name(options_f64: f64) -> String { - let opts_bits = options_f64.to_bits(); - let opts_undef = - opts_bits == JSValue::undefined().bits() || opts_bits == JSValue::null().bits(); - - if opts_undef { - return "localhost::".to_string(); - } - - let host = - get_object_string_field(options_f64, "host").unwrap_or_else(|| "localhost".to_string()); - let port = get_object_string_field(options_f64, "port").unwrap_or_default(); - let local_address = get_object_string_field(options_f64, "localAddress").unwrap_or_default(); - - let mut name = format!("{}:{}:{}", host, port, local_address); - - // Per Node's lib/_http_agent.js: family is appended FIRST (when 4 or 6), - // then socketPath. Both are independent — Node appends each separately - // if present. - if let Some(family) = get_object_number_field(options_f64, "family") { - let f = family as i64; - if f == 4 || f == 6 { - name.push(':'); - name.push_str(&f.to_string()); - } - } - if let Some(socket_path) = get_object_string_field(options_f64, "socketPath") { - name.push(':'); - name.push_str(&socket_path); - } - - name -} - -/// Append the 20 https.Agent.getName extension fields onto an already-built -/// http.Agent.getName prefix. Mirrors `Agent.prototype.getName` in Node's -/// `lib/https.js` (v22.x): every field gets its own `:` separator regardless -/// of whether the value is present, so an Agent with no options produces 20 -/// trailing colons. -unsafe fn append_https_agent_name_fields(name: &mut String, options_f64: f64) { - let opts_bits = options_f64.to_bits(); - let opts_undef = - opts_bits == JSValue::undefined().bits() || opts_bits == JSValue::null().bits(); - - if opts_undef { - // 20 empty fields → 20 trailing colons (1 separator per field). - for _ in 0..20 { - name.push(':'); - } - return; - } - - // Most fields use the `if (options.field) name += options.field;` shape - // — truthy → append ToString-coerced value. A small group - // (rejectUnauthorized, honorCipherOrder, secureOptions) checks - // `!== undefined` instead, so `false` and `0` are appended. - let host_value = get_object_field_raw(options_f64, "host"); - - let push_truthy_string = |name: &mut String, field: &str| { - name.push(':'); - if let Some(v) = get_object_field_raw(options_f64, field) { - if jsvalue_is_truthy(v) { - name.push_str(&jsvalue_to_string(v)); - } - } - }; - let push_defined = |name: &mut String, field: &str| { - name.push(':'); - if let Some(v) = get_object_field_raw(options_f64, field) { - if !v.is_undefined() { - name.push_str(&jsvalue_to_string(v)); - } - } - }; - - push_truthy_string(name, "ca"); - push_truthy_string(name, "cert"); - push_truthy_string(name, "clientCertEngine"); - push_truthy_string(name, "ciphers"); - push_truthy_string(name, "key"); - push_truthy_string(name, "pfx"); - push_defined(name, "rejectUnauthorized"); - - // servername appears only when defined AND distinct from host. - name.push(':'); - if let Some(sn) = get_object_field_raw(options_f64, "servername") { - if jsvalue_is_truthy(sn) { - let same_as_host = match host_value { - Some(h) if jsvalue_is_truthy(h) => jsvalue_to_string(h) == jsvalue_to_string(sn), - _ => false, - }; - if !same_as_host { - name.push_str(&jsvalue_to_string(sn)); - } - } - } - - push_truthy_string(name, "minVersion"); - push_truthy_string(name, "maxVersion"); - push_truthy_string(name, "secureProtocol"); - push_truthy_string(name, "crl"); - push_defined(name, "honorCipherOrder"); - push_truthy_string(name, "ecdhCurve"); - push_truthy_string(name, "dhparam"); - push_defined(name, "secureOptions"); - push_truthy_string(name, "sessionIdContext"); - - // sigalgs is JSON-stringified (Node: `name += JSONStringify(options.sigalgs)`). - name.push(':'); - if let Some(v) = get_object_field_raw(options_f64, "sigalgs") { - if jsvalue_is_truthy(v) { - name.push_str(&jsvalue_to_json_string(v)); - } - } - - push_truthy_string(name, "privateKeyIdentifier"); - push_truthy_string(name, "privateKeyEngine"); -} - -/// `agent.keepSocketAlive(socket)` / `agent.reuseSocket(socket, req)` — -/// this Agent flavor exposes no per-socket hooks to act on, so these return -/// the receiver for chainability but otherwise do nothing. Warn once instead -/// of silently succeeding (#4917). (Default builds route http through -/// perry-ext-http, where reqwest owns the keep-alive pool.) -#[no_mangle] -pub extern "C" fn js_http_agent_noop_self(handle: Handle) -> Handle { - perry_runtime::stub_diag::perry_stub_warn( - "http.Agent keepSocketAlive/reuseSocket", - "this http Agent has no per-socket hooks; the call is a no-op", - Some("#4917"), - ); - handle -} - -/// Property getters — `agent.maxSockets`, `agent.keepAlive`, etc. Return -/// the per-instance value where one was set; fall back to Node defaults -/// when the handle is missing (synthetic agent reads). -#[no_mangle] -pub extern "C" fn js_http_agent_max_sockets(handle: Handle) -> f64 { - get_handle_mut::(handle) - .map(|a| a.max_sockets) - .unwrap_or(f64::INFINITY) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_max_free_sockets(handle: Handle) -> f64 { - get_handle_mut::(handle) - .map(|a| a.max_free_sockets) - .unwrap_or(256.0) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_max_total_sockets(handle: Handle) -> f64 { - get_handle_mut::(handle) - .map(|a| a.max_total_sockets) - .unwrap_or(f64::INFINITY) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_keep_alive_msecs(handle: Handle) -> f64 { - get_handle_mut::(handle) - .map(|a| a.keep_alive_msecs) - .unwrap_or(1000.0) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_keep_alive(handle: Handle) -> f64 { - let keep_alive = get_handle_mut::(handle) - .map(|a| a.keep_alive) - .unwrap_or(false); - f64::from_bits(JSValue::bool(keep_alive).bits()) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_protocol(handle: Handle) -> *mut StringHeader { - let s = get_handle_mut::(handle) - .and_then(|a| a.protocol.clone()) - .unwrap_or_else(|| "http:".to_string()); - unsafe { js_string_from_bytes(s.as_ptr(), s.len() as u32) } -} - -#[no_mangle] -pub unsafe extern "C" fn js_http_agent_set_protocol( - handle: Handle, - value_ptr: *const StringHeader, -) { - if let Some(agent) = get_handle_mut::(handle) { - if value_ptr.is_null() { - agent.protocol = None; - } else if let Some(s) = string_from_header(value_ptr) { - agent.protocol = Some(s); - } - } -} - -// #2154: validating setters for the tunable Agent properties. Node lets -// user code do `agent.maxSockets = 4` and rejects invalid writes with the -// same RangeError the constructor throws. - -#[no_mangle] -pub extern "C" fn js_http_agent_set_max_sockets(handle: Handle, value: f64) { - validate_agent_positive("maxSockets", value); - if let Some(agent) = get_handle_mut::(handle) { - agent.max_sockets = value; - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_max_free_sockets(handle: Handle, value: f64) { - validate_agent_positive("maxFreeSockets", value); - if let Some(agent) = get_handle_mut::(handle) { - agent.max_free_sockets = value; - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_max_total_sockets(handle: Handle, value: f64) { - validate_agent_positive("maxTotalSockets", value); - if let Some(agent) = get_handle_mut::(handle) { - agent.max_total_sockets = value; - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_keep_alive_msecs(handle: Handle, value: f64) { - if value.is_nan() || value < 0.0 { - throw_agent_out_of_range("keepAliveMsecs", ">= 0", value); - } - if let Some(agent) = get_handle_mut::(handle) { - agent.keep_alive_msecs = value; - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_keep_alive(handle: Handle, value: f64) { - let on = value != 0.0 && !value.is_nan(); - if let Some(agent) = get_handle_mut::(handle) { - agent.keep_alive = on; - } -} - -/// `agent.destroyed`. Always 0/1 (matches the runtime's number ABI on -/// the `__get_` path). -#[no_mangle] -pub extern "C" fn js_http_agent_destroyed(handle: Handle) -> f64 { - let destroyed = get_handle_mut::(handle) - .map(|a| a.destroyed) - .unwrap_or(false); - f64::from_bits(JSValue::bool(destroyed).bits()) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_default_port(handle: Handle) -> f64 { - match get_handle_mut::(handle) - .and_then(|a| a.protocol.clone()) - .unwrap_or_else(|| "http:".to_string()) - .as_str() - { - "https:" => 443.0, - "http:" => 80.0, - _ => 0.0, - } -} - -/// `agent.destroy()` — flag the agent as destroyed (so the `destroyed` -/// getter returns true) and return the handle for chainability. -#[no_mangle] -pub extern "C" fn js_http_agent_destroy(handle: Handle) -> Handle { - if let Some(agent) = get_handle_mut::(handle) { - agent.destroyed = true; - } - handle -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_create_connection(handle: Handle, closure_ptr: i64) { - if let Some(agent) = get_handle_mut::(handle) { - agent.create_connection = closure_ptr; - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_set_create_socket(handle: Handle, closure_ptr: i64) { - if let Some(agent) = get_handle_mut::(handle) { - agent.create_socket = closure_ptr; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn root_scanner_emits_request_and_response_listeners() { - let mut req_listeners = HashMap::new(); - req_listeners.insert("error".to_string(), vec![0x1234_5678]); - let req_handle = register_handle(ClientRequestHandle { - method: "GET".to_string(), - url: "http://example.test".to_string(), - headers: HashMap::new(), - body: Vec::new(), - response_callback: 0x2345_6780, - listeners: req_listeners, - timeout_ms: None, - ended: false, - agent_handle: 0, - }); - - let mut msg_listeners = HashMap::new(); - msg_listeners.insert("data".to_string(), vec![0x3456_7890]); - let msg_handle = register_handle(IncomingMessageHandle { - status_code: 200, - status_message: "OK".to_string(), - headers: HashMap::new(), - body: Vec::new(), - listeners: msg_listeners, - encoding: None, - }); - - let mut emitted = Vec::new(); - scan_http_roots(&mut |value| emitted.push(value.to_bits())); - - assert!(emitted.contains(&(0x7FFD_0000_0000_0000 | 0x1234_5678))); - assert!(emitted.contains(&(0x7FFD_0000_0000_0000 | 0x2345_6780))); - assert!(emitted.contains(&(0x7FFD_0000_0000_0000 | 0x3456_7890))); - crate::common::drop_handle(req_handle); - crate::common::drop_handle(msg_handle); - } -} diff --git a/crates/perry-stdlib/src/http/agent_dispatch.rs b/crates/perry-stdlib/src/http/agent_dispatch.rs deleted file mode 100644 index cdba7653f8..0000000000 --- a/crates/perry-stdlib/src/http/agent_dispatch.rs +++ /dev/null @@ -1,164 +0,0 @@ -use perry_runtime::JSValue; - -use crate::common::{get_handle_mut, Handle}; - -use super::{ - js_class_method_bind, js_http_agent_destroy, js_http_agent_get_name, js_http_agent_noop_self, - AgentHandle, POINTER_TAG, PTR_MASK, -}; - -fn bind_agent_method(handle: Handle, name: &'static [u8]) -> i64 { - (bind_agent_method_value(handle, name).to_bits() & PTR_MASK) as i64 -} - -fn bind_agent_method_value(handle: Handle, name: &'static [u8]) -> f64 { - let instance = f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)); - unsafe { js_class_method_bind(instance, name.as_ptr(), name.len()) } -} - -fn pointer_value(ptr: i64) -> f64 { - if ptr == 0 { - f64::from_bits(JSValue::undefined().bits()) - } else { - f64::from_bits(POINTER_TAG | (ptr as u64 & PTR_MASK)) - } -} - -pub(crate) fn dispatch_agent_property(handle: Handle, property: &str) -> Option { - get_handle_mut::(handle)?; - Some(match property { - "createConnection" => pointer_value(js_http_agent_create_connection(handle)), - "createSocket" => pointer_value(js_http_agent_create_socket(handle)), - "getName" => bind_agent_method_value(handle, b"getName"), - "destroy" => bind_agent_method_value(handle, b"destroy"), - "keepSocketAlive" => bind_agent_method_value(handle, b"keepSocketAlive"), - "reuseSocket" => bind_agent_method_value(handle, b"reuseSocket"), - // #4904: data properties — Agents constructed through the dynamic - // value path (`const { Agent } = require('http'); new Agent(...)`) - // read these through handle property dispatch rather than the - // class-filtered native rows. - "maxSockets" => super::js_http_agent_max_sockets(handle), - "maxFreeSockets" => super::js_http_agent_max_free_sockets(handle), - "maxTotalSockets" => super::js_http_agent_max_total_sockets(handle), - "keepAliveMsecs" => super::js_http_agent_keep_alive_msecs(handle), - "keepAlive" => super::js_http_agent_keep_alive(handle), - "destroyed" => super::js_http_agent_destroyed(handle), - "defaultPort" => super::js_http_agent_default_port(handle), - "protocol" => { - let ptr = super::js_http_agent_protocol(handle); - if ptr.is_null() { - f64::from_bits(JSValue::undefined().bits()) - } else { - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - } - "sockets" => js_http_agent_sockets(handle), - "freeSockets" => js_http_agent_free_sockets(handle), - "requests" => js_http_agent_requests(handle), - _ => return None, - }) -} - -/// #4904: property writes on a dynamically-dispatched Agent — -/// `agent.maxSockets = 4` and the `agent.createConnection = fn` -/// monkeypatch pattern Node's own tests use. Returns `true` when claimed. -pub(crate) fn dispatch_agent_property_set(handle: Handle, property: &str, value: f64) -> bool { - if get_handle_mut::(handle).is_none() { - return false; - } - match property { - "maxSockets" => super::js_http_agent_set_max_sockets(handle, value), - "maxFreeSockets" => super::js_http_agent_set_max_free_sockets(handle, value), - "maxTotalSockets" => super::js_http_agent_set_max_total_sockets(handle, value), - "keepAliveMsecs" => super::js_http_agent_set_keep_alive_msecs(handle, value), - "keepAlive" => super::js_http_agent_set_keep_alive(handle, value), - "createConnection" | "createSocket" => { - let bits = value.to_bits(); - let ptr = if JSValue::from_bits(bits).is_pointer() { - (bits & PTR_MASK) as i64 - } else { - 0 - }; - if property == "createConnection" { - super::js_http_agent_set_create_connection(handle, ptr); - } else { - super::js_http_agent_set_create_socket(handle, ptr); - } - } - _ => return false, - } - true -} - -pub(crate) unsafe fn dispatch_agent_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - get_handle_mut::(handle)?; - Some(match method { - "getName" => { - let options = args - .first() - .copied() - .unwrap_or_else(|| f64::from_bits(JSValue::undefined().bits())); - let ptr = js_http_agent_get_name(handle, options); - f64::from_bits(JSValue::string_ptr(ptr).bits()) - } - "destroy" => pointer_value(js_http_agent_destroy(handle)), - "keepSocketAlive" | "reuseSocket" => pointer_value(js_http_agent_noop_self(handle)), - _ => return None, - }) -} - -/// Allocate the empty object Node exposes for `agent.sockets`, -/// `agent.freeSockets`, and `agent.requests` before any requests are pooled. -fn empty_object_bits_f64() -> f64 { - let obj = perry_runtime::js_object_alloc(0, 0); - if obj.is_null() { - return f64::from_bits(JSValue::undefined().bits()); - } - f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) -} - -#[no_mangle] -pub extern "C" fn js_http_agent_sockets(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - -#[no_mangle] -pub extern "C" fn js_http_agent_free_sockets(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - -#[no_mangle] -pub extern "C" fn js_http_agent_requests(handle: Handle) -> f64 { - let _ = handle; - empty_object_bits_f64() -} - -#[no_mangle] -pub extern "C" fn js_http_agent_create_connection(handle: Handle) -> i64 { - let stored = get_handle_mut::(handle) - .map(|a| a.create_connection) - .unwrap_or(0); - if stored != 0 { - stored - } else { - bind_agent_method(handle, b"createConnection") - } -} - -#[no_mangle] -pub extern "C" fn js_http_agent_create_socket(handle: Handle) -> i64 { - let stored = get_handle_mut::(handle) - .map(|a| a.create_socket) - .unwrap_or(0); - if stored != 0 { - stored - } else { - bind_agent_method(handle, b"createSocket") - } -} diff --git a/crates/perry-stdlib/src/http/client_request_surface.rs b/crates/perry-stdlib/src/http/client_request_surface.rs deleted file mode 100644 index 7be0a8c070..0000000000 --- a/crates/perry-stdlib/src/http/client_request_surface.rs +++ /dev/null @@ -1,409 +0,0 @@ -use super::*; -use std::sync::Mutex; - -#[derive(Default)] -struct ClientRequestSurfaceState { - aborted: bool, - destroyed: bool, - socket: f64, -} - -static CLIENT_REQUEST_SURFACE: once_cell::sync::Lazy< - Mutex>, -> = once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new())); - -fn undefined_value() -> f64 { - f64::from_bits(JSValue::undefined().bits()) -} - -fn null_value() -> f64 { - f64::from_bits(JSValue::null().bits()) -} - -fn bool_value(value: bool) -> f64 { - f64::from_bits(JSValue::bool(value).bits()) -} - -fn string_value(value: &str) -> f64 { - let ptr = js_string_from_bytes(value.as_ptr(), value.len() as u32); - f64::from_bits(JSValue::string_ptr(ptr).bits()) -} - -fn handle_value(handle: Handle) -> f64 { - f64::from_bits(POINTER_TAG | (handle as u64 & PTR_MASK)) -} - -pub(super) fn scan_roots(visitor: &mut perry_runtime::gc::RuntimeRootVisitor<'_>) { - for state in CLIENT_REQUEST_SURFACE.lock().unwrap().values_mut() { - if state.socket != 0.0 { - visitor.visit_nanbox_f64_slot(&mut state.socket); - } - } -} - -pub(crate) fn is_client_request_handle(handle: Handle) -> bool { - get_handle_mut::(handle).is_some() -} - -fn with_state_mut(handle: Handle, f: impl FnOnce(&mut ClientRequestSurfaceState) -> T) -> T { - let mut states = CLIENT_REQUEST_SURFACE.lock().unwrap(); - f(states.entry(handle).or_default()) -} - -fn find_header_key(req: &ClientRequestHandle, name: &str) -> Option { - req.headers - .keys() - .find(|key| key.eq_ignore_ascii_case(name)) - .cloned() -} - -fn header_names(handle: Handle, raw: bool) -> Vec { - let mut names = get_handle_mut::(handle) - .map(|req| { - req.headers - .keys() - .map(|key| { - if raw { - key.clone() - } else { - key.to_ascii_lowercase() - } - }) - .collect::>() - }) - .unwrap_or_default(); - names.sort(); - names.dedup(); - names -} - -pub(super) fn set_header(handle: Handle, name: &str, value: String) { - if let Some(req) = get_handle_mut::(handle) { - if let Some(existing) = find_header_key(req, name) { - req.headers.remove(&existing); - } - req.headers.insert(name.to_string(), value); - } -} - -fn get_header_by_name(handle: Handle, name: &str) -> Option { - get_handle_mut::(handle).and_then(|req| { - let key = find_header_key(req, name)?; - req.headers.get(&key).cloned() - }) -} - -fn remove_header_by_name(handle: Handle, name: &str) { - if let Some(req) = get_handle_mut::(handle) { - if let Some(key) = find_header_key(req, name) { - req.headers.remove(&key); - } - } -} - -fn headers_array(handle: Handle, raw: bool) -> f64 { - let names = header_names(handle, raw); - let mut arr = perry_runtime::js_array_alloc(names.len() as u32); - for name in names { - let ptr = js_string_from_bytes(name.as_ptr(), name.len() as u32); - arr = perry_runtime::js_array_push(arr, JSValue::string_ptr(ptr)); - } - f64::from_bits(JSValue::array_ptr(arr).bits()) -} - -fn headers_object(handle: Handle) -> f64 { - let mut entries = get_handle_mut::(handle) - .map(|req| { - req.headers - .iter() - .map(|(key, value)| (key.to_ascii_lowercase(), value.clone())) - .collect::>() - }) - .unwrap_or_default(); - entries.sort_by(|a, b| a.0.cmp(&b.0)); - entries.dedup_by(|a, b| a.0 == b.0); - - let obj = perry_runtime::js_object_alloc_null_proto(0, entries.len() as u32); - let mut keys = perry_runtime::js_array_alloc(entries.len() as u32); - for (index, (key, value)) in entries.iter().enumerate() { - let key_ptr = js_string_from_bytes(key.as_ptr(), key.len() as u32); - let value_ptr = js_string_from_bytes(value.as_ptr(), value.len() as u32); - perry_runtime::js_object_set_field(obj, index as u32, JSValue::string_ptr(value_ptr)); - keys = perry_runtime::js_array_push(keys, JSValue::string_ptr(key_ptr)); - } - perry_runtime::js_object_set_keys(obj, keys); - f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()) -} - -fn socket_value(handle: Handle) -> f64 { - if !is_client_request_handle(handle) { - return undefined_value(); - } - with_state_mut(handle, |state| { - if state.socket == 0.0 { - let obj = perry_runtime::js_object_alloc(0, 0); - state.socket = f64::from_bits(JSValue::object_ptr(obj as *mut u8).bits()); - } - state.socket - }) -} - -fn state_bool(handle: Handle, property: &str) -> f64 { - let ended = get_handle_mut::(handle) - .map(|req| req.ended) - .unwrap_or(false); - let states = CLIENT_REQUEST_SURFACE.lock().unwrap(); - let state = states.get(&handle); - bool_value(match property { - "aborted" => state.map(|s| s.aborted).unwrap_or(false), - "destroyed" => state.map(|s| s.destroyed).unwrap_or(false), - "finished" | "writableEnded" | "writableFinished" => ended, - "reusedSocket" => false, - _ => false, - }) -} - -fn string_arg(args: &[f64], index: usize) -> Option { - args.get(index) - .copied() - .and_then(|value| unsafe { extract_string_value(value) }) -} - -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_get_header( - handle: Handle, - name_ptr: *const StringHeader, -) -> f64 { - string_from_header(name_ptr) - .and_then(|name| get_header_by_name(handle, &name)) - .map(|value| string_value(&value)) - .unwrap_or_else(undefined_value) -} - -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_has_header( - handle: Handle, - name_ptr: *const StringHeader, -) -> f64 { - let has = string_from_header(name_ptr) - .and_then(|name| get_header_by_name(handle, &name)) - .is_some(); - bool_value(has) -} - -#[no_mangle] -pub unsafe extern "C" fn js_http_client_request_remove_header( - handle: Handle, - name_ptr: *const StringHeader, -) -> f64 { - if let Some(name) = string_from_header(name_ptr) { - remove_header_by_name(handle, &name); - } - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_get_header_names(handle: Handle) -> f64 { - headers_array(handle, false) -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_get_raw_header_names(handle: Handle) -> f64 { - headers_array(handle, true) -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_get_headers(handle: Handle) -> f64 { - headers_object(handle) -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_abort(handle: Handle) -> f64 { - if is_client_request_handle(handle) { - with_state_mut(handle, |state| { - state.aborted = true; - state.destroyed = true; - }); - } - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_destroy(handle: Handle, _error: f64) -> Handle { - if is_client_request_handle(handle) { - with_state_mut(handle, |state| state.destroyed = true); - } - handle -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_noop_undefined( - handle: Handle, - _arg0: f64, - _arg1: f64, -) -> f64 { - let _ = handle; - undefined_value() -} - -/// Twin of perry-ext-http's `js_http_client_request_flush_headers` for -/// non-auto-optimize links: the stdlib client dispatches the whole exchange -/// at `end()`, so flushHeaders stays a no-op here. -#[no_mangle] -pub extern "C" fn js_http_client_request_flush_headers( - handle: Handle, - _arg0: f64, - _arg1: f64, -) -> f64 { - let _ = handle; - undefined_value() -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_aborted(handle: Handle) -> f64 { - state_bool(handle, "aborted") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_destroyed(handle: Handle) -> f64 { - state_bool(handle, "destroyed") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_finished(handle: Handle) -> f64 { - state_bool(handle, "finished") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_reused_socket(handle: Handle) -> f64 { - state_bool(handle, "reusedSocket") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_max_headers_count(handle: Handle) -> f64 { - let _ = handle; - null_value() -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_writable_ended(handle: Handle) -> f64 { - state_bool(handle, "writableEnded") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_writable_finished(handle: Handle) -> f64 { - state_bool(handle, "writableFinished") -} - -#[no_mangle] -pub extern "C" fn js_http_client_request_socket(handle: Handle) -> f64 { - socket_value(handle) -} - -pub(crate) fn dispatch_client_request_property(handle: Handle, property: &str) -> Option { - if !is_client_request_handle(handle) { - return None; - } - let method: Option<&'static [u8]> = match property { - "on" => Some(b"on"), - "end" => Some(b"end"), - "write" => Some(b"write"), - "setHeader" => Some(b"setHeader"), - "setTimeout" => Some(b"setTimeout"), - "listenerCount" => Some(b"listenerCount"), - "getHeader" => Some(b"getHeader"), - "hasHeader" => Some(b"hasHeader"), - "removeHeader" => Some(b"removeHeader"), - "getHeaderNames" => Some(b"getHeaderNames"), - "getHeaders" => Some(b"getHeaders"), - "getRawHeaderNames" => Some(b"getRawHeaderNames"), - "abort" => Some(b"abort"), - "destroy" => Some(b"destroy"), - "flushHeaders" => Some(b"flushHeaders"), - "cork" => Some(b"cork"), - "uncork" => Some(b"uncork"), - "setNoDelay" => Some(b"setNoDelay"), - "setSocketKeepAlive" => Some(b"setSocketKeepAlive"), - _ => None, - }; - if let Some(name) = method { - return Some(unsafe { - js_class_method_bind(handle_value(handle), name.as_ptr(), name.len()) - }); - } - Some(match property { - "method" => { - f64::from_bits(JSValue::string_ptr(js_http_client_request_method(handle)).bits()) - } - "protocol" => { - f64::from_bits(JSValue::string_ptr(js_http_client_request_protocol(handle)).bits()) - } - "host" => f64::from_bits(JSValue::string_ptr(js_http_client_request_host(handle)).bits()), - "path" => f64::from_bits(JSValue::string_ptr(js_http_client_request_path(handle)).bits()), - "aborted" => js_http_client_request_aborted(handle), - "destroyed" => js_http_client_request_destroyed(handle), - "finished" => js_http_client_request_finished(handle), - "reusedSocket" => js_http_client_request_reused_socket(handle), - "maxHeadersCount" => js_http_client_request_max_headers_count(handle), - "writableEnded" => js_http_client_request_writable_ended(handle), - "writableFinished" => js_http_client_request_writable_finished(handle), - "socket" | "connection" => js_http_client_request_socket(handle), - _ => return None, - }) -} - -pub(crate) fn dispatch_client_request_method( - handle: Handle, - method: &str, - args: &[f64], -) -> Option { - if !is_client_request_handle(handle) { - return None; - } - Some(match method { - "setHeader" => { - let name = string_arg(args, 0).unwrap_or_default(); - let value = string_arg(args, 1).unwrap_or_default(); - set_header(handle, &name, value); - handle_value(handle) - } - "getHeader" => string_arg(args, 0) - .and_then(|name| get_header_by_name(handle, &name)) - .map(|value| string_value(&value)) - .unwrap_or_else(undefined_value), - "hasHeader" => bool_value( - string_arg(args, 0) - .and_then(|name| get_header_by_name(handle, &name)) - .is_some(), - ), - "removeHeader" => { - if let Some(name) = string_arg(args, 0) { - remove_header_by_name(handle, &name); - } - undefined_value() - } - "getHeaderNames" => headers_array(handle, false), - "getHeaders" => headers_object(handle), - "getRawHeaderNames" => headers_array(handle, true), - "listenerCount" => { - let event = string_arg(args, 0).unwrap_or_default(); - get_handle_mut::(handle) - .map(|req| { - let explicit = req.listeners.get(&event).map(|v| v.len()).unwrap_or(0); - let implicit_response = if event == "response" && req.response_callback != 0 { - 1 - } else { - 0 - }; - (explicit + implicit_response) as f64 - }) - .unwrap_or(0.0) - } - "abort" => js_http_client_request_abort(handle), - "destroy" => handle_value(js_http_client_request_destroy(handle, undefined_value())), - "flushHeaders" | "cork" | "uncork" | "setNoDelay" | "setSocketKeepAlive" => { - undefined_value() - } - _ => return None, - }) -} diff --git a/crates/perry-stdlib/src/http/external_client_request.rs b/crates/perry-stdlib/src/http/external_client_request.rs deleted file mode 100644 index a39bb7cd43..0000000000 --- a/crates/perry-stdlib/src/http/external_client_request.rs +++ /dev/null @@ -1,62 +0,0 @@ -use perry_runtime::StringHeader; - -use crate::common::Handle; - -use super::{POINTER_TAG, PTR_MASK}; - -pub(super) unsafe fn dispatch_method(handle: Handle, method: &str, args: &[f64]) -> Option { - extern "C" { - fn js_ext_http_client_request_is_handle(handle: i64) -> i32; - fn js_ext_http_client_request_dispatch_method( - handle: i64, - method_ptr: *const u8, - method_len: usize, - args_ptr: *const f64, - args_len: usize, - ) -> f64; - } - if unsafe { js_ext_http_client_request_is_handle(handle) } == 0 { - return None; - } - Some(unsafe { - js_ext_http_client_request_dispatch_method( - handle, - method.as_ptr(), - method.len(), - args.as_ptr(), - args.len(), - ) - }) -} - -unsafe fn dispatch_property(handle: Handle, property: &str) -> Option { - extern "C" { - fn js_ext_http_client_request_is_handle(handle: i64) -> i32; - fn js_ext_http_client_request_dispatch_property( - handle: i64, - property_ptr: *const u8, - property_len: usize, - ) -> f64; - } - if unsafe { js_ext_http_client_request_is_handle(handle) } == 0 { - return None; - } - Some(unsafe { - js_ext_http_client_request_dispatch_property(handle, property.as_ptr(), property.len()) - }) -} - -pub(super) unsafe fn string_property(handle: Handle, property: &str) -> Option<*mut StringHeader> { - let value = unsafe { dispatch_property(handle, property) }?; - let bits = value.to_bits(); - let tag = bits & !PTR_MASK; - if tag != 0x7FFF_0000_0000_0000 && tag != POINTER_TAG { - return None; - } - let ptr = (bits & PTR_MASK) as *mut StringHeader; - if ptr.is_null() { - None - } else { - Some(ptr) - } -} diff --git a/crates/perry-stdlib/src/jsonwebtoken.rs b/crates/perry-stdlib/src/jsonwebtoken.rs index 6120a2bffd..d7b9f7cbba 100644 --- a/crates/perry-stdlib/src/jsonwebtoken.rs +++ b/crates/perry-stdlib/src/jsonwebtoken.rs @@ -260,7 +260,7 @@ pub unsafe extern "C" fn js_jwt_sign_dyn( } /// Coerce a NaN-boxed JSValue (`f64`) into a raw `*const ObjectHeader` -/// pointer. Mirrors the upper-bits sniff used in `perry-stdlib/src/http.rs`. +/// pointer. Mirrors the upper-bits sniff used by the native HTTP bindings. /// Returns null when the value isn't pointer-shaped. unsafe fn jsvalue_to_object_ptr(obj_f64: f64) -> *const ObjectHeader { let obj_bits = obj_f64.to_bits(); diff --git a/crates/perry-stdlib/src/lib.rs b/crates/perry-stdlib/src/lib.rs index b85d7c74ce..944176c634 100644 --- a/crates/perry-stdlib/src/lib.rs +++ b/crates/perry-stdlib/src/lib.rs @@ -6,7 +6,7 @@ //! # Features //! - `core` - Minimal runtime (always included) //! - `http-server` - Native HTTP server (hyper-based) -//! - `http-client` - HTTP client (reqwest/node-fetch) +//! - `http-client` - Web Fetch and Axios compatibility surface //! - `database` - All databases (postgres, mysql, sqlite, redis, mongodb) //! - `crypto` - Cryptographic functions //! - `compression` - zlib compression @@ -131,13 +131,8 @@ pub use framework::*; // `external-fastify-pump` feature (drained from `async_bridge`). // === Web Fetch API (fetch / Headers / Request / Response / Blob) === -// #5174: gated on `web-fetch`, NOT `http-client`. The Web Fetch surface -// (reqwest-backed `fetch()` + the WHATWG data types) is independent of -// the bundled node:http client below, so a program that only needs -// `new Headers()` while routing `node:http` to perry-ext-http keeps -// these without dragging in the colliding bundled http.rs symbols. -// `http-client = ["web-fetch"]`, so `--features http-client` still -// compiles all of this exactly as before. +// #5174: gated on `web-fetch`, not `http-client`, so Web Fetch stays +// independent from the external node:http implementation. #[cfg(feature = "web-fetch")] pub mod fetch; #[cfg(feature = "web-fetch")] @@ -149,16 +144,7 @@ pub mod fetch_blob; #[cfg(feature = "web-fetch")] pub use fetch_blob::*; -// === Bundled node:http client (http.request / http.get / axios) === -// Stays on `http-client`. The well-known flip strips `http-client` -// (keeping `web-fetch`) when `node:http` routes to perry-ext-http, so -// these modules — which export the same `js_http_*` symbols as -// perry-ext-http — are absent and can't collide (#5174). -#[cfg(feature = "http-client")] -pub mod http; -#[cfg(feature = "http-client")] -pub use http::*; - +// === Axios compatibility surface === #[cfg(feature = "http-client")] pub mod axios; #[cfg(feature = "http-client")] diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index bfc93dc634..eaf72cfd27 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -143,23 +143,8 @@ pub(crate) fn build_optimized_libs( // through perry-stdlib's tokio. Their workspace-built .a stays // fine. let mut tokio_using_bindings: Vec<(String, String, Option)> = Vec::new(); - // Closes #589: hono + node:http combinations dropped js_headers_new / - // js_response_new / js_request_new at link time. The well-known flip - // strips perry-stdlib's `http-client` feature when `node:http` is - // imported and routes to perry-ext-http — but perry-ext-http only - // exports the HTTP-client surface (`js_http_*` / `js_node_http_*`), - // not the Web Fetch ctors that hono's compiled output references. - // - // When the user's TS code (or any compilePackages-resolved module like - // hono) constructs `new Headers(...)` / `new Request(...)` / `new Response(...)`, - // the HIR sets `ctx.uses_fetch = true` (see - // `crates/perry-hir/src/destructuring.rs::1469-1492` + the explicit - // `fetch(...)` arms in `lower/expr_call.rs`). Keep `http-client` below - // so perry-stdlib supplies both the constructors and the erased-type - // Request/Response/Headers/Blob dispatch registries. Do not synthesize - // the `"fetch"` well-known binding from `uses_fetch`: perry-ext-fetch has - // separate registries, so a builtin `new Request()` constructed there - // would make `(req as any).url` miss stdlib's dispatch path. + // Web Fetch is selected independently from the external node:http + // binding. `uses_fetch` adds `web-fetch` in compute_required_features. if use_well_known { for module in &iteration_set { let module_normalized = module.strip_prefix("node:").unwrap_or(module); @@ -248,31 +233,6 @@ pub(crate) fn build_optimized_libs( // `compute_required_features` consulted above, so we // know exactly what to remove. for feat in crate::commands::stdlib_features::module_to_features(module_normalized) { - // Fix #589 / #5174: `node:http` / `node:https` / - // `node:http2` map to `http-client`, but that umbrella - // covers BOTH the bundled node:http client - // (`src/http.rs` + `src/axios.rs`) AND the Web Fetch - // FFIs (`js_headers_new`, `js_response_new`, - // `js_request_new`, …). When a program uses - // `new Headers()` / `new Response()` (directly or via a - // compilePackages package like hono) while also - // importing `node:http`, we must keep the Web Fetch - // half but drop the bundled client — otherwise its - // `js_http_process_pending` (and the rest of the - // `js_http_*` surface) duplicate perry-ext-http's - // symbols, and perry-ext-http's aux-pump call binds to - // perry-stdlib's empty-queue copy, wedging the - // in-process response pump (#5174). Since `http-client - // = ["web-fetch"]`, strip the umbrella and re-assert - // `web-fetch`: fetch.rs/fetch_blob.rs stay, - // http.rs/axios.rs go. The well-known staticlib - // (perry-ext-http) is still - // added for the actual node:http surface. - if *feat == "http-client" && ctx.uses_fetch { - features.remove("http-client"); - features.insert("web-fetch"); - continue; - } // Refs #643: keep `database-sqlite` enabled even when // `better-sqlite3` routes to perry-ext-better-sqlite3. // perry-stdlib's `dispatch_sqlite_stmt` (the dynamic diff --git a/crates/perry/src/commands/stdlib_features.rs b/crates/perry/src/commands/stdlib_features.rs index df7222684e..5438864028 100644 --- a/crates/perry/src/commands/stdlib_features.rs +++ b/crates/perry/src/commands/stdlib_features.rs @@ -33,16 +33,11 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { // spellings need the same feature for auto-optimized stdlib builds. "streams" | "stream/web" | "stream_web" | "fs/promises" => &["bundled-streams"], - // ── HTTP client (reqwest) ───────────────────────────────────── - // `http` / `https` / `http2` join the `http-client` umbrella since - // they bottom out in reqwest just like axios + node-fetch — and - // perry-ext-http (issue #577) needs the same async-runtime - // bridge for `perry_ffi_spawn_blocking_with_reactor`. The - // well-known flip swaps perry-stdlib's http.rs for perry-ext-http - // (v0.5.571); `http2` flips to the same staticlib. Programs that import `streams` - // should NOT also use the well-known flip — streams stays in - // perry-stdlib until its own port lands. - "axios" | "node-fetch" | "http" | "https" | "http2" => &["http-client"], + // ── Web Fetch and Axios compatibility surface ──────────────── + // Node HTTP/HTTPS/HTTP2 are provided by perry-ext-http and need + // no perry-stdlib feature. Axios and node-fetch still use the + // legacy umbrella for compatibility. + "axios" | "node-fetch" => &["http-client"], // ── WebSocket ───────────────────────────────────────────────── // `websocket` umbrella retained for backwards-compat; @@ -221,17 +216,16 @@ pub fn module_to_features(module: &str) -> &'static [&'static str] { // events won't propagate to user callbacks. "readline" => &["async-runtime"], - // Modules with no optional perry-stdlib dependency (decimal.js, - // bignumber.js, lru-cache, commander, exponential-backoff, http, - // https, events, async_hooks, worker_threads, …) — handled by - // always-on stdlib code. + // Modules with no optional perry-stdlib dependency (http, https, + // http2, events, async_hooks, worker_threads, …) are provided by + // external bindings or always-on runtime code. _ => &[], } } /// Compute the union of perry-stdlib features required to cover every /// native module the project imports, plus features needed to satisfy -/// non-import-based usage flags (e.g. `uses_fetch` ⇒ `http-client`). +/// non-import-based usage flags (e.g. `uses_fetch` ⇒ `web-fetch`). pub fn compute_required_features( native_module_imports: &BTreeSet, uses_fetch: bool, @@ -244,13 +238,8 @@ pub fn compute_required_features( } } // Built-in `fetch()` / `node-fetch` and the WHATWG data types - // (`Headers` / `Request` / `Response` / `Blob`) bottom out in reqwest - // but do NOT need perry-stdlib's bundled node:http client. #5174: ask - // for `web-fetch` (just `src/fetch/` + `src/fetch_blob.rs`), not the - // `http-client` umbrella that also pulls in `src/http.rs` / `src/axios.rs`. - // When the program ALSO imports `node:http`, that import adds - // `http-client` separately and the well-known flip strips it down to - // `web-fetch` — so the bundled client never collides with perry-ext-http. + // (`Headers` / `Request` / `Response` / `Blob`) use the Web Fetch + // feature directly, without enabling Axios. if uses_fetch { features.insert("web-fetch"); } @@ -285,4 +274,12 @@ mod tests { let features = compute_required_features(&imports, false, false); assert!(features.contains("bundled-streams")); } + + #[test] + fn node_http_uses_external_binding_without_legacy_client_feature() { + assert!(module_to_features("http").is_empty()); + assert!(module_to_features("node:https").is_empty()); + assert!(module_to_features("http2").is_empty()); + assert_eq!(module_to_features("axios"), &["http-client"]); + } } From 0f5085eaf935452728ec2969613bb0e66d1e7df3 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 29 Jul 2026 22:24:43 +0200 Subject: [PATCH 4/7] fix(http): preserve full runtime features --- crates/perry-ext-http/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/perry-ext-http/Cargo.toml b/crates/perry-ext-http/Cargo.toml index 288dbecf44..874ceec72d 100644 --- a/crates/perry-ext-http/Cargo.toml +++ b/crates/perry-ext-http/Cargo.toml @@ -40,5 +40,5 @@ socket2.workspace = true [dev-dependencies] # GC and async test shims call runtime internals; production code uses only perry-ffi. -perry-runtime.workspace = true +perry-runtime = { workspace = true, features = ["default", "stdlib"] } perry-ffi = { workspace = true, features = ["runtime-link"] } From 5d8b46e74f99d61d5a84355fa01e70e8bb860c0b Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 29 Jul 2026 23:58:54 +0200 Subject: [PATCH 5/7] fix(http): address post-merge review findings --- .../src/server/http2_session_settings.rs | 3 +- crates/perry-ext-http/src/server/tls.rs | 39 ++++++++++--------- .../commands/compile/optimized_libs/driver.rs | 4 +- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/crates/perry-ext-http/src/server/http2_session_settings.rs b/crates/perry-ext-http/src/server/http2_session_settings.rs index 77d110bf6a..da81b7c528 100644 --- a/crates/perry-ext-http/src/server/http2_session_settings.rs +++ b/crates/perry-ext-http/src/server/http2_session_settings.rs @@ -60,8 +60,7 @@ impl Http2SettingsState { if let Some(v) = obj.get("maxHeaderSize").and_then(json_u32) { self.max_header_size = v; self.max_header_list_size = v; - } - if let Some(v) = obj.get("maxHeaderListSize").and_then(json_u32) { + } else if let Some(v) = obj.get("maxHeaderListSize").and_then(json_u32) { self.max_header_list_size = v; self.max_header_size = v; } diff --git a/crates/perry-ext-http/src/server/tls.rs b/crates/perry-ext-http/src/server/tls.rs index 7d041a0749..b9bbd1cb8f 100644 --- a/crates/perry-ext-http/src/server/tls.rs +++ b/crates/perry-ext-http/src/server/tls.rs @@ -87,7 +87,7 @@ pub fn parse_private_key(pem_bytes: &[u8]) -> Option> { /// that: rustls accepts the TCP connection, then aborts the handshake /// with a fatal alert when no certificate resolves (#4974). pub fn build_certless_server_config(enable_http2: bool) -> Arc { - ensure_crypto_provider_installed(); + let provider = crypto_provider(); #[derive(Debug)] struct NoCert; @@ -100,7 +100,9 @@ pub fn build_certless_server_config(enable_http2: bool) -> Arc { } } - let mut config = ServerConfig::builder() + let mut config = ServerConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .expect("ring provider must support rustls default protocol versions") .with_no_client_auth() .with_cert_resolver(Arc::new(NoCert)); if enable_http2 { @@ -116,19 +118,18 @@ pub fn build_certless_server_config(enable_http2: bool) -> Arc { /// pulls in both `ring` via our direct dep and `aws-lc-rs` via /// reqwest's rustls-tls feature). Without an explicit install, /// `ServerConfig::builder()` panics with "Could not automatically -/// determine the process-level CryptoProvider". Idempotent — the -/// `Once` makes repeated calls safe across multiple createServer -/// invocations within a single process. -fn ensure_crypto_provider_installed() { - use std::sync::Once; - static INSTALLED: Once = Once::new(); - INSTALLED.call_once(|| { - // Best-effort install. If a provider was already installed - // by another crate (or by user code), `install_default()` - // returns Err; we ignore it because in that case the - // existing provider is already usable. - let _ = rustls::crypto::ring::default_provider().install_default(); - }); +/// determine the process-level CryptoProvider". Keep one explicit ring +/// provider so key loading and every server builder use the same backend. +fn crypto_provider() -> Arc { + use std::sync::OnceLock; + static PROVIDER: OnceLock> = OnceLock::new(); + PROVIDER + .get_or_init(|| { + let provider = rustls::crypto::ring::default_provider(); + let _ = provider.clone().install_default(); + Arc::new(provider) + }) + .clone() } #[cfg(test)] @@ -185,7 +186,7 @@ pub fn build_server_config( if cert_chain.is_empty() { return Err("https.createServer: empty certificate chain".to_string()); } - ensure_crypto_provider_installed(); + let provider = crypto_provider(); // #4906: don't route through `ServerConfig::with_single_cert` — it // parses the leaf with webpki, which rejects the X.509 **v1** certs in @@ -194,7 +195,7 @@ pub fn build_server_config( // user supplies without re-validating the leaf, so we mirror that by // loading the signing key directly and installing a fixed-cert // resolver. The client is the party that validates the served cert. - let signing_key = rustls::crypto::ring::default_provider() + let signing_key = provider .key_provider .load_private_key(private_key) .map_err(|e| format!("rustls: build server config: {}", e))?; @@ -211,7 +212,9 @@ pub fn build_server_config( } } - let mut config = ServerConfig::builder() + let mut config = ServerConfig::builder_with_provider(provider) + .with_safe_default_protocol_versions() + .map_err(|e| format!("rustls: build server config: {}", e))? .with_no_client_auth() .with_cert_resolver(Arc::new(FixedCert(certified_key))); if enable_http2 { diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 03b4095a08..885c634098 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -457,8 +457,8 @@ pub(crate) fn build_optimized_libs( // we can't rebuild perry-stdlib with a stripped feature set, // so the link uses the prebuilt full `libperry_stdlib.a`. // That full stdlib does NOT carry the `perry-ext-*` host - // functions — `node:http`'s server lives in perry-ext-http / - // perry-ext-http, which aren't perry-stdlib deps — so + // functions — `node:http`'s server lives in perry-ext-http, + // which isn't a perry-stdlib dependency — so // an out-of-box `node:http` server otherwise fails to link // with `Undefined symbols: _js_node_http_create_server…`. // Resolve the well-known ext staticlibs the program needs From 1cf75c80ac0555f005c828590f2cb81b0d299aac Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 00:01:07 +0200 Subject: [PATCH 6/7] style(codegen): format loop purity match arm --- crates/perry-codegen/src/loop_purity.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/perry-codegen/src/loop_purity.rs b/crates/perry-codegen/src/loop_purity.rs index aeace93d48..364d34cb3b 100644 --- a/crates/perry-codegen/src/loop_purity.rs +++ b/crates/perry-codegen/src/loop_purity.rs @@ -121,9 +121,7 @@ fn expr_alloc_free(e: &Expr) -> bool { // Element READS never allocate — they return an existing element / a // number. Recurse so the object and index are themselves alloc-free. Expr::IndexGet { object, index } => expr_alloc_free(object) && expr_alloc_free(index), - Expr::BufferIndexGet { buffer, index } => { - expr_alloc_free(buffer) && expr_alloc_free(index) - } + Expr::BufferIndexGet { buffer, index } => expr_alloc_free(buffer) && expr_alloc_free(index), Expr::Uint8ArrayGet { array, index } => expr_alloc_free(array) && expr_alloc_free(index), // `arr[i]++` / `--`: read-modify-write of an existing numeric slot, no // growth, no allocation. From 6e7754a275bce30358d6a67aacf6cb7c325a3663 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 07:35:06 +0200 Subject: [PATCH 7/7] test(ffi): cover null-prototype object helpers --- Cargo.lock | 15 +-------------- crates/perry-codegen/src/lower_call/native/mod.rs | 5 +---- crates/perry-ffi/src/jsvalue.rs | 8 ++++++++ .../src/object/global_this/install_static.rs | 6 +++++- crates/perry/src/commands/check.rs | 6 +++++- crates/perry/src/commands/deps.rs | 5 +---- 6 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ededa3857..e756715b6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5862,20 +5862,6 @@ dependencies = [ [[package]] name = "perry-ext-http" version = "0.5.1265" -dependencies = [ - "bytes", - "lazy_static", - "perry-ext-http-server", - "perry-ffi", - "perry-runtime", - "reqwest", - "serde_json", - "tokio", -] - -[[package]] -name = "perry-ext-http-server" -version = "0.5.1265" dependencies = [ "bytes", "h2", @@ -5887,6 +5873,7 @@ dependencies = [ "perry-ext-ws", "perry-ffi", "perry-runtime", + "reqwest", "rustls", "rustls-pemfile", "serde_json", diff --git a/crates/perry-codegen/src/lower_call/native/mod.rs b/crates/perry-codegen/src/lower_call/native/mod.rs index 508e310c21..0f278eb167 100644 --- a/crates/perry-codegen/src/lower_call/native/mod.rs +++ b/crates/perry-codegen/src/lower_call/native/mod.rs @@ -409,10 +409,7 @@ pub(crate) fn lower_native_method_call( blk.call( DOUBLE, "js_node_submodule_namespace", - &[ - (PTR, &submod_label), - (I32, &submod_key.len().to_string()), - ], + &[(PTR, &submod_label), (I32, &submod_key.len().to_string())], ) }; let mut lowered_args: Vec = Vec::with_capacity(args.len()); diff --git a/crates/perry-ffi/src/jsvalue.rs b/crates/perry-ffi/src/jsvalue.rs index b36e2cce20..aa6c317e21 100644 --- a/crates/perry-ffi/src/jsvalue.rs +++ b/crates/perry-ffi/src/jsvalue.rs @@ -435,6 +435,14 @@ mod tests { } } + #[cfg(feature = "runtime-link")] + #[test] + fn null_proto_object_field_round_trips() { + let object = alloc_null_proto_object(&[("status", JsValue::from_int32(204))]); + assert_eq!(object_field_by_name(object, "status").to_int32(), 204); + assert!(object_field_by_name(object, "missing").is_undefined()); + } + #[test] fn int32_round_trips() { for n in [0, 1, -1, i32::MIN, i32::MAX] { diff --git a/crates/perry-runtime/src/object/global_this/install_static.rs b/crates/perry-runtime/src/object/global_this/install_static.rs index 585cd404f6..78019b6189 100644 --- a/crates/perry-runtime/src/object/global_this/install_static.rs +++ b/crates/perry-runtime/src/object/global_this/install_static.rs @@ -899,7 +899,11 @@ pub(crate) fn install_reflect_namespace_members(ns_obj: *mut ObjectHeader) { 1, ), ("set", reflect_set_thunk as *const u8, 3), - ("setPrototypeOf", reflect_set_prototype_of_thunk as *const u8, 2), + ( + "setPrototypeOf", + reflect_set_prototype_of_thunk as *const u8, + 2, + ), ]; for (name, func_ptr, arity) in methods { install_proto_method(ns_obj, name, func_ptr, arity); diff --git a/crates/perry/src/commands/check.rs b/crates/perry/src/commands/check.rs index 1bbe332ff7..8dcd5a7411 100644 --- a/crates/perry/src/commands/check.rs +++ b/crates/perry/src/commands/check.rs @@ -783,6 +783,10 @@ mod tests { let mut walked = collect_ts_files(&dir.path().to_path_buf()).expect("collect from directory input"); walked.sort(); - assert_eq!(walked, vec![main, other], "directory input walks all sources"); + assert_eq!( + walked, + vec![main, other], + "directory input walks all sources" + ); } } diff --git a/crates/perry/src/commands/deps.rs b/crates/perry/src/commands/deps.rs index e75d84f7a0..6ddadb423a 100644 --- a/crates/perry/src/commands/deps.rs +++ b/crates/perry/src/commands/deps.rs @@ -819,10 +819,7 @@ mod tests { /// found" because the full specifier was joined as a path). #[test] fn package_base_name_splits_scoped_subpath_exports() { - assert_eq!( - package_base_name("@acme/toolkit/fs/safe"), - "@acme/toolkit" - ); + assert_eq!(package_base_name("@acme/toolkit/fs/safe"), "@acme/toolkit"); assert_eq!(package_base_name("@acme/toolkit"), "@acme/toolkit"); assert_eq!(package_base_name("@scope/name/a/b/c"), "@scope/name"); assert_eq!(package_base_name("lodash/map"), "lodash");