UNPKG

12.6 kBJavaScriptView Raw
1'use strict'
2
3const FindMyWay = require('find-my-way')
4const proxyAddr = require('proxy-addr')
5const Context = require('./context')
6const { buildMiddie, onRunMiddlewares } = require('./middleware')
7const { hookRunner, hookIterator } = require('./hooks')
8const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
9const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization', 'onResponse', 'onSend']
10const validation = require('./validation')
11const buildSchema = validation.build
12const { buildSchemaCompiler } = validation
13const { beforeHandlerWarning } = require('./warnings')
14
15const {
16 codes: {
17 FST_ERR_SCH_BUILD,
18 FST_ERR_SCH_MISSING_COMPILER
19 }
20} = require('./errors')
21
22const {
23 kRoutePrefix,
24 kLogLevel,
25 kLogSerializers,
26 kHooks,
27 kSchemas,
28 kOptions,
29 kSchemaCompiler,
30 kSchemaResolver,
31 kContentTypeParser,
32 kReply,
33 kReplySerializerDefault,
34 kRequest,
35 kMiddlewares,
36 kGlobalHooks,
37 kDisableRequestLogging
38} = require('./symbols.js')
39
40function buildRouting (options) {
41 const router = FindMyWay(options.config)
42
43 const schemaCache = new Map()
44 schemaCache.put = schemaCache.set
45
46 let avvio
47 let fourOhFour
48 let trustProxy
49 let requestIdHeader
50 let querystringParser
51 let requestIdLogLabel
52 let logger
53 let hasLogger
54 let setupResponseListeners
55 let throwIfAlreadyStarted
56 let proxyFn
57 let modifyCoreObjects
58 let genReqId
59 let disableRequestLogging
60 let ignoreTrailingSlash
61 let return503OnClosing
62
63 let closing = false
64
65 return {
66 setup (options, fastifyArgs) {
67 avvio = fastifyArgs.avvio
68 fourOhFour = fastifyArgs.fourOhFour
69 logger = fastifyArgs.logger
70 hasLogger = fastifyArgs.hasLogger
71 setupResponseListeners = fastifyArgs.setupResponseListeners
72 throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
73
74 proxyFn = getTrustProxyFn(options)
75 trustProxy = options.trustProxy
76 requestIdHeader = options.requestIdHeader
77 querystringParser = options.querystringParser
78 requestIdLogLabel = options.requestIdLogLabel
79 modifyCoreObjects = options.modifyCoreObjects
80 genReqId = options.genReqId
81 disableRequestLogging = options.disableRequestLogging
82 ignoreTrailingSlash = options.ignoreTrailingSlash
83 return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true
84 },
85 routing: router.lookup.bind(router), // router func to find the right handler to call
86 route, // configure a route in the fastify instance
87 prepareRoute,
88 routeHandler,
89 closeRoutes: () => { closing = true },
90 printRoutes: router.prettyPrint.bind(router)
91 }
92
93 // Convert shorthand to extended route declaration
94 function prepareRoute (method, url, options, handler) {
95 if (!handler && typeof options === 'function') {
96 handler = options
97 options = {}
98 } else if (handler && typeof handler === 'function') {
99 if (Object.prototype.toString.call(options) !== '[object Object]') {
100 throw new Error(`Options for ${method}:${url} route must be an object`)
101 } else if (options.handler) {
102 if (typeof options.handler === 'function') {
103 throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`)
104 } else {
105 throw new Error(`Handler for ${method}:${url} route must be a function`)
106 }
107 }
108 }
109
110 options = Object.assign({}, options, {
111 method,
112 url,
113 handler: handler || (options && options.handler)
114 })
115
116 return route.call(this, options)
117 }
118
119 // Route management
120 function route (opts) {
121 throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
122
123 if (Array.isArray(opts.method)) {
124 for (var i = 0; i < opts.method.length; i++) {
125 if (supportedMethods.indexOf(opts.method[i]) === -1) {
126 throw new Error(`${opts.method[i]} method is not supported!`)
127 }
128 }
129 } else {
130 if (supportedMethods.indexOf(opts.method) === -1) {
131 throw new Error(`${opts.method} method is not supported!`)
132 }
133 }
134
135 if (!opts.handler) {
136 throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
137 }
138
139 validateBodyLimitOption(opts.bodyLimit)
140
141 if (opts.preHandler == null && opts.beforeHandler != null) {
142 beforeHandlerWarning()
143 opts.preHandler = opts.beforeHandler
144 }
145
146 const prefix = this[kRoutePrefix]
147
148 this.after((notHandledErr, done) => {
149 var path = opts.url || opts.path
150 if (path === '/' && prefix.length > 0) {
151 switch (opts.prefixTrailingSlash) {
152 case 'slash':
153 afterRouteAdded.call(this, path, notHandledErr, done)
154 break
155 case 'no-slash':
156 afterRouteAdded.call(this, '', notHandledErr, done)
157 break
158 case 'both':
159 default:
160 afterRouteAdded.call(this, '', notHandledErr, done)
161 // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
162 if (ignoreTrailingSlash !== true) {
163 afterRouteAdded.call(this, path, notHandledErr, done)
164 }
165 }
166 } else if (path[0] === '/' && prefix.endsWith('/')) {
167 // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
168 afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
169 } else {
170 afterRouteAdded.call(this, path, notHandledErr, done)
171 }
172 })
173
174 // chainable api
175 return this
176
177 function afterRouteAdded (path, notHandledErr, done) {
178 const url = prefix + path
179
180 opts.url = url
181 opts.path = url
182 opts.prefix = prefix
183 opts.logLevel = opts.logLevel || this[kLogLevel]
184
185 if (this[kLogSerializers] || opts.logSerializers) {
186 opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
187 }
188
189 if (opts.attachValidation == null) {
190 opts.attachValidation = false
191 }
192
193 // run 'onRoute' hooks
194 for (const hook of this[kGlobalHooks].onRoute) {
195 try {
196 hook.call(this, opts)
197 } catch (error) {
198 done(error)
199 return
200 }
201 }
202
203 const config = opts.config || {}
204 config.url = url
205
206 const context = new Context(
207 opts.schema,
208 opts.handler.bind(this),
209 this[kReply],
210 this[kRequest],
211 this[kContentTypeParser],
212 config,
213 this._errorHandler,
214 opts.bodyLimit,
215 opts.logLevel,
216 opts.logSerializers,
217 opts.attachValidation,
218 this[kReplySerializerDefault]
219 )
220
221 for (const hook of supportedHooks) {
222 if (opts[hook]) {
223 if (Array.isArray(opts[hook])) {
224 opts[hook] = opts[hook].map(fn => fn.bind(this))
225 } else {
226 opts[hook] = opts[hook].bind(this)
227 }
228 }
229 }
230
231 try {
232 router.on(opts.method, opts.url, { version: opts.version }, routeHandler, context)
233 } catch (err) {
234 done(err)
235 return
236 }
237
238 // It can happen that a user register a plugin with some hooks/middlewares *after*
239 // the route registration. To be sure to load also that hooks/middlewares,
240 // we must listen for the avvio's preReady event, and update the context object accordingly.
241 avvio.once('preReady', () => {
242 const onResponse = this[kHooks].onResponse
243 const onSend = this[kHooks].onSend
244 const onError = this[kHooks].onError
245
246 context.onSend = onSend.length ? onSend : null
247 context.onError = onError.length ? onError : null
248 context.onResponse = onResponse.length ? onResponse : null
249
250 for (const hook of supportedHooks) {
251 const toSet = this[kHooks][hook].concat(opts[hook] || [])
252 context[hook] = toSet.length ? toSet : null
253 }
254
255 context._middie = buildMiddie(this[kMiddlewares])
256
257 // Must store the 404 Context in 'preReady' because it is only guaranteed to
258 // be available after all of the plugins and routes have been loaded.
259 fourOhFour.setContext(this, context)
260
261 if (opts.schema) {
262 if (this[kSchemaCompiler] == null && this[kSchemaResolver]) {
263 throw new FST_ERR_SCH_MISSING_COMPILER(opts.method, url)
264 }
265
266 try {
267 if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) {
268 const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true })
269 this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, this[kOptions].ajv, schemaCache))
270 }
271
272 buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas], this[kSchemaResolver])
273 } catch (error) {
274 // bubble up the FastifyError instance
275 throw (error.code ? error : new FST_ERR_SCH_BUILD(opts.method, url, error.message))
276 }
277 }
278 })
279
280 done(notHandledErr)
281 }
282 }
283
284 // HTTP request entry point, the routing has already been executed
285 function routeHandler (req, res, params, context) {
286 if (closing === true) {
287 if (req.httpVersionMajor !== 2) {
288 res.once('finish', () => req.destroy())
289 res.setHeader('Connection', 'close')
290 }
291
292 if (return503OnClosing) {
293 const headers = {
294 'Content-Type': 'application/json',
295 'Content-Length': '80'
296 }
297 res.writeHead(503, headers)
298 res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
299 return
300 }
301 }
302
303 req.id = req.headers[requestIdHeader] || genReqId(req)
304 req.originalUrl = req.url
305 var hostname = req.headers.host || req.headers[':authority']
306 var ip = req.connection.remoteAddress
307 var ips
308
309 if (trustProxy) {
310 ip = proxyAddr(req, proxyFn)
311 ips = proxyAddr.all(req, proxyFn)
312 if (ip !== undefined && req.headers['x-forwarded-host']) {
313 hostname = req.headers['x-forwarded-host']
314 }
315 }
316
317 var loggerOpts = {
318 [requestIdLogLabel]: req.id,
319 level: context.logLevel
320 }
321
322 if (context.logSerializers) {
323 loggerOpts.serializers = context.logSerializers
324 }
325 var childLogger = logger.child(loggerOpts)
326 childLogger[kDisableRequestLogging] = disableRequestLogging
327
328 // added hostname, ip, and ips back to the Node req object to maintain backward compatibility
329 if (modifyCoreObjects) {
330 req.hostname = hostname
331 req.ip = ip
332 req.ips = ips
333
334 req.log = res.log = childLogger
335 }
336
337 if (disableRequestLogging === false) {
338 childLogger.info({ req }, 'incoming request')
339 }
340
341 var queryPrefix = req.url.indexOf('?')
342 var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
343 var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname)
344 var reply = new context.Reply(res, context, request, childLogger)
345
346 if (hasLogger === true || context.onResponse !== null) {
347 setupResponseListeners(reply)
348 }
349
350 if (context.onRequest !== null) {
351 hookRunner(
352 context.onRequest,
353 hookIterator,
354 request,
355 reply,
356 middlewareCallback
357 )
358 } else {
359 middlewareCallback(null, request, reply)
360 }
361 }
362}
363
364function validateBodyLimitOption (bodyLimit) {
365 if (bodyLimit === undefined) return
366 if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
367 throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`)
368 }
369}
370
371function middlewareCallback (err, request, reply) {
372 if (reply.sent === true) return
373 if (err != null) {
374 reply.send(err)
375 return
376 }
377
378 if (reply.context._middie !== null) {
379 reply.context._middie.run(request.raw, reply.res, reply)
380 } else {
381 onRunMiddlewares(null, null, null, reply)
382 }
383}
384
385function getTrustProxyFn (options) {
386 const tp = options.trustProxy
387 if (typeof tp === 'function') {
388 return tp
389 }
390 if (tp === true) {
391 // Support plain true/false
392 return function () { return true }
393 }
394 if (typeof tp === 'number') {
395 // Support trusting hop count
396 return function (a, i) { return i < tp }
397 }
398 if (typeof tp === 'string') {
399 // Support comma-separated tps
400 const vals = tp.split(',').map(it => it.trim())
401 return proxyAddr.compile(vals)
402 }
403 return proxyAddr.compile(tp || [])
404}
405
406module.exports = { buildRouting, validateBodyLimitOption }