Spaces:
Runtime error
Runtime error
| 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 { | |
| 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') | |
| 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.ignoreTrailingSlash | |
| ignoreDuplicateSlashes = options.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) | |
| } | |
| } | |
| /** | |
| * 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 } | |