Repository: denoland/deno
Pull request: fix(cli/test): close Node HTTP connections before sanitizer check

Closes https://github.com/denoland/deno/issues/31777

Fixes false positive "leaking resources" errors when running tests that use Node.js HTTP/HTTPS client with connection pooling.

  - Add `closeIdleConnections()` to close HTTP Agent pooled connections before test sanitizer runs
  - Remove `sanitizeResources: false` workarounds from HTTPS tests that no longer need them


Changed files:
filename: cli/tools/test/mod.rs
patch: @@ -1126,6 +1126,14 @@ async fn run_tests_for_worker_inner(
       continue;
     }
 
+    // Close idle Node.js HTTP Agent connections to prevent cross-test
+    // pollution and false positive resource leak detection from pooled
+    // keepAlive connections.
+    _ = worker.js_runtime.execute_script(
+      located_script_name!(),
+      "Deno[Deno.internal].node?.closeIdleConnections?.()",
+    );
+
     // Await activity stabilization
     if let Some(diff) = sanitizers::wait_for_activity_to_stabilize(
       worker,

filename: ext/node/polyfills/02_init.js
patch: @@ -2,10 +2,11 @@
 
 // deno-lint-ignore-file
 
-import { internals } from "ext:core/mod.js";
+import { core, internals } from "ext:core/mod.js";
 const requireImpl = internals.requireImpl;
 
 import { nodeGlobals } from "ext:deno_node/00_globals.js";
+import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts";
 import "node:module";
 
 let initialized = false;
@@ -68,9 +69,78 @@ function loadCjsModule(moduleName, isMain, inspectBrk) {
 
 globalThis.nodeBootstrap = initialize;
 
+function closeIdleConnections() {
+  // Close all idle connections in Node.js HTTP Agent pools.
+  // This is called by the test runner before sanitizer checks to prevent
+  // false positive resource leak detection for pooled keepAlive connections.
+  const { internalRidSymbol } = core;
+
+  // Collect resource IDs that need to be closed.
+  // We do this in two phases:
+  // 1. Destroy sockets via agent.destroy() (graceful cleanup)
+  // 2. Close any remaining resources directly (force cleanup)
+  const ridsToClose = [];
+
+  function collectRidsFromAgent(agent) {
+    if (!agent) return;
+    const sets = [agent.freeSockets, agent.sockets];
+    for (const set of sets) {
+      if (!set) continue;
+      for (const key of Object.keys(set)) {
+        const sockets = set[key];
+        if (!Array.isArray(sockets)) continue;
+        for (const socket of sockets) {
+          try {
+            const stream = socket?._handle?.[kStreamBaseField];
+            if (stream) {
+              const rid = stream[internalRidSymbol];
+              if (rid !== undefined) {
+                ridsToClose.push(rid);
+              }
+            }
+          } catch {
+            // Ignore
+          }
+        }
+      }
+    }
+  }
+
+  // Phase 1: Collect RIDs and destroy agents
+  try {
+    const http = nativeModuleExports["http"];
+    if (http?.globalAgent) {
+      collectRidsFromAgent(http.globalAgent);
+      http.globalAgent.destroy();
+    }
+  } catch {
+    // Ignore
+  }
+  try {
+    const https = nativeModuleExports["https"];
+    if (https?.globalAgent) {
+      collectRidsFromAgent(https.globalAgent);
+      https.globalAgent.destroy();
+    }
+  } catch {
+    // Ignore
+  }
+
+  // Phase 2: Force close any remaining resources
+  // This handles the case where socket.destroy() is a no-op due to HandleWrap state
+  for (const rid of ridsToClose) {
+    try {
+      core.tryClose(rid);
+    } catch {
+      // Ignore - already closed
+    }
+  }
+}
+
 internals.node = {
   initialize,
   loadCjsModule,
+  closeIdleConnections,
 };
 
 const nativeModuleExports = requireImpl.nativeModuleExports;

filename: tests/unit_node/http_test.ts
patch: @@ -745,7 +745,6 @@ Deno.test("[node/http] ClientRequest handle non-string headers", async () => {
 
 Deno.test({
   name: "[node/https] ClientRequest uses HTTP/1.1",
-  sanitizeResources: false,
   async fn() {
     let body = "";
     const { promise, resolve, reject } = Promise.withResolvers<void>();
@@ -2001,7 +2000,6 @@ const ca = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
 Deno.test({
   name: "[node/https] `request` requires net permission to host and port",
   permissions: { net: ["localhost:5545"] },
-  sanitizeResources: false,
   async fn() {
     const { promise, resolve } = Promise.withResolvers<void>();
     https.request("https://localhost:5545/echo.ts", { ca }, async (res) => {
@@ -2149,7 +2147,6 @@ Deno.test("[node/http] client http over unix socket works", {
 Deno.test({
   name: "[node/https] null ca, key and cert req options",
   permissions: { net: ["localhost:5545"] },
-  sanitizeResources: false,
   async fn() {
     const { promise, resolve } = Promise.withResolvers<void>();
     https.request("https://localhost:5545/echo.ts", {