621 lines
20 KiB
JavaScript
621 lines
20 KiB
JavaScript
'use strict'
|
|
|
|
const FindMyWay = require('find-my-way')
|
|
const Context = require('./context')
|
|
const handleRequest = require('./handleRequest')
|
|
const { onRequestAbortHookRunner, lifecycleHooks, preParsingHookRunner, onTimeoutHookRunner, onRequestHookRunner } = require('./hooks')
|
|
const { normalizeSchema } = require('./schemas')
|
|
const { parseHeadOnSendHandlers } = require('./headRoute')
|
|
|
|
const {
|
|
compileSchemasForValidation,
|
|
compileSchemasForSerialization
|
|
} = require('./validation')
|
|
|
|
const {
|
|
FST_ERR_SCH_VALIDATION_BUILD,
|
|
FST_ERR_SCH_SERIALIZATION_BUILD,
|
|
FST_ERR_DUPLICATED_ROUTE,
|
|
FST_ERR_INVALID_URL,
|
|
FST_ERR_HOOK_INVALID_HANDLER,
|
|
FST_ERR_ROUTE_OPTIONS_NOT_OBJ,
|
|
FST_ERR_ROUTE_DUPLICATED_HANDLER,
|
|
FST_ERR_ROUTE_HANDLER_NOT_FN,
|
|
FST_ERR_ROUTE_MISSING_HANDLER,
|
|
FST_ERR_ROUTE_METHOD_NOT_SUPPORTED,
|
|
FST_ERR_ROUTE_METHOD_INVALID,
|
|
FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED,
|
|
FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT,
|
|
FST_ERR_HOOK_INVALID_ASYNC_HANDLER
|
|
} = require('./errors')
|
|
|
|
const { FSTDEP022 } = require('./warnings')
|
|
|
|
const {
|
|
kRoutePrefix,
|
|
kSupportedHTTPMethods,
|
|
kLogLevel,
|
|
kLogSerializers,
|
|
kHooks,
|
|
kSchemaController,
|
|
kOptions,
|
|
kReplySerializerDefault,
|
|
kReplyIsError,
|
|
kRequestPayloadStream,
|
|
kDisableRequestLogging,
|
|
kSchemaErrorFormatter,
|
|
kErrorHandler,
|
|
kHasBeenDecorated,
|
|
kRequestAcceptVersion,
|
|
kRouteByFastify,
|
|
kRouteContext
|
|
} = require('./symbols.js')
|
|
const { buildErrorHandler } = require('./error-handler')
|
|
const { createChildLogger } = require('./logger-factory.js')
|
|
const { getGenReqId } = require('./reqIdGenFactory.js')
|
|
|
|
const routerKeys = [
|
|
'allowUnsafeRegex',
|
|
'buildPrettyMeta',
|
|
'caseSensitive',
|
|
'constraints',
|
|
'defaultRoute',
|
|
'ignoreDuplicateSlashes',
|
|
'ignoreTrailingSlash',
|
|
'maxParamLength',
|
|
'onBadUrl',
|
|
'querystringParser',
|
|
'useSemicolonDelimiter'
|
|
]
|
|
|
|
function buildRouting (options) {
|
|
const router = FindMyWay(options.config)
|
|
|
|
let avvio
|
|
let fourOhFour
|
|
let logger
|
|
let hasLogger
|
|
let setupResponseListeners
|
|
let throwIfAlreadyStarted
|
|
let disableRequestLogging
|
|
let ignoreTrailingSlash
|
|
let ignoreDuplicateSlashes
|
|
let return503OnClosing
|
|
let globalExposeHeadRoutes
|
|
let keepAliveConnections
|
|
|
|
let closing = false
|
|
|
|
return {
|
|
/**
|
|
* @param {import('../fastify').FastifyServerOptions} options
|
|
* @param {*} fastifyArgs
|
|
*/
|
|
setup (options, fastifyArgs) {
|
|
avvio = fastifyArgs.avvio
|
|
fourOhFour = fastifyArgs.fourOhFour
|
|
logger = fastifyArgs.logger
|
|
hasLogger = fastifyArgs.hasLogger
|
|
setupResponseListeners = fastifyArgs.setupResponseListeners
|
|
throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
|
|
|
|
globalExposeHeadRoutes = options.exposeHeadRoutes
|
|
disableRequestLogging = options.disableRequestLogging
|
|
ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
|
|
ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
|
|
return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
|
|
keepAliveConnections = fastifyArgs.keepAliveConnections
|
|
},
|
|
routing: router.lookup.bind(router), // router func to find the right handler to call
|
|
route, // configure a route in the fastify instance
|
|
hasRoute,
|
|
prepareRoute,
|
|
routeHandler,
|
|
closeRoutes: () => { closing = true },
|
|
printRoutes: router.prettyPrint.bind(router),
|
|
addConstraintStrategy,
|
|
hasConstraintStrategy,
|
|
isAsyncConstraint,
|
|
findRoute
|
|
}
|
|
|
|
function addConstraintStrategy (strategy) {
|
|
throwIfAlreadyStarted('Cannot add constraint strategy!')
|
|
return router.addConstraintStrategy(strategy)
|
|
}
|
|
|
|
function hasConstraintStrategy (strategyName) {
|
|
return router.hasConstraintStrategy(strategyName)
|
|
}
|
|
|
|
function isAsyncConstraint () {
|
|
return router.constrainer.asyncStrategiesInUse.size > 0
|
|
}
|
|
|
|
// Convert shorthand to extended route declaration
|
|
function prepareRoute ({ method, url, options, handler, isFastify }) {
|
|
if (typeof url !== 'string') {
|
|
throw new FST_ERR_INVALID_URL(typeof url)
|
|
}
|
|
|
|
if (!handler && typeof options === 'function') {
|
|
handler = options // for support over direct function calls such as fastify.get() options are reused as the handler
|
|
options = {}
|
|
} else if (handler && typeof handler === 'function') {
|
|
if (Object.prototype.toString.call(options) !== '[object Object]') {
|
|
throw new FST_ERR_ROUTE_OPTIONS_NOT_OBJ(method, url)
|
|
} else if (options.handler) {
|
|
if (typeof options.handler === 'function') {
|
|
throw new FST_ERR_ROUTE_DUPLICATED_HANDLER(method, url)
|
|
} else {
|
|
throw new FST_ERR_ROUTE_HANDLER_NOT_FN(method, url)
|
|
}
|
|
}
|
|
}
|
|
|
|
options = Object.assign({}, options, {
|
|
method,
|
|
url,
|
|
path: url,
|
|
handler: handler || (options && options.handler)
|
|
})
|
|
|
|
return route.call(this, { options, isFastify })
|
|
}
|
|
|
|
function hasRoute ({ options }) {
|
|
const normalizedMethod = options.method?.toUpperCase() ?? ''
|
|
return router.hasRoute(
|
|
normalizedMethod,
|
|
options.url || '',
|
|
options.constraints
|
|
)
|
|
}
|
|
|
|
function findRoute (options) {
|
|
const route = router.find(
|
|
options.method,
|
|
options.url || '',
|
|
options.constraints
|
|
)
|
|
if (route) {
|
|
// we must reduce the expose surface, otherwise
|
|
// we provide the ability for the user to modify
|
|
// all the route and server information in runtime
|
|
return {
|
|
handler: route.handler,
|
|
params: route.params,
|
|
searchParams: route.searchParams
|
|
}
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Route management
|
|
* @param {{ options: import('../fastify').RouteOptions, isFastify: boolean }}
|
|
*/
|
|
function route ({ options, isFastify }) {
|
|
throwIfAlreadyStarted('Cannot add route!')
|
|
|
|
// Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator
|
|
const opts = { ...options }
|
|
|
|
const path = opts.url || opts.path || ''
|
|
|
|
if (!opts.handler) {
|
|
throw new FST_ERR_ROUTE_MISSING_HANDLER(opts.method, path)
|
|
}
|
|
|
|
if (opts.errorHandler !== undefined && typeof opts.errorHandler !== 'function') {
|
|
throw new FST_ERR_ROUTE_HANDLER_NOT_FN(opts.method, path)
|
|
}
|
|
|
|
validateBodyLimitOption(opts.bodyLimit)
|
|
|
|
const shouldExposeHead = opts.exposeHeadRoute ?? globalExposeHeadRoutes
|
|
|
|
let isGetRoute = false
|
|
let isHeadRoute = false
|
|
|
|
if (Array.isArray(opts.method)) {
|
|
for (let i = 0; i < opts.method.length; ++i) {
|
|
opts.method[i] = normalizeAndValidateMethod.call(this, opts.method[i])
|
|
validateSchemaBodyOption.call(this, opts.method[i], path, opts.schema)
|
|
|
|
isGetRoute = opts.method.includes('GET')
|
|
isHeadRoute = opts.method.includes('HEAD')
|
|
}
|
|
} else {
|
|
opts.method = normalizeAndValidateMethod.call(this, opts.method)
|
|
validateSchemaBodyOption.call(this, opts.method, path, opts.schema)
|
|
|
|
isGetRoute = opts.method === 'GET'
|
|
isHeadRoute = opts.method === 'HEAD'
|
|
}
|
|
|
|
// we need to clone a set of initial options for HEAD route
|
|
const headOpts = shouldExposeHead && isGetRoute ? { ...options } : null
|
|
|
|
const prefix = this[kRoutePrefix]
|
|
|
|
if (path === '/' && prefix.length > 0 && opts.method !== 'HEAD') {
|
|
switch (opts.prefixTrailingSlash) {
|
|
case 'slash':
|
|
addNewRoute.call(this, { path, isFastify })
|
|
break
|
|
case 'no-slash':
|
|
addNewRoute.call(this, { path: '', isFastify })
|
|
break
|
|
case 'both':
|
|
default:
|
|
addNewRoute.call(this, { path: '', isFastify })
|
|
// If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
|
|
if (ignoreTrailingSlash !== true && (ignoreDuplicateSlashes !== true || !prefix.endsWith('/'))) {
|
|
addNewRoute.call(this, { path, prefixing: true, isFastify })
|
|
}
|
|
}
|
|
} else if (path[0] === '/' && prefix.endsWith('/')) {
|
|
// Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
|
|
addNewRoute.call(this, { path: path.slice(1), isFastify })
|
|
} else {
|
|
addNewRoute.call(this, { path, isFastify })
|
|
}
|
|
|
|
// chainable api
|
|
return this
|
|
|
|
function addNewRoute ({ path, prefixing = false, isFastify = false }) {
|
|
const url = prefix + path
|
|
|
|
opts.url = url
|
|
opts.path = url
|
|
opts.routePath = path
|
|
opts.prefix = prefix
|
|
opts.logLevel = opts.logLevel || this[kLogLevel]
|
|
|
|
if (this[kLogSerializers] || opts.logSerializers) {
|
|
opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
|
|
}
|
|
|
|
if (opts.attachValidation == null) {
|
|
opts.attachValidation = false
|
|
}
|
|
|
|
if (prefixing === false) {
|
|
// run 'onRoute' hooks
|
|
for (const hook of this[kHooks].onRoute) {
|
|
hook.call(this, opts)
|
|
}
|
|
}
|
|
|
|
for (const hook of lifecycleHooks) {
|
|
if (opts && hook in opts) {
|
|
if (Array.isArray(opts[hook])) {
|
|
for (const func of opts[hook]) {
|
|
if (typeof func !== 'function') {
|
|
throw new FST_ERR_HOOK_INVALID_HANDLER(hook, Object.prototype.toString.call(func))
|
|
}
|
|
|
|
if (hook === 'onSend' || hook === 'preSerialization' || hook === 'onError' || hook === 'preParsing') {
|
|
if (func.constructor.name === 'AsyncFunction' && func.length === 4) {
|
|
throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
|
|
}
|
|
} else if (hook === 'onRequestAbort') {
|
|
if (func.constructor.name === 'AsyncFunction' && func.length !== 1) {
|
|
throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
|
|
}
|
|
} else {
|
|
if (func.constructor.name === 'AsyncFunction' && func.length === 3) {
|
|
throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
|
|
}
|
|
}
|
|
}
|
|
} else if (opts[hook] !== undefined && typeof opts[hook] !== 'function') {
|
|
throw new FST_ERR_HOOK_INVALID_HANDLER(hook, Object.prototype.toString.call(opts[hook]))
|
|
}
|
|
}
|
|
}
|
|
|
|
const constraints = opts.constraints || {}
|
|
const config = {
|
|
...opts.config,
|
|
url,
|
|
method: opts.method
|
|
}
|
|
|
|
const context = new Context({
|
|
schema: opts.schema,
|
|
handler: opts.handler.bind(this),
|
|
config,
|
|
errorHandler: opts.errorHandler,
|
|
childLoggerFactory: opts.childLoggerFactory,
|
|
bodyLimit: opts.bodyLimit,
|
|
logLevel: opts.logLevel,
|
|
logSerializers: opts.logSerializers,
|
|
attachValidation: opts.attachValidation,
|
|
schemaErrorFormatter: opts.schemaErrorFormatter,
|
|
replySerializer: this[kReplySerializerDefault],
|
|
validatorCompiler: opts.validatorCompiler,
|
|
serializerCompiler: opts.serializerCompiler,
|
|
exposeHeadRoute: shouldExposeHead,
|
|
prefixTrailingSlash: (opts.prefixTrailingSlash || 'both'),
|
|
server: this,
|
|
isFastify
|
|
})
|
|
|
|
const headHandler = router.findRoute('HEAD', opts.url, constraints)
|
|
const hasHEADHandler = headHandler !== null
|
|
|
|
try {
|
|
router.on(opts.method, opts.url, { constraints }, routeHandler, context)
|
|
} catch (error) {
|
|
// any route insertion error created by fastify can be safely ignore
|
|
// because it only duplicate route for head
|
|
if (!context[kRouteByFastify]) {
|
|
const isDuplicatedRoute = error.message.includes(`Method '${opts.method}' already declared for route`)
|
|
if (isDuplicatedRoute) {
|
|
throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
this.after((notHandledErr, done) => {
|
|
// Send context async
|
|
context.errorHandler = opts.errorHandler ? buildErrorHandler(this[kErrorHandler], opts.errorHandler) : this[kErrorHandler]
|
|
context._parserOptions.limit = opts.bodyLimit || null
|
|
context.logLevel = opts.logLevel
|
|
context.logSerializers = opts.logSerializers
|
|
context.attachValidation = opts.attachValidation
|
|
context[kReplySerializerDefault] = this[kReplySerializerDefault]
|
|
context.schemaErrorFormatter = opts.schemaErrorFormatter || this[kSchemaErrorFormatter] || context.schemaErrorFormatter
|
|
|
|
// Run hooks and more
|
|
avvio.once('preReady', () => {
|
|
for (const hook of lifecycleHooks) {
|
|
const toSet = this[kHooks][hook]
|
|
.concat(opts[hook] || [])
|
|
.map(h => h.bind(this))
|
|
context[hook] = toSet.length ? toSet : null
|
|
}
|
|
|
|
// Optimization: avoid encapsulation if no decoration has been done.
|
|
while (!context.Request[kHasBeenDecorated] && context.Request.parent) {
|
|
context.Request = context.Request.parent
|
|
}
|
|
while (!context.Reply[kHasBeenDecorated] && context.Reply.parent) {
|
|
context.Reply = context.Reply.parent
|
|
}
|
|
|
|
// Must store the 404 Context in 'preReady' because it is only guaranteed to
|
|
// be available after all of the plugins and routes have been loaded.
|
|
fourOhFour.setContext(this, context)
|
|
|
|
if (opts.schema) {
|
|
context.schema = normalizeSchema(context.schema, this.initialConfig)
|
|
|
|
const schemaController = this[kSchemaController]
|
|
if (!opts.validatorCompiler && (opts.schema.body || opts.schema.headers || opts.schema.querystring || opts.schema.params)) {
|
|
schemaController.setupValidator(this[kOptions])
|
|
}
|
|
try {
|
|
const isCustom = typeof opts?.validatorCompiler === 'function' || schemaController.isCustomValidatorCompiler
|
|
compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler, isCustom)
|
|
} catch (error) {
|
|
throw new FST_ERR_SCH_VALIDATION_BUILD(opts.method, url, error.message)
|
|
}
|
|
|
|
if (opts.schema.response && !opts.serializerCompiler) {
|
|
schemaController.setupSerializer(this[kOptions])
|
|
}
|
|
try {
|
|
compileSchemasForSerialization(context, opts.serializerCompiler || schemaController.serializerCompiler)
|
|
} catch (error) {
|
|
throw new FST_ERR_SCH_SERIALIZATION_BUILD(opts.method, url, error.message)
|
|
}
|
|
}
|
|
})
|
|
|
|
done(notHandledErr)
|
|
})
|
|
|
|
// register head route in sync
|
|
// we must place it after the `this.after`
|
|
|
|
if (shouldExposeHead && isGetRoute && !isHeadRoute && !hasHEADHandler) {
|
|
const onSendHandlers = parseHeadOnSendHandlers(headOpts.onSend)
|
|
prepareRoute.call(this, { method: 'HEAD', url: path, options: { ...headOpts, onSend: onSendHandlers }, isFastify: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
// HTTP request entry point, the routing has already been executed
|
|
function routeHandler (req, res, params, context, query) {
|
|
const id = getGenReqId(context.server, req)
|
|
|
|
const loggerOpts = {
|
|
level: context.logLevel
|
|
}
|
|
|
|
if (context.logSerializers) {
|
|
loggerOpts.serializers = context.logSerializers
|
|
}
|
|
const childLogger = createChildLogger(context, logger, req, id, loggerOpts)
|
|
childLogger[kDisableRequestLogging] = disableRequestLogging
|
|
|
|
if (closing === true) {
|
|
/* istanbul ignore next mac, windows */
|
|
if (req.httpVersionMajor !== 2) {
|
|
res.setHeader('Connection', 'close')
|
|
}
|
|
|
|
// TODO remove return503OnClosing after Node v18 goes EOL
|
|
/* istanbul ignore else */
|
|
if (return503OnClosing) {
|
|
// On Node v19 we cannot test this behavior as it won't be necessary
|
|
// anymore. It will close all the idle connections before they reach this
|
|
// stage.
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': '80'
|
|
}
|
|
res.writeHead(503, headers)
|
|
res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
|
|
childLogger.info({ res: { statusCode: 503 } }, 'request aborted - refusing to accept new requests as server is closing')
|
|
return
|
|
}
|
|
}
|
|
|
|
// When server.forceCloseConnections is true, we will collect any requests
|
|
// that have indicated they want persistence so that they can be reaped
|
|
// on server close. Otherwise, the container is a noop container.
|
|
const connHeader = String.prototype.toLowerCase.call(req.headers.connection || '')
|
|
if (connHeader === 'keep-alive') {
|
|
if (keepAliveConnections.has(req.socket) === false) {
|
|
keepAliveConnections.add(req.socket)
|
|
req.socket.on('close', removeTrackedSocket.bind({ keepAliveConnections, socket: req.socket }))
|
|
}
|
|
}
|
|
|
|
// we revert the changes in defaultRoute
|
|
if (req.headers[kRequestAcceptVersion] !== undefined) {
|
|
req.headers['accept-version'] = req.headers[kRequestAcceptVersion]
|
|
req.headers[kRequestAcceptVersion] = undefined
|
|
}
|
|
|
|
const request = new context.Request(id, params, req, query, childLogger, context)
|
|
const reply = new context.Reply(res, request, childLogger)
|
|
if (disableRequestLogging === false) {
|
|
childLogger.info({ req: request }, 'incoming request')
|
|
}
|
|
|
|
if (hasLogger === true || context.onResponse !== null) {
|
|
setupResponseListeners(reply)
|
|
}
|
|
|
|
if (context.onRequest !== null) {
|
|
onRequestHookRunner(
|
|
context.onRequest,
|
|
request,
|
|
reply,
|
|
runPreParsing
|
|
)
|
|
} else {
|
|
runPreParsing(null, request, reply)
|
|
}
|
|
|
|
if (context.onRequestAbort !== null) {
|
|
req.on('close', () => {
|
|
/* istanbul ignore else */
|
|
if (req.aborted) {
|
|
onRequestAbortHookRunner(
|
|
context.onRequestAbort,
|
|
request,
|
|
handleOnRequestAbortHooksErrors.bind(null, reply)
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (context.onTimeout !== null) {
|
|
if (!request.raw.socket._meta) {
|
|
request.raw.socket.on('timeout', handleTimeout)
|
|
}
|
|
request.raw.socket._meta = { context, request, reply }
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleOnRequestAbortHooksErrors (reply, err) {
|
|
if (err) {
|
|
reply.log.error({ err }, 'onRequestAborted hook failed')
|
|
}
|
|
}
|
|
|
|
function handleTimeout () {
|
|
const { context, request, reply } = this._meta
|
|
onTimeoutHookRunner(
|
|
context.onTimeout,
|
|
request,
|
|
reply,
|
|
noop
|
|
)
|
|
}
|
|
|
|
function normalizeAndValidateMethod (method) {
|
|
if (typeof method !== 'string') {
|
|
throw new FST_ERR_ROUTE_METHOD_INVALID()
|
|
}
|
|
method = method.toUpperCase()
|
|
if (!this[kSupportedHTTPMethods].bodyless.has(method) &&
|
|
!this[kSupportedHTTPMethods].bodywith.has(method)) {
|
|
throw new FST_ERR_ROUTE_METHOD_NOT_SUPPORTED(method)
|
|
}
|
|
|
|
return method
|
|
}
|
|
|
|
function validateSchemaBodyOption (method, path, schema) {
|
|
if (this[kSupportedHTTPMethods].bodyless.has(method) && schema?.body) {
|
|
throw new FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED(method, path)
|
|
}
|
|
}
|
|
|
|
function validateBodyLimitOption (bodyLimit) {
|
|
if (bodyLimit === undefined) return
|
|
if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
|
|
throw new FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT(bodyLimit)
|
|
}
|
|
}
|
|
|
|
function runPreParsing (err, request, reply) {
|
|
if (reply.sent === true) return
|
|
if (err != null) {
|
|
reply[kReplyIsError] = true
|
|
reply.send(err)
|
|
return
|
|
}
|
|
|
|
request[kRequestPayloadStream] = request.raw
|
|
|
|
if (request[kRouteContext].preParsing !== null) {
|
|
preParsingHookRunner(request[kRouteContext].preParsing, request, reply, handleRequest.bind(request.server))
|
|
} else {
|
|
handleRequest.call(request.server, null, request, reply)
|
|
}
|
|
}
|
|
|
|
function buildRouterOptions (options, defaultOptions) {
|
|
const routerOptions = options.routerOptions || Object.create(null)
|
|
|
|
const usedDeprecatedOptions = routerKeys.filter(key => Object.hasOwn(options, key))
|
|
|
|
if (usedDeprecatedOptions.length > 0) {
|
|
FSTDEP022(usedDeprecatedOptions.join(', '))
|
|
}
|
|
|
|
for (const key of routerKeys) {
|
|
if (!Object.hasOwn(routerOptions, key)) {
|
|
routerOptions[key] = options[key] ?? defaultOptions[key]
|
|
}
|
|
}
|
|
|
|
return routerOptions
|
|
}
|
|
|
|
/**
|
|
* Used within the route handler as a `net.Socket.close` event handler.
|
|
* The purpose is to remove a socket from the tracked sockets collection when
|
|
* the socket has naturally timed out.
|
|
*/
|
|
function removeTrackedSocket () {
|
|
this.keepAliveConnections.delete(this.socket)
|
|
}
|
|
|
|
function noop () { }
|
|
|
|
module.exports = { buildRouting, validateBodyLimitOption, buildRouterOptions }
|