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