From 1d78027e2b3a9032c2c3aadc9b666ea43d86b309 Mon Sep 17 00:00:00 2001 From: Ricardo Gobbo de Souza Date: Fri, 30 Nov 2018 13:32:15 -0200 Subject: [PATCH] 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 770347adb9c902e1ccde49d87ee89c4f3701b223. * Revert "simplify return" This reverts commit 26f2588b2f25b522acb44a5fa6d35906882e6331. * 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 --- packages/server/src/listener.js | 49 +++++++++++++++++++++++++++++---- packages/server/src/server.js | 3 +- test/unit/server.listen.test.js | 44 +++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 test/unit/server.listen.test.js diff --git a/packages/server/src/listener.js b/packages/server/src/listener.js index 6556a665f4..d3d753c88a 100644 --- a/packages/server/src/listener.js +++ b/packages/server/src/listener.js @@ -6,13 +6,14 @@ import consola from 'consola' import pify from 'pify' export default class Listener { - constructor({ port, host, socket, https, app }) { + constructor({ port, host, socket, https, app, dev }) { // Options this.port = port this.host = host this.socket = socket this.https = https this.app = app + this.dev = dev // After listen this.listening = false @@ -24,10 +25,17 @@ export default class Listener { async close() { // Destroy server by forcing every connection to be closed - if (this.server.listening) { + if (this.server && this.server.listening) { await this.server.destroy() consola.debug('server closed') } + + // Delete references + this.listening = false + this._server = null + this.server = null + this.address = null + this.url = null } computeURL() { @@ -37,6 +45,7 @@ export default class Listener { case '127.0.0.1': this.host = 'localhost'; 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}` return } @@ -51,25 +60,53 @@ export default class Listener { // Initialize underlying http(s) server 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)) + // Call server.listen // Prepare listenArgs const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port } listenArgs.exclusive = false // Call server.listen - this.server = await new Promise((resolve, reject) => { - const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s)) - }) + try { + 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)) + }) + } catch (error) { + return this.serverErrorHandler(error) + } // Enable destroy support enableDestroy(this.server) pify(this.server.destroy) + // Compute listen URL this.computeURL() // Set this.listening to 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 + } } diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 85725946c2..13a89a8fb8 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -219,7 +219,8 @@ export default class Server { host: host || this.options.server.host, socket: socket || this.options.server.socket, https: this.options.server.https, - app: this.app + app: this.app, + dev: this.options.dev }) // Listen diff --git a/test/unit/server.listen.test.js b/test/unit/server.listen.test.js new file mode 100644 index 0000000000..af646aff88 --- /dev/null +++ b/test/unit/server.listen.test.js @@ -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() + }) +})