UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

376 lines (336 loc) 11.7 kB
'use strict' const http = require('node:http') const https = require('node:https') const dns = require('node:dns') const os = require('node:os') const { kState, kOptions, kServerBindings } = require('./symbols') const { FSTWRN003 } = require('./warnings') const { onListenHookRunner } = require('./hooks') const { FST_ERR_HTTP2_INVALID_VERSION, FST_ERR_REOPENED_CLOSE_SERVER, FST_ERR_REOPENED_SERVER, FST_ERR_LISTEN_OPTIONS_INVALID } = require('./errors') module.exports.createServer = createServer function defaultResolveServerListeningText (address) { return `Server listening at ${address}` } function createServer (options, httpHandler) { const server = getServerInstance(options, httpHandler) // `this` is the Fastify object function listen ( listenOptions = { port: 0, host: 'localhost' }, cb = undefined ) { if (typeof cb === 'function') { if (cb.constructor.name === 'AsyncFunction') { FSTWRN003('listen method') } listenOptions.cb = cb } if (listenOptions.signal) { if (typeof listenOptions.signal.on !== 'function' && typeof listenOptions.signal.addEventListener !== 'function') { throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal') } if (listenOptions.signal.aborted) { this.close() } else { const onAborted = () => { this.close() } listenOptions.signal.addEventListener('abort', onAborted, { once: true }) } } // If we have a path specified, don't default host to 'localhost' so we don't end up listening // on both path and host // See https://github.com/fastify/fastify/issues/4007 let host if (listenOptions.path == null) { host = listenOptions.host ?? 'localhost' } else { host = listenOptions.host } if (!Object.hasOwn(listenOptions, 'host') || listenOptions.host == null) { listenOptions.host = host } if (host === 'localhost') { listenOptions.cb = (err, address) => { if (err) { // the server did not start cb(err, address) return } multipleBindings.call(this, server, httpHandler, options, listenOptions, () => { this[kState].listening = true cb(null, address) onListenHookRunner(this) }) } } else { listenOptions.cb = (err, address) => { // the server did not start if (err) { cb(err, address) return } this[kState].listening = true cb(null, address) onListenHookRunner(this) } } // https://github.com/nodejs/node/issues/9390 // If listening to 'localhost', listen to both 127.0.0.1 or ::1 if they are available. // If listening to 127.0.0.1, only listen to 127.0.0.1. // If listening to ::1, only listen to ::1. if (cb === undefined) { const listening = listenPromise.call(this, server, listenOptions) /* istanbul ignore else */ return listening.then(address => { return new Promise((resolve, reject) => { if (host === 'localhost') { multipleBindings.call(this, server, httpHandler, options, listenOptions, () => { this[kState].listening = true resolve(address) onListenHookRunner(this) }) } else { resolve(address) onListenHookRunner(this) } }) }) } this.ready(listenCallback.call(this, server, listenOptions)) } return { server, listen } } function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, onListen) { // the main server is started, we need to start the secondary servers this[kState].listening = false // let's check if we need to bind additional addresses dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => { if (dnsErr) { // not blocking the main server listening // this.log.warn('dns.lookup error:', dnsErr) onListen() return } const isMainServerListening = mainServer.listening && serverOpts.serverFactory let binding = 0 let bound = 0 if (!isMainServerListening) { const primaryAddress = mainServer.address() for (const adr of addresses) { if (adr.address !== primaryAddress.address) { binding++ const secondaryOpts = Object.assign({}, listenOptions, { host: adr.address, port: primaryAddress.port, cb: (_ignoreErr) => { bound++ if (!_ignoreErr) { this[kServerBindings].push(secondaryServer) } if (bound === binding) { // regardless of the error, we are done onListen() } } }) const secondaryServer = getServerInstance(serverOpts, httpHandler) const closeSecondary = () => { // To avoid falling into situations where the close of the // secondary server is triggered before the preClose hook // is done running, we better wait until the main server is closed. // No new TCP connections are accepted // We swallow any error from the secondary server secondaryServer.close(() => {}) if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections === true) { secondaryServer.closeAllConnections() } } secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade')) mainServer.on('unref', closeSecondary) mainServer.on('close', closeSecondary) mainServer.on('error', closeSecondary) this[kState].listening = false listenCallback.call(this, secondaryServer, secondaryOpts)() } } } // no extra bindings are necessary if (binding === 0) { onListen() return } // in test files we are using unref so we need to propagate the unref event // to the secondary servers. It is valid only when the user is // listening on localhost const originUnref = mainServer.unref /* c8 ignore next 4 */ mainServer.unref = function () { originUnref.call(mainServer) mainServer.emit('unref') } }) } function listenCallback (server, listenOptions) { const wrap = (err) => { server.removeListener('error', wrap) server.removeListener('listening', wrap) if (!err) { const address = logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText) listenOptions.cb(null, address) } else { this[kState].listening = false listenOptions.cb(err, null) } } return (err) => { if (err != null) return listenOptions.cb(err) if (this[kState].listening && this[kState].closing) { return listenOptions.cb(new FST_ERR_REOPENED_CLOSE_SERVER(), null) } else if (this[kState].listening) { return listenOptions.cb(new FST_ERR_REOPENED_SERVER(), null) } server.once('error', wrap) if (!this[kState].closing) { server.once('listening', wrap) server.listen(listenOptions) this[kState].listening = true } } } function listenPromise (server, listenOptions) { if (this[kState].listening && this[kState].closing) { return Promise.reject(new FST_ERR_REOPENED_CLOSE_SERVER()) } else if (this[kState].listening) { return Promise.reject(new FST_ERR_REOPENED_SERVER()) } return this.ready().then(() => { let errEventHandler let listeningEventHandler function cleanup () { server.removeListener('error', errEventHandler) server.removeListener('listening', listeningEventHandler) } const errEvent = new Promise((resolve, reject) => { errEventHandler = (err) => { cleanup() this[kState].listening = false reject(err) } server.once('error', errEventHandler) }) const listeningEvent = new Promise((resolve, reject) => { listeningEventHandler = () => { cleanup() this[kState].listening = true resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText)) } server.once('listening', listeningEventHandler) }) server.listen(listenOptions) return Promise.race([ errEvent, // e.g invalid port range error is always emitted before the server listening listeningEvent ]) }) } function getServerInstance (options, httpHandler) { let server = null // node@20 do not accepts options as boolean // we need to provide proper https option const httpsOptions = options.https === true ? {} : options.https if (options.serverFactory) { server = options.serverFactory(httpHandler, options) } else if (options.http2) { if (typeof httpsOptions === 'object') { server = http2().createSecureServer(httpsOptions, httpHandler) } else { server = http2().createServer(httpHandler) } server.on('session', sessionTimeout(options.http2SessionTimeout)) } else { // this is http1 if (httpsOptions) { server = https.createServer(httpsOptions, httpHandler) } else { server = http.createServer(options.http, httpHandler) } server.keepAliveTimeout = options.keepAliveTimeout server.requestTimeout = options.requestTimeout // we treat zero as null // and null is the default setting from nodejs // so we do not pass the option to server if (options.maxRequestsPerSocket > 0) { server.maxRequestsPerSocket = options.maxRequestsPerSocket } } if (!options.serverFactory) { server.setTimeout(options.connectionTimeout) } return server } /** * Inspects the provided `server.address` object and returns a * normalized list of IP address strings. Normalization in this * case refers to mapping wildcard `0.0.0.0` to the list of IP * addresses the wildcard refers to. * * @see https://nodejs.org/docs/latest/api/net.html#serveraddress * * @param {object} A server address object as described in the * linked docs. * * @returns {string[]} */ function getAddresses (address) { if (address.address === '0.0.0.0') { return Object.values(os.networkInterfaces()).flatMap((iface) => { return iface.filter((iface) => iface.family === 'IPv4') }).sort((iface) => { /* c8 ignore next 2 */ // Order the interfaces so that internal ones come first return iface.internal ? -1 : 1 }).map((iface) => { return iface.address }) } return [address.address] } function logServerAddress (server, listenTextResolver) { let addresses const isUnixSocket = typeof server.address() === 'string' if (!isUnixSocket) { if (server.address().address.indexOf(':') === -1) { // IPv4 addresses = getAddresses(server.address()).map((address) => address + ':' + server.address().port) } else { // IPv6 addresses = ['[' + server.address().address + ']:' + server.address().port] } addresses = addresses.map((address) => ('http' + (this[kOptions].https ? 's' : '') + '://') + address) } else { addresses = [server.address()] } for (const address of addresses) { this.log.info(listenTextResolver(address)) } return addresses[0] } function http2 () { try { return require('node:http2') } catch (err) { throw new FST_ERR_HTTP2_INVALID_VERSION() } } function sessionTimeout (timeout) { return function (session) { session.setTimeout(timeout, close) } } function close () { this.close() }