mirror of https://github.com/nuxt/nuxt.git
fix: offer a new port and listen if already used, use consola on server error (#4428)
* Use consola on server error * fix style * ignore coverage * use `consola.error(e)` * formatting server error * fix style * offer a new port and listen * fix style * simplify return * Revert "fix style" This reverts commit770347adb9
. * Revert "simplify return" This reverts commit26f2588b2f
. * simplified tests * remove dependency `get-port` * using port `0` to assign a random free port * update `this.port` value with `address.port` * For production, use `consola.fatal` * pass options.dev from server to listener constructor * add dev on constructor * improve serverErrorHandler and close * Update listener.js * improve serverErrorHandler * improve the way to handle listen errors * fix missed line * fully close old server before listening on a random port * update listen.test
This commit is contained in:
parent
71136fc9b6
commit
1d78027e2b
|
@ -6,13 +6,14 @@ import consola from 'consola'
|
||||||
import pify from 'pify'
|
import pify from 'pify'
|
||||||
|
|
||||||
export default class Listener {
|
export default class Listener {
|
||||||
constructor({ port, host, socket, https, app }) {
|
constructor({ port, host, socket, https, app, dev }) {
|
||||||
// Options
|
// Options
|
||||||
this.port = port
|
this.port = port
|
||||||
this.host = host
|
this.host = host
|
||||||
this.socket = socket
|
this.socket = socket
|
||||||
this.https = https
|
this.https = https
|
||||||
this.app = app
|
this.app = app
|
||||||
|
this.dev = dev
|
||||||
|
|
||||||
// After listen
|
// After listen
|
||||||
this.listening = false
|
this.listening = false
|
||||||
|
@ -24,10 +25,17 @@ export default class Listener {
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
// Destroy server by forcing every connection to be closed
|
// Destroy server by forcing every connection to be closed
|
||||||
if (this.server.listening) {
|
if (this.server && this.server.listening) {
|
||||||
await this.server.destroy()
|
await this.server.destroy()
|
||||||
consola.debug('server closed')
|
consola.debug('server closed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete references
|
||||||
|
this.listening = false
|
||||||
|
this._server = null
|
||||||
|
this.server = null
|
||||||
|
this.address = null
|
||||||
|
this.url = null
|
||||||
}
|
}
|
||||||
|
|
||||||
computeURL() {
|
computeURL() {
|
||||||
|
@ -37,6 +45,7 @@ export default class Listener {
|
||||||
case '127.0.0.1': this.host = 'localhost'; break
|
case '127.0.0.1': this.host = 'localhost'; break
|
||||||
case '0.0.0.0': this.host = ip.address(); break
|
case '0.0.0.0': this.host = ip.address(); break
|
||||||
}
|
}
|
||||||
|
this.port = address.port
|
||||||
this.url = `http${this.https ? 's' : ''}://${this.host}:${this.port}`
|
this.url = `http${this.https ? 's' : ''}://${this.host}:${this.port}`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -51,25 +60,53 @@ export default class Listener {
|
||||||
|
|
||||||
// Initialize underlying http(s) server
|
// Initialize underlying http(s) server
|
||||||
const protocol = this.https ? https : http
|
const protocol = this.https ? https : http
|
||||||
const protocolOpts = typeof this.https === 'object' ? [ this.https ] : []
|
const protocolOpts = typeof this.https === 'object' ? [this.https] : []
|
||||||
this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app))
|
this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app))
|
||||||
|
|
||||||
|
// Call server.listen
|
||||||
// Prepare listenArgs
|
// Prepare listenArgs
|
||||||
const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port }
|
const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port }
|
||||||
listenArgs.exclusive = false
|
listenArgs.exclusive = false
|
||||||
|
|
||||||
// Call server.listen
|
// Call server.listen
|
||||||
|
try {
|
||||||
this.server = await new Promise((resolve, reject) => {
|
this.server = await new Promise((resolve, reject) => {
|
||||||
|
this._server.on('error', error => reject(error))
|
||||||
const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s))
|
const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s))
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return this.serverErrorHandler(error)
|
||||||
|
}
|
||||||
|
|
||||||
// Enable destroy support
|
// Enable destroy support
|
||||||
enableDestroy(this.server)
|
enableDestroy(this.server)
|
||||||
pify(this.server.destroy)
|
pify(this.server.destroy)
|
||||||
|
|
||||||
|
// Compute listen URL
|
||||||
this.computeURL()
|
this.computeURL()
|
||||||
|
|
||||||
// Set this.listening to true
|
// Set this.listening to true
|
||||||
this.listening = true
|
this.listening = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverErrorHandler(error) {
|
||||||
|
// Detect if port is not available
|
||||||
|
const addressInUse = error.code === 'EADDRINUSE'
|
||||||
|
|
||||||
|
// Use better error message
|
||||||
|
if (addressInUse) {
|
||||||
|
error.message = `Address \`${this.host}:${this.port}\` is already in use.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to a random port on dev as a fallback
|
||||||
|
if (addressInUse && this.dev && this.port !== '0') {
|
||||||
|
consola.warn(error.message)
|
||||||
|
consola.info('Trying a random port...')
|
||||||
|
this.port = '0'
|
||||||
|
return this.close().then(() => this.listen())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw error
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -219,7 +219,8 @@ export default class Server {
|
||||||
host: host || this.options.server.host,
|
host: host || this.options.server.host,
|
||||||
socket: socket || this.options.server.socket,
|
socket: socket || this.options.server.socket,
|
||||||
https: this.options.server.https,
|
https: this.options.server.https,
|
||||||
app: this.app
|
app: this.app,
|
||||||
|
dev: this.options.dev
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen
|
// Listen
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import consola from 'consola'
|
||||||
|
|
||||||
|
import { loadFixture, getPort, Nuxt } from '../utils'
|
||||||
|
|
||||||
|
let config
|
||||||
|
|
||||||
|
describe('server listen', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
config = await loadFixture('empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should throw error when listening on same port (prod)', async () => {
|
||||||
|
const nuxt = new Nuxt(config)
|
||||||
|
const port = await getPort()
|
||||||
|
const listen = () => nuxt.server.listen(port, 'localhost')
|
||||||
|
|
||||||
|
// Listen for first time
|
||||||
|
await listen()
|
||||||
|
expect(nuxt.server.listeners[0].port).toBe(port)
|
||||||
|
|
||||||
|
// Listen for second time
|
||||||
|
await expect(listen()).rejects.toThrow(`Address \`localhost:${port}\` is already in use.`)
|
||||||
|
|
||||||
|
await nuxt.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should assign a random port when listening on same port (dev)', async () => {
|
||||||
|
const nuxt = new Nuxt({ ...config, dev: true })
|
||||||
|
const port = await getPort()
|
||||||
|
const listen = () => nuxt.server.listen(port, 'localhost')
|
||||||
|
|
||||||
|
// Listen for first time
|
||||||
|
await listen()
|
||||||
|
expect(nuxt.server.listeners[0].port).toBe(port)
|
||||||
|
|
||||||
|
// Listen for second time
|
||||||
|
await listen()
|
||||||
|
expect(nuxt.server.listeners[1].port).not.toBe(nuxt.server.listeners[0].port)
|
||||||
|
expect(consola.warn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.warn).toHaveBeenCalledWith(`Address \`localhost:${port}\` is already in use.`)
|
||||||
|
|
||||||
|
await nuxt.close()
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue