You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							517 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
	
	
							517 lines
						
					
					
						
							16 KiB
						
					
					
				'use strict'
 | 
						|
 | 
						|
const FindMyWay = require('find-my-way')
 | 
						|
const Context = require('./context')
 | 
						|
const handleRequest = require('./handleRequest')
 | 
						|
const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks')
 | 
						|
const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
 | 
						|
const { normalizeSchema } = require('./schemas')
 | 
						|
const { parseHeadOnSendHandlers } = require('./headRoute')
 | 
						|
const warning = require('./warnings')
 | 
						|
const { kRequestAcceptVersion } = require('./symbols')
 | 
						|
 | 
						|
const {
 | 
						|
  compileSchemasForValidation,
 | 
						|
  compileSchemasForSerialization
 | 
						|
} = require('./validation')
 | 
						|
 | 
						|
const {
 | 
						|
  FST_ERR_SCH_VALIDATION_BUILD,
 | 
						|
  FST_ERR_SCH_SERIALIZATION_BUILD,
 | 
						|
  FST_ERR_DEFAULT_ROUTE_INVALID_TYPE,
 | 
						|
  FST_ERR_INVALID_URL
 | 
						|
} = require('./errors')
 | 
						|
 | 
						|
const {
 | 
						|
  kRoutePrefix,
 | 
						|
  kLogLevel,
 | 
						|
  kLogSerializers,
 | 
						|
  kHooks,
 | 
						|
  kHooksDeprecatedPreParsing,
 | 
						|
  kSchemaController,
 | 
						|
  kOptions,
 | 
						|
  kContentTypeParser,
 | 
						|
  kReply,
 | 
						|
  kReplySerializerDefault,
 | 
						|
  kReplyIsError,
 | 
						|
  kRequest,
 | 
						|
  kRequestPayloadStream,
 | 
						|
  kDisableRequestLogging,
 | 
						|
  kSchemaErrorFormatter,
 | 
						|
  kErrorHandler
 | 
						|
} = require('./symbols.js')
 | 
						|
 | 
						|
