diff --git a/packages/server/package.json b/packages/server/package.json index 26cb0931a6..17b7273097 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,6 +19,7 @@ "etag": "^1.8.1", "fresh": "^0.5.2", "fs-extra": "^7.0.1", + "get-port": "^4.0.0", "ip": "^1.1.5", "launch-editor-middleware": "^2.2.1", "pify": "^4.0.1", diff --git a/packages/server/src/listener.js b/packages/server/src/listener.js index 0149c42d98..5dbd8c7b8b 100644 --- a/packages/server/src/listener.js +++ b/packages/server/src/listener.js @@ -4,6 +4,7 @@ import enableDestroy from 'server-destroy' import ip from 'ip' import consola from 'consola' import pify from 'pify' +import getPort from 'get-port' export default class Listener { constructor({ port, host, socket, https, app }) { @@ -55,16 +56,10 @@ export default class Listener { this._server = protocol.createServer.apply(protocol, protocolOpts.concat(this.app)) // Listen server error - this._server.on('error', this.errorHandler) - - // Prepare listenArgs - const listenArgs = this.socket ? { path: this.socket } : { host: this.host, port: this.port } - listenArgs.exclusive = false + this._server.on('error', this.serverErrorHandler.bind(this)) // Call server.listen - this.server = await new Promise((resolve, reject) => { - const s = this._server.listen(listenArgs, error => error ? reject(error) : resolve(s)) - }) + await this.serverListen() // Enable destroy support enableDestroy(this.server) @@ -76,13 +71,29 @@ export default class Listener { this.listening = true } - errorHandler(e) { - const errors = { - EACCES: 'Permission denied. Does your user have permission?', - EADDRINUSE: `Address \`${this.host}:${this.port}\` is already in use. Do you run another service on the same port?`, - EDQUOT: 'Disk quota exceeded. Do you have space in disk?' + async serverErrorHandler(e) { + if(e.code === 'EADDRINUSE') { + consola.warn(`Address \`${this.host}:${this.port}\` is already in use.`) + + this._server.close() + this.port = await getPort() + + await this.serverListen() + + return } - consola.error(errors[e.code] || e) + consola.error(e) + } + + async serverListen() { + // 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)) + }) } } diff --git a/test/unit/server.listen.test.js b/test/unit/server.listen.test.js new file mode 100644 index 0000000000..5ba028e9c2 --- /dev/null +++ b/test/unit/server.listen.test.js @@ -0,0 +1,35 @@ +import consola from 'consola' +import { loadFixture, getPort, Nuxt } from '../utils' + +let port = null +let nuxt1 = null +let nuxt2 = null + +describe('server listen', () => { + beforeAll(async () => { + const config = await loadFixture('empty') + nuxt1 = new Nuxt(config) + nuxt2 = new Nuxt(config) + port = await getPort() + }) + + test('should listen different ports', async () => { + await nuxt1.server.listen(port, 'localhost') + await nuxt2.server.listen(port, 'localhost') + + const host = nuxt1.server.listeners[0].host + const port1 = nuxt1.server.listeners[0].port + const port2 = nuxt2.server.listeners[0].port + + expect(port1).toBe(port) + expect(port2).not.toBe(port1) + + expect(consola.warn).toHaveBeenCalledTimes(1) + expect(consola.warn).toHaveBeenCalledWith(`Address \`${host}:${port}\` is already in use.`) + }) + + afterAll(async () => { + await nuxt1.close() + await nuxt2.close() + }) +})