From 98fc36e5bf4d7aec4722935c959e6b3d535b7d19 Mon Sep 17 00:00:00 2001 From: ran Date: Thu, 3 Sep 2026 14:32:34 +0200 Subject: [PATCH] test(aws-strands): bind the test servers to the address the probes dial The CORS suite failed roughly one run in six under load, and the failure moved around: a wrong header, an agent that never ran, an HTTP parse error carrying a body no route in this package emits, a bare close with nothing read. All four were the same defect. `app.listen(0)` with no host binds the IPv6 wildcard, while every probe in these suites dials `127.0.0.1`. Those are not the same port. An unrelated process already holding the IPv4 wildcard on the number the kernel hands out keeps receiving the loopback traffic, so the probe is answered by that process and the test's own server never sees a connection. Confirmed by `lsof` at the moment of failure, which showed a foreign IPv4 listener and the test's IPv6 listener on one port, and reproduced deliberately with a stand-in process: an unspecified bind hands the reply to the stranger, a loopback bind reaches the intended app. Pin the bind address in the shared transport harness and in the seven sibling suites that bound the same way, and add a harness self-test on the address so dropping it fails here rather than silently reappearing as a flake somewhere else. A specific bind takes precedence over a wildcard holder, so a probe now reaches its own app or nothing at all. No behaviour under test changes: no assertion is weakened, no retry or timeout is added, and no runtime string is touched. Verified: a stress rig that failed three times in 2000 cycles now runs 6000 clean; 35 and then 32 consecutive green runs of the CORS suite under ten busy cores; the full package green at 1612 passing; typecheck and Prettier clean. --- .../disconnect-unhandled-rejection.test.ts | 2 +- .../src/__tests__/endpoint-accept.test.ts | 2 +- .../__tests__/endpoint-capabilities.test.ts | 2 +- .../src/__tests__/endpoint-disconnect.test.ts | 2 +- .../src/__tests__/endpoint-validation.test.ts | 2 +- .../typescript/src/__tests__/endpoint.test.ts | 2 +- .../__tests__/terminal-error-paths.test.ts | 2 +- .../src/__tests__/transport-harness.test.ts | 35 ++++++++++++++++--- .../src/__tests__/transport-harness.ts | 14 ++++++-- 9 files changed, 50 insertions(+), 13 deletions(-) diff --git a/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts b/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts index 3424602124..573fd6085a 100644 --- a/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts @@ -42,7 +42,7 @@ describe("endpoint disconnect + throwing generator finally", () => { app.use(express.json({ limit: "1mb" })); addStrandsExpressEndpoint(app, agent, { path: "/" }); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts index 6f5bffb885..2d86bc1ad8 100644 --- a/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts @@ -49,7 +49,7 @@ async function startApp(): Promise<{ { path: "/" }, ); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; return { diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts index 0b2092c235..6836570c2d 100644 --- a/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts @@ -17,7 +17,7 @@ async function startApp(configure: (app: express.Express) => void): Promise<{ app.use(express.json({ limit: "1mb" })); configure(app); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; return { diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts index 44c86f517c..4e33d48ce0 100644 --- a/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts @@ -59,7 +59,7 @@ async function startApp(agent: StrandsAgent): Promise<{ app.use(express.json({ limit: "1mb" })); addStrandsExpressEndpoint(app, agent, { path: "/" }); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; return { diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts index 1f97946be5..898c615b31 100644 --- a/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts @@ -62,7 +62,7 @@ async function startApp(): Promise<{ const agent = new RecordingStrandsAgent(); addStrandsExpressEndpoint(app, agent, { path: "/" }); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; return { diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts index 622ff9a6ab..926f4af7c0 100644 --- a/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts @@ -50,7 +50,7 @@ async function startApp(agent: StrandsAgent): Promise<{ addStrandsExpressEndpoint(app, agent, { path: "/" }); addPing(app, "/ping"); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; return { diff --git a/integrations/aws-strands/typescript/src/__tests__/terminal-error-paths.test.ts b/integrations/aws-strands/typescript/src/__tests__/terminal-error-paths.test.ts index 822cfc25d4..09dbef02d3 100644 --- a/integrations/aws-strands/typescript/src/__tests__/terminal-error-paths.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/terminal-error-paths.test.ts @@ -439,7 +439,7 @@ describe("terminal error paths", () => { app.use(express.json({ limit: "10mb" })); addStrandsExpressEndpoint(app, new UnencodableAgent(), { path: "/" }); const server = await new Promise((resolve) => { - const s = app.listen(0, () => resolve(s)); + const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); const port = (server.address() as AddressInfo).port; diff --git a/integrations/aws-strands/typescript/src/__tests__/transport-harness.test.ts b/integrations/aws-strands/typescript/src/__tests__/transport-harness.test.ts index f51de76b80..228d854033 100644 --- a/integrations/aws-strands/typescript/src/__tests__/transport-harness.test.ts +++ b/integrations/aws-strands/typescript/src/__tests__/transport-harness.test.ts @@ -27,16 +27,32 @@ function appThatFailsToBind(failure: Error): Express { } as unknown as Express; } -/** An app that binds successfully, exposing the emitter the harness wires up. */ -function appThatBinds(): { app: Express; emitter: EventEmitter } { +/** + * An app that binds successfully, exposing the emitter the harness wires up + * and the arguments it was asked to bind with. + */ +function appThatBinds(): { + app: Express; + emitter: EventEmitter; + boundTo: () => { port: number; host: string }; +} { const emitter = new EventEmitter(); + let bound: { port: number; host: string } | undefined; const app = { - listen: (_port: number, onListening: () => void) => { + listen: (port: number, host: string, onListening: () => void) => { + bound = { port, host }; setImmediate(onListening); return emitter; }, } as unknown as Express; - return { app, emitter }; + return { + app, + emitter, + boundTo: () => { + if (!bound) throw new Error("listen was never called"); + return bound; + }, + }; } describe("listen reports a bind failure instead of hanging", () => { @@ -61,6 +77,17 @@ describe("listen reports a bind failure instead of hanging", () => { expect(settled).toBe(failure); }); + // The probes all dial `127.0.0.1`, and an unspecified host binds the IPv6 + // wildcard instead. A process already holding the IPv4 wildcard on the port + // the kernel hands out then answers those probes in this server's place, so + // dropping the host does not fail here or anywhere obvious: it hands a + // stranger's reply to whichever suite drew the colliding number. + it("binds the loopback address the probes dial, not the wildcard", async () => { + const { app, boundTo } = appThatBinds(); + await listen(app); + expect(boundTo()).toEqual({ port: 0, host: "127.0.0.1" }); + }); + it("does not absorb a post-bind server error into the settled promise", async () => { const { app, emitter } = appThatBinds(); const server = await listen(app); diff --git a/integrations/aws-strands/typescript/src/__tests__/transport-harness.ts b/integrations/aws-strands/typescript/src/__tests__/transport-harness.ts index 6a10c18cd6..de0ef0055c 100644 --- a/integrations/aws-strands/typescript/src/__tests__/transport-harness.ts +++ b/integrations/aws-strands/typescript/src/__tests__/transport-harness.ts @@ -53,7 +53,17 @@ export interface StartedApp { } /** - * Bind an app to an ephemeral port. + * Bind an app to an ephemeral port on the IPv4 loopback. + * + * The address is pinned because every probe below dials `127.0.0.1`, and an + * unspecified host binds the IPv6 wildcard instead. That pair is not the same + * port: an unrelated process already holding the IPv4 wildcard on the number + * the kernel hands out keeps receiving the loopback traffic, so the probe is + * answered by that process while this server never sees a connection. It + * reaches the suite as whatever that stranger happens to reply -- a foreign + * status and body, an HTTP parse error, or a bare close -- on whichever test + * drew the colliding number. Binding the loopback explicitly takes precedence + * over a wildcard holder, so the probe reaches this app or nothing at all. * * Two failure modes, and each needs its own handler. A bind failure * (EADDRINUSE, a permission denial) arrives before the promise settles, so it @@ -68,7 +78,7 @@ export function listen( app: import("express").Express, ): Promise { return new Promise((resolve, reject) => { - const server = app.listen(0, () => { + const server = app.listen(0, "127.0.0.1", () => { server.removeListener("error", reject); // Thrown synchronously out of `emit`, which surfaces it as an uncaught // exception and fails the run. Loud beats lost for a server fault the