diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index fa9d276c7358b..d30ad016af28b 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -82,7 +82,7 @@ function addTestServerCommand(program: Command) { const command = program.command('test-server', { hidden: true }); command.description('start test server'); command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); - command.option('--host ', 'Host to start the server on', 'localhost'); + command.option('--host ', 'Host to start the server on', '127.0.0.1'); command.option('--port ', 'Port to start the server on', '0'); command.action(async opts => { await runTestServerAction(opts); diff --git a/packages/utils/network.ts b/packages/utils/network.ts index c21027290b906..00445bf186bb5 100644 --- a/packages/utils/network.ts +++ b/packages/utils/network.ts @@ -177,7 +177,10 @@ export function createHttp2Server(...args: any[]): http2.Http2SecureServer { } export async function startHttpServer(server: http.Server, options: { host?: string, port?: number }) { - const { host = 'localhost', port = 0 } = options; + // Prefer IPv4 loopback over 'localhost' to avoid ambiguous DNS resolution + // on dual-stack systems (some environments resolve 'localhost' to '::1' + // first and then fail to connect over IPv6 loopback). + const { host = '127.0.0.1', port = 0 } = options; const errorPromise = new ManualPromise(); const errorListener = (error: Error) => errorPromise.reject(error); server.on('error', errorListener); diff --git a/tests/playwright-test/test-server.spec.ts b/tests/playwright-test/test-server.spec.ts index e5cc1c31b2330..5502f94f4b409 100644 --- a/tests/playwright-test/test-server.spec.ts +++ b/tests/playwright-test/test-server.spec.ts @@ -347,3 +347,17 @@ test('runGlobalSetup returns env', async ({ startTestServer, writeFiles }) => { expect(result2.env).toContainEqual(['MAGIC_BEFORE', null]); expect(result2.env).toContainEqual(['MAGIC_AFTER', '43']); }); + +test('listens on 127.0.0.1 by default', async ({ startCLICommand, writeFiles }) => { + // 'localhost' resolution is ambiguous on dual-stack systems — some resolve + // to '::1' first, and IPv6 loopback has proven unreliable in certain + // environments (see https://github.com/microsoft/playwright/issues/40226). + // Default to IPv4 loopback so clients can always connect. + await writeFiles({ 'playwright.config.ts': `export default {};` }); + const testServerProcess = await startCLICommand({}, 'test-server', []); + await testServerProcess.waitForOutput('Listening on'); + const line = testServerProcess.output.split('\n').find(l => l.includes('Listening on'))!; + const wsEndpoint = line.split(' ')[2]; + expect(wsEndpoint).toMatch(/^ws:\/\/127\.0\.0\.1:\d+\//); + await testServerProcess.kill(); +});