UNPKG

15.6 kBJavaScriptView Raw
1'use strict'
2
3const Avvio = require('avvio')
4const http = require('http')
5const querystring = require('querystring')
6let lightMyRequest
7
8const {
9 kChildren,
10 kBodyLimit,
11 kRoutePrefix,
12 kLogLevel,
13 kHooks,
14 kSchemas,
15 kSchemaCompiler,
16 kReplySerializerDefault,
17 kContentTypeParser,
18 kReply,
19 kRequest,
20 kMiddlewares,
21 kFourOhFour,
22 kState,
23 kOptions,
24 kGlobalHooks
25} = require('./lib/symbols.js')
26
27const { createServer } = require('./lib/server')
28const Reply = require('./lib/reply')
29const Request = require('./lib/request')
30const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
31const decorator = require('./lib/decorate')
32const ContentTypeParser = require('./lib/contentTypeParser')
33const { Hooks, buildHooks } = require('./lib/hooks')
34const { Schemas, buildSchemas } = require('./lib/schemas')
35const { createLogger } = require('./lib/logger')
36const pluginUtils = require('./lib/pluginUtils')
37const reqIdGenFactory = require('./lib/reqIdGenFactory')
38const { buildRouting, validateBodyLimitOption } = require('./lib/route')
39const build404 = require('./lib/fourOhFour')
40const getSecuredInitialConfig = require('./lib/initialConfigValidation')
41const { defaultInitOptions } = getSecuredInitialConfig
42
43function build (options) {
44 // Options validations
45 options = options || {}
46
47 if (typeof options !== 'object') {
48 throw new TypeError('Options must be an object')
49 }
50
51 if (options.querystringParser && typeof options.querystringParser !== 'function') {
52 throw new Error(`querystringParser option should be a function, instead got '${typeof options.querystringParser}'`)
53 }
54
55 validateBodyLimitOption(options.bodyLimit)
56
57 if (options.logger && options.logger.genReqId) {
58 process.emitWarning("Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id")
59 options.genReqId = options.logger.genReqId
60 }
61
62 const modifyCoreObjects = options.modifyCoreObjects !== false
63 const requestIdHeader = options.requestIdHeader || defaultInitOptions.requestIdHeader
64 const querystringParser = options.querystringParser || querystring.parse
65 const genReqId = options.genReqId || reqIdGenFactory()
66 const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
67 const bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
68 const disableRequestLogging = options.disableRequestLogging || false
69
70 // Instance Fastify components
71 const { logger, hasLogger } = createLogger(options)
72
73 // Update the options with the fixed values
74 options.logger = logger
75 options.modifyCoreObjects = modifyCoreObjects
76 options.genReqId = genReqId
77 options.requestIdHeader = requestIdHeader
78 options.querystringParser = querystringParser
79 options.requestIdLogLabel = requestIdLogLabel
80 options.modifyCoreObjects = modifyCoreObjects
81 options.disableRequestLogging = disableRequestLogging
82
83 // Default router
84 const router = buildRouting({
85 config: {
86 defaultRoute: defaultRoute,
87 ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash,
88 maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength,
89 caseSensitive: options.caseSensitive,
90 versioning: options.versioning
91 }
92 })
93 // 404 router, used for handling encapsulated 404 handlers
94 const fourOhFour = build404(options)
95
96 // HTTP server and its handler
97 const httpHandler = router.routing
98 const { server, listen } = createServer(options, httpHandler)
99 if (Number(process.version.match(/v(\d+)/)[1]) >= 6) {
100 server.on('clientError', handleClientError)
101 }
102
103 const setupResponseListeners = Reply.setupResponseListeners
104 const schemas = new Schemas()
105
106 // Public API
107 const fastify = {
108 // Fastify internals
109 [kState]: {
110 listening: false,
111 closing: false,
112 started: false
113 },
114 [kOptions]: options,
115 [kChildren]: [],
116 [kBodyLimit]: bodyLimit,
117 [kRoutePrefix]: '',
118 [kLogLevel]: '',
119 [kHooks]: new Hooks(),
120 [kSchemas]: schemas,
121 [kSchemaCompiler]: null,
122 [kReplySerializerDefault]: null,
123 [kContentTypeParser]: new ContentTypeParser(bodyLimit, (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning)),
124 [kReply]: Reply.buildReply(Reply),
125 [kRequest]: Request.buildRequest(Request),
126 [kMiddlewares]: [],
127 [kFourOhFour]: fourOhFour,
128 [kGlobalHooks]: {
129 onRoute: [],
130 onRegister: []
131 },
132 [pluginUtils.registeredPlugins]: [],
133 // routes shorthand methods
134 delete: function _delete (url, opts, handler) {
135 return router.prepareRoute.call(this, 'DELETE', url, opts, handler)
136 },
137 get: function _get (url, opts, handler) {
138 return router.prepareRoute.call(this, 'GET', url, opts, handler)
139 },
140 head: function _head (url, opts, handler) {
141 return router.prepareRoute.call(this, 'HEAD', url, opts, handler)
142 },
143 patch: function _patch (url, opts, handler) {
144 return router.prepareRoute.call(this, 'PATCH', url, opts, handler)
145 },
146 post: function _post (url, opts, handler) {
147 return router.prepareRoute.call(this, 'POST', url, opts, handler)
148 },
149 put: function _put (url, opts, handler) {
150 return router.prepareRoute.call(this, 'PUT', url, opts, handler)
151 },
152 options: function _options (url, opts, handler) {
153 return router.prepareRoute.call(this, 'OPTIONS', url, opts, handler)
154 },
155 all: function _all (url, opts, handler) {
156 return router.prepareRoute.call(this, supportedMethods, url, opts, handler)
157 },
158 // extended route
159 route: function _route (opts) {
160 // we need the fastify object that we are producing so we apply a lazy loading of the function,
161 // otherwise we should bind it after the declaration
162 return router.route.call(this, opts)
163 },
164 // expose logger instance
165 log: logger,
166 // hooks
167 addHook: addHook,
168 // schemas
169 addSchema: addSchema,
170 getSchemas: schemas.getSchemas.bind(schemas),
171 setSchemaCompiler: setSchemaCompiler,
172 setReplySerializer: setReplySerializer,
173 // custom parsers
174 addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser,
175 hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser,
176 // Fastify architecture methods (initialized by Avvio)
177 register: null,
178 after: null,
179 ready: null,
180 onClose: null,
181 close: null,
182 // http server
183 listen: listen,
184 server: server,
185 // extend fastify objects
186 decorate: decorator.add,
187 hasDecorator: decorator.exist,
188 decorateReply: decorator.decorateReply,
189 decorateRequest: decorator.decorateRequest,
190 hasRequestDecorator: decorator.existRequest,
191 hasReplyDecorator: decorator.existReply,
192 // middleware support
193 use: use,
194 // fake http injection
195 inject: inject,
196 // pretty print of the registered routes
197 printRoutes: router.printRoutes,
198 // custom error handling
199 setNotFoundHandler: setNotFoundHandler,
200 setErrorHandler: setErrorHandler,
201 // Set fastify initial configuration options read-only object
202 initialConfig: getSecuredInitialConfig(options)
203 }
204
205 Object.defineProperty(fastify, 'schemaCompiler', {
206 get: function () {
207 return this[kSchemaCompiler]
208 },
209 set: function (schemaCompiler) {
210 this.setSchemaCompiler(schemaCompiler)
211 }
212 })
213
214 Object.defineProperty(fastify, 'prefix', {
215 get: function () {
216 return this[kRoutePrefix]
217 }
218 })
219
220 Object.defineProperty(fastify, 'basePath', {
221 get: function () {
222 process.emitWarning('basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix')
223 return this[kRoutePrefix]
224 }
225 })
226
227 // Install and configure Avvio
228 // Avvio will update the following Fastify methods:
229 // - register
230 // - after
231 // - ready
232 // - onClose
233 // - close
234 const avvio = Avvio(fastify, {
235 autostart: false,
236 timeout: Number(options.pluginTimeout) || defaultInitOptions.pluginTimeout,
237 expose: { use: 'register' }
238 })
239 // Override to allow the plugin incapsulation
240 avvio.override = override
241 avvio.on('start', () => (fastify[kState].started = true))
242 // cache the closing value, since we are checking it in an hot path
243 avvio.once('preReady', () => {
244 fastify.onClose((instance, done) => {
245 fastify[kState].closing = true
246 router.closeRoutes()
247 if (fastify[kState].listening) {
248 // No new TCP connections are accepted
249 instance.server.close(done)
250 } else {
251 done(null)
252 }
253 })
254 })
255
256 // Set the default 404 handler
257 fastify.setNotFoundHandler()
258 fourOhFour.arrange404(fastify)
259
260 router.setup(options, {
261 avvio,
262 fourOhFour,
263 logger,
264 hasLogger,
265 setupResponseListeners,
266 throwIfAlreadyStarted
267 })
268
269 return fastify
270
271 function throwIfAlreadyStarted (msg) {
272 if (fastify[kState].started) throw new Error(msg)
273 }
274
275 // HTTP injection handling
276 // If the server is not ready yet, this
277 // utility will automatically force it.
278 function inject (opts, cb) {
279 // lightMyRequest is dynamically laoded as it seems very expensive
280 // because of Ajv
281 if (lightMyRequest === undefined) {
282 lightMyRequest = require('light-my-request')
283 }
284
285 if (fastify[kState].started) {
286 if (fastify[kState].closing) {
287 // Force to return an error
288 const error = new Error('Server is closed')
289 if (cb) {
290 cb(error)
291 return
292 } else {
293 return Promise.reject(error)
294 }
295 }
296 return lightMyRequest(httpHandler, opts, cb)
297 }
298
299 if (cb) {
300 this.ready(err => {
301 if (err) cb(err, null)
302 else lightMyRequest(httpHandler, opts, cb)
303 })
304 } else {
305 return this.ready()
306 .then(() => lightMyRequest(httpHandler, opts))
307 }
308 }
309
310 // wrapper tha we expose to the user for middlewares handling
311 function use (url, fn) {
312 throwIfAlreadyStarted('Cannot call "use" when fastify instance is already started!')
313 if (typeof url === 'string') {
314 const prefix = this[kRoutePrefix]
315 url = prefix + (url === '/' && prefix.length > 0 ? '' : url)
316 }
317 return this.after((err, done) => {
318 addMiddleware.call(this, [url, fn])
319 done(err)
320 })
321
322 function addMiddleware (middleware) {
323 this[kMiddlewares].push(middleware)
324 this[kChildren].forEach(child => addMiddleware.call(child, middleware))
325 }
326 }
327
328 // wrapper that we expose to the user for hooks handling
329 function addHook (name, fn) {
330 throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
331
332 // TODO: v3 instead of log a warning, throw an error
333 if (name === 'onSend' || name === 'preSerialization') {
334 if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) {
335 fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
336 }
337 } else {
338 if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
339 fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
340 }
341 }
342
343 if (name === 'onClose') {
344 this[kHooks].validate(name, fn)
345 this.onClose(fn)
346 } else if (name === 'onRoute') {
347 this[kHooks].validate(name, fn)
348 this[kGlobalHooks].onRoute.push(fn)
349 } else if (name === 'onRegister') {
350 this[kHooks].validate(name, fn)
351 this[kGlobalHooks].onRegister.push(fn)
352 } else {
353 this.after((err, done) => {
354 _addHook.call(this, name, fn)
355 done(err)
356 })
357 }
358 return this
359
360 function _addHook (name, fn) {
361 this[kHooks].add(name, fn.bind(this))
362 this[kChildren].forEach(child => _addHook.call(child, name, fn))
363 }
364 }
365
366 // wrapper that we expose to the user for schemas handling
367 function addSchema (schema) {
368 throwIfAlreadyStarted('Cannot call "addSchema" when fastify instance is already started!')
369 this[kSchemas].add(schema)
370 this[kChildren].forEach(child => child.addSchema(schema))
371 return this
372 }
373
374 function handleClientError (err, socket) {
375 const body = JSON.stringify({
376 error: http.STATUS_CODES['400'],
377 message: 'Client Error',
378 statusCode: 400
379 })
380 logger.debug({ err }, 'client error')
381 socket.end(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`)
382 }
383
384 // If the router does not match any route, every request will land here
385 // req and res are Node.js core objects
386 function defaultRoute (req, res) {
387 if (req.headers['accept-version'] !== undefined) {
388 req.headers['accept-version'] = undefined
389 }
390 fourOhFour.router.lookup(req, res)
391 }
392
393 function setNotFoundHandler (opts, handler) {
394 throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!')
395
396 fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, router.routeHandler)
397 }
398
399 // wrapper that we expose to the user for schemas compiler handling
400 function setSchemaCompiler (schemaCompiler) {
401 throwIfAlreadyStarted('Cannot call "setSchemaCompiler" when fastify instance is already started!')
402
403 this[kSchemaCompiler] = schemaCompiler
404 return this
405 }
406
407 function setReplySerializer (replySerializer) {
408 throwIfAlreadyStarted('Cannot call "setReplySerializer" when fastify instance is already started!')
409
410 this[kReplySerializerDefault] = replySerializer
411 return this
412 }
413
414 // wrapper that we expose to the user for configure the custom error handler
415 function setErrorHandler (func) {
416 throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!')
417
418 this._errorHandler = func
419 return this
420 }
421}
422
423// Function that runs the encapsulation magic.
424// Everything that need to be encapsulated must be handled in this function.
425function override (old, fn, opts) {
426 const shouldSkipOverride = pluginUtils.registerPlugin.call(old, fn)
427 if (shouldSkipOverride) {
428 return old
429 }
430
431 const instance = Object.create(old)
432 old[kChildren].push(instance)
433 instance[kChildren] = []
434 instance[kReply] = Reply.buildReply(instance[kReply])
435 instance[kRequest] = Request.buildRequest(instance[kRequest])
436 instance[kContentTypeParser] = ContentTypeParser.helpers.buildContentTypeParser(instance[kContentTypeParser])
437 instance[kHooks] = buildHooks(instance[kHooks])
438 instance[kRoutePrefix] = buildRoutePrefix(instance[kRoutePrefix], opts.prefix)
439 instance[kLogLevel] = opts.logLevel || instance[kLogLevel]
440 instance[kMiddlewares] = old[kMiddlewares].slice()
441 instance[kSchemas] = buildSchemas(old[kSchemas])
442 instance.getSchemas = instance[kSchemas].getSchemas.bind(instance[kSchemas])
443 instance[pluginUtils.registeredPlugins] = Object.create(instance[pluginUtils.registeredPlugins])
444
445 if (opts.prefix) {
446 instance[kFourOhFour].arrange404(instance)
447 }
448
449 for (const hook of instance[kGlobalHooks].onRegister) hook.call(this, instance)
450
451 return instance
452}
453
454function buildRoutePrefix (instancePrefix, pluginPrefix) {
455 if (!pluginPrefix) {
456 return instancePrefix
457 }
458
459 // Ensure that there is a '/' between the prefixes
460 if (instancePrefix.endsWith('/')) {
461 if (pluginPrefix[0] === '/') {
462 // Remove the extra '/' to avoid: '/first//second'
463 pluginPrefix = pluginPrefix.slice(1)
464 }
465 } else if (pluginPrefix[0] !== '/') {
466 pluginPrefix = '/' + pluginPrefix
467 }
468
469 return instancePrefix + pluginPrefix
470}
471
472module.exports = build