1 | 'use strict'
|
2 |
|
3 | const Avvio = require('avvio')
|
4 | const http = require('http')
|
5 | const querystring = require('querystring')
|
6 | let lightMyRequest
|
7 |
|
8 | const {
|
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 |
|
27 | const { createServer } = require('./lib/server')
|
28 | const Reply = require('./lib/reply')
|
29 | const Request = require('./lib/request')
|
30 | const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
|
31 | const decorator = require('./lib/decorate')
|
32 | const ContentTypeParser = require('./lib/contentTypeParser')
|
33 | const { Hooks, buildHooks } = require('./lib/hooks')
|
34 | const { Schemas, buildSchemas } = require('./lib/schemas')
|
35 | const { createLogger } = require('./lib/logger')
|
36 | const pluginUtils = require('./lib/pluginUtils')
|
37 | const reqIdGenFactory = require('./lib/reqIdGenFactory')
|
38 | const { buildRouting, validateBodyLimitOption } = require('./lib/route')
|
39 | const build404 = require('./lib/fourOhFour')
|
40 | const getSecuredInitialConfig = require('./lib/initialConfigValidation')
|
41 | const { defaultInitOptions } = getSecuredInitialConfig
|
42 |
|
43 | function build (options) {
|
44 |
|
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 |
|
71 | const { logger, hasLogger } = createLogger(options)
|
72 |
|
73 |
|
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 |
|
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 |
|
94 | const fourOhFour = build404(options)
|
95 |
|
96 |
|
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 |
|
107 | const fastify = {
|
108 |
|
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 |
|
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 |
|
159 | route: function _route (opts) {
|
160 |
|
161 |
|
162 | return router.route.call(this, opts)
|
163 | },
|
164 |
|
165 | log: logger,
|
166 |
|
167 | addHook: addHook,
|
168 |
|
169 | addSchema: addSchema,
|
170 | getSchemas: schemas.getSchemas.bind(schemas),
|
171 | setSchemaCompiler: setSchemaCompiler,
|
172 | setReplySerializer: setReplySerializer,
|
173 |
|
174 | addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser,
|
175 | hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser,
|
176 |
|
177 | register: null,
|
178 | after: null,
|
179 | ready: null,
|
180 | onClose: null,
|
181 | close: null,
|
182 |
|
183 | listen: listen,
|
184 | server: server,
|
185 |
|
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 |
|
193 | use: use,
|
194 |
|
195 | inject: inject,
|
196 |
|
197 | printRoutes: router.printRoutes,
|
198 |
|
199 | setNotFoundHandler: setNotFoundHandler,
|
200 | setErrorHandler: setErrorHandler,
|
201 |
|
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 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | const avvio = Avvio(fastify, {
|
235 | autostart: false,
|
236 | timeout: Number(options.pluginTimeout) || defaultInitOptions.pluginTimeout,
|
237 | expose: { use: 'register' }
|
238 | })
|
239 |
|
240 | avvio.override = override
|
241 | avvio.on('start', () => (fastify[kState].started = true))
|
242 |
|
243 | avvio.once('preReady', () => {
|
244 | fastify.onClose((instance, done) => {
|
245 | fastify[kState].closing = true
|
246 | router.closeRoutes()
|
247 | if (fastify[kState].listening) {
|
248 |
|
249 | instance.server.close(done)
|
250 | } else {
|
251 | done(null)
|
252 | }
|
253 | })
|
254 | })
|
255 |
|
256 |
|
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 |
|
276 |
|
277 |
|
278 | function inject (opts, cb) {
|
279 |
|
280 |
|
281 | if (lightMyRequest === undefined) {
|
282 | lightMyRequest = require('light-my-request')
|
283 | }
|
284 |
|
285 | if (fastify[kState].started) {
|
286 | if (fastify[kState].closing) {
|
287 |
|
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 |
|
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 |
|
329 | function addHook (name, fn) {
|
330 | throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
|
331 |
|
332 |
|
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 |
|
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 |
|
385 |
|
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 |
|
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 |
|
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 |
|
424 |
|
425 | function 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 |
|
454 | function buildRoutePrefix (instancePrefix, pluginPrefix) {
|
455 | if (!pluginPrefix) {
|
456 | return instancePrefix
|
457 | }
|
458 |
|
459 |
|
460 | if (instancePrefix.endsWith('/')) {
|
461 | if (pluginPrefix[0] === '/') {
|
462 |
|
463 | pluginPrefix = pluginPrefix.slice(1)
|
464 | }
|
465 | } else if (pluginPrefix[0] !== '/') {
|
466 | pluginPrefix = '/' + pluginPrefix
|
467 | }
|
468 |
|
469 | return instancePrefix + pluginPrefix
|
470 | }
|
471 |
|
472 | module.exports = build
|