UNPKG

31.8 kBJavaScriptView Raw
1'use strict'
2
3const VERSION = '4.28.0'
4
5const Avvio = require('avvio')
6const http = require('node:http')
7let lightMyRequest
8
9const {
10 kAvvioBoot,
11 kChildren,
12 kServerBindings,
13 kBodyLimit,
14 kRoutePrefix,
15 kLogLevel,
16 kLogSerializers,
17 kHooks,
18 kSchemaController,
19 kRequestAcceptVersion,
20 kReplySerializerDefault,
21 kContentTypeParser,
22 kReply,
23 kRequest,
24 kFourOhFour,
25 kState,
26 kOptions,
27 kPluginNameChain,
28 kSchemaErrorFormatter,
29 kErrorHandler,
30 kKeepAliveConnections,
31 kChildLoggerFactory,
32 kGenReqId
33} = require('./lib/symbols.js')
34
35const { createServer, compileValidateHTTPVersion } = require('./lib/server')
36const Reply = require('./lib/reply')
37const Request = require('./lib/request')
38const Context = require('./lib/context.js')
39const { supportedMethods } = require('./lib/httpMethods')
40const decorator = require('./lib/decorate')
41const ContentTypeParser = require('./lib/contentTypeParser')
42const SchemaController = require('./lib/schema-controller')
43const { Hooks, hookRunnerApplication, supportedHooks } = require('./lib/hooks')
44const { createLogger, createChildLogger, defaultChildLoggerFactory } = require('./lib/logger')
45const pluginUtils = require('./lib/pluginUtils')
46const { getGenReqId, reqIdGenFactory } = require('./lib/reqIdGenFactory')
47const { buildRouting, validateBodyLimitOption } = require('./lib/route')
48const build404 = require('./lib/fourOhFour')
49const getSecuredInitialConfig = require('./lib/initialConfigValidation')
50const override = require('./lib/pluginOverride')
51const { FSTDEP009 } = require('./lib/warnings')
52const noopSet = require('./lib/noop-set')
53const {
54 appendStackTrace,
55 AVVIO_ERRORS_MAP,
56 ...errorCodes
57} = require('./lib/errors')
58
59const { defaultInitOptions } = getSecuredInitialConfig
60
61const {
62 FST_ERR_ASYNC_CONSTRAINT,
63 FST_ERR_BAD_URL,
64 FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE,
65 FST_ERR_OPTIONS_NOT_OBJ,
66 FST_ERR_QSP_NOT_FN,
67 FST_ERR_SCHEMA_CONTROLLER_BUCKET_OPT_NOT_FN,
68 FST_ERR_AJV_CUSTOM_OPTIONS_OPT_NOT_OBJ,
69 FST_ERR_AJV_CUSTOM_OPTIONS_OPT_NOT_ARR,
70 FST_ERR_VERSION_CONSTRAINT_NOT_STR,
71 FST_ERR_INSTANCE_ALREADY_LISTENING,
72 FST_ERR_REOPENED_CLOSE_SERVER,
73 FST_ERR_ROUTE_REWRITE_NOT_STR,
74 FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN,
75 FST_ERR_ERROR_HANDLER_NOT_FN
76} = errorCodes
77
78const { buildErrorHandler } = require('./lib/error-handler.js')
79
80function defaultBuildPrettyMeta (route) {
81 // return a shallow copy of route's sanitized context
82
83 const cleanKeys = {}
84 const allowedProps = ['errorHandler', 'logLevel', 'logSerializers']
85
86 allowedProps.concat(supportedHooks).forEach(k => {
87 cleanKeys[k] = route.store[k]
88 })
89
90 return Object.assign({}, cleanKeys)
91}
92
93/**
94 * @param {import('./fastify.js').FastifyServerOptions} options
95 */
96function fastify (options) {
97 // Options validations
98 options = options || {}
99
100 if (typeof options !== 'object') {
101 throw new FST_ERR_OPTIONS_NOT_OBJ()
102 }
103
104 if (options.querystringParser && typeof options.querystringParser !== 'function') {
105 throw new FST_ERR_QSP_NOT_FN(typeof options.querystringParser)
106 }
107
108 if (options.schemaController && options.schemaController.bucket && typeof options.schemaController.bucket !== 'function') {
109 throw new FST_ERR_SCHEMA_CONTROLLER_BUCKET_OPT_NOT_FN(typeof options.schemaController.bucket)
110 }
111
112 validateBodyLimitOption(options.bodyLimit)
113
114 const requestIdHeader = (options.requestIdHeader === false) ? false : (options.requestIdHeader || defaultInitOptions.requestIdHeader).toLowerCase()
115 const genReqId = reqIdGenFactory(requestIdHeader, options.genReqId)
116 const requestIdLogLabel = options.requestIdLogLabel || 'reqId'
117 const bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit
118 const disableRequestLogging = options.disableRequestLogging || false
119
120 const ajvOptions = Object.assign({
121 customOptions: {},
122 plugins: []
123 }, options.ajv)
124 const frameworkErrors = options.frameworkErrors
125
126 // Ajv options
127 if (!ajvOptions.customOptions || Object.prototype.toString.call(ajvOptions.customOptions) !== '[object Object]') {
128 throw new FST_ERR_AJV_CUSTOM_OPTIONS_OPT_NOT_OBJ(typeof ajvOptions.customOptions)
129 }
130 if (!ajvOptions.plugins || !Array.isArray(ajvOptions.plugins)) {
131 throw new FST_ERR_AJV_CUSTOM_OPTIONS_OPT_NOT_ARR(typeof ajvOptions.plugins)
132 }
133
134 // Instance Fastify components
135 const { logger, hasLogger } = createLogger(options)
136
137 // Update the options with the fixed values
138 options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout
139 options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout
140 options.maxRequestsPerSocket = options.maxRequestsPerSocket || defaultInitOptions.maxRequestsPerSocket
141 options.requestTimeout = options.requestTimeout || defaultInitOptions.requestTimeout
142 options.logger = logger
143 options.requestIdHeader = requestIdHeader
144 options.requestIdLogLabel = requestIdLogLabel
145 options.disableRequestLogging = disableRequestLogging
146 options.ajv = ajvOptions
147 options.clientErrorHandler = options.clientErrorHandler || defaultClientErrorHandler
148
149 const initialConfig = getSecuredInitialConfig(options)
150
151 // exposeHeadRoutes have its default set from the validator
152 options.exposeHeadRoutes = initialConfig.exposeHeadRoutes
153
154 let constraints = options.constraints
155 if (options.versioning) {
156 FSTDEP009()
157 constraints = {
158 ...constraints,
159 version: {
160 name: 'version',
161 mustMatchWhenDerived: true,
162 storage: options.versioning.storage,
163 deriveConstraint: options.versioning.deriveVersion,
164 validate (value) {
165 if (typeof value !== 'string') {
166 throw new FST_ERR_VERSION_CONSTRAINT_NOT_STR()
167 }
168 }
169 }
170 }
171 }
172
173 // Default router
174 const router = buildRouting({
175 config: {
176 defaultRoute,
177 onBadUrl,
178 constraints,
179 ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash,
180 ignoreDuplicateSlashes: options.ignoreDuplicateSlashes || defaultInitOptions.ignoreDuplicateSlashes,
181 maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength,
182 caseSensitive: options.caseSensitive,
183 allowUnsafeRegex: options.allowUnsafeRegex || defaultInitOptions.allowUnsafeRegex,
184 buildPrettyMeta: defaultBuildPrettyMeta,
185 querystringParser: options.querystringParser,
186 useSemicolonDelimiter: options.useSemicolonDelimiter ?? defaultInitOptions.useSemicolonDelimiter
187 }
188 })
189
190 // 404 router, used for handling encapsulated 404 handlers
191 const fourOhFour = build404(options)
192
193 // HTTP server and its handler
194 const httpHandler = wrapRouting(router, options)
195
196 // we need to set this before calling createServer
197 options.http2SessionTimeout = initialConfig.http2SessionTimeout
198 const { server, listen } = createServer(options, httpHandler)
199
200 const serverHasCloseAllConnections = typeof server.closeAllConnections === 'function'
201 const serverHasCloseIdleConnections = typeof server.closeIdleConnections === 'function'
202
203 let forceCloseConnections = options.forceCloseConnections
204 if (forceCloseConnections === 'idle' && !serverHasCloseIdleConnections) {
205 throw new FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE()
206 } else if (typeof forceCloseConnections !== 'boolean') {
207 /* istanbul ignore next: only one branch can be valid in a given Node.js version */
208 forceCloseConnections = serverHasCloseIdleConnections ? 'idle' : false
209 }
210
211 const keepAliveConnections = !serverHasCloseAllConnections && forceCloseConnections === true ? new Set() : noopSet()
212
213 const setupResponseListeners = Reply.setupResponseListeners
214 const schemaController = SchemaController.buildSchemaController(null, options.schemaController)
215
216 // Public API
217 const fastify = {
218 // Fastify internals
219 [kState]: {
220 listening: false,
221 closing: false,
222 started: false,
223 ready: false,
224 booting: false,
225 readyPromise: null
226 },
227 [kKeepAliveConnections]: keepAliveConnections,
228 [kOptions]: options,
229 [kChildren]: [],
230 [kServerBindings]: [],
231 [kBodyLimit]: bodyLimit,
232 [kRoutePrefix]: '',
233 [kLogLevel]: '',
234 [kLogSerializers]: null,
235 [kHooks]: new Hooks(),
236 [kSchemaController]: schemaController,
237 [kSchemaErrorFormatter]: null,
238 [kErrorHandler]: buildErrorHandler(),
239 [kChildLoggerFactory]: defaultChildLoggerFactory,
240 [kReplySerializerDefault]: null,
241 [kContentTypeParser]: new ContentTypeParser(
242 bodyLimit,
243 (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning),
244 (options.onConstructorPoisoning || defaultInitOptions.onConstructorPoisoning)
245 ),
246 [kReply]: Reply.buildReply(Reply),
247 [kRequest]: Request.buildRequest(Request, options.trustProxy),
248 [kFourOhFour]: fourOhFour,
249 [pluginUtils.kRegisteredPlugins]: [],
250 [kPluginNameChain]: ['fastify'],
251 [kAvvioBoot]: null,
252 [kGenReqId]: genReqId,
253 // routing method
254 routing: httpHandler,
255 getDefaultRoute: router.getDefaultRoute.bind(router),
256 setDefaultRoute: router.setDefaultRoute.bind(router),
257 // routes shorthand methods
258 delete: function _delete (url, options, handler) {
259 return router.prepareRoute.call(this, { method: 'DELETE', url, options, handler })
260 },
261 get: function _get (url, options, handler) {
262 return router.prepareRoute.call(this, { method: 'GET', url, options, handler })
263 },
264 head: function _head (url, options, handler) {
265 return router.prepareRoute.call(this, { method: 'HEAD', url, options, handler })
266 },
267 patch: function _patch (url, options, handler) {
268 return router.prepareRoute.call(this, { method: 'PATCH', url, options, handler })
269 },
270 post: function _post (url, options, handler) {
271 return router.prepareRoute.call(this, { method: 'POST', url, options, handler })
272 },
273 put: function _put (url, options, handler) {
274 return router.prepareRoute.call(this, { method: 'PUT', url, options, handler })
275 },
276 options: function _options (url, options, handler) {
277 return router.prepareRoute.call(this, { method: 'OPTIONS', url, options, handler })
278 },
279 all: function _all (url, options, handler) {
280 return router.prepareRoute.call(this, { method: supportedMethods, url, options, handler })
281 },
282 // extended route
283 route: function _route (options) {
284 // we need the fastify object that we are producing so we apply a lazy loading of the function,
285 // otherwise we should bind it after the declaration
286 return router.route.call(this, { options })
287 },
288 hasRoute: function _route (options) {
289 return router.hasRoute.call(this, { options })
290 },
291 findRoute: function _findRoute (options) {
292 return router.findRoute(options)
293 },
294 // expose logger instance
295 log: logger,
296 // type provider
297 withTypeProvider,
298 // hooks
299 addHook,
300 // schemas
301 addSchema,
302 getSchema: schemaController.getSchema.bind(schemaController),
303 getSchemas: schemaController.getSchemas.bind(schemaController),
304 setValidatorCompiler,
305 setSerializerCompiler,
306 setSchemaController,
307 setReplySerializer,
308 setSchemaErrorFormatter,
309 // set generated request id
310 setGenReqId,
311 // custom parsers
312 addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser,
313 hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser,
314 getDefaultJsonParser: ContentTypeParser.defaultParsers.getDefaultJsonParser,
315 defaultTextParser: ContentTypeParser.defaultParsers.defaultTextParser,
316 removeContentTypeParser: ContentTypeParser.helpers.removeContentTypeParser,
317 removeAllContentTypeParsers: ContentTypeParser.helpers.removeAllContentTypeParsers,
318 // Fastify architecture methods (initialized by Avvio)
319 register: null,
320 after: null,
321 ready: null,
322 onClose: null,
323 close: null,
324 printPlugins: null,
325 hasPlugin: function (name) {
326 return this[pluginUtils.kRegisteredPlugins].includes(name) || this[kPluginNameChain].includes(name)
327 },
328 // http server
329 listen,
330 server,
331 addresses: function () {
332 /* istanbul ignore next */
333 const binded = this[kServerBindings].map(b => b.address())
334 binded.push(this.server.address())
335 return binded.filter(adr => adr)
336 },
337 // extend fastify objects
338 decorate: decorator.add,
339 hasDecorator: decorator.exist,
340 decorateReply: decorator.decorateReply,
341 decorateRequest: decorator.decorateRequest,
342 hasRequestDecorator: decorator.existRequest,
343 hasReplyDecorator: decorator.existReply,
344 // fake http injection
345 inject,
346 // pretty print of the registered routes
347 printRoutes,
348 // custom error handling
349 setNotFoundHandler,
350 setErrorHandler,
351 // child logger
352 setChildLoggerFactory,
353 // Set fastify initial configuration options read-only object
354 initialConfig,
355 // constraint strategies
356 addConstraintStrategy: router.addConstraintStrategy.bind(router),
357 hasConstraintStrategy: router.hasConstraintStrategy.bind(router)
358 }
359
360 Object.defineProperties(fastify, {
361 listeningOrigin: {
362 get () {
363 const address = this.addresses().slice(-1).pop()
364 /* ignore if windows: unix socket is not testable on Windows platform */
365 /* c8 ignore next 3 */
366 if (typeof address === 'string') {
367 return address
368 }
369 const host = address.family === 'IPv6' ? `[${address.address}]` : address.address
370 return `${this[kOptions].https ? 'https' : 'http'}://${host}:${address.port}`
371 }
372 },
373 pluginName: {
374 configurable: true,
375 get () {
376 if (this[kPluginNameChain].length > 1) {
377 return this[kPluginNameChain].join(' -> ')
378 }
379 return this[kPluginNameChain][0]
380 }
381 },
382 prefix: {
383 configurable: true,
384 get () { return this[kRoutePrefix] }
385 },
386 validatorCompiler: {
387 configurable: true,
388 get () { return this[kSchemaController].getValidatorCompiler() }
389 },
390 serializerCompiler: {
391 configurable: true,
392 get () { return this[kSchemaController].getSerializerCompiler() }
393 },
394 childLoggerFactory: {
395 configurable: true,
396 get () { return this[kChildLoggerFactory] }
397 },
398 version: {
399 configurable: true,
400 get () { return VERSION }
401 },
402 errorHandler: {
403 configurable: true,
404 get () {
405 return this[kErrorHandler].func
406 }
407 },
408 genReqId: {
409 configurable: true,
410 get () { return this[kGenReqId] }
411 }
412 })
413
414 if (options.schemaErrorFormatter) {
415 validateSchemaErrorFormatter(options.schemaErrorFormatter)
416 fastify[kSchemaErrorFormatter] = options.schemaErrorFormatter.bind(fastify)
417 }
418
419 // Install and configure Avvio
420 // Avvio will update the following Fastify methods:
421 // - register
422 // - after
423 // - ready
424 // - onClose
425 // - close
426
427 const avvioPluginTimeout = Number(options.pluginTimeout)
428 const avvio = Avvio(fastify, {
429 autostart: false,
430 timeout: isNaN(avvioPluginTimeout) === false ? avvioPluginTimeout : defaultInitOptions.pluginTimeout,
431 expose: {
432 use: 'register'
433 }
434 })
435 // Override to allow the plugin encapsulation
436 avvio.override = override
437 avvio.on('start', () => (fastify[kState].started = true))
438 fastify[kAvvioBoot] = fastify.ready // the avvio ready function
439 fastify.ready = ready // overwrite the avvio ready function
440 fastify.printPlugins = avvio.prettyPrint.bind(avvio)
441
442 // cache the closing value, since we are checking it in an hot path
443 avvio.once('preReady', () => {
444 fastify.onClose((instance, done) => {
445 fastify[kState].closing = true
446 router.closeRoutes()
447
448 hookRunnerApplication('preClose', fastify[kAvvioBoot], fastify, function () {
449 if (fastify[kState].listening) {
450 /* istanbul ignore next: Cannot test this without Node.js core support */
451 if (forceCloseConnections === 'idle') {
452 // Not needed in Node 19
453 instance.server.closeIdleConnections()
454 /* istanbul ignore next: Cannot test this without Node.js core support */
455 } else if (serverHasCloseAllConnections && forceCloseConnections) {
456 instance.server.closeAllConnections()
457 } else if (forceCloseConnections === true) {
458 for (const conn of fastify[kKeepAliveConnections]) {
459 // We must invoke the destroy method instead of merely unreffing
460 // the sockets. If we only unref, then the callback passed to
461 // `fastify.close` will never be invoked; nor will any of the
462 // registered `onClose` hooks.
463 conn.destroy()
464 fastify[kKeepAliveConnections].delete(conn)
465 }
466 }
467 }
468
469 // No new TCP connections are accepted.
470 // We must call close on the server even if we are not listening
471 // otherwise memory will be leaked.
472 // https://github.com/nodejs/node/issues/48604
473 if (!options.serverFactory || fastify[kState].listening) {
474 instance.server.close(function (err) {
475 /* c8 ignore next 6 */
476 if (err && err.code !== 'ERR_SERVER_NOT_RUNNING') {
477 done(null)
478 } else {
479 done()
480 }
481 })
482 } else {
483 process.nextTick(done, null)
484 }
485 })
486 })
487 })
488
489 // Create bad URL context
490 const onBadUrlContext = new Context({
491 server: fastify,
492 config: {}
493 })
494
495 // Set the default 404 handler
496 fastify.setNotFoundHandler()
497 fourOhFour.arrange404(fastify)
498
499 router.setup(options, {
500 avvio,
501 fourOhFour,
502 logger,
503 hasLogger,
504 setupResponseListeners,
505 throwIfAlreadyStarted,
506 validateHTTPVersion: compileValidateHTTPVersion(options),
507 keepAliveConnections
508 })
509
510 // Delay configuring clientError handler so that it can access fastify state.
511 server.on('clientError', options.clientErrorHandler.bind(fastify))
512
513 try {
514 const dc = require('node:diagnostics_channel')
515 const initChannel = dc.channel('fastify.initialization')
516 if (initChannel.hasSubscribers) {
517 initChannel.publish({ fastify })
518 }
519 } catch (e) {
520 // This only happens if `diagnostics_channel` isn't available, i.e. earlier
521 // versions of Node.js. In that event, we don't care, so ignore the error.
522 }
523
524 // Older nodejs versions may not have asyncDispose
525 if ('asyncDispose' in Symbol) {
526 fastify[Symbol.asyncDispose] = function dispose () {
527 return fastify.close()
528 }
529 }
530
531 return fastify
532
533 function throwIfAlreadyStarted (msg) {
534 if (fastify[kState].started) throw new FST_ERR_INSTANCE_ALREADY_LISTENING(msg)
535 }
536
537 // HTTP injection handling
538 // If the server is not ready yet, this
539 // utility will automatically force it.
540 function inject (opts, cb) {
541 // lightMyRequest is dynamically loaded as it seems very expensive
542 // because of Ajv
543 if (lightMyRequest === undefined) {
544 lightMyRequest = require('light-my-request')
545 }
546
547 if (fastify[kState].started) {
548 if (fastify[kState].closing) {
549 // Force to return an error
550 const error = new FST_ERR_REOPENED_CLOSE_SERVER()
551 if (cb) {
552 cb(error)
553 return
554 } else {
555 return Promise.reject(error)
556 }
557 }
558 return lightMyRequest(httpHandler, opts, cb)
559 }
560
561 if (cb) {
562 this.ready(err => {
563 if (err) cb(err, null)
564 else lightMyRequest(httpHandler, opts, cb)
565 })
566 } else {
567 return lightMyRequest((req, res) => {
568 this.ready(function (err) {
569 if (err) {
570 res.emit('error', err)
571 return
572 }
573 httpHandler(req, res)
574 })
575 }, opts)
576 }
577 }
578
579 function ready (cb) {
580 if (this[kState].readyPromise !== null) {
581 if (cb != null) {
582 this[kState].readyPromise.then(() => cb(null, fastify), cb)
583 return
584 }
585
586 return this[kState].readyPromise
587 }
588
589 let resolveReady
590 let rejectReady
591
592 // run the hooks after returning the promise
593 process.nextTick(runHooks)
594
595 // Create a promise no matter what
596 // It will work as a barrier for all the .ready() calls (ensuring single hook execution)
597 // as well as a flow control mechanism to chain cbs and further
598 // promises
599 this[kState].readyPromise = new Promise(function (resolve, reject) {
600 resolveReady = resolve
601 rejectReady = reject
602 })
603
604 if (!cb) {
605 return this[kState].readyPromise
606 } else {
607 this[kState].readyPromise.then(() => cb(null, fastify), cb)
608 }
609
610 function runHooks () {
611 // start loading
612 fastify[kAvvioBoot]((err, done) => {
613 if (err || fastify[kState].started || fastify[kState].ready || fastify[kState].booting) {
614 manageErr(err)
615 } else {
616 fastify[kState].booting = true
617 hookRunnerApplication('onReady', fastify[kAvvioBoot], fastify, manageErr)
618 }
619 done()
620 })
621 }
622
623 function manageErr (err) {
624 // If the error comes out of Avvio's Error codes
625 // We create a make and preserve the previous error
626 // as cause
627 err = err != null && AVVIO_ERRORS_MAP[err.code] != null
628 ? appendStackTrace(err, new AVVIO_ERRORS_MAP[err.code](err.message))
629 : err
630
631 if (err) {
632 return rejectReady(err)
633 }
634
635 resolveReady(fastify)
636 fastify[kState].booting = false
637 fastify[kState].ready = true
638 fastify[kState].promise = null
639 }
640 }
641
642 // Used exclusively in TypeScript contexts to enable auto type inference from JSON schema.
643 function withTypeProvider () {
644 return this
645 }
646
647 // wrapper that we expose to the user for hooks handling
648 function addHook (name, fn) {
649 throwIfAlreadyStarted('Cannot call "addHook"!')
650
651 if (fn == null) {
652 throw new errorCodes.FST_ERR_HOOK_INVALID_HANDLER(name, fn)
653 }
654
655 if (name === 'onSend' || name === 'preSerialization' || name === 'onError' || name === 'preParsing') {
656 if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) {
657 throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
658 }
659 } else if (name === 'onReady' || name === 'onListen') {
660 if (fn.constructor.name === 'AsyncFunction' && fn.length !== 0) {
661 throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
662 }
663 } else if (name === 'onRequestAbort') {
664 if (fn.constructor.name === 'AsyncFunction' && fn.length !== 1) {
665 throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
666 }
667 } else {
668 if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
669 throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
670 }
671 }
672
673 if (name === 'onClose') {
674 this.onClose(fn)
675 } else if (name === 'onReady' || name === 'onListen' || name === 'onRoute') {
676 this[kHooks].add(name, fn)
677 } else {
678 this.after((err, done) => {
679 _addHook.call(this, name, fn)
680 done(err)
681 })
682 }
683 return this
684
685 function _addHook (name, fn) {
686 this[kHooks].add(name, fn)
687 this[kChildren].forEach(child => _addHook.call(child, name, fn))
688 }
689 }
690
691 // wrapper that we expose to the user for schemas handling
692 function addSchema (schema) {
693 throwIfAlreadyStarted('Cannot call "addSchema"!')
694 this[kSchemaController].add(schema)
695 this[kChildren].forEach(child => child.addSchema(schema))
696 return this
697 }
698
699 function defaultClientErrorHandler (err, socket) {
700 // In case of a connection reset, the socket has been destroyed and there is nothing that needs to be done.
701 // https://nodejs.org/api/http.html#http_event_clienterror
702 if (err.code === 'ECONNRESET' || socket.destroyed) {
703 return
704 }
705
706 let body, errorCode, errorStatus, errorLabel
707
708 if (err.code === 'ERR_HTTP_REQUEST_TIMEOUT') {
709 errorCode = '408'
710 errorStatus = http.STATUS_CODES[errorCode]
711 body = `{"error":"${errorStatus}","message":"Client Timeout","statusCode":408}`
712 errorLabel = 'timeout'
713 } else if (err.code === 'HPE_HEADER_OVERFLOW') {
714 errorCode = '431'
715 errorStatus = http.STATUS_CODES[errorCode]
716 body = `{"error":"${errorStatus}","message":"Exceeded maximum allowed HTTP header size","statusCode":431}`
717 errorLabel = 'header_overflow'
718 } else {
719 errorCode = '400'
720 errorStatus = http.STATUS_CODES[errorCode]
721 body = `{"error":"${errorStatus}","message":"Client Error","statusCode":400}`
722 errorLabel = 'error'
723 }
724
725 // Most devs do not know what to do with this error.
726 // In the vast majority of cases, it's a network error and/or some
727 // config issue on the load balancer side.
728 this.log.trace({ err }, `client ${errorLabel}`)
729 // Copying standard node behavior
730 // https://github.com/nodejs/node/blob/6ca23d7846cb47e84fd344543e394e50938540be/lib/_http_server.js#L666
731
732 // If the socket is not writable, there is no reason to try to send data.
733 if (socket.writable) {
734 socket.write(`HTTP/1.1 ${errorCode} ${errorStatus}\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`)
735 }
736 socket.destroy(err)
737 }
738
739 // If the router does not match any route, every request will land here
740 // req and res are Node.js core objects
741 function defaultRoute (req, res) {
742 if (req.headers['accept-version'] !== undefined) {
743 // we remove the accept-version header for performance result
744 // because we do not want to go through the constraint checking
745 // the usage of symbol here to prevent any collision on custom header name
746 req.headers[kRequestAcceptVersion] = req.headers['accept-version']
747 req.headers['accept-version'] = undefined
748 }
749 fourOhFour.router.lookup(req, res)
750 }
751
752 function onBadUrl (path, req, res) {
753 if (frameworkErrors) {
754 const id = getGenReqId(onBadUrlContext.server, req)
755 const childLogger = createChildLogger(onBadUrlContext, logger, req, id)
756
757 const request = new Request(id, null, req, null, childLogger, onBadUrlContext)
758 const reply = new Reply(res, request, childLogger)
759
760 if (disableRequestLogging === false) {
761 childLogger.info({ req: request }, 'incoming request')
762 }
763
764 return frameworkErrors(new FST_ERR_BAD_URL(path), request, reply)
765 }
766 const body = `{"error":"Bad Request","code":"FST_ERR_BAD_URL","message":"'${path}' is not a valid url component","statusCode":400}`
767 res.writeHead(400, {
768 'Content-Type': 'application/json',
769 'Content-Length': body.length
770 })
771 res.end(body)
772 }
773
774 function buildAsyncConstraintCallback (isAsync, req, res) {
775 if (isAsync === false) return undefined
776 return function onAsyncConstraintError (err) {
777 if (err) {
778 if (frameworkErrors) {
779 const id = getGenReqId(onBadUrlContext.server, req)
780 const childLogger = createChildLogger(onBadUrlContext, logger, req, id)
781
782 const request = new Request(id, null, req, null, childLogger, onBadUrlContext)
783 const reply = new Reply(res, request, childLogger)
784
785 if (disableRequestLogging === false) {
786 childLogger.info({ req: request }, 'incoming request')
787 }
788
789 return frameworkErrors(new FST_ERR_ASYNC_CONSTRAINT(), request, reply)
790 }
791 const body = '{"error":"Internal Server Error","message":"Unexpected error from async constraint","statusCode":500}'
792 res.writeHead(500, {
793 'Content-Type': 'application/json',
794 'Content-Length': body.length
795 })
796 res.end(body)
797 }
798 }
799 }
800
801 function setNotFoundHandler (opts, handler) {
802 throwIfAlreadyStarted('Cannot call "setNotFoundHandler"!')
803
804 fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, router.routeHandler)
805 return this
806 }
807
808 function setValidatorCompiler (validatorCompiler) {
809 throwIfAlreadyStarted('Cannot call "setValidatorCompiler"!')
810 this[kSchemaController].setValidatorCompiler(validatorCompiler)
811 return this
812 }
813
814 function setSchemaErrorFormatter (errorFormatter) {
815 throwIfAlreadyStarted('Cannot call "setSchemaErrorFormatter"!')
816 validateSchemaErrorFormatter(errorFormatter)
817 this[kSchemaErrorFormatter] = errorFormatter.bind(this)
818 return this
819 }
820
821 function setSerializerCompiler (serializerCompiler) {
822 throwIfAlreadyStarted('Cannot call "setSerializerCompiler"!')
823 this[kSchemaController].setSerializerCompiler(serializerCompiler)
824 return this
825 }
826
827 function setSchemaController (schemaControllerOpts) {
828 throwIfAlreadyStarted('Cannot call "setSchemaController"!')
829 const old = this[kSchemaController]
830 const schemaController = SchemaController.buildSchemaController(old, Object.assign({}, old.opts, schemaControllerOpts))
831 this[kSchemaController] = schemaController
832 this.getSchema = schemaController.getSchema.bind(schemaController)
833 this.getSchemas = schemaController.getSchemas.bind(schemaController)
834 return this
835 }
836
837 function setReplySerializer (replySerializer) {
838 throwIfAlreadyStarted('Cannot call "setReplySerializer"!')
839
840 this[kReplySerializerDefault] = replySerializer
841 return this
842 }
843
844 // wrapper that we expose to the user for configure the custom error handler
845 function setErrorHandler (func) {
846 throwIfAlreadyStarted('Cannot call "setErrorHandler"!')
847
848 if (typeof func !== 'function') {
849 throw new FST_ERR_ERROR_HANDLER_NOT_FN()
850 }
851
852 this[kErrorHandler] = buildErrorHandler(this[kErrorHandler], func.bind(this))
853 return this
854 }
855
856 function setChildLoggerFactory (factory) {
857 throwIfAlreadyStarted('Cannot call "setChildLoggerFactory"!')
858
859 this[kChildLoggerFactory] = factory
860 return this
861 }
862
863 function printRoutes (opts = {}) {
864 // includeHooks:true - shortcut to include all supported hooks exported by fastify.Hooks
865 opts.includeMeta = opts.includeHooks ? opts.includeMeta ? supportedHooks.concat(opts.includeMeta) : supportedHooks : opts.includeMeta
866 return router.printRoutes(opts)
867 }
868
869 function wrapRouting (router, { rewriteUrl, logger }) {
870 let isAsync
871 return function preRouting (req, res) {
872 // only call isAsyncConstraint once
873 if (isAsync === undefined) isAsync = router.isAsyncConstraint()
874 if (rewriteUrl) {
875 req.originalUrl = req.url
876 const url = rewriteUrl.call(fastify, req)
877 if (typeof url === 'string') {
878 req.url = url
879 } else {
880 const err = new FST_ERR_ROUTE_REWRITE_NOT_STR(req.url, typeof url)
881 req.destroy(err)
882 }
883 }
884 router.routing(req, res, buildAsyncConstraintCallback(isAsync, req, res))
885 }
886 }
887
888 function setGenReqId (func) {
889 throwIfAlreadyStarted('Cannot call "setGenReqId"!')
890
891 this[kGenReqId] = reqIdGenFactory(this[kOptions].requestIdHeader, func)
892 return this
893 }
894}
895
896function validateSchemaErrorFormatter (schemaErrorFormatter) {
897 if (typeof schemaErrorFormatter !== 'function') {
898 throw new FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN(typeof schemaErrorFormatter)
899 } else if (schemaErrorFormatter.constructor.name === 'AsyncFunction') {
900 throw new FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN('AsyncFunction')
901 }
902}
903
904/**
905 * These export configurations enable JS and TS developers
906 * to consumer fastify in whatever way best suits their needs.
907 * Some examples of supported import syntax includes:
908 * - `const fastify = require('fastify')`
909 * - `const { fastify } = require('fastify')`
910 * - `import * as Fastify from 'fastify'`
911 * - `import { fastify, TSC_definition } from 'fastify'`
912 * - `import fastify from 'fastify'`
913 * - `import fastify, { TSC_definition } from 'fastify'`
914 */
915module.exports = fastify
916module.exports.errorCodes = errorCodes
917module.exports.fastify = fastify
918module.exports.default = fastify