Skip to content

exposeHostPorts: connection to a closed host port causes an uncaught ECONNREFUSED in the test process #1607

Closed

Description

@moltar

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

  1. 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();
    });
  2. Run npx vitest run repro.test.ts.

  3. The test passes, but Vitest reports Errors 1 error with ECONNREFUSED 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions