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.

561 lines
18 KiB

'use strict'
const Avvio = require('avvio')
const http = require('http')
const querystring = require('querystring')
let lightMyRequest
const {
kChildren,
kBodyLimit,
kRoutePrefix,
kLogLevel,
kLogSerializers,
kHooks,
kSchemas,
kSchemaCompiler,
kSchemaResolver,
kReplySerializerDefault,
kContentTypeParser,
kReply,
kRequest,
kMiddlewares,
kFourOhFour,
kState,
kOptions,
kGlobalHooks,
kPluginNameChain
} = require('./lib/symbols.js')
const { createServer } = require('./lib/server')
const Reply = require('./lib/reply')
const Request = require('./lib/request')
const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
const decorator = require('./lib/decorate')
const ContentTypeParser = require('./lib/contentTypeParser')
const { Hooks, buildHooks } = require('./lib/hooks')
const { Schemas, buildSchemas } = require('./lib/schemas')
const { createLogger } = require('./lib/logger')
const pluginUtils = require('./lib/pluginUtils')
const reqIdGenFactory = require('./lib/reqIdGenFactory')
const { buildRouting, validateBodyLimitOption } = require('./lib/route')
const build404 = require('./lib/fourOhFour')
const getSecuredInitialConfig = require('./lib/initialConfigValidation')
const { defaultInitOptions } = getSecuredInitialConfig
const {
codes: {
FST_ERR_BAD_URL
}
} = require('./lib/errors')
function build (options) {
// Options validations
options = options || {}
if (typeof options !== 'object') {
throw new TypeError('Options must be an object')
}
if (options.querystringParser && typeof options.querystringParser !== 'function') {
throw new Error(`querystringParser option should be a function, instead got '${typeof options.querystringParser}'`)
}
validateBodyLimitOption(options.bodyLimit)
if (options.logger && options.logger.genReqId) {
process.emitWarning("Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id")
options.genReqId = options.logger.genReqId
}
const modifyCoreObjects = options.modifyCoreObjects !== false
const requestIdHeader = options.requestIdHeader || defaultInitOptions.requestIdHeader
const querystringParser = options.querystringParser || querystring.parse
const genReqId = options.genReqId || reqIdGenFactory()
const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
const bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
const disableRequestLogging = options.disableRequestLogging || false
const ajvOptions = Object.assign({
customOptions: {},
plugins: []
}, options.ajv)
const frameworkErrors = options.frameworkErrors
// Ajv options
if (!ajvOptions.customOptions || Object.prototype.toString.call(ajvOptions.customOptions) !== '[object Object]') {
throw new Error(`ajv.customOptions option should be an object, instead got '${typeof ajvOptions.customOptions}'`)
}
if (!ajvOptions.plugins || !Array.isArray(ajvOptions.plugins)) {
throw new Error(`ajv.plugins option should be an array, instead got '${typeof ajvOptions.customOptions}'`)
}
ajvOptions.plugins = ajvOptions.plugins.map(plugin => {
return Array.isArray(plugin) ? plugin : [plugin]
})
// Instance Fastify components
const { logger, hasLogger } = createLogger(options)
// Update the options with the fixed values
options.logger = logger
options.modifyCoreObjects = modifyCoreObjects
options.genReqId = genReqId
options.requestIdHeader = requestIdHeader
options.querystringParser = querystringParser
options.requestIdLogLabel = requestIdLogLabel
options.modifyCoreObjects = modifyCoreObjects
options.disableRequestLogging = disableRequestLogging
options.ajv = ajvOptions
const initialConfig = getSecuredInitialConfig(options)
// Default router
const router = buildRouting({
config: {
defaultRoute: defaultRoute,
onBadUrl: onBadUrl,
ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash,
maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength,
caseSensitive: options.caseSensitive,
versioning: options.versioning
}
})
// 404 router, used for handling encapsulated 404 handlers
const fourOhFour = build404(options)
// HTTP server and its handler
const httpHandler = router.routing
// we need to set this before calling createServer
options.http2SessionTimeout = initialConfig.http2SessionTimeout
const { server, listen } = createServer(options, httpHandler)
server.on('clientError', handleClientError)
const setupResponseListeners = Reply.setupResponseListeners
const schemas = new Schemas()
// Public API
const fastify = {
// Fastify internals
[kState]: {
listening: false,
closing: false,
started: false
},
[kOptions]: options,
[kChildren]: [],
[kBodyLimit]: bodyLimit,
[kRoutePrefix]: '',
[kLogLevel]: '',
[kLogSerializers]: null,
[kHooks]: new Hooks(),
[kSchemas]: schemas,
[kSchemaCompiler]: null,
[kSchemaResolver]: null,
[kReplySerializerDefault]: null,
[kContentTypeParser]: new ContentTypeParser(
bodyLimit,
(options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning),
(options.onConstructorPoisoning || defaultInitOptions.onConstructorPoisoning)
),
[kReply]: Reply.buildReply(Reply),
[kRequest]: Request.buildRequest(Request),
[kMiddlewares]: [],
[kFourOhFour]: fourOhFour,
[kGlobalHooks]: {
onRoute: [],
onRegister: []
},
[pluginUtils.registeredPlugins]: [],
[kPluginNameChain]: [],
// routes shorthand methods
delete: function _delete (url, opts, handler) {
return router.prepareRoute.call(this, 'DELETE', url, opts, handler)
},
get: function _get (url, opts, handler) {
return router.prepareRoute.call(this, 'GET', url, opts, handler)
},
head: function _head (url, opts, handler) {
return router.prepareRoute.call(this, 'HEAD', url, opts, handler)
},
patch: function _patch (url, opts, handler) {
return router.prepareRoute.call(this, 'PATCH', url, opts, handler)
},
post: function _post (url, opts, handler) {
return router.prepareRoute.call(this, 'POST', url, opts, handler)
},
put: function _put (url, opts, handler) {
return router.prepareRoute.call(this, 'PUT', url, opts, handler)
},
options: function _options (url, opts, handler) {
return router.prepareRoute.call(this, 'OPTIONS', url, opts, handler)
},
all: function _all (url, opts, handler) {
return router.prepareRoute.call(this, supportedMethods, url, opts, handler)
},
// extended route
route: function _route (opts) {
// we need the fastify object that we are producing so we apply a lazy loading of the function,
// otherwise we should bind it after the declaration
return router.route.call(this, opts)
},
// expose logger instance
log: logger,
// hooks
addHook: addHook,
// schemas
addSchema: addSchema,
getSchemas: schemas.getSchemas.bind(schemas),
setSchemaCompiler: setSchemaCompiler,
setSchemaResolver: setSchemaResolver,
setReplySerializer: setReplySerializer,
// custom parsers
addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser,
hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser,
// Fastify architecture methods (initialized by Avvio)
register: null,
after: null,
ready: null,
onClose: null,
close: null,
// http server
listen: listen,
server: server,
// extend fastify objects
decorate: decorator.add,
hasDecorator: decorator.exist,
decorateReply: decorator.decorateReply,
decorateRequest: decorator.decorateRequest,
hasRequestDecorator: decorator.existRequest,
hasReplyDecorator: decorator.existReply,
// middleware support
use: use,
// fake http injection
inject: inject,
// pretty print of the registered routes
printRoutes: router.printRoutes,
// custom error handling
setNotFoundHandler: setNotFoundHandler,
setErrorHandler: setErrorHandler,
// Set fastify initial configuration options read-only object
initialConfig
}
Object.defineProperty(fastify, 'schemaCompiler', {
get: function () {
return this[kSchemaCompiler]
},
set: function (schemaCompiler) {
this.setSchemaCompiler(schemaCompiler)
}
})
Object.defineProperty(fastify, 'prefix', {
get: function () {
return this[kRoutePrefix]
}
})
Object.defineProperty(fastify, 'basePath', {
get: function () {
process.emitWarning('basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix')
return this[kRoutePrefix]
}
})
Object.defineProperty(fastify, 'pluginName', {
get: function () {
if (this[kPluginNameChain].length > 1) {
return this[kPluginNameChain].join(' -> ')
}
return this[kPluginNameChain][0]
}
})
// Install and configure Avvio
// Avvio will update the following Fastify methods:
// - register
// - after
// - ready
// - onClose
// - close
const avvio = Avvio(fastify, {
autostart: false,
timeout: Number(options.pluginTimeout) || defaultInitOptions.pluginTimeout,
expose: { use: 'register' }
})
// Override to allow the plugin incapsulation
avvio.override = override
avvio.on('start', () => (fastify[kState].started = true))
// cache the closing value, since we are checking it in an hot path
avvio.once('preReady', () => {
fastify.onClose((instance, done) => {
fastify[kState].closing = true
router.closeRoutes()
if (fastify[kState].listening) {
// No new TCP connections are accepted
instance.server.close(done)
} else {
done(null)
}
})
})
// Set the default 404 handler
fastify.setNotFoundHandler()
fourOhFour.arrange404(fastify)
router.setup(options, {
avvio,
fourOhFour,
logger,
hasLogger,
setupResponseListeners,
throwIfAlreadyStarted
})
return fastify
function throwIfAlreadyStarted (msg) {
if (fastify[kState].started) throw new Error(msg)
}
// HTTP injection handling
// If the server is not ready yet, this
// utility will automatically force it.
function inject (opts, cb) {
// lightMyRequest is dynamically laoded as it seems very expensive
// because of Ajv
if (lightMyRequest === undefined) {
lightMyRequest = require('light-my-request')
}
if (fastify[kState].started) {
if (fastify[kState].closing) {
// Force to return an error
const error = new Error('Server is closed')
if (cb) {
cb(error)
return
} else {
return Promise.reject(error)
}
}
return lightMyRequest(httpHandler, opts, cb)
}
if (cb) {
this.ready(err => {
if (err) cb(err, null)
else lightMyRequest(httpHandler, opts, cb)
})
} else {
return this.ready()
.then(() => lightMyRequest(httpHandler, opts))
}
}
// wrapper tha we expose to the user for middlewares handling
function use (url, fn) {
throwIfAlreadyStarted('Cannot call "use" when fastify instance is already started!')
if (typeof url === 'string') {
const prefix = this[kRoutePrefix]
url = prefix + (url === '/' && prefix.length > 0 ? '' : url)
}
return this.after((err, done) => {
addMiddleware.call(this, [url, fn])
done(err)
})
function addMiddleware (middleware) {
this[kMiddlewares].push(middleware)
this[kChildren].forEach(child => addMiddleware.call(child, middleware))
}
}
// wrapper that we expose to the user for hooks handling
function addHook (name, fn) {
throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
// TODO: v3 instead of log a warning, throw an error
if (name === 'onSend' || name === 'preSerialization' || name === 'onError') {
if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) {
fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
}
} else {
if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
}
}
if (name === 'onClose') {
this[kHooks].validate(name, fn)
this.onClose(fn)
} else if (name === 'onRoute') {
this[kHooks].validate(name, fn)
this[kGlobalHooks].onRoute.push(fn)
} else if (name === 'onRegister') {
this[kHooks].validate(name, fn)
this[kGlobalHooks].onRegister.push(fn)
} else {
this.after((err, done) => {
_addHook.call(this, name, fn)
done(err)
})
}
return this
function _addHook (name, fn) {
this[kHooks].add(name, fn.bind(this))
this[kChildren].forEach(child => _addHook.call(child, name, fn))
}
}
// wrapper that we expose to the user for schemas handling
function addSchema (schema) {
throwIfAlreadyStarted('Cannot call "addSchema" when fastify instance is already started!')
this[kSchemas].add(schema)
this[kChildren].forEach(child => child.addSchema(schema))
return this
}
function handleClientError (err, socket) {
const body = JSON.stringify({
error: http.STATUS_CODES['400'],
message: 'Client Error',
statusCode: 400
})
// Most devs do not know what to do with this error.
// In the vast majority of cases, it's a network error and/or some
// config issue on the the load balancer side.
logger.trace({ err }, 'client error')
socket.end(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`)
}
// If the router does not match any route, every request will land here
// req and res are Node.js core objects
function defaultRoute (req, res) {
if (req.headers['accept-version'] !== undefined) {
req.headers['accept-version'] = undefined
}
fourOhFour.router.lookup(req, res)
}
function onBadUrl (path, req, res) {
if (frameworkErrors) {
req.id = genReqId(req)
req.originalUrl = req.url
var childLogger = logger.child({ reqId: req.id })
if (modifyCoreObjects) {
req.log = res.log = childLogger
}
childLogger.info({ req }, 'incoming request')
const request = new Request(null, req, null, req.headers, childLogger)
const reply = new Reply(res, { onSend: [], onError: [] }, request, childLogger)
return frameworkErrors(new FST_ERR_BAD_URL(path), request, reply)
}
const body = `{"error":"Bad Request","message":"'${path}' is not a valid url component","statusCode":400}`
res.writeHead(400, {
'Content-Type': 'application/json',
'Content-Length': body.length
})
res.end(body)
}
function setNotFoundHandler (opts, handler) {
throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!')
fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, router.routeHandler)
}
// wrapper that we expose to the user for schemas compiler handling
function setSchemaCompiler (schemaCompiler) {
throwIfAlreadyStarted('Cannot call "setSchemaCompiler" when fastify instance is already started!')
this[kSchemaCompiler] = schemaCompiler
return this
}
function setSchemaResolver (schemaRefResolver) {
throwIfAlreadyStarted('Cannot call "setSchemaResolver" when fastify instance is already started!')
this[kSchemaResolver] = schemaRefResolver
return this
}
function setReplySerializer (replySerializer) {
throwIfAlreadyStarted('Cannot call "setReplySerializer" when fastify instance is already started!')
this[kReplySerializerDefault] = replySerializer
return this
}
// wrapper that we expose to the user for configure the custom error handler
function setErrorHandler (func) {
throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!')
this._errorHandler = func
return this
}
}
// Function that runs the encapsulation magic.
// Everything that need to be encapsulated must be handled in this function.
function override (old, fn, opts) {
const shouldSkipOverride = pluginUtils.registerPlugin.call(old, fn)
if (shouldSkipOverride) {
// after every plugin registration we will enter a new name
old[kPluginNameChain].push(pluginUtils.getDisplayName(fn))
return old
}
const instance = Object.create(old)
old[kChildren].push(instance)
instance[kChildren] = []
instance[kReply] = Reply.buildReply(instance[kReply])
instance[kRequest] = Request.buildRequest(instance[kRequest])
instance[kContentTypeParser] = ContentTypeParser.helpers.buildContentTypeParser(instance[kContentTypeParser])
instance[kHooks] = buildHooks(instance[kHooks])
instance[kRoutePrefix] = buildRoutePrefix(instance[kRoutePrefix], opts.prefix)
instance[kLogLevel] = opts.logLevel || instance[kLogLevel]
instance[kMiddlewares] = old[kMiddlewares].slice()
instance[kSchemas] = buildSchemas(old[kSchemas])
instance.getSchemas = instance[kSchemas].getSchemas.bind(instance[kSchemas])
instance[pluginUtils.registeredPlugins] = Object.create(instance[pluginUtils.registeredPlugins])
instance[kPluginNameChain] = [pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)]
if (instance[kLogSerializers] || opts.logSerializers) {
instance[kLogSerializers] = Object.assign(Object.create(instance[kLogSerializers]), opts.logSerializers)
}
if (opts.prefix) {
instance[kFourOhFour].arrange404(instance)
}
for (const hook of instance[kGlobalHooks].onRegister) hook.call(this, instance, opts)
return instance
}
function buildRoutePrefix (instancePrefix, pluginPrefix) {
if (!pluginPrefix) {
return instancePrefix
}
// Ensure that there is a '/' between the prefixes
if (instancePrefix.endsWith('/')) {
if (pluginPrefix[0] === '/') {
// Remove the extra '/' to avoid: '/first//second'
pluginPrefix = pluginPrefix.slice(1)
}
} else if (pluginPrefix[0] !== '/') {
pluginPrefix = '/' + pluginPrefix
}
return instancePrefix + pluginPrefix
}
module.exports = build