CoolFace
Apppublic

strong-tie/inbound-calls

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
route.js587 linesDownload Raw Back to lib
1'use strict'2 3const FindMyWay = require('find-my-way')4const Context = require('./context')5const handleRequest = require('./handleRequest')6const { onRequestAbortHookRunner, lifecycleHooks, preParsingHookRunner, onTimeoutHookRunner, onRequestHookRunner } = require('./hooks')7const { normalizeSchema } = require('./schemas')8const { parseHeadOnSendHandlers } = require('./headRoute')9 10const {11  compileSchemasForValidation,12  compileSchemasForSerialization13} = require('./validation')14 15const {16  FST_ERR_SCH_VALIDATION_BUILD,17  FST_ERR_SCH_SERIALIZATION_BUILD,18  FST_ERR_DUPLICATED_ROUTE,19  FST_ERR_INVALID_URL,20  FST_ERR_HOOK_INVALID_HANDLER,21  FST_ERR_ROUTE_OPTIONS_NOT_OBJ,22  FST_ERR_ROUTE_DUPLICATED_HANDLER,23  FST_ERR_ROUTE_HANDLER_NOT_FN,24  FST_ERR_ROUTE_MISSING_HANDLER,25  FST_ERR_ROUTE_METHOD_NOT_SUPPORTED,26  FST_ERR_ROUTE_METHOD_INVALID,27  FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED,28  FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT,29  FST_ERR_HOOK_INVALID_ASYNC_HANDLER30} = require('./errors')31 32const {33  kRoutePrefix,34  kSupportedHTTPMethods,35  kLogLevel,36  kLogSerializers,37  kHooks,38  kSchemaController,39  kOptions,40  kReplySerializerDefault,41  kReplyIsError,42  kRequestPayloadStream,43  kDisableRequestLogging,44  kSchemaErrorFormatter,45  kErrorHandler,46  kHasBeenDecorated,47  kRequestAcceptVersion,48  kRouteByFastify,49  kRouteContext50} = require('./symbols.js')51const { buildErrorHandler } = require('./error-handler')52const { createChildLogger } = require('./logger-factory.js')53const { getGenReqId } = require('./reqIdGenFactory.js')54 55function buildRouting (options) {56  const router = FindMyWay(options.config)57 58  let avvio59  let fourOhFour60  let logger61  let hasLogger62  let setupResponseListeners63  let throwIfAlreadyStarted64  let disableRequestLogging65  let ignoreTrailingSlash66  let ignoreDuplicateSlashes67  let return503OnClosing68  let globalExposeHeadRoutes69  let keepAliveConnections70 71  let closing = false72 73  return {74    /**75     * @param {import('../fastify').FastifyServerOptions} options76     * @param {*} fastifyArgs77     */78    setup (options, fastifyArgs) {79      avvio = fastifyArgs.avvio80      fourOhFour = fastifyArgs.fourOhFour81      logger = fastifyArgs.logger82      hasLogger = fastifyArgs.hasLogger83      setupResponseListeners = fastifyArgs.setupResponseListeners84      throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted85 86      globalExposeHeadRoutes = options.exposeHeadRoutes87      disableRequestLogging = options.disableRequestLogging88      ignoreTrailingSlash = options.ignoreTrailingSlash89      ignoreDuplicateSlashes = options.ignoreDuplicateSlashes90      return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true91      keepAliveConnections = fastifyArgs.keepAliveConnections92    },93    routing: router.lookup.bind(router), // router func to find the right handler to call94    route, // configure a route in the fastify instance95    hasRoute,96    prepareRoute,97    routeHandler,98    closeRoutes: () => { closing = true },99    printRoutes: router.prettyPrint.bind(router),100    addConstraintStrategy,101    hasConstraintStrategy,102    isAsyncConstraint,103    findRoute104  }105 106  function addConstraintStrategy (strategy) {107    throwIfAlreadyStarted('Cannot add constraint strategy!')108    return router.addConstraintStrategy(strategy)109  }110 111  function hasConstraintStrategy (strategyName) {112    return router.hasConstraintStrategy(strategyName)113  }114 115  function isAsyncConstraint () {116    return router.constrainer.asyncStrategiesInUse.size > 0117  }118 119  // Convert shorthand to extended route declaration120  function prepareRoute ({ method, url, options, handler, isFastify }) {121    if (typeof url !== 'string') {122      throw new FST_ERR_INVALID_URL(typeof url)123    }124 125    if (!handler && typeof options === 'function') {126      handler = options // for support over direct function calls such as fastify.get() options are reused as the handler127      options = {}128    } else if (handler && typeof handler === 'function') {129      if (Object.prototype.toString.call(options) !== '[object Object]') {130        throw new FST_ERR_ROUTE_OPTIONS_NOT_OBJ(method, url)131      } else if (options.handler) {132        if (typeof options.handler === 'function') {133          throw new FST_ERR_ROUTE_DUPLICATED_HANDLER(method, url)134        } else {135          throw new FST_ERR_ROUTE_HANDLER_NOT_FN(method, url)136        }137      }138    }139 140    options = Object.assign({}, options, {141      method,142      url,143      path: url,144      handler: handler || (options && options.handler)145    })146 147    return route.call(this, { options, isFastify })148  }149 150  function hasRoute ({ options }) {151    const normalizedMethod = options.method?.toUpperCase() ?? ''152    return router.hasRoute(153      normalizedMethod,154      options.url || '',155      options.constraints156    )157  }158 159  function findRoute (options) {160    const route = router.find(161      options.method,162      options.url || '',163      options.constraints164    )165    if (route) {166      // we must reduce the expose surface, otherwise167      // we provide the ability for the user to modify168      // all the route and server information in runtime169      return {170        handler: route.handler,171        params: route.params,172        searchParams: route.searchParams173      }174    } else {175      return null176    }177  }178 179  /**180   * Route management181   * @param {{ options: import('../fastify').RouteOptions, isFastify: boolean }}182   */183  function route ({ options, isFastify }) {184    throwIfAlreadyStarted('Cannot add route!')185 186    // Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator187    const opts = { ...options }188 189    const path = opts.url || opts.path || ''190 191    if (!opts.handler) {192      throw new FST_ERR_ROUTE_MISSING_HANDLER(opts.method, path)193    }194 195    if (opts.errorHandler !== undefined && typeof opts.errorHandler !== 'function') {196      throw new FST_ERR_ROUTE_HANDLER_NOT_FN(opts.method, path)197    }198 199    validateBodyLimitOption(opts.bodyLimit)200 201    const shouldExposeHead = opts.exposeHeadRoute ?? globalExposeHeadRoutes202 203    let isGetRoute = false204    let isHeadRoute = false205 206    if (Array.isArray(opts.method)) {207      for (let i = 0; i < opts.method.length; ++i) {208        opts.method[i] = normalizeAndValidateMethod.call(this, opts.method[i])209        validateSchemaBodyOption.call(this, opts.method[i], path, opts.schema)210 211        isGetRoute = opts.method.includes('GET')212        isHeadRoute = opts.method.includes('HEAD')213      }214    } else {215      opts.method = normalizeAndValidateMethod.call(this, opts.method)216      validateSchemaBodyOption.call(this, opts.method, path, opts.schema)217 218      isGetRoute = opts.method === 'GET'219      isHeadRoute = opts.method === 'HEAD'220    }221 222    // we need to clone a set of initial options for HEAD route223    const headOpts = shouldExposeHead && isGetRoute ? { ...options } : null224 225    const prefix = this[kRoutePrefix]226 227    if (path === '/' && prefix.length > 0 && opts.method !== 'HEAD') {228      switch (opts.prefixTrailingSlash) {229        case 'slash':230          addNewRoute.call(this, { path, isFastify })231          break232        case 'no-slash':233          addNewRoute.call(this, { path: '', isFastify })234          break235        case 'both':236        default:237          addNewRoute.call(this, { path: '', isFastify })238          // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.239          if (ignoreTrailingSlash !== true && (ignoreDuplicateSlashes !== true || !prefix.endsWith('/'))) {240            addNewRoute.call(this, { path, prefixing: true, isFastify })241          }242      }243    } else if (path[0] === '/' && prefix.endsWith('/')) {244      // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'245      addNewRoute.call(this, { path: path.slice(1), isFastify })246    } else {247      addNewRoute.call(this, { path, isFastify })248    }249 250    // chainable api251    return this252 253    function addNewRoute ({ path, prefixing = false, isFastify = false }) {254      const url = prefix + path255 256      opts.url = url257      opts.path = url258      opts.routePath = path259      opts.prefix = prefix260      opts.logLevel = opts.logLevel || this[kLogLevel]261 262      if (this[kLogSerializers] || opts.logSerializers) {263        opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)264      }265 266      if (opts.attachValidation == null) {267        opts.attachValidation = false268      }269 270      if (prefixing === false) {271        // run 'onRoute' hooks272        for (const hook of this[kHooks].onRoute) {273          hook.call(this, opts)274        }275      }276 277      for (const hook of lifecycleHooks) {278        if (opts && hook in opts) {279          if (Array.isArray(opts[hook])) {280            for (const func of opts[hook]) {281              if (typeof func !== 'function') {282                throw new FST_ERR_HOOK_INVALID_HANDLER(hook, Object.prototype.toString.call(func))283              }284 285              if (hook === 'onSend' || hook === 'preSerialization' || hook === 'onError' || hook === 'preParsing') {286                if (func.constructor.name === 'AsyncFunction' && func.length === 4) {287                  throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()288                }289              } else if (hook === 'onRequestAbort') {290                if (func.constructor.name === 'AsyncFunction' && func.length !== 1) {291                  throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()292                }293              } else {294                if (func.constructor.name === 'AsyncFunction' && func.length === 3) {295                  throw new FST_ERR_HOOK_INVALID_ASYNC_HANDLER()296                }297              }298            }299          } else if (opts[hook] !== undefined && typeof opts[hook] !== 'function') {300            throw new FST_ERR_HOOK_INVALID_HANDLER(hook, Object.prototype.toString.call(opts[hook]))301          }302        }303      }304 305      const constraints = opts.constraints || {}306      const config = {307        ...opts.config,308        url,309        method: opts.method310      }311 312      const context = new Context({313        schema: opts.schema,314        handler: opts.handler.bind(this),315        config,316        errorHandler: opts.errorHandler,317        childLoggerFactory: opts.childLoggerFactory,318        bodyLimit: opts.bodyLimit,319        logLevel: opts.logLevel,320        logSerializers: opts.logSerializers,321        attachValidation: opts.attachValidation,322        schemaErrorFormatter: opts.schemaErrorFormatter,323        replySerializer: this[kReplySerializerDefault],324        validatorCompiler: opts.validatorCompiler,325        serializerCompiler: opts.serializerCompiler,326        exposeHeadRoute: shouldExposeHead,327        prefixTrailingSlash: (opts.prefixTrailingSlash || 'both'),328        server: this,329        isFastify330      })331 332      const headHandler = router.findRoute('HEAD', opts.url, constraints)333      const hasHEADHandler = headHandler !== null334 335      try {336        router.on(opts.method, opts.url, { constraints }, routeHandler, context)337      } catch (error) {338        // any route insertion error created by fastify can be safely ignore339        // because it only duplicate route for head340        if (!context[kRouteByFastify]) {341          const isDuplicatedRoute = error.message.includes(`Method '${opts.method}' already declared for route`)342          if (isDuplicatedRoute) {343            throw new FST_ERR_DUPLICATED_ROUTE(opts.method, opts.url)344          }345 346          throw error347        }348      }349 350      this.after((notHandledErr, done) => {351        // Send context async352        context.errorHandler = opts.errorHandler ? buildErrorHandler(this[kErrorHandler], opts.errorHandler) : this[kErrorHandler]353        context._parserOptions.limit = opts.bodyLimit || null354        context.logLevel = opts.logLevel355        context.logSerializers = opts.logSerializers356        context.attachValidation = opts.attachValidation357        context[kReplySerializerDefault] = this[kReplySerializerDefault]358        context.schemaErrorFormatter = opts.schemaErrorFormatter || this[kSchemaErrorFormatter] || context.schemaErrorFormatter359 360        // Run hooks and more361        avvio.once('preReady', () => {362          for (const hook of lifecycleHooks) {363            const toSet = this[kHooks][hook]364              .concat(opts[hook] || [])365              .map(h => h.bind(this))366            context[hook] = toSet.length ? toSet : null367          }368 369          // Optimization: avoid encapsulation if no decoration has been done.370          while (!context.Request[kHasBeenDecorated] && context.Request.parent) {371            context.Request = context.Request.parent372          }373          while (!context.Reply[kHasBeenDecorated] && context.Reply.parent) {374            context.Reply = context.Reply.parent375          }376 377          // Must store the 404 Context in 'preReady' because it is only guaranteed to378          // be available after all of the plugins and routes have been loaded.379          fourOhFour.setContext(this, context)380 381          if (opts.schema) {382            context.schema = normalizeSchema(context.schema, this.initialConfig)383 384            const schemaController = this[kSchemaController]385            if (!opts.validatorCompiler && (opts.schema.body || opts.schema.headers || opts.schema.querystring || opts.schema.params)) {386              schemaController.setupValidator(this[kOptions])387            }388            try {389              const isCustom = typeof opts?.validatorCompiler === 'function' || schemaController.isCustomValidatorCompiler390              compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler, isCustom)391            } catch (error) {392              throw new FST_ERR_SCH_VALIDATION_BUILD(opts.method, url, error.message)393            }394 395            if (opts.schema.response && !opts.serializerCompiler) {396              schemaController.setupSerializer(this[kOptions])397            }398            try {399              compileSchemasForSerialization(context, opts.serializerCompiler || schemaController.serializerCompiler)400            } catch (error) {401              throw new FST_ERR_SCH_SERIALIZATION_BUILD(opts.method, url, error.message)402            }403          }404        })405 406        done(notHandledErr)407      })408 409      // register head route in sync410      // we must place it after the `this.after`411 412      if (shouldExposeHead && isGetRoute && !isHeadRoute && !hasHEADHandler) {413        const onSendHandlers = parseHeadOnSendHandlers(headOpts.onSend)414        prepareRoute.call(this, { method: 'HEAD', url: path, options: { ...headOpts, onSend: onSendHandlers }, isFastify: true })415      }416    }417  }418 419  // HTTP request entry point, the routing has already been executed420  function routeHandler (req, res, params, context, query) {421    const id = getGenReqId(context.server, req)422 423    const loggerOpts = {424      level: context.logLevel425    }426 427    if (context.logSerializers) {428      loggerOpts.serializers = context.logSerializers429    }430    const childLogger = createChildLogger(context, logger, req, id, loggerOpts)431    childLogger[kDisableRequestLogging] = disableRequestLogging432 433    if (closing === true) {434      /* istanbul ignore next mac, windows */435      if (req.httpVersionMajor !== 2) {436        res.setHeader('Connection', 'close')437      }438 439      // TODO remove return503OnClosing after Node v18 goes EOL440      /* istanbul ignore else */441      if (return503OnClosing) {442        // On Node v19 we cannot test this behavior as it won't be necessary443        // anymore. It will close all the idle connections before they reach this444        // stage.445        const headers = {446          'Content-Type': 'application/json',447          'Content-Length': '80'448        }449        res.writeHead(503, headers)450        res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')451        childLogger.info({ res: { statusCode: 503 } }, 'request aborted - refusing to accept new requests as server is closing')452        return453      }454    }455 456    // When server.forceCloseConnections is true, we will collect any requests457    // that have indicated they want persistence so that they can be reaped458    // on server close. Otherwise, the container is a noop container.459    const connHeader = String.prototype.toLowerCase.call(req.headers.connection || '')460    if (connHeader === 'keep-alive') {461      if (keepAliveConnections.has(req.socket) === false) {462        keepAliveConnections.add(req.socket)463        req.socket.on('close', removeTrackedSocket.bind({ keepAliveConnections, socket: req.socket }))464      }465    }466 467    // we revert the changes in defaultRoute468    if (req.headers[kRequestAcceptVersion] !== undefined) {469      req.headers['accept-version'] = req.headers[kRequestAcceptVersion]470      req.headers[kRequestAcceptVersion] = undefined471    }472 473    const request = new context.Request(id, params, req, query, childLogger, context)474    const reply = new context.Reply(res, request, childLogger)475    if (disableRequestLogging === false) {476      childLogger.info({ req: request }, 'incoming request')477    }478 479    if (hasLogger === true || context.onResponse !== null) {480      setupResponseListeners(reply)481    }482 483    if (context.onRequest !== null) {484      onRequestHookRunner(485        context.onRequest,486        request,487        reply,488        runPreParsing489      )490    } else {491      runPreParsing(null, request, reply)492    }493 494    if (context.onRequestAbort !== null) {495      req.on('close', () => {496        /* istanbul ignore else */497        if (req.aborted) {498          onRequestAbortHookRunner(499            context.onRequestAbort,500            request,501            handleOnRequestAbortHooksErrors.bind(null, reply)502          )503        }504      })505    }506 507    if (context.onTimeout !== null) {508      if (!request.raw.socket._meta) {509        request.raw.socket.on('timeout', handleTimeout)510      }511      request.raw.socket._meta = { context, request, reply }512    }513  }514}515 516function handleOnRequestAbortHooksErrors (reply, err) {517  if (err) {518    reply.log.error({ err }, 'onRequestAborted hook failed')519  }520}521 522function handleTimeout () {523  const { context, request, reply } = this._meta524  onTimeoutHookRunner(525    context.onTimeout,526    request,527    reply,528    noop529  )530}531 532function normalizeAndValidateMethod (method) {533  if (typeof method !== 'string') {534    throw new FST_ERR_ROUTE_METHOD_INVALID()535  }536  method = method.toUpperCase()537  if (!this[kSupportedHTTPMethods].bodyless.has(method) &&538    !this[kSupportedHTTPMethods].bodywith.has(method)) {539    throw new FST_ERR_ROUTE_METHOD_NOT_SUPPORTED(method)540  }541 542  return method543}544 545function validateSchemaBodyOption (method, path, schema) {546  if (this[kSupportedHTTPMethods].bodyless.has(method) && schema?.body) {547    throw new FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED(method, path)548  }549}550 551function validateBodyLimitOption (bodyLimit) {552  if (bodyLimit === undefined) return553  if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {554    throw new FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT(bodyLimit)555  }556}557 558function runPreParsing (err, request, reply) {559  if (reply.sent === true) return560  if (err != null) {561    reply[kReplyIsError] = true562    reply.send(err)563    return564  }565 566  request[kRequestPayloadStream] = request.raw567 568  if (request[kRouteContext].preParsing !== null) {569    preParsingHookRunner(request[kRouteContext].preParsing, request, reply, handleRequest.bind(request.server))570  } else {571    handleRequest.call(request.server, null, request, reply)572  }573}574 575/**576 * Used within the route handler as a `net.Socket.close` event handler.577 * The purpose is to remove a socket from the tracked sockets collection when578 * the socket has naturally timed out.579 */580function removeTrackedSocket () {581  this.keepAliveConnections.delete(this.socket)582}583 584function noop () { }585 586module.exports = { buildRouting, validateBodyLimitOption }587