From c6bf5d565971cede0a95d1306e71acb02f83e2f5 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Mon, 12 Oct 2026 15:42:02 +0100 Subject: [PATCH] Fail the start when the container exits before its ports are bound --- ...spect-container-util-ports-exposed.test.ts | 33 ++++++++++++++++++- .../inspect-container-util-ports-exposed.ts | 24 ++++++++++---- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.test.ts b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.test.ts index dd43ea47e..1a1924063 100644 --- a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.test.ts +++ b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.test.ts @@ -3,9 +3,11 @@ import { inspectContainerUntilPortsExposed } from "./inspect-container-util-port function mockInspectResult( portBindings: ContainerInspectInfo["HostConfig"]["PortBindings"], - ports: ContainerInspectInfo["NetworkSettings"]["Ports"] + ports: ContainerInspectInfo["NetworkSettings"]["Ports"], + state: Partial = { Status: "running" } ): ContainerInspectInfo { return { + State: state, HostConfig: { PortBindings: portBindings, }, @@ -74,4 +76,33 @@ describe("inspectContainerUntilPortsExposed", { concurrent: false }, () => { "Timed out after 0ms while waiting for container ports to be bound to the host" ); }); + + it("throws an error when the container exits before its ports are exposed", async () => { + const data = mockInspectResult({ "8080/tcp": [] }, {}, { Status: "exited", ExitCode: 1 }); + const inspectFn = vi.fn().mockResolvedValue(data); + + await expect(inspectContainerUntilPortsExposed(inspectFn, "container-id")).rejects.toThrow( + "Container exited with code 1 before its ports were bound to the host" + ); + expect(inspectFn).toHaveBeenCalledTimes(1); + }); + + it("returns the inspect result when the container has exited and no ports are exposed", async () => { + const data = mockInspectResult({}, {}, { Status: "exited", ExitCode: 0 }); + const inspectFn = vi.fn().mockResolvedValueOnce(data); + + const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id"); + + expect(result).toEqual(data); + }); + + it("retries the inspect while the container is restarting", async () => { + const data1 = mockInspectResult({ "8080/tcp": [] }, {}, { Status: "restarting" }); + const data2 = mockInspectResult({ "8080/tcp": [] }, { "8080/tcp": [{ HostIp: "0.0.0.0", HostPort: "45000" }] }); + const inspectFn = vi.fn().mockResolvedValueOnce(data1).mockResolvedValueOnce(data2); + + const result = await inspectContainerUntilPortsExposed(inspectFn, "container-id"); + + expect(result).toEqual(data2); + }); }); diff --git a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts index 964db47ec..bcc14f549 100644 --- a/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts +++ b/packages/testcontainers/src/generic-container/inspect-container-util-ports-exposed.ts @@ -8,12 +8,7 @@ export async function inspectContainerUntilPortsExposed( ): Promise { const result = await new IntervalRetry(250).retryUntil( () => inspectFn(), - (inspectResult) => { - const portBindings = inspectResult?.HostConfig?.PortBindings; - if (!portBindings) return true; - const expectedlyBoundPorts = Object.keys(portBindings); - return expectedlyBoundPorts.every((exposedPort) => inspectResult.NetworkSettings.Ports[exposedPort]?.length > 0); - }, + (inspectResult) => arePortsBound(inspectResult) || hasExited(inspectResult), () => { const message = `Timed out after ${timeout}ms while waiting for container ports to be bound to the host`; log.error(message, { containerId }); @@ -26,5 +21,22 @@ export async function inspectContainerUntilPortsExposed( throw result; } + if (!arePortsBound(result)) { + const message = `Container exited with code ${result.State.ExitCode} before its ports were bound to the host`; + log.error(message, { containerId }); + throw new Error(message); + } + return result; } + +function arePortsBound(inspectResult: ContainerInspectInfo): boolean { + const portBindings = inspectResult?.HostConfig?.PortBindings; + if (!portBindings) return true; + const expectedlyBoundPorts = Object.keys(portBindings); + return expectedlyBoundPorts.every((exposedPort) => inspectResult.NetworkSettings.Ports[exposedPort]?.length > 0); +} + +function hasExited(inspectResult: ContainerInspectInfo): boolean { + return inspectResult?.State?.Status === "exited"; +}