Skip to content

Commit 98dcb1c

Browse files
committed
fix(dev): warn on invalid --host and explain failed binds
1 parent f93bd1d commit 98dcb1c

2 files changed

Lines changed: 50 additions & 2 deletions

File tree

packages/nuxt-cli/src/dev/listen.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ export interface Listener {
6565

6666
const ANY_HOSTS = new Set(['', '0.0.0.0', '::'])
6767

68+
const HOSTNAME_RE = /^(?!-)[\d.:a-z-]{1,253}(?<!-)$/i
69+
70+
/**
71+
* Check `hostname` is a plausible host or IP address, falling back to a
72+
* bindable default (with a warning) when it is not.
73+
*/
74+
export function validateHostname(hostname: string | undefined, isPublic?: boolean): string | undefined {
75+
const isValid = !!hostname
76+
&& HOSTNAME_RE.test(hostname)
77+
&& hostname.split('.').every(label => label.length <= 63)
78+
if (!hostname || isValid) {
79+
return hostname
80+
}
81+
const fallback = isPublic ? '' : 'localhost'
82+
logger.warn(`Invalid host \`${hostname}\`, using \`${fallback || '0.0.0.0'}\` instead.`)
83+
return fallback
84+
}
85+
6886
/**
6987
* External IPv4 addresses other devices can reach. IPv4 link-local addresses
7088
* (169.254.0.0/16, from a macOS Thunderbolt Bridge or an unconfigured adapter)
@@ -105,7 +123,7 @@ function createSecureServer(certificate: ResolvedCertificate, handler: RequestLi
105123
}
106124

107125
export async function listen(handler: RequestListener, options: ListenOptions = {}): Promise<Listener> {
108-
const hostname = options.hostname ?? (options.public ? '' : 'localhost')
126+
const hostname = validateHostname(options.hostname, options.public) ?? (options.public ? '' : 'localhost')
109127

110128
const requestedPort = options.port === undefined || options.port === '' ? undefined : Number(options.port)
111129
const port = await resolvePort(requestedPort, hostname, options.strictPort)
@@ -264,6 +282,8 @@ function describeBindError(error: NodeJS.ErrnoException, port: number, hostname:
264282
return new Error(`Port ${port} requires elevated privileges. Pass \`--port\` with a port above 1023.`, { cause: error })
265283
case 'EADDRNOTAVAIL':
266284
return new Error(`\`${hostname}\` is not an address of this machine. Pass \`--host\` with a local address, or omit it to listen on localhost.`, { cause: error })
285+
case 'ENOTFOUND':
286+
return new Error(`\`${hostname}\` could not be resolved. Pass \`--host\` with a local address, or omit it to listen on localhost.`, { cause: error })
267287
}
268288
return error
269289
}

packages/nuxt-cli/test/unit/listen.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { networkInterfaces } from 'node:os'
44

55
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
66

7-
import { copyURL, getNetworkAddresses, listen, openBrowser, resolveOpenCommand } from '../../src/dev/listen'
7+
import { copyURL, getNetworkAddresses, listen, openBrowser, resolveOpenCommand, validateHostname } from '../../src/dev/listen'
88

99
const writeText = vi.hoisted(() => vi.fn())
1010

@@ -62,6 +62,28 @@ describe('getNetworkAddresses', () => {
6262
})
6363
})
6464

65+
describe('validateHostname', () => {
66+
it('should pass through valid hosts', () => {
67+
expect(validateHostname('localhost')).toBe('localhost')
68+
expect(validateHostname('127.0.0.1')).toBe('127.0.0.1')
69+
expect(validateHostname('::1')).toBe('::1')
70+
expect(validateHostname('my-app.example.com')).toBe('my-app.example.com')
71+
expect(validateHostname('')).toBe('')
72+
expect(validateHostname(undefined)).toBeUndefined()
73+
})
74+
75+
it('should fall back to `localhost` for an invalid host', () => {
76+
expect(validateHostname('local host')).toBe('localhost')
77+
expect(validateHostname('http://localhost')).toBe('localhost')
78+
expect(validateHostname('-nope')).toBe('localhost')
79+
expect(validateHostname(`${'a'.repeat(64)}.com`)).toBe('localhost')
80+
})
81+
82+
it('should fall back to all interfaces when public', () => {
83+
expect(validateHostname('local host', true)).toBe('')
84+
})
85+
})
86+
6587
describe('resolveOpenCommand', () => {
6688
const url = 'http://localhost:3000/'
6789

@@ -139,6 +161,12 @@ describe('listen', () => {
139161
expect(spawn.mock.lastCall?.[1]).toContain('https://example.com/foo')
140162
})
141163

164+
it('should bind a fallback host for an invalid `hostname`', async () => {
165+
const listener = await start({ port: 0, hostname: 'not a host' })
166+
167+
expect(listener.url).toBe(`http://localhost:${listener.address.port}/`)
168+
})
169+
142170
it('should use the requested port with `strictPort`', async () => {
143171
const { address } = await start({ port: 0 })
144172
const port = address.port

0 commit comments

Comments
 (0)