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