Repository navigation
exposeHostPorts: connection to a closed host port causes an uncaught ECONNREFUSED in the test process #1607
Description
Expected Behaviour
When a container connects to host.testcontainers.internal:<port> and nothing on the host listens on that port, the connection fails inside the container. The test process keeps running.
Actual Behaviour
The test process gets an uncaught exception:
Error: connect ECONNREFUSED 127.0.0.1:<port>
❯ TCPConnectWrap.afterConnect [as oncomplete] node:net:1864:16
Vitest reports this as an unhandled error and fails the run, even when every test passes.
The cause is in ssh-remote-port-forward, which PortForwarder uses for exposeHostPorts. Its tcp connection handler opens a socket and never listens for error on it:
.on("tcp connection", (info, accept) => {
const stream = accept();
stream.pause();
const socket = net.connect(info.destPort, info.destIP, () => {
stream.pipe(socket);
socket.pipe(stream);
stream.resume();
});
})If the host port is closed, net.connect emits error with no listener, so Node throws it. The SSH stream also stays open. A fix is to add socket.on("error", () => stream.close()), and probably stream.on("error", () => socket.destroy()) as well.
ssh-remote-port-forward@1.0.4 is the newest version and was last published in 2022. testcontainers@12.2.1 still depends on ^1.0.4.
Testcontainers Logs
The script below, run as plain Node with an uncaughtException handler that prints the error:
testcontainers [INFO] Exposing host port 35041...
testcontainers [INFO] Exposed host port 35041
testcontainers [DEBUG] [<id>] Executing command "sh -c nc -w 2 host.testcontainers.internal 35041 </dev/null; echo nc_exit=$?"...
UNCAUGHT: connect ECONNREFUSED 127.0.0.1:35041
testcontainers [DEBUG] [<id>] Executed command "sh -c nc -w 2 host.testcontainers.internal 35041 </dev/null; echo nc_exit=$?"...
Steps to Reproduce
-
Save this as
repro.test.ts:import { createServer } from "node:net"; import { once } from "node:events"; import { GenericContainer, TestContainers } from "testcontainers"; import { it } from "vitest"; it("survives a connection to a closed exposed port", async () => { const server = createServer().listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); if (address === null || typeof address === "string") throw new Error("no port"); await TestContainers.exposeHostPorts(address.port); server.close(); const container = await new GenericContainer("alpine:3.20") .withCommand(["sleep", "infinity"]) .start(); await container.exec(["sh", "-c", `nc -w 2 host.testcontainers.internal ${address.port} </dev/null || true`]); await container.stop(); });
-
Run
npx vitest run repro.test.ts. -
The test passes, but Vitest reports
Errors 1 errorwithECONNREFUSED 127.0.0.1:<port>.
Environment Information
- Operating System: Ubuntu 22.04.5 LTS
- Container runtime and version: Docker Engine 29.7.2
- Node version: 24.19.0
- Test runner and version: Vitest 4.0.18, ESM
- Testcontainers version: 10.28.0 (the same dependency is in 12.2.1)
- Last Testcontainers version where this worked, if any: none known
Activity