function buildRouting (options) {
 | 
						|
  const { keepAliveConnections } = options
 | 
						|
  const router = FindMyWay(options.config)
 | 
						|
 | 
						|
  let avvio
 | 
						|
  let fourOhFour
 | 
						|
  let requestIdHeader
 | 
						|
  let querystringParser
 | 
						|
  let requestIdLogLabel
 | 
						|
  let logger
 | 
						|
  let hasLogger
 | 
						|
  let setupResponseListeners
 | 
						|
  let throwIfAlreadyStarted
 | 
						|
  let genReqId
 | 
						|
  let disableRequestLogging
 | 
						|
  let ignoreTrailingSlash
 | 
						|
  let return503OnClosing
 | 
						|
  let globalExposeHeadRoutes
 | 
						|
 | 
						|
  let closing = false
 | 
						|
 | 
						|
  return {
 | 
						|
    setup (options, fastifyArgs) {
 | 
						|
      avvio = fastifyArgs.avvio
 | 
						|
      fourOhFour = fastifyArgs.fourOhFour
 | 
						|
      logger = fastifyArgs.logger
 | 
						|
      hasLogger = fastifyArgs.hasLogger
 | 
						|
      setupResponseListeners = fastifyArgs.setupResponseListeners
 | 
						|
      throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
 | 
						|
 | 
						|
      globalExposeHeadRoutes = options.exposeHeadRoutes
 | 
						|
      requestIdHeader = options.requestIdHeader
 | 
						|
      querystringParser = options.querystringParser
 | 
						|
      requestIdLogLabel = options.requestIdLogLabel
 | 
						|
      genReqId = options.genReqId
 | 
						|
      disableRequestLogging = options.disableRequestLogging
 | 
						|
      ignoreTrailingSlash = options.ignoreTrailingSlash
 | 
						|
      return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true
 | 
						|
    },
 | 
						|
    routing: router.lookup.bind(router), // router func to find the right handler to call
 | 
						|
    route, // configure a route in the fastify instance
 | 
						|
    prepareRoute,
 | 
						|
    getDefaultRoute: function () {
 | 
						|
      return router.defaultRoute
 | 
						|
    },
 | 
						|
    setDefaultRoute: function (defaultRoute) {
 | 
						|
      if (typeof defaultRoute !== 'function') {
 | 
						|
        throw new FST_ERR_DEFAULT_ROUTE_INVALID_TYPE()
 | 
						|
      }
 | 
						|
 | 
						|
      router.defaultRoute = defaultRoute
 | 
						|
    },
 | 
						|
    routeHandler,
 | 
						|
    closeRoutes: () => { closing = true },
 | 
						|
    printRoutes: router.prettyPrint.bind(router)
 | 
						|
  }
 | 
						|
 | 
						|
  // Convert shorthand to extended route declaration
 | 
						|
  function prepareRoute (method, url, options, handler) {
 | 
						|
    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 Error(`Options for ${method}:${url} route must be an object`)
 | 
						|
      } else if (options.handler) {
 | 
						|
        if (typeof options.handler === 'function') {
 | 
						|
          throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`)
 | 
						|
        } else {
 | 
						|
          throw new Error(`Handler for ${method}:${url} route must be a function`)
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    options = Object.assign({}, options, {
 | 
						|
      method,
 | 
						|
      url,
 | 
						|
      path: url,
 | 
						|
      handler: handler || (options && options.handler)
 | 
						|
    })
 | 
						|
 | 
						|
    return route.call(this, options)
 | 
						|
  }
 | 
						|
 | 
						|
  // Route management
 | 
						|
  function route (options) {
 | 
						|
    // Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator
 | 
						|
    const opts = { ...options }
 | 
						|
 | 
						|
    throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
 | 
						|
 | 
						|
    if (Array.isArray(opts.method)) {
 | 
						|
      // eslint-disable-next-line no-var
 | 
						|
      for (var i = 0; i < opts.method.length; ++i) {
 | 
						|
        const method = opts.method[i]
 | 
						|
        if (supportedMethods.indexOf(method) === -1) {
 | 
						|
          throw new Error(`${method} method is not supported!`)
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      if (supportedMethods.indexOf(opts.method) === -1) {
 | 
						|
        throw new Error(`${opts.method} method is not supported!`)
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const path = opts.url || opts.path
 | 
						|
 | 
						|
    if (!opts.handler) {
 | 
						|
      throw new Error(`Missing handler function for ${opts.method}:${path} route.`)
 | 
						|
    }
 | 
						|
 | 
						|
    if (opts.errorHandler !== undefined && typeof opts.errorHandler !== 'function') {
 | 
						|
      throw new Error(`Error Handler for ${opts.method}:${path} route, if defined, must be a function`)
 | 
						|
    }
 | 
						|
 | 
						|
    validateBodyLimitOption(opts.bodyLimit)
 | 
						|
 | 
						|
    const prefix = this[kRoutePrefix]
 | 
						|
 | 
						|
    this.after((notHandledErr, done) => {
 | 
						|
      if (path === '/' && prefix.length && opts.method !== 'HEAD') {
 | 
						|
        switch (opts.prefixTrailingSlash) {
 | 
						|
          case 'slash':
 | 
						|
            afterRouteAdded.call(this, { path }, notHandledErr, done)
 | 
						|
            break
 | 
						|
          case 'no-slash':
 | 
						|
            afterRouteAdded.call(this, { path: '' }, notHandledErr, done)
 | 
						|
            break
 | 
						|
          case 'both':
 | 
						|
          default:
 | 
						|
            afterRouteAdded.call(this, { path: '' }, notHandledErr, done)
 | 
						|
            // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
 | 
						|
            if (ignoreTrailingSlash !== true) {
 | 
						|
              afterRouteAdded.call(this, { path, prefixing: true }, notHandledErr, done)
 | 
						|
            }
 | 
						|
        }
 | 
						|
      } else if (path && path[0] === '/' && prefix.endsWith('/')) {
 | 
						|
        // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
 | 
						|
        afterRouteAdded.call(this, { path: path.slice(1) }, notHandledErr, done)
 | 
						|
      } else {
 | 
						|
        afterRouteAdded.call(this, { path }, notHandledErr, done)
 | 
						|
      }
 | 
						|
    })
 | 
						|
 | 
						|
    // chainable api
 | 
						|
    return this
 | 
						|
 | 
						|
    /**
 | 
						|
     * This function sets up a new route, its log serializers, and triggers route hooks.
 | 
						|
     *
 | 
						|
     * @param {object} opts contains route `path` and `prefixing` flag which indicates if this is an auto-prefixed route, e.g. `fastify.register(routes, { prefix: '/foo' })`
 | 
						|
     * @param {*} notHandledErr error object to be passed back to the original invoker
 | 
						|
     * @param {*} done callback
 | 
						|
     */
 | 
						|
    function afterRouteAdded ({ path, prefixing = false }, notHandledErr, done) {
 | 
						|
      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) {
 | 
						|
          try {
 | 
						|
            hook.call(this, opts)
 | 
						|
          } catch (error) {
 | 
						|
            done(error)
 | 
						|
            return
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      const config = {
 | 
						|
        ...opts.config,
 | 
						|
        url,
 | 
						|
        method: opts.method
 | 
						|
      }
 | 
						|
      const constraints = opts.constraints || {}
 | 
						|
      if (opts.version) {
 | 
						|
        warning.emit('FSTDEP008')
 | 
						|
        constraints.version = opts.version
 | 
						|
      }
 | 
						|
 | 
						|
      const context = new Context(
 | 
						|
        opts.schema,
 | 
						|
        opts.handler.bind(this),
 | 
						|
        this[kReply],
 | 
						|
        this[kRequest],
 | 
						|
        this[kContentTypeParser],
 | 
						|
        config,
 | 
						|
        opts.errorHandler || this[kErrorHandler],
 | 
						|
        opts.bodyLimit,
 | 
						|
        opts.logLevel,
 | 
						|
        opts.logSerializers,
 | 
						|
        opts.attachValidation,
 | 
						|
        this[kReplySerializerDefault],
 | 
						|
        opts.schemaErrorFormatter || this[kSchemaErrorFormatter]
 | 
						|
      )
 | 
						|
 | 
						|
      const headRouteExists = router.find('HEAD', url, constraints) != null
 | 
						|
 | 
						|
      try {
 | 
						|
        router.on(opts.method, opts.url, { constraints }, routeHandler, context)
 | 
						|
      } catch (err) {
 | 
						|
        done(err)
 | 
						|
        return
 | 
						|
      }
 | 
						|
 | 
						|
      const { exposeHeadRoute } = opts
 | 
						|
      const hasRouteExposeHeadRouteFlag = exposeHeadRoute != null
 | 
						|
      const shouldExposeHead = hasRouteExposeHeadRouteFlag ? exposeHeadRoute : globalExposeHeadRoutes
 | 
						|
 | 
						|
      if (shouldExposeHead && options.method === 'GET' && !headRouteExists) {
 | 
						|
        const onSendHandlers = parseHeadOnSendHandlers(opts.onSend)
 | 
						|
        prepareRoute.call(this, 'HEAD', path, { ...opts, onSend: onSendHandlers })
 | 
						|
      } else if (headRouteExists && exposeHeadRoute) {
 | 
						|
        warning.emit('FSTDEP007')
 | 
						|
      }
 | 
						|
 | 
						|
      // It can happen that a user registers a plugin with some hooks *after*
 | 
						|
      // the route registration. To be sure to also load those hooks,
 | 
						|
      // we must listen for the avvio's preReady event, and update the context object accordingly.
 | 
						|
      avvio.once('preReady', () => {
 | 
						|
        for (const hook of lifecycleHooks) {
 | 
						|
          const toSet = this[kHooks][hook]
 | 
						|
            .concat(opts[hook] || [])
 | 
						|
            .map(h => {
 | 
						|
              const bound = h.bind(this)
 | 
						|
 | 
						|
              // Track hooks deprecation markers
 | 
						|
              if (hook === 'preParsing') {
 | 
						|
                // Check for deprecation syntax
 | 
						|
                if (h.length === (h.constructor.name === 'AsyncFunction' ? 2 : 3)) {
 | 
						|
                  warning.emit('FSTDEP004')
 | 
						|
                  bound[kHooksDeprecatedPreParsing] = true
 | 
						|
                }
 | 
						|
              }
 | 
						|
 | 
						|
              return bound
 | 
						|
            })
 | 
						|
          context[hook] = toSet.length ? toSet : null
 | 
						|
        }
 | 
						|
 | 
						|
        // 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 {
 | 
						|
            compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler)
 | 
						|
          } 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)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // HTTP request entry point, the routing has already been executed
 | 
						|
  function routeHandler (req, res, params, context) {
 | 
						|
    if (closing === true) {
 | 
						|
      /* istanbul ignore next mac, windows */
 | 
						|
      if (req.httpVersionMajor !== 2) {
 | 
						|
        res.once('finish', () => req.destroy())
 | 
						|
        res.setHeader('Connection', 'close')
 | 
						|
      }
 | 
						|
 | 
						|
      if (return503OnClosing) {
 | 
						|
        const headers = {
 | 
						|
          'Content-Type': 'application/json',
 | 
						|
          'Content-Length': '80'
 | 
						|
        }
 | 
						|
        res.writeHead(503, headers)
 | 
						|
        res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
 | 
						|
        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 id = req.headers[requestIdHeader] || genReqId(req)
 | 
						|
 | 
						|
    const loggerBinding = {
 | 
						|
      [requestIdLogLabel]: id
 | 
						|
    }
 | 
						|
 | 
						|
    const loggerOpts = {
 | 
						|
      level: context.logLevel
 | 
						|
    }
 | 
						|
 | 
						|
    if (context.logSerializers) {
 | 
						|
      loggerOpts.serializers = context.logSerializers
 | 
						|
    }
 | 
						|
    const childLogger = logger.child(loggerBinding, loggerOpts)
 | 
						|
    childLogger[kDisableRequestLogging] = disableRequestLogging
 | 
						|
 | 
						|
    const queryPrefix = req.url.indexOf('?')
 | 
						|
    const query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
 | 
						|
    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) {
 | 
						|
      hookRunner(
 | 
						|
        context.onRequest,
 | 
						|
        hookIterator,
 | 
						|
        request,
 | 
						|
        reply,
 | 
						|
        runPreParsing
 | 
						|
      )
 | 
						|
    } else {
 | 
						|
      runPreParsing(null, request, reply)
 | 
						|
    }
 | 
						|
 | 
						|
    if (context.onTimeout !== null) {
 | 
						|
      if (!request.raw.socket._meta) {
 | 
						|
        request.raw.socket.on('timeout', handleTimeout)
 | 
						|
      }
 | 
						|
      request.raw.socket._meta = { context, request, reply }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function handleTimeout () {
 | 
						|
  const { context, request, reply } = this._meta
 | 
						|
  hookRunner(
 | 
						|
    context.onTimeout,
 | 
						|
    hookIterator,
 | 
						|
    request,
 | 
						|
    reply,
 | 
						|
    noop
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
function validateBodyLimitOption (bodyLimit) {
 | 
						|
  if (bodyLimit === undefined) return
 | 
						|
  if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
 | 
						|
    throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function runPreParsing (err, request, reply) {
 | 
						|
  if (reply.sent === true) return
 | 
						|
  if (err != null) {
 | 
						|
    reply.send(err)
 | 
						|
    return
 | 
						|
  }
 | 
						|
 | 
						|
  request[kRequestPayloadStream] = request.raw
 | 
						|
 | 
						|
  if (reply.context.preParsing !== null) {
 | 
						|
    preParsingHookRunner(reply.context.preParsing, request, reply, handleRequest)
 | 
						|
  } else {
 | 
						|
    handleRequest(null, request, reply)
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function preParsingHookRunner (functions, request, reply, cb) {
 | 
						|
  let i = 0
 | 
						|
 | 
						|
  function next (err, stream) {
 | 
						|
    if (reply.sent) {
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    if (typeof stream !== 'undefined') {
 | 
						|
      request[kRequestPayloadStream] = stream
 | 
						|
    }
 | 
						|
 | 
						|
    if (err || i === functions.length) {
 | 
						|
      if (err && !(err instanceof Error)) {
 | 
						|
        reply[kReplyIsError] = true
 | 
						|
      }
 | 
						|
 | 
						|
      cb(err, request, reply)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    const fn = functions[i++]
 | 
						|
    let result
 | 
						|
    try {
 | 
						|
      if (fn[kHooksDeprecatedPreParsing]) {
 | 
						|
        result = fn(request, reply, next)
 | 
						|
      } else {
 | 
						|
        result = fn(request, reply, request[kRequestPayloadStream], next)
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      next(error)
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    if (result && typeof result.then === 'function') {
 | 
						|
      result.then(handleResolve, handleReject)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  function handleResolve (stream) {
 | 
						|
    next(null, stream)
 | 
						|
  }
 | 
						|
 | 
						|
  function handleReject (err) {
 | 
						|
    next(err)
 | 
						|
  }
 | 
						|
 | 
						|
  next(null, request[kRequestPayloadStream])
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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 }
 |