diff --git a/packages/vite/src/node/__tests__/http.spec.ts b/packages/vite/src/node/__tests__/http.spec.ts index d38ffe3e31f1b2..ec9d8d29245a7e 100644 --- a/packages/vite/src/node/__tests__/http.spec.ts +++ b/packages/vite/src/node/__tests__/http.spec.ts @@ -1,5 +1,7 @@ import http from 'node:http' -import { afterEach, describe, expect, test } from 'vitest' +import net from 'node:net' +import { afterEach, describe, expect, test, vi } from 'vitest' +import { wildcardHosts } from '../constants' import { createServer } from '..' import type { ViteDevServer } from '..' @@ -157,6 +159,44 @@ describe('port detection', () => { }) }) + test('non-EADDRINUSE errors on wildcard do not block port selection', async () => { + const originalCreateServer = net.createServer.bind(net) + using _ = vi.spyOn(net, 'createServer').mockImplementation(() => { + const server = originalCreateServer() + const originalListen = server.listen.bind(server) + // @ts-expect-error this is the overload used internally + server.listen = ( + port: number, + host: string, + ...args: unknown[] + ): net.Server => { + if (wildcardHosts.has(host)) { + process.nextTick(() => { + const err: NodeJS.ErrnoException = new Error( + 'listen EACCES: permission denied', + ) + err.code = 'EACCES' + server.emit('error', err) + }) + return server + } + // @ts-expect-error this is the overload used internally + return originalListen(port, host, ...args) + } + return server + }) + + viteServer = await createServer({ + root: import.meta.dirname, + logLevel: 'silent', + server: { port: BASE_PORT, strictPort: false, ws: false }, + }) + await viteServer.listen() + + const address = viteServer.httpServer!.address() + expect(address).toStrictEqual(expect.objectContaining({ port: BASE_PORT })) + }) + test('throws error when port is blocked and strictPort is true', async () => { await using _blockingServer = await createSimpleServer( BASE_PORT, diff --git a/packages/vite/src/node/http.ts b/packages/vite/src/node/http.ts index 95eeb662232279..78ef9060ababcd 100644 --- a/packages/vite/src/node/http.ts +++ b/packages/vite/src/node/http.ts @@ -177,9 +177,8 @@ async function isPortAvailable(port: number): Promise { function tryListen(port: number, host: string): Promise { return new Promise((resolve) => { const server = net.createServer() - server.once('error', () => { - // Ensure server is closed even on error to prevent resource leaks - server.close(() => resolve(false)) + server.once('error', (e: NodeJS.ErrnoException) => { + server.close(() => resolve(e.code !== 'EADDRINUSE')) }) server.once('listening', () => { server.close(() => resolve(true)) @@ -193,10 +192,10 @@ async function tryBindServer( port: number, host: string | undefined, ): Promise< - { success: true } | { success: false; error: Error & { code?: string } } + { success: true } | { success: false; error: NodeJS.ErrnoException } > { return new Promise((resolve) => { - const onError = (e: Error & { code?: string }) => { + const onError = (e: NodeJS.ErrnoException) => { httpServer.off('error', onError) httpServer.off('listening', onListening) resolve({ success: false, error: e })