Aktueller Stand

This commit is contained in:
2026-01-22 19:05:45 +01:00
parent 85dee61a4d
commit e280e4eadb
1967 changed files with 397327 additions and 74093 deletions

View File

@@ -2,20 +2,23 @@
const http = require('node:http')
const https = require('node:https')
const http2 = require('node:http2')
const dns = require('node:dns')
const os = require('node:os')
const { FSTDEP011 } = require('./warnings')
const { kState, kOptions, kServerBindings } = require('./symbols')
const { kState, kOptions, kServerBindings, kHttp2ServerSessions } = 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
FST_ERR_LISTEN_OPTIONS_INVALID,
FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE
} = require('./errors')
const noopSet = require('./noop-set')
const PonyPromise = require('./promise')
module.exports.createServer = createServer
module.exports.compileValidateHTTPVersion = compileValidateHTTPVersion
function defaultResolveServerListeningText (address) {
return `Server listening at ${address}`
@@ -25,27 +28,15 @@ function createServer (options, httpHandler) {
const server = getServerInstance(options, httpHandler)
// `this` is the Fastify object
function listen (listenOptions, ...args) {
let cb = args.slice(-1).pop()
// When the variadic signature deprecation is complete, the function
// declaration should become:
// function listen (listenOptions = { port: 0, host: 'localhost' }, cb = undefined)
// Upon doing so, the `normalizeListenArgs` function is no longer needed,
// and all of this preamble to feed it correctly also no longer needed.
const firstArgType = Object.prototype.toString.call(arguments[0])
if (arguments.length === 0) {
listenOptions = normalizeListenArgs([])
} else if (arguments.length > 0 && (firstArgType !== '[object Object]' && firstArgType !== '[object Function]')) {
FSTDEP011()
listenOptions = normalizeListenArgs(Array.from(arguments))
cb = listenOptions.cb
} else if (args.length > 1) {
// `.listen(obj, a, ..., n, callback )`
FSTDEP011()
// Deal with `.listen(port, host, backlog, [cb])`
const hostPath = listenOptions.path ? [listenOptions.path] : [listenOptions.port ?? 0, listenOptions.host ?? 'localhost']
Object.assign(listenOptions, normalizeListenArgs([...hostPath, ...args]))
} else {
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) {
@@ -53,10 +44,14 @@ function createServer (options, httpHandler) {
throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal')
}
if (listenOptions.signal.aborted) {
this.close()
// copy the current signal state
this[kState].aborted = listenOptions.signal.aborted
if (this[kState].aborted) {
return this.close()
} else {
const onAborted = () => {
this[kState].aborted = true
this.close()
}
listenOptions.signal.addEventListener('abort', onAborted, { once: true })
@@ -72,7 +67,7 @@ function createServer (options, httpHandler) {
} else {
host = listenOptions.host
}
if (Object.prototype.hasOwnProperty.call(listenOptions, 'host') === false ||
if (!Object.hasOwn(listenOptions, 'host') ||
listenOptions.host == null) {
listenOptions.host = host
}
@@ -110,27 +105,47 @@ function createServer (options, httpHandler) {
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 {
const { promise, resolve } = PonyPromise.withResolvers()
if (host === 'localhost') {
multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
this[kState].listening = true
resolve(address)
onListenHookRunner(this)
}
})
})
} else {
resolve(address)
onListenHookRunner(this)
}
return promise
})
}
this.ready(listenCallback.call(this, server, listenOptions))
}
return { server, listen }
const serverHasCloseAllConnections = typeof server.closeAllConnections === 'function'
const serverHasCloseIdleConnections = typeof server.closeIdleConnections === 'function'
const serverHasCloseHttp2Sessions = typeof server.closeHttp2Sessions === 'function'
let forceCloseConnections = options.forceCloseConnections
if (forceCloseConnections === 'idle' && !serverHasCloseIdleConnections) {
throw new FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE()
} else if (typeof forceCloseConnections !== 'boolean') {
/* istanbul ignore next: only one branch can be valid in a given Node.js version */
forceCloseConnections = serverHasCloseIdleConnections ? 'idle' : false
}
const keepAliveConnections = !serverHasCloseAllConnections && forceCloseConnections === true ? new Set() : noopSet()
return {
server,
listen,
forceCloseConnections,
serverHasCloseAllConnections,
serverHasCloseHttp2Sessions,
keepAliveConnections
}
}
function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, onListen) {
@@ -139,7 +154,7 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
// let's check if we need to bind additional addresses
dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {
if (dnsErr) {
if (dnsErr || this[kState].aborted) {
// not blocking the main server listening
// this.log.warn('dns.lookup error:', dnsErr)
onListen()
@@ -161,7 +176,6 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
cb: (_ignoreErr) => {
bound++
/* istanbul ignore next: the else won't be taken unless listening fails */
if (!_ignoreErr) {
this[kServerBindings].push(secondaryServer)
}
@@ -175,20 +189,18 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
const secondaryServer = getServerInstance(serverOpts, httpHandler)
const closeSecondary = () => {
// To avoid fall into situations where the close of the
// 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.
// 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
// We swallow any error from the secondary server
secondaryServer.close(() => {})
if (serverOpts.forceCloseConnections === 'idle') {
// Not needed in Node 19
secondaryServer.closeIdleConnections()
} else if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections) {
if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections === true) {
secondaryServer.closeAllConnections()
}
if (typeof secondaryServer.closeHttp2Sessions === 'function') {
secondaryServer.closeHttp2Sessions()
}
}
secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade'))
@@ -210,7 +222,6 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
// 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')
@@ -223,7 +234,11 @@ function listenCallback (server, listenOptions) {
server.removeListener('error', wrap)
server.removeListener('listening', wrap)
if (!err) {
const address = logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText)
const address = logServerAddress.call(
this,
server,
listenOptions.listenTextResolver || defaultResolveServerListeningText
)
listenOptions.cb(null, address)
} else {
this[kState].listening = false
@@ -236,7 +251,8 @@ function listenCallback (server, listenOptions) {
if (this[kState].listening && this[kState].closing) {
return listenOptions.cb(new FST_ERR_REOPENED_CLOSE_SERVER(), null)
} else if (this[kState].listening) {
}
if (this[kState].listening) {
return listenOptions.cb(new FST_ERR_REOPENED_SERVER(), null)
}
@@ -252,196 +268,174 @@ function listenCallback (server, listenOptions) {
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) {
}
if (this[kState].listening) {
return Promise.reject(new FST_ERR_REOPENED_SERVER())
}
return this.ready().then(() => {
let errEventHandler
let listeningEventHandler
// skip listen when aborted during ready
if (this[kState].aborted) return
const { promise, resolve, reject } = PonyPromise.withResolvers()
const errEventHandler = (err) => {
cleanup()
this[kState].listening = false
reject(err)
}
const listeningEventHandler = () => {
cleanup()
this[kState].listening = true
resolve(logServerAddress.call(
this,
server,
listenOptions.listenTextResolver || defaultResolveServerListeningText
))
}
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.once('error', errEventHandler)
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
])
return promise
})
}
/**
* Creates a function that, based upon initial configuration, will
* verify that every incoming request conforms to allowed
* HTTP versions for the Fastify instance, e.g. a Fastify HTTP/1.1
* server will not serve HTTP/2 requests upon the result of the
* verification function.
*
* @param {object} options fastify option
* @param {function} [options.serverFactory] If present, the
* validator function will skip all checks.
* @param {boolean} [options.http2 = false] If true, the validator
* function will allow HTTP/2 requests.
* @param {object} [options.https = null] https server options
* @param {boolean} [options.https.allowHTTP1] If true and use
* with options.http2 the validator function will allow HTTP/1
* request to http2 server.
*
* @returns {function} HTTP version validator function.
*/
function compileValidateHTTPVersion (options) {
let bypass = false
// key-value map to store valid http version
const map = new Map()
if (options.serverFactory) {
// When serverFactory is passed, we cannot identify how to check http version reliably
// So, we should skip the http version check
bypass = true
}
if (options.http2) {
// HTTP2 must serve HTTP/2.0
map.set('2.0', true)
if (options.https && options.https.allowHTTP1 === true) {
// HTTP2 with HTTPS.allowHTTP1 allow fallback to HTTP/1.1 and HTTP/1.0
map.set('1.1', true)
map.set('1.0', true)
}
} else {
// HTTP must server HTTP/1.1 and HTTP/1.0
map.set('1.1', true)
map.set('1.0', true)
}
// The compiled function here placed in one of the hottest path inside fastify
// the implementation here must be as performant as possible
return function validateHTTPVersion (httpVersion) {
// `bypass` skip the check when custom server factory provided
// `httpVersion in obj` check for the valid http version we should support
return bypass || map.has(httpVersion)
}
}
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
}
// User provided server instance
return options.serverFactory(httpHandler, options)
}
if (!options.serverFactory) {
// We have accepted true as a valid way to init https but node requires an options obj
const httpsOptions = options.https === true ? {} : options.https
if (options.http2) {
const server = typeof httpsOptions === 'object' ? http2.createSecureServer(httpsOptions, httpHandler) : http2.createServer(options.http, httpHandler)
server.on('session', (session) => session.setTimeout(options.http2SessionTimeout, () => {
session.close()
}))
// This is only needed for Node.js versions < 24.0.0 since Node.js added native
// closeAllSessions() on server.close() support for HTTP/2 servers in v24.0.0
if (options.forceCloseConnections === true) {
server.closeHttp2Sessions = createCloseHttp2SessionsByHttp2Server(server)
}
server.setTimeout(options.connectionTimeout)
return server
}
// HTTP1 server instance
const server = httpsOptions
? https.createServer(httpsOptions, httpHandler)
: http.createServer(options.http, httpHandler)
server.keepAliveTimeout = options.keepAliveTimeout
server.requestTimeout = options.requestTimeout
server.setTimeout(options.connectionTimeout)
// We treat zero as null(node default) so we do not pass zero to the server instance
if (options.maxRequestsPerSocket > 0) {
server.maxRequestsPerSocket = options.maxRequestsPerSocket
}
return server
}
function normalizeListenArgs (args) {
if (args.length === 0) {
return { port: 0, host: 'localhost' }
/**
* 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 })
}
const cb = typeof args[args.length - 1] === 'function' ? args.pop() : undefined
const options = { cb }
const firstArg = args[0]
const argsLength = args.length
const lastArg = args[argsLength - 1]
if (typeof firstArg === 'string' && isNaN(firstArg)) {
/* Deal with listen (pipe[, backlog]) */
options.path = firstArg
options.backlog = argsLength > 1 ? lastArg : undefined
} else {
/* Deal with listen ([port[, host[, backlog]]]) */
options.port = argsLength >= 1 && Number.isInteger(firstArg) ? firstArg : normalizePort(firstArg)
// This will listen to what localhost is.
// It can be 127.0.0.1 or ::1, depending on the operating system.
// Fixes https://github.com/fastify/fastify/issues/1022.
options.host = argsLength >= 2 && args[1] ? args[1] : 'localhost'
options.backlog = argsLength >= 3 ? args[2] : undefined
}
return options
}
function normalizePort (firstArg) {
const port = Number(firstArg)
return port >= 0 && !Number.isNaN(port) && Number.isInteger(port) ? port : 0
return [address.address]
}
function logServerAddress (server, listenTextResolver) {
let address = server.address()
const isUnixSocket = typeof address === 'string'
/* istanbul ignore next */
let addresses
const isUnixSocket = typeof server.address() === 'string'
if (!isUnixSocket) {
if (address.address.indexOf(':') === -1) {
address = address.address + ':' + address.port
if (server.address().address.indexOf(':') === -1) {
// IPv4
addresses = getAddresses(server.address()).map((address) => address + ':' + server.address().port)
} else {
address = '[' + address.address + ']:' + address.port
// 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]
}
/**
* @param {http2.Http2Server} http2Server
* @returns {() => void}
*/
function createCloseHttp2SessionsByHttp2Server (http2Server) {
/**
* @type {Set<http2.Http2Session>}
*/
http2Server[kHttp2ServerSessions] = new Set()
http2Server.on('session', function (session) {
session.once('connect', function () {
http2Server[kHttp2ServerSessions].add(session)
})
session.once('close', function () {
http2Server[kHttp2ServerSessions].delete(session)
})
session.once('frameError', function (type, code, streamId) {
if (streamId === 0) {
// The stream ID is 0, which means that the error is related to the session itself.
// If the event is not associated with a stream, the Http2Session will be shut down immediately
http2Server[kHttp2ServerSessions].delete(session)
}
})
session.once('goaway', function () {
// The Http2Session instance will be shut down automatically when the 'goaway' event is emitted.
http2Server[kHttp2ServerSessions].delete(session)
})
})
return function closeHttp2Sessions () {
if (http2Server[kHttp2ServerSessions].size === 0) {
return
}
for (const session of http2Server[kHttp2ServerSessions]) {
session.close()
}
}
/* istanbul ignore next */
address = (isUnixSocket ? '' : ('http' + (this[kOptions].https ? 's' : '') + '://')) + address
const serverListeningText = listenTextResolver(address)
this.log.info(serverListeningText)
return address
}
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()
}