1 | 'use strict'
|
2 |
|
3 | const FindMyWay = require('find-my-way')
|
4 | const Context = require('./context')
|
5 | const handleRequest = require('./handleRequest')
|
6 | const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks')
|
7 | const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
|
8 | const validation = require('./validation')
|
9 | const { normalizeSchema } = require('./schemas')
|
10 | const {
|
11 | ValidatorSelector,
|
12 | SerializerCompiler: buildDefaultSerializer
|
13 | } = require('./schema-compilers')
|
14 | const warning = require('./warnings')
|
15 |
|
16 | const {
|
17 | compileSchemasForValidation,
|
18 | compileSchemasForSerialization
|
19 | } = validation
|
20 |
|
21 | const {
|
22 | FST_ERR_SCH_VALIDATION_BUILD,
|
23 | FST_ERR_SCH_SERIALIZATION_BUILD
|
24 | } = require('./errors')
|
25 |
|
26 | const {
|
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 |
|
46 | function 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),
|
85 | route,
|
86 | prepareRoute,
|
87 | routeHandler,
|
88 | closeRoutes: () => { closing = true },
|
89 | printRoutes: router.prettyPrint.bind(router)
|
90 | }
|
91 |
|
92 |
|
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 |
|
119 | function route (options) {
|
120 |
|
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 |
|
163 | if (ignoreTrailingSlash !== true) {
|
164 | afterRouteAdded.call(this, path, notHandledErr, done)
|
165 | }
|
166 | }
|
167 | } else if (path[0] === '/' && prefix.endsWith('/')) {
|
168 |
|
169 | afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
|
170 | } else {
|
171 | afterRouteAdded.call(this, path, notHandledErr, done)
|
172 | }
|
173 | })
|
174 |
|
175 |
|
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 |
|
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 |
|
233 |
|
234 |
|
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 |
|
243 | if (hook === 'preParsing') {
|
244 |
|
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 |
|
257 |
|
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 |
|
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 |
|
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 |
|
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 |
|
360 | function handleTimeout () {
|
361 | const { context, request, reply } = this._meta
|
362 | hookRunner(
|
363 | context.onTimeout,
|
364 | hookIterator,
|
365 | request,
|
366 | reply,
|
367 | noop
|
368 | )
|
369 | }
|
370 |
|
371 | function 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 |
|
378 | function 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 |
|
394 | function 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 |
|
440 | function noop () { }
|
441 |
|
442 | module.exports = { buildRouting, validateBodyLimitOption }
|