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
constANY_HOSTS=newSet(['','0.0.0.0','::'])
6767

68+
constHOSTNAME_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+
exportfunctionvalidateHostname(hostname: string|undefined,isPublic?: boolean): string|undefined{
75+
constisValid=!!hostname
76+
&&HOSTNAME_RE.test(hostname)
77+
&&hostname.split('.').every(label=>label.length<=63)
78+
if(!hostname||isValid){
79+
returnhostname
80+
}
81+
constfallback=isPublic ? '' : 'localhost'
82+
logger.warn(`Invalid host \`${hostname}\`, using \`${fallback||'0.0.0.0'}\` instead.`)
83+
returnfallback
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
exportasyncfunctionlisten(handler: RequestListener,options: ListenOptions={}): Promise<Listener>{
108-
consthostname=options.hostname??(options.public ? '' : 'localhost')
126+
consthostname=validateHostname(options.hostname,options.public)??(options.public ? '' : 'localhost')
109127

110128
constrequestedPort=options.port===undefined||options.port==='' ? undefined : Number(options.port)
111129
constport=awaitresolvePort(requestedPort,hostname,options.strictPort)
@@ -264,6 +282,8 @@ function describeBindError(error: NodeJS.ErrnoException, port: number, hostname:
264282
returnnewError(`Port ${port} requires elevated privileges. Pass \`--port\` with a port above 1023.`,{cause: error})
265283
case'EADDRNOTAVAIL':
266284
returnnewError(`\`${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+
returnnewError(`\`${hostname}\` could not be resolved. Pass \`--host\` with a local address, or omit it to listen on localhost.`,{cause: error})
267287
}
268288
returnerror
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
constwriteText=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
consturl='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+
constlistener=awaitstart({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 }=awaitstart({port: 0})
144172
constport=address.port

0 commit comments

Comments
 (0)