Skip to content
124 changes: 124 additions & 0 deletions packages/vite/src/node/__tests__/http.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,128 @@ describe('port detection', () => {
`Port ${BASE_PORT} is already in use`,
)
})

describe('specific host with wildcard conflict', () => {
function mockWildcardEADDRINUSE() {
const originalCreateServer = net.createServer.bind(net)
return 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 EADDRINUSE: address already in use ${host}:${port}`,
)
err.code = 'EADDRINUSE'
server.emit('error', err)
})
return server
}
// @ts-expect-error this is the overload used internally
return originalListen(port, host, ...args)
}
return server
})
}

test('allows binding to specific host when wildcard port is in use', async () => {
using wildcardMock = mockWildcardEADDRINUSE()

viteServer = await createServer({
root: import.meta.dirname,
logLevel: 'silent',
server: {
port: BASE_PORT,
host: '127.0.0.1',
strictPort: false,
ws: false,
},
})
await viteServer.listen()

const address = viteServer.httpServer!.address()
expect(address).toStrictEqual(
expect.objectContaining({ port: BASE_PORT }),
)
expect(wildcardMock).toHaveBeenCalled()
})

test('allows binding to specific host with strictPort when wildcard port is in use', async () => {
using wildcardMock = mockWildcardEADDRINUSE()

viteServer = await createServer({
root: import.meta.dirname,
logLevel: 'silent',
server: {
port: BASE_PORT,
host: '127.0.0.1',
strictPort: true,
ws: false,
},
})
await viteServer.listen()

const address = viteServer.httpServer!.address()
expect(address).toStrictEqual(
expect.objectContaining({ port: BASE_PORT }),
)
expect(wildcardMock).toHaveBeenCalled()
})

test('emits warning when specific host binds but wildcard port is in use', async () => {
using _wildcardMock = mockWildcardEADDRINUSE()

const warnMessages: string[] = []
viteServer = await createServer({
root: import.meta.dirname,
customLogger: {
info: () => {},
warn: (msg) => warnMessages.push(msg),
warnOnce: () => {},
error: () => {},
clearScreen: () => {},
hasErrorLogged: () => false,
hasWarned: false,
},
server: {
port: BASE_PORT,
host: '127.0.0.1',
strictPort: false,
ws: false,
},
})
await viteServer.listen()

expect(warnMessages.some((msg) => msg.includes(`wildcard`))).toBe(true)
})

test('throws when specific host port is also in use with strictPort', async () => {
await using _blockingServer = await createSimpleServer(
BASE_PORT,
'127.0.0.1',
)
using _wildcardMock = mockWildcardEADDRINUSE()

viteServer = await createServer({
root: import.meta.dirname,
logLevel: 'silent',
server: {
port: BASE_PORT,
host: '127.0.0.1',
strictPort: true,
ws: false,
},
})

await expect(viteServer.listen()).rejects.toThrow(
`Port ${BASE_PORT} is already in use`,
)
})
})
})
24 changes: 24 additions & 0 deletions packages/vite/src/node/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ export async function httpServerStart(
): Promise<number> {
const { port: startPort, strictPort, host, logger } = serverOptions

// When the configured host is an explicit IP address (not a hostname like
// "localhost" and not a wildcard), allow binding even if the wildcard
// pre-check fails. A port may be in use on other interfaces while remaining
// free on the specific IP address.
const isSpecificIpHost =
host !== undefined && net.isIP(host) > 0 && !wildcardHosts.has(host)

for (let port = startPort; port <= MAX_PORT; port++) {
// Pre-check port availability on wildcard addresses (0.0.0.0, ::)
// so that we avoid conflicts with other servers listening on all interfaces
Expand All @@ -237,6 +244,23 @@ export async function httpServerStart(
if (result.error.code !== 'EADDRINUSE') {
throw result.error
}
} else if (isSpecificIpHost) {
Comment thread
sapphi-red marked this conversation as resolved.
Outdated
// Port is not available on a wildcard address, but we have a specific
// host configured. The port may still be free on that specific host, so
// try binding directly rather than skipping to the next port.
const result = await tryBindServer(httpServer, port, host)
if (result.success) {
logger.warn(
colors.yellow(
`Port ${port} is in use on a wildcard address, but ${host}:${port} is available. ` +
`There may be another server running on a wildcard IP on port ${port}.`,
),
)
return port
}
if (result.error.code !== 'EADDRINUSE') {
throw result.error
}
}

if (strictPort) {
Expand Down
Loading