UNPKG

13.4 kBJavaScriptView Raw
1'use strict'
2
3const FindMyWay = require('find-my-way')
4const Context = require('./context')
5const handleRequest = require('./handleRequest')
6const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks')
7const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
8const validation = require('./validation')
9const { normalizeSchema } = require('./schemas')
10const {
11 ValidatorSelector,
12 SerializerCompiler: buildDefaultSerializer
13} = require('./schema-compilers')
14const warning = require('./warnings')
15
16const {
17 compileSchemasForValidation,
18 compileSchemasForSerialization
19} = validation
20
21const {
22 FST_ERR_SCH_VALIDATION_BUILD,
23 FST_ERR_SCH_SERIALIZATION_BUILD
24} = require('./errors')
25
26const {
27 kRoutePrefix,
28 kLogLevel,
29 kLogSerializers,
30 kHooks,
31 kHooksDeprecatedPreParsing,
32 kSchemas,
33 kOptions,
34 kValidatorCompiler,
35 kSerializerCompiler,
36 kContentTypeParser,
37 kReply,
38 kReplySerializerDefault,
39 kReplyIsError,
40 kRequest,
41 kRequestPayloadStream,
42 kDisableRequestLogging,
43 kSchemaErrorFormatter
44} = require('./symbols.js')
45
46function buildRouting (options) {
47 const router = FindMyWay(options.config)
48
49 let avvio
50 let fourOhFour
51 let requestIdHeader
52 let querystringParser
53 let requestIdLogLabel
54 let logger
55 let hasLogger
56 let setupResponseListeners
57 let throwIfAlreadyStarted
58 let genReqId
59 let disableRequestLogging
60 let ignoreTrailingSlash
61 let return503OnClosing
62 let buildPerformanceValidator
63
64 let closing = false
65
66 return {
67 setup (options, fastifyArgs) {
68 avvio = fastifyArgs.avvio
69 fourOhFour = fastifyArgs.fourOhFour
70 logger = fastifyArgs.logger
71 hasLogger = fastifyArgs.hasLogger
72 setupResponseListeners = fastifyArgs.setupResponseListeners
73 throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
74
75 requestIdHeader = options.requestIdHeader
76 querystringParser = options.querystringParser
77 requestIdLogLabel = options.requestIdLogLabel
78 genReqId = options.genReqId
79 disableRequestLogging = options.disableRequestLogging
80 ignoreTrailingSlash = options.ignoreTrailingSlash
81 return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true
82 buildPerformanceValidator = ValidatorSelector()
83 },
84 routing: router.lookup.bind(router), // router func to find the right handler to call
85 route, // configure a route in the fastify instance
86 prepareRoute,
87 routeHandler,
88 closeRoutes: () => { closing = true },
89 printRoutes: router.prettyPrint.bind(router)
90 }
91
92 // Convert shorthand to extended route declaration
93 function prepareRoute (method, url, options, handler) {
94 if (!handler && typeof options === 'function') {
95 handler = options
96 options = {}
97 } else if (handler && typeof handler === 'function') {
98 if (Object.prototype.toString.call(options) !== '[object Object]') {
99 throw new Error(`Options for ${method}:${url} route must be an object`)
100 } else if (options.handler) {
101 if (typeof options.handler === 'function') {
102 throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`)
103 } else {
104 throw new Error(`Handler for ${method}:${url} route must be a function`)
105 }
106 }
107 }
108
109 options = Object.assign({}, options, {
110 method,
111 url,
112 handler: handler || (options && options.handler)
113 })
114
115 return route.call(this, options)
116 }
117
118 // Route management
119 function route (options) {
120 // Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator
121 const opts = { ...options }
122
123 throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
124
125 if (Array.isArray(opts.method)) {
126 for (var i = 0; i < opts.method.length; i++) {
127 if (supportedMethods.indexOf(opts.method[i]) === -1) {
128 throw new Error(`${opts.method[i]} method is not supported!`)
129 }
130 }
131 } else {
132 if (supportedMethods.indexOf(opts.method) === -1) {
133 throw new Error(`${opts.method} method is not supported!`)
134 }
135 }
136
137 if (!opts.handler) {
138 throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
139 }
140
141 if (opts.errorHandler !== undefined && typeof opts.errorHandler !== 'function') {
142 throw new Error(`Error Handler for ${opts.method}:${opts.url} route, if defined, must be a function`)
143 }
144
145 validateBodyLimitOption(opts.bodyLimit)
146
147 const prefix = this[kRoutePrefix]
148
149 this.after((notHandledErr, done) => {
150 var path = opts.url || opts.path
151 if (path === '/' && prefix.length > 0) {
152 switch (opts.prefixTrailingSlash) {
153 case 'slash':
154 afterRouteAdded.call(this, path, notHandledErr, done)
155 break
156 case 'no-slash':
157 afterRouteAdded.call(this, '', notHandledErr, done)
158 break
159 case 'both':
160 default:
161 afterRouteAdded.call(this, '', notHandledErr, done)
162 // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
163 if (ignoreTrailingSlash !== true) {
164 afterRouteAdded.call(this, path, notHandledErr, done)
165 }
166 }
167 } else if (path[0] === '/' && prefix.endsWith('/')) {
168 // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
169 afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
170 } else {
171 afterRouteAdded.call(this, path, notHandledErr, done)
172 }
173 })
174
175 // chainable api
176 return this
177
178 function afterRouteAdded (path, notHandledErr, done) {
179 const url = prefix + path
180
181 opts.url = url
182 opts.path = url
183 opts.routePath = path
184 opts.prefix = prefix
185 opts.logLevel = opts.logLevel || this[kLogLevel]
186
187 if (this[kLogSerializers] || opts.logSerializers) {
188 opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
189 }
190
191 if (opts.attachValidation == null) {
192 opts.attachValidation = false
193 }
194
195 // run 'onRoute' hooks
196 for (const hook of this[kHooks].onRoute) {
197 try {
198 hook.call(this, opts)
199 } catch (error) {
200 done(error)
201 return
202 }
203 }
204
205 const config = opts.config || {}
206 config.url = url
207 config.method = opts.method
208
209 const context = new Context(
210 opts.schema,
211 opts.handler.bind(this),
212 this[kReply],
213 this[kRequest],
214 this[kContentTypeParser],
215 config,
216 opts.errorHandler || this._errorHandler,
217 opts.bodyLimit,
218 opts.logLevel,
219 opts.logSerializers,
220 opts.attachValidation,
221 this[kReplySerializerDefault],
222 opts.schemaErrorFormatter || this[kSchemaErrorFormatter]
223 )
224
225 try {
226 router.on(opts.method, opts.url, { version: opts.version }, routeHandler, context)
227 } catch (err) {
228 done(err)
229 return
230 }
231
232 // It can happen that a user registers a plugin with some hooks *after*
233 // the route registration. To be sure to also load also those hooks,
234 // we must listen for the avvio's preReady event, and update the context object accordingly.
235 avvio.once('preReady', () => {
236 for (const hook of lifecycleHooks) {
237 const toSet = this[kHooks][hook]
238 .concat(opts[hook] || [])
239 .map(h => {
240 const bound = h.bind(this)
241
242 // Track hooks deprecation markers
243 if (hook === 'preParsing') {
244 // Check for deprecation syntax
245 if (h.length === (h.constructor.name === 'AsyncFunction' ? 2 : 3)) {
246 warning.emit('FSTDEP004')
247 bound[kHooksDeprecatedPreParsing] = true
248 }
249 }
250
251 return bound
252 })
253 context[hook] = toSet.length ? toSet : null
254 }
255
256 // Must store the 404 Context in 'preReady' because it is only guaranteed to
257 // be available after all of the plugins and routes have been loaded.
258 fourOhFour.setContext(this, context)
259
260 if (opts.schema) {
261 context.schema = normalizeSchema(context.schema)
262
263 const schemaBucket = this[kSchemas]
264 if (!opts.validatorCompiler && (
265 !this[kValidatorCompiler] ||
266 (this[kValidatorCompiler] && schemaBucket.hasNewSchemas()))) {
267 // if the instance doesn't have a validator, build the default one for the single fastify instance
268 this.setValidatorCompiler(buildPerformanceValidator(schemaBucket.getSchemas(), this[kOptions].ajv))
269 }
270 try {
271 compileSchemasForValidation(context, opts.validatorCompiler || this[kValidatorCompiler])
272 } catch (error) {
273 throw new FST_ERR_SCH_VALIDATION_BUILD(opts.method, url, error.message)
274 }
275
276 if (opts.schema.response &&
277 !opts.serializerCompiler &&
278 (!this[kSerializerCompiler] || (this[kSerializerCompiler] && schemaBucket.hasNewSchemas()))) {
279 // if the instance doesn't have a serializer, build the default one for the single fastify instance
280 this.setSerializerCompiler(buildDefaultSerializer(schemaBucket.getSchemas()))
281 }
282 try {
283 compileSchemasForSerialization(context, opts.serializerCompiler || this[kSerializerCompiler])
284 } catch (error) {
285 throw new FST_ERR_SCH_SERIALIZATION_BUILD(opts.method, url, error.message)
286 }
287 }
288 })
289
290 done(notHandledErr)
291 }
292 }
293
294 // HTTP request entry point, the routing has already been executed
295 function routeHandler (req, res, params, context) {
296 if (closing === true) {
297 if (req.httpVersionMajor !== 2) {
298 res.once('finish', () => req.destroy())
299 res.setHeader('Connection', 'close')
300 }
301
302 if (return503OnClosing) {
303 const headers = {
304 'Content-Type': 'application/json',
305 'Content-Length': '80'
306 }
307 res.writeHead(503, headers)
308 res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
309 return
310 }
311 }
312
313 var id = req.headers[requestIdHeader] || genReqId(req)
314
315 var loggerOpts = {
316 [requestIdLogLabel]: id,
317 level: context.logLevel
318 }
319
320 if (context.logSerializers) {
321 loggerOpts.serializers = context.logSerializers
322 }
323 var childLogger = logger.child(loggerOpts)
324 childLogger[kDisableRequestLogging] = disableRequestLogging
325
326 var queryPrefix = req.url.indexOf('?')
327 var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
328 var request = new context.Request(id, params, req, query, childLogger, context)
329 var reply = new context.Reply(res, request, childLogger)
330
331 if (disableRequestLogging === false) {
332 childLogger.info({ req: request }, 'incoming request')
333 }
334
335 if (hasLogger === true || context.onResponse !== null) {
336 setupResponseListeners(reply)
337 }
338
339 if (context.onRequest !== null) {
340 hookRunner(
341 context.onRequest,
342 hookIterator,
343 request,
344 reply,
345 runPreParsing
346 )
347 } else {
348 runPreParsing(null, request, reply)
349 }
350
351 if (context.onTimeout !== null) {
352 if (!request.raw.socket._meta) {
353 request.raw.socket.on('timeout', handleTimeout)
354 }
355 request.raw.socket._meta = { context, request, reply }
356 }
357 }
358}
359
360function handleTimeout () {
361 const { context, request, reply } = this._meta
362 hookRunner(
363 context.onTimeout,
364 hookIterator,
365 request,
366 reply,
367 noop
368 )
369}
370
371function validateBodyLimitOption (bodyLimit) {
372 if (bodyLimit === undefined) return
373 if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
374 throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`)
375 }
376}
377
378function runPreParsing (err, request, reply) {
379 if (reply.sent === true) return
380 if (err != null) {
381 reply.send(err)
382 return
383 }
384
385 request[kRequestPayloadStream] = request.raw
386
387 if (reply.context.preParsing !== null) {
388 preParsingHookRunner(reply.context.preParsing, request, reply, handleRequest)
389 } else {
390 handleRequest(null, request, reply)
391 }
392}
393
394function preParsingHookRunner (functions, request, reply, cb) {
395 let i = 0
396
397 function next (err, stream) {
398 if (reply.sent) {
399 return
400 }
401
402 if (typeof stream !== 'undefined') {
403 request[kRequestPayloadStream] = stream
404 }
405
406 if (err || i === functions.length) {
407 if (err && !(err instanceof Error)) {
408 reply[kReplyIsError] = true
409 }
410
411 cb(err, request, reply)
412 return
413 }
414
415 const fn = functions[i++]
416 let result
417
418 if (fn[kHooksDeprecatedPreParsing]) {
419 result = fn(request, reply, next)
420 } else {
421 result = fn(request, reply, request[kRequestPayloadStream], next)
422 }
423
424 if (result && typeof result.then === 'function') {
425 result.then(handleResolve, handleReject)
426 }
427 }
428
429 function handleResolve (stream) {
430 next(null, stream)
431 }
432
433 function handleReject (err) {
434 next(err)
435 }
436
437 next(null, request[kRequestPayloadStream])
438}
439
440function noop () { }
441
442module.exports = { buildRouting, validateBodyLimitOption }