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 }
 |