From 6696bb0c6dbe9e53a04f370cd03553748725fefc Mon Sep 17 00:00:00 2001 From: Sree Narayanan Date: Wed, 21 Oct 2026 09:33:30 +0400 Subject: [PATCH] fix(build-tools): make http.Server unref() release the event loop http.Server ref() and unref() were no-ops, so the inner net.Server kept its active handle and an unref'd HTTP server held the guest process open. Forward both calls to the inner net server, and apply an early unref() when listen() creates it. --- .../tests/builtin_conformance.rs | 72 +++++++++++++++++++ .../build-tools/bridge-src/builtins/http.ts | 6 ++ 2 files changed, 78 insertions(+) diff --git a/crates/native-sidecar/tests/builtin_conformance.rs b/crates/native-sidecar/tests/builtin_conformance.rs index fcd6eda05e..42d954a056 100644 --- a/crates/native-sidecar/tests/builtin_conformance.rs +++ b/crates/native-sidecar/tests/builtin_conformance.rs @@ -4905,6 +4905,77 @@ fn unrefd_timeout_does_not_keep_guest_process_alive() { run_isolated_builtin_conformance_test("timer-unref-exit"); } +fn unrefd_http_server_does_not_keep_guest_process_alive_impl() { + let cwd = temp_dir("builtin-http-server-unref-exit"); + let entrypoint = cwd.join("entry.mjs"); + write_fixture( + &entrypoint, + r#" +import http from "node:http"; + +const server = http.createServer((_request, response) => response.end("ok")); +server.listen(0, "127.0.0.1", () => { + server.unref(); + console.log(JSON.stringify({ listening: server.listening })); +}); +"#, + ); + + let mut sidecar = new_sidecar("http-server-unref-exit"); + let connection_id = authenticate_wire(&mut sidecar, "conn-http-server-unref-exit"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let allowed_builtins = + serde_json::to_string(&["http", "net"]).expect("serialize http builtin allowlist"); + let mut metadata = HashMap::new(); + metadata.insert( + String::from("env.AGENTOS_ALLOWED_NODE_BUILTINS"), + allowed_builtins, + ); + let vm_id = create_vm_with_metadata_and_permissions( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + metadata, + wire_permissions_allow_all(), + ); + + execute_wire( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "proc-http-server-unref-exit", + GuestRuntimeKind::JavaScript, + &entrypoint, + Vec::new(), + ); + + // Host Node exits as soon as the only listener is unref'd; the guest must + // not wait for the server to close. + let (stdout, stderr, exit_code) = collect_builtin_process_output_with_timeout( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "proc-http-server-unref-exit", + Duration::from_secs(5), + ); + dispose_vm_and_close_session_wire(&mut sidecar, &connection_id, &session_id, &vm_id); + + assert_eq!(exit_code, 0, "guest process should exit cleanly: {stderr}"); + let payload: Value = serde_json::from_str(stdout.trim()).expect("parse server stdout JSON"); + assert_eq!(payload["listening"], Value::Bool(true)); +} + +#[test] +fn unrefd_http_server_does_not_keep_guest_process_alive() { + run_isolated_builtin_conformance_test("http-server-unref-exit"); +} + fn run_named_case(case_name: &str) { match case_name { "fs" => fs_conformance_matches_host_node(), @@ -4998,6 +5069,7 @@ fn __builtin_conformance_extra_test_runner() { "os-resource-limits" => os_resource_limits_are_vm_scoped_impl(), "timer-handle-ref-refresh" => timer_handle_ref_refresh_matches_host_node_impl(), "timer-unref-exit" => unrefd_timeout_does_not_keep_guest_process_alive_impl(), + "http-server-unref-exit" => unrefd_http_server_does_not_keep_guest_process_alive_impl(), "fs-write-file-numeric-fd" => write_file_sync_numeric_fd_matches_host_node_impl(), other => panic!("unknown builtin conformance extra test: {other}"), } diff --git a/packages/build-tools/bridge-src/builtins/http.ts b/packages/build-tools/bridge-src/builtins/http.ts index cec4fcbb58..b9b452f025 100644 --- a/packages/build-tools/bridge-src/builtins/http.ts +++ b/packages/build-tools/bridge-src/builtins/http.ts @@ -3556,6 +3556,7 @@ var Server = class { _listeners = {}; _serverId; _netServer = null; + _refed = true; _listenPromise = null; _address = null; _handleId = null; @@ -3633,6 +3634,7 @@ var Server = class { debugBridgeNetwork("server listen start", this._serverId, port, hostname); const netServer = new NetServer({ allowHalfOpen: true }); this._netServer = netServer; + if (!this._refed) netServer.unref(); netServer.on("connection", (socket) => { if (this._tlsOptions) { const tlsSocket = new TLSSocket(socket, { @@ -3806,9 +3808,13 @@ var Server = class { return this; } ref() { + this._refed = true; + this._netServer?.ref(); return this; } unref() { + this._refed = false; + this._netServer?.unref(); return this; } };