UNPKG

20.6 kBJavaScriptView Raw
1'use strict'
2
3const Avvio = require('avvio')
4const http = require('http')
5const querystring = require('querystring')
6let lightMyRequest
7let version
8let versionLoaded = false
9
10const {
11 kAvvioBoot,
12 kChildren,
13 kBodyLimit,
14 kRoutePrefix,
15 kLogLevel,
16 kLogSerializers,
17 kHooks,
18 kSchemas,
19 kValidatorCompiler,
20 kSerializerCompiler,
21 kReplySerializerDefault,
22 kContentTypeParser,
23 kReply,
24 kRequest,
25 kFourOhFour,
26 kState,
27 kOptions,
28 kPluginNameChain,
29 kSchemaErrorFormatter,
30 kErrorHandler
31} = require('./lib/symbols.js')
32
33const { createServer } = require('./lib/server')
34const Reply = require('./lib/reply')
35const Request = require('./lib/request')
36const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
37const decorator = require('./lib/decorate')
38const ContentTypeParser = require('./lib/contentTypeParser')
39const { Hooks, hookRunnerApplication } = require('./lib/hooks')
40const { Schemas } = require('./lib/schemas')
41const { createLogger } = require('./lib/logger')
42const pluginUtils = require('./lib/pluginUtils')
43const reqIdGenFactory = require('./lib/reqIdGenFactory')
44const { buildRouting, validateBodyLimitOption } = require('./lib/route')
45const build404 = require('./lib/fourOhFour')
46const getSecuredInitialConfig = require('./lib/initialConfigValidation')
47const override = require('./lib/pluginOverride')
48const { defaultInitOptions } = getSecuredInitialConfig
49
50const {
51 FST_ERR_BAD_URL,
52 FST_ERR_MISSING_MIDDLEWARE
53} = require('./lib/errors')
54
55const onBadUrlContext = {
56 config: {
57 },
58 onSend: [],
59 onError: []
60}
61
62function defaultErrorHandler (error, request, reply) {
63 if (reply.statusCode >= 500) {
64 reply.log.error(
65 { req: request, res: reply, err: error },
66 error && error.message
67 )
68 } else if (reply.statusCode >= 400) {
69 reply.log.info(
70 { res: reply, err: error },
71 error && error.message
72 )
73 }
74 reply.send(error)
75}
76
77function fastify (options) {
78 // Options validations
79 options = options || {}
80
81 if (typeof options !== 'object') {
82 throw new TypeError('Options must be an object')
83 }
84
85 if (options.querystringParser && typeof options.querystringParser !== 'function') {
86 throw new Error(`querystringParser option should be a function, instead got '${typeof options.querystringParser}'`)
87 }
88
89 validateBodyLimitOption(options.bodyLimit)
90
91 const requestIdHeader = options.requestIdHeader || defaultInitOptions.requestIdHeader
92 const querystringParser = options.querystringParser || querystring.parse
93 const genReqId = options.genReqId || reqIdGenFactory()
94 const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
95 const bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
96 const disableRequestLogging = options.disableRequestLogging || false
97
98 if (options.schemaErrorFormatter) {
99 validateSchemaErrorFormatter(options.schemaErrorFormatter)
100 }
101
102 const ajvOptions = Object.assign({
103 customOptions: {},
104 plugins: []
105 }, options.ajv)
106 const frameworkErrors = options.frameworkErrors
107
108 // Ajv options
109 if (!ajvOptions.customOptions || Object.prototype.toString.call(ajvOptions.customOptions) !== '[object Object]') {
110 throw new Error(`ajv.customOptions option should be an object, instead got '${typeof ajvOptions.customOptions}'`)
111 }
112 if (!ajvOptions.plugins || !Array.isArray(ajvOptions.plugins)) {
113 throw new Error(`ajv.plugins option should be an array, instead got '${typeof ajvOptions.plugins}'`)
114 }
115
116 ajvOptions.plugins = ajvOptions.plugins.map(plugin => {
117 return Array.isArray(plugin) ? plugin : [plugin]
118 })
119
120 // Instance Fastify components
121 const { logger, hasLogger } = createLogger(options)
122
123 // Update the options with the fixed values
124 options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout
125 options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout
126 options.logger = logger
127 options.genReqId = genReqId
128 options.requestIdHeader = requestIdHeader
129 options.querystringParser = querystringParser
130 options.requestIdLogLabel = requestIdLogLabel
131 options.disableRequestLogging = disableRequestLogging
132 options.ajv = ajvOptions
133 options.clientErrorHandler = options.clientErrorHandler || defaultClientErrorHandler
134
135 const initialConfig = getSecuredInitialConfig(options)
136
137 // Default router
138 const router = buildRouting({
139 config: {
140 defaultRoute: defaultRoute,
141 onBadUrl: onBadUrl,
142 ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash,
143 maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength,
144 caseSensitive: options.caseSensitive,
145 versioning: options.versioning
146 }
147 })
148
149 // 404 router, used for handling encapsulated 404 handlers
150 const fourOhFour = build404(options)
151
152 // HTTP server and its handler
153 const httpHandler = wrapRouting(router.routing, options)
154
155 // we need to set this before calling createServer
156 options.http2SessionTimeout = initialConfig.http2SessionTimeout
157 const { server, listen } = createServer(options, httpHandler)
158
159 const setupResponseListeners = Reply.setupResponseListeners
160 const schemas = new Schemas()
161
162 // Public API
163 const fastify = {
164 // Fastify internals
165 [kState]: {
166 listening: false,
167 closing: false,
168 started: false
169 },
170 [kOptions]: options,
171 [kChildren]: [],
172 [kBodyLimit]: bodyLimit,
173 [kRoutePrefix]: '',
174 [kLogLevel]: '',
175 [kLogSerializers]: null,
176 [kHooks]: new Hooks(),
177 [kSchemas]: schemas,
178 [kValidatorCompiler]: null,
179 [kSchemaErrorFormatter]: options.schemaErrorFormatter,
180 [kErrorHandler]: defaultErrorHandler,
181 [kSerializerCompiler]: null,
182 [kReplySerializerDefault]: null,
183 [kContentTypeParser]: new ContentTypeParser(
184 bodyLimit,
185 (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning),
186 (options.onConstructorPoisoning || defaultInitOptions.onConstructorPoisoning)
187 ),
188 [kReply]: Reply.buildReply(Reply),
189 [kRequest]: Request.buildRequest(Request, options.trustProxy),
190 [kFourOhFour]: fourOhFour,
191 [pluginUtils.registeredPlugins]: [],
192 [kPluginNameChain]: [],
193 [kAvvioBoot]: null,
194 // routes shorthand methods
195 delete: function _delete (url, opts, handler) {
196 return router.prepareRoute.call(this, 'DELETE', url, opts, handler)
197 },
198 get: function _get (url, opts, handler) {
199 return router.prepareRoute.call(this, 'GET', url, opts, handler)
200 },
201 head: function _head (url, opts, handler) {
202 return router.prepareRoute.call(this, 'HEAD', url, opts, handler)
203 },
204 patch: function _patch (url, opts, handler) {
205 return router.prepareRoute.call(this, 'PATCH', url, opts, handler)
206 },
207 post: function _post (url, opts, handler) {
208 return router.prepareRoute.call(this, 'POST', url, opts, handler)
209 },
210 put: function _put (url, opts, handler) {
211 return router.prepareRoute.call(this, 'PUT', url, opts, handler)
212 },
213 options: function _options (url, opts, handler) {
214 return router.prepareRoute.call(this, 'OPTIONS', url, opts, handler)
215 },
216 all: function _all (url, opts, handler) {
217 return router.prepareRoute.call(this, supportedMethods, url, opts, handler)
218 },
219 // extended route
220 route: function _route (opts) {
221 // we need the fastify object that we are producing so we apply a lazy loading of the function,
222 // otherwise we should bind it after the declaration
223 return router.route.call(this, opts)
224 },
225 // expose logger instance
226 log: logger,
227 // hooks
228 addHook: addHook,
229 // schemas
230 addSchema: addSchema,
231 getSchema: schemas.getSchema.bind(schemas),
232 getSchemas: schemas.getSchemas.bind(schemas),
233 setValidatorCompiler: setValidatorCompiler,
234 setSerializerCompiler: setSerializerCompiler,
235 setReplySerializer: setReplySerializer,
236 setSchemaErrorFormatter: setSchemaErrorFormatter,
237 // custom parsers
238 addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser,
239 hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser,
240 getDefaultJsonParser: ContentTypeParser.defaultParsers.getDefaultJsonParser,
241 defaultTextParser: ContentTypeParser.defaultParsers.defaultTextParser,
242 // Fastify architecture methods (initialized by Avvio)
243 register: null,
244 after: null,
245 ready: null,
246 onClose: null,
247 close: null,
248 // http server
249 listen: listen,
250 server: server,
251 // extend fastify objects
252 decorate: decorator.add,
253 hasDecorator: decorator.exist,
254 decorateReply: decorator.decorateReply,
255 decorateRequest: decorator.decorateRequest,
256 hasRequestDecorator: decorator.existRequest,
257 hasReplyDecorator: decorator.existReply,
258 // fake http injection
259 inject: inject,
260 // pretty print of the registered routes
261 printRoutes: router.printRoutes,
262 // custom error handling
263 setNotFoundHandler: setNotFoundHandler,
264 setErrorHandler: setErrorHandler,
265 // Set fastify initial configuration options read-only object
266 initialConfig
267 }
268
269 Object.defineProperties(fastify, {
270 pluginName: {
271 get () {
272 if (this[kPluginNameChain].length > 1) {
273 return this[kPluginNameChain].join(' -> ')
274 }
275 return this[kPluginNameChain][0]
276 }
277 },
278 prefix: {
279 get () { return this[kRoutePrefix] }
280 },
281 validatorCompiler: {
282 get () { return this[kValidatorCompiler] }
283 },
284 serializerCompiler: {
285 get () { return this[kSerializerCompiler] }
286 },
287 version: {
288 get () {
289 if (versionLoaded === false) {
290 version = loadVersion()
291 }
292 return version
293 }
294 },
295 errorHandler: {
296 get () {
297 return this[kErrorHandler]
298 }
299 }
300 })
301
302 // We are adding `use` to the fastify prototype so the user
303 // can still access it (and get the expected error), but `decorate`
304 // will not detect it, and allow the user to override it.
305 Object.setPrototypeOf(fastify, { use })
306
307 // Install and configure Avvio
308 // Avvio will update the following Fastify methods:
309 // - register
310 // - after
311 // - ready
312 // - onClose
313 // - close
314 const avvio = Avvio(fastify, {
315 autostart: false,
316 timeout: Number(options.pluginTimeout) || defaultInitOptions.pluginTimeout,
317 expose: {
318 use: 'register'
319 }
320 })
321 // Override to allow the plugin incapsulation
322 avvio.override = override
323 avvio.on('start', () => (fastify[kState].started = true))
324 fastify[kAvvioBoot] = fastify.ready // the avvio ready function
325 fastify.ready = ready // overwrite the avvio ready function
326 // cache the closing value, since we are checking it in an hot path
327 avvio.once('preReady', () => {
328 fastify.onClose((instance, done) => {
329 fastify[kState].closing = true
330 router.closeRoutes()
331 if (fastify[kState].listening) {
332 // No new TCP connections are accepted
333 instance.server.close(done)
334 } else {
335 done(null)
336 }
337 })
338 })
339
340 // Set the default 404 handler
341 fastify.setNotFoundHandler()
342 fourOhFour.arrange404(fastify)
343
344 router.setup(options, {
345 avvio,
346 fourOhFour,
347 logger,
348 hasLogger,
349 setupResponseListeners,
350 throwIfAlreadyStarted
351 })
352
353 // Delay configuring clientError handler so that it can access fastify state.
354 server.on('clientError', options.clientErrorHandler.bind(fastify))
355
356 return fastify
357
358 function throwIfAlreadyStarted (msg) {
359 if (fastify[kState].started) throw new Error(msg)
360 }
361
362 // HTTP injection handling
363 // If the server is not ready yet, this
364 // utility will automatically force it.
365 function inject (opts, cb) {
366 // lightMyRequest is dynamically laoded as it seems very expensive
367 // because of Ajv
368 if (lightMyRequest === undefined) {
369 lightMyRequest = require('light-my-request')
370 }
371
372 if (fastify[kState].started) {
373 if (fastify[kState].closing) {
374 // Force to return an error
375 const error = new Error('Server is closed')
376 if (cb) {
377 cb(error)
378 return
379 } else {
380 return Promise.reject(error)
381 }
382 }
383 return lightMyRequest(httpHandler, opts, cb)
384 }
385
386 if (cb) {
387 this.ready(err => {
388 if (err) cb(err, null)
389 else lightMyRequest(httpHandler, opts, cb)
390 })
391 } else {
392 return lightMyRequest((req, res) => {
393 this.ready(function (err) {
394 if (err) {
395 res.emit('error', err)
396 return
397 }
398 httpHandler(req, res)
399 })
400 }, opts)
401 }
402 }
403
404 function ready (cb) {
405 let resolveReady
406 let rejectReady
407
408 // run the hooks after returning the promise
409 process.nextTick(runHooks)
410
411 if (!cb) {
412 return new Promise(function (resolve, reject) {
413 resolveReady = resolve
414 rejectReady = reject
415 })
416 }
417
418 function runHooks () {
419 // start loading
420 fastify[kAvvioBoot]((err, done) => {
421 if (err || fastify[kState].started) {
422 manageErr(err)
423 } else {
424 hookRunnerApplication('onReady', fastify[kAvvioBoot], fastify, manageErr)
425 }
426 done()
427 })
428 }
429
430 function manageErr (err) {
431 if (cb) {
432 if (err) {
433 cb(err)
434 } else {
435 cb(undefined, fastify)
436 }
437 } else {
438 if (err) {
439 return rejectReady(err)
440 }
441 resolveReady(fastify)
442 }
443 }
444 }
445
446 function use () {
447 throw new FST_ERR_MISSING_MIDDLEWARE()
448 }
449
450 // wrapper that we expose to the user for hooks handling
451 function addHook (name, fn) {
452 throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
453
454 if (name === 'onSend' || name === 'preSerialization' || name === 'onError') {
455 if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) {
456 throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.')
457 }
458 } else if (name === 'onReady') {
459 if (fn.constructor.name === 'AsyncFunction' && fn.length !== 0) {
460 throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.')
461 }
462 } else if (name !== 'preParsing') {
463 if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
464 throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.')
465 }
466 }
467
468 if (name === 'onClose') {
469 this[kHooks].validate(name, fn)
470 this.onClose(fn)
471 } else if (name === 'onReady') {
472 this[kHooks].validate(name, fn)
473 this[kHooks].add(name, fn)
474 } else {
475 this.after((err, done) => {
476 _addHook.call(this, name, fn)
477 done(err)
478 })
479 }
480 return this
481
482 function _addHook (name, fn) {
483 this[kHooks].add(name, fn)
484 this[kChildren].forEach(child => _addHook.call(child, name, fn))
485 }
486 }
487
488 // wrapper that we expose to the user for schemas handling
489 function addSchema (schema) {
490 throwIfAlreadyStarted('Cannot call "addSchema" when fastify instance is already started!')
491 this[kSchemas].add(schema)
492 this[kChildren].forEach(child => child.addSchema(schema))
493 return this
494 }
495
496 function defaultClientErrorHandler (err, socket) {
497 // In case of a connection reset, the socket has been destroyed and there is nothing that needs to be done.
498 // https://github.com/fastify/fastify/issues/2036
499 // https://github.com/nodejs/node/issues/33302
500 if (err.code === 'ECONNRESET') {
501 return
502 }
503
504 const body = JSON.stringify({
505 error: http.STATUS_CODES['400'],
506 message: 'Client Error',
507 statusCode: 400
508 })
509
510 // Most devs do not know what to do with this error.
511 // In the vast majority of cases, it's a network error and/or some
512 // config issue on the the load balancer side.
513 this.log.trace({ err }, 'client error')
514
515 // If the socket is not writable, there is no reason to try to send data.
516 if (socket.writable) {
517 socket.end(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`)
518 }
519 }
520
521 // If the router does not match any route, every request will land here
522 // req and res are Node.js core objects
523 function defaultRoute (req, res) {
524 if (req.headers['accept-version'] !== undefined) {
525 req.headers['accept-version'] = undefined
526 }
527 fourOhFour.router.lookup(req, res)
528 }
529
530 function onBadUrl (path, req, res) {
531 if (frameworkErrors) {
532 var id = genReqId(req)
533 var childLogger = logger.child({ reqId: id })
534
535 childLogger.info({ req }, 'incoming request')
536
537 const request = new Request(id, null, req, null, childLogger, onBadUrlContext)
538 const reply = new Reply(res, request, childLogger)
539 return frameworkErrors(new FST_ERR_BAD_URL(path), request, reply)
540 }
541 const body = `{"error":"Bad Request","message":"'${path}' is not a valid url component","statusCode":400}`
542 res.writeHead(400, {
543 'Content-Type': 'application/json',
544 'Content-Length': body.length
545 })
546 res.end(body)
547 }
548
549 function setNotFoundHandler (opts, handler) {
550 throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!')
551
552 fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, router.routeHandler)
553 return this
554 }
555
556 function setValidatorCompiler (validatorCompiler) {
557 throwIfAlreadyStarted('Cannot call "setValidatorCompiler" when fastify instance is already started!')
558 this[kValidatorCompiler] = validatorCompiler
559 return this
560 }
561
562 function setSchemaErrorFormatter (errorFormatter) {
563 throwIfAlreadyStarted('Cannot call "setSchemaErrorFormatter" when fastify instance is already started!')
564 validateSchemaErrorFormatter(errorFormatter)
565 this[kSchemaErrorFormatter] = errorFormatter
566 return this
567 }
568
569 function setSerializerCompiler (serializerCompiler) {
570 throwIfAlreadyStarted('Cannot call "setSerializerCompiler" when fastify instance is already started!')
571 this[kSerializerCompiler] = serializerCompiler
572 return this
573 }
574
575 function setReplySerializer (replySerializer) {
576 throwIfAlreadyStarted('Cannot call "setReplySerializer" when fastify instance is already started!')
577
578 this[kReplySerializerDefault] = replySerializer
579 return this
580 }
581
582 // wrapper that we expose to the user for configure the custom error handler
583 function setErrorHandler (func) {
584 throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!')
585
586 this[kErrorHandler] = func.bind(this)
587 return this
588 }
589}
590
591function validateSchemaErrorFormatter (schemaErrorFormatter) {
592 if (typeof schemaErrorFormatter !== 'function') {
593 throw new Error(`schemaErrorFormatter option should be a function, instead got ${typeof schemaErrorFormatter}`)
594 } else if (schemaErrorFormatter.constructor.name === 'AsyncFunction') {
595 throw new Error('schemaErrorFormatter option should not be an async function')
596 }
597}
598
599function wrapRouting (httpHandler, { rewriteUrl, logger }) {
600 if (!rewriteUrl) {
601 return httpHandler
602 }
603 return function preRouting (req, res) {
604 const originalUrl = req.url
605 const url = rewriteUrl(req)
606 if (originalUrl !== url) {
607 logger.debug({ originalUrl, url }, 'rewrite url')
608 if (typeof url === 'string') {
609 req.url = url
610 } else {
611 req.destroy(new Error(`Rewrite url for "${req.url}" needs to be of type "string" but received "${typeof url}"`))
612 }
613 }
614 httpHandler(req, res)
615 }
616}
617
618function loadVersion () {
619 versionLoaded = true
620 const fs = require('fs')
621 const path = require('path')
622 try {
623 const pkgPath = path.join(__dirname, 'package.json')
624 fs.accessSync(pkgPath, fs.constants.R_OK)
625 const pkg = JSON.parse(fs.readFileSync(pkgPath))
626 return pkg.name === 'fastify' ? pkg.version : undefined
627 } catch (e) {
628 return undefined
629 }
630}
631
632/**
633 * These export configurations enable JS and TS developers
634 * to consumer fastify in whatever way best suits their needs.
635 * Some examples of supported import syntax includes:
636 * - `const fastify = require('fastify')`
637 * - `const { fastify } = require('fastify')`
638 * - `import * as Fastify from 'fastify'`
639 * - `import { fastify, TSC_definition } from 'fastify'`
640 * - `import fastify from 'fastify'`
641 * - `import fastify, { TSC_definition } from 'fastify'`
642 */
643module.exports = fastify
644module.exports.fastify = fastify
645module.exports.default = fastify