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 commit 770347adb9.

* Revert "simplify return"

This reverts commit 26f2588b2f.

* 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:
Ricardo Gobbo de Souza 2018-11-30 13:32:15 -02:00 committed by Sébastien Chopin
parent 71136fc9b6
commit 1d78027e2b
3 changed files with 89 additions and 7 deletions

View File

@ -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
} }
@ -54,22 +63,50 @@ export default class Listener {
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
}
} }

View File

@ -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

View File

@ -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()
})
})