fastify 2.14.1 → 2.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/Hooks.md CHANGED
@@ -18,6 +18,7 @@ By using hooks you can interact directly with the lifecycle of Fastify. There ar
18
18
  - [Manage Errors from a hook](#manage-errors-from-a-hook)
19
19
  - [Respond to a request from a hook](#respond-to-a-request-from-a-hook)
20
20
  - [Application Hooks](#application-hooks)
21
+ - [onReady](#onready)
21
22
  - [onClose](#onclose)
22
23
  - [onRoute](#onroute)
23
24
  - [onRegister](#onregister)
@@ -107,7 +108,7 @@ If you are using the `preSerialization` hook, you can change (or replace) the pa
107
108
 
108
109
  ```js
109
110
  fastify.addHook('preSerialization', (request, reply, payload, done) => {
110
- const err = null;
111
+ const err = null
111
112
  const newPayload = { wrapped: payload }
112
113
  done(err, newPayload)
113
114
  })
@@ -282,12 +283,34 @@ fastify.addHook('preHandler', async (request, reply) => {
282
283
 
283
284
  You can hook into the application-lifecycle as well. It's important to note that these hooks aren't fully encapsulated. The `this` inside the hooks are encapsulated but the handlers can respond to an event outside the encapsulation boundaries.
284
285
 
286
+ - [onReady](#onready)
285
287
  - [onClose](#onclose)
286
288
  - [onRoute](#onroute)
287
289
  - [onRegister](#onregister)
288
290
 
289
- <a name="on-close"></a>
291
+ ### onReady
292
+ Triggered before the server starts listening for requests. It cannot change the routes or add new hooks.
293
+ Registered hook functions are executed serially.
294
+ Only after all `onReady` hook functions have completed will the server start listening for requests.
295
+ Hook functions accept one argument: a callback, `done`, to be invoked after the hook function is complete.
296
+ Hook functions are invoked with `this` bound to the associated Fastify instance.
297
+
298
+ ```js
299
+ // callback style
300
+ fastify.addHook('onReady', function (done) {
301
+ // Some code
302
+ const err = null;
303
+ done(err)
304
+ })
305
+
306
+ // or async/await style
307
+ fastify.addHook('onReady', async function () {
308
+ // Some async code
309
+ await loadCacheFromDatabase()
310
+ })
311
+ ```
290
312
 
313
+ <a name="on-close"></a>
291
314
  ### onClose
292
315
  Triggered when `fastify.close()` is invoked to stop the server. It is useful when [plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins.md) need a "shutdown" event, for example to close an open connection to a database.<br>
293
316
  The first argument is the Fastify instance, the second one the `done` callback.
@@ -297,6 +320,7 @@ fastify.addHook('onClose', (instance, done) => {
297
320
  done()
298
321
  })
299
322
  ```
323
+
300
324
  <a name="on-route"></a>
301
325
  ### onRoute
302
326
  Triggered when a new route is registered. Listeners are passed a `routeOptions` object as the sole parameter. The interface is synchronous, and, as such, the listeners do not get passed a callback.
package/docs/Server.md CHANGED
@@ -748,13 +748,15 @@ fastify.register(function (instance, options, done) {
748
748
  <a name="set-error-handler"></a>
749
749
  #### setErrorHandler
750
750
 
751
- `fastify.setErrorHandler(handler(error, request, reply))`: Set a function that will be called whenever an error happens. The handler is fully encapsulated, so different plugins can set different error handlers. *async-await* is supported as well.<br>
751
+ `fastify.setErrorHandler(handler(error, request, reply))`: Set a function that will be called whenever an error happens. The handler is bound to the Fastify instance, and is fully encapsulated, so different plugins can set different error handlers. *async-await* is supported as well.<br>
752
752
  *Note: If the error `statusCode` is less than 400, Fastify will automatically set it at 500 before calling the error handler.*
753
753
 
754
754
  ```js
755
755
  fastify.setErrorHandler(function (error, request, reply) {
756
756
  // Log error
757
+ this.log.error(error)
757
758
  // Send error response
759
+ reply.status(409).send({ ok: false })
758
760
  })
759
761
  ```
760
762
 
@@ -14,7 +14,7 @@ Fastify uses a schema-based approach, and even if it is not mandatory we recomme
14
14
  <a name="validation"></a>
15
15
  ### Validation
16
16
  The route validation internally relies upon [Ajv](https://www.npmjs.com/package/ajv), which is a high-performance JSON schema validator. Validating the input is very easy: just add the fields that you need inside the route schema, and you are done! The supported validations are:
17
- - `body`: validates the body of the request if it is a POST or a PUT.
17
+ - `body`: validates the body of the request if it is a POST, a PATCH or a PUT.
18
18
  - `querystring` or `query`: validates the query string. This can be a complete JSON Schema object (with a `type` property of `'object'` and a `'properties'` object containing parameters) or a simpler variation in which the `type` and `properties` attributes are forgone and the query parameters are listed at the top level (see the example below).
19
19
  - `params`: validates the route params.
20
20
  - `headers`: validates the request headers.
@@ -338,7 +338,6 @@ Fastify's [baseline ajv configuration](https://github.com/epoberezkin/ajv#option
338
338
  removeAdditional: true, // remove additional properties
339
339
  useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
340
340
  coerceTypes: true, // change data type of data to match type keyword
341
- allErrors: true, // check for all errors
342
341
  nullable: true // support keyword "nullable" from Open API 3 specification.
343
342
  }
344
343
  ```
@@ -355,7 +354,6 @@ const ajv = new Ajv({
355
354
  removeAdditional: true,
356
355
  useDefaults: true,
357
356
  coerceTypes: true,
358
- allErrors: true,
359
357
  nullable: true,
360
358
  // any other options
361
359
  // ...
@@ -666,7 +664,7 @@ Inline comments in the schema below describe how to configure it to show a diffe
666
664
  ```js
667
665
  const fastify = Fastify({
668
666
  ajv: {
669
- customOptions: { allErrors: true, jsonPointers: true },
667
+ customOptions: { jsonPointers: true },
670
668
  plugins: [
671
669
  require('ajv-errors')
672
670
  ]
@@ -713,11 +711,7 @@ If you want to return localized error messages, take a look at [ajv-i18n](https:
713
711
  ```js
714
712
  const localize = require('ajv-i18n')
715
713
 
716
- const fastify = Fastify({
717
- ajv: {
718
- customOptions: { allErrors: true }
719
- }
720
- })
714
+ const fastify = Fastify()
721
715
 
722
716
  const schema = {
723
717
  body: {
package/fastify.d.ts CHANGED
@@ -667,7 +667,7 @@ declare namespace fastify {
667
667
  /**
668
668
  * Set a function that will be called whenever an error happens
669
669
  */
670
- setErrorHandler<E = FastifyError>(handler: (error: E, request: FastifyRequest<HttpRequest>, reply: FastifyReply<HttpResponse>) => void): void
670
+ setErrorHandler<E = FastifyError>(handler: (this: FastifyInstance<HttpServer, HttpRequest, HttpResponse>, error: E, request: FastifyRequest<HttpRequest>, reply: FastifyReply<HttpResponse>) => void): void
671
671
 
672
672
  /**
673
673
  * Set a function that will be called whenever an error happens
package/fastify.js CHANGED
@@ -6,6 +6,7 @@ const querystring = require('querystring')
6
6
  let lightMyRequest
7
7
 
8
8
  const {
9
+ kAvvioBoot,
9
10
  kChildren,
10
11
  kBodyLimit,
11
12
  kRoutePrefix,
@@ -33,7 +34,7 @@ const Request = require('./lib/request')
33
34
  const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
34
35
  const decorator = require('./lib/decorate')
35
36
  const ContentTypeParser = require('./lib/contentTypeParser')
36
- const { Hooks, buildHooks } = require('./lib/hooks')
37
+ const { Hooks, buildHooks, hookRunnerApplication } = require('./lib/hooks')
37
38
  const { Schemas, buildSchemas } = require('./lib/schemas')
38
39
  const { createLogger } = require('./lib/logger')
39
40
  const pluginUtils = require('./lib/pluginUtils')
@@ -166,6 +167,7 @@ function build (options) {
166
167
  },
167
168
  [pluginUtils.registeredPlugins]: [],
168
169
  [kPluginNameChain]: [],
170
+ [kAvvioBoot]: null,
169
171
  // routes shorthand methods
170
172
  delete: function _delete (url, opts, handler) {
171
173
  return router.prepareRoute.call(this, 'DELETE', url, opts, handler)
@@ -285,6 +287,8 @@ function build (options) {
285
287
  // Override to allow the plugin incapsulation
286
288
  avvio.override = override
287
289
  avvio.on('start', () => (fastify[kState].started = true))
290
+ fastify[kAvvioBoot] = fastify.ready // the avvio ready function
291
+ fastify.ready = ready // overwrite the avvio ready function
288
292
  // cache the closing value, since we are checking it in an hot path
289
293
  avvio.once('preReady', () => {
290
294
  fastify.onClose((instance, done) => {
@@ -348,8 +352,15 @@ function build (options) {
348
352
  else lightMyRequest(httpHandler, opts, cb)
349
353
  })
350
354
  } else {
351
- return this.ready()
352
- .then(() => lightMyRequest(httpHandler, opts))
355
+ return lightMyRequest((req, res) => {
356
+ this.ready(function (err) {
357
+ if (err) {
358
+ res.emit('error', err)
359
+ return
360
+ }
361
+ httpHandler(req, res)
362
+ })
363
+ }, opts)
353
364
  }
354
365
  }
355
366
 
@@ -371,6 +382,48 @@ function build (options) {
371
382
  }
372
383
  }
373
384
 
385
+ function ready (cb) {
386
+ let resolveReady
387
+ let rejectReady
388
+
389
+ // run the hooks after returning the promise
390
+ process.nextTick(runHooks)
391
+
392
+ if (!cb) {
393
+ return new Promise(function (resolve, reject) {
394
+ resolveReady = resolve
395
+ rejectReady = reject
396
+ })
397
+ }
398
+
399
+ function runHooks () {
400
+ // start loading
401
+ fastify[kAvvioBoot]((err, done) => {
402
+ if (err || fastify[kState].started) {
403
+ manageErr(err)
404
+ } else {
405
+ hookRunnerApplication('onReady', fastify[kAvvioBoot], fastify, manageErr)
406
+ }
407
+ done()
408
+ })
409
+ }
410
+
411
+ function manageErr (err) {
412
+ if (cb) {
413
+ if (err) {
414
+ cb(err)
415
+ } else {
416
+ cb(undefined, fastify)
417
+ }
418
+ } else {
419
+ if (err) {
420
+ return rejectReady(err)
421
+ }
422
+ resolveReady(fastify)
423
+ }
424
+ }
425
+ }
426
+
374
427
  // wrapper that we expose to the user for hooks handling
375
428
  function addHook (name, fn) {
376
429
  throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!')
@@ -380,6 +433,10 @@ function build (options) {
380
433
  if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) {
381
434
  fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
382
435
  }
436
+ } else if (name === 'onReady') {
437
+ if (fn.constructor.name === 'AsyncFunction' && fn.length !== 0) {
438
+ throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.')
439
+ }
383
440
  } else {
384
441
  if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
385
442
  fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack)
@@ -395,6 +452,9 @@ function build (options) {
395
452
  } else if (name === 'onRegister') {
396
453
  this[kHooks].validate(name, fn)
397
454
  this[kGlobalHooks].onRegister.push(fn)
455
+ } else if (name === 'onReady') {
456
+ this[kHooks].validate(name, fn)
457
+ this[kHooks].add(name, fn)
398
458
  } else {
399
459
  this.after((err, done) => {
400
460
  _addHook.call(this, name, fn)
@@ -495,7 +555,7 @@ function build (options) {
495
555
  function setErrorHandler (func) {
496
556
  throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!')
497
557
 
498
- this._errorHandler = func
558
+ this._errorHandler = func.bind(this)
499
559
  return this
500
560
  }
501
561
  }
@@ -513,6 +573,7 @@ function override (old, fn, opts) {
513
573
 
514
574
  const instance = Object.create(old)
515
575
  old[kChildren].push(instance)
576
+ instance.ready = old[kAvvioBoot].bind(instance)
516
577
  instance[kChildren] = []
517
578
  instance[kReply] = Reply.buildReply(instance[kReply])
518
579
  instance[kRequest] = Request.buildRequest(instance[kRequest])
package/lib/hooks.js CHANGED
@@ -12,6 +12,7 @@ const supportedHooks = [
12
12
  // executed at start/close time
13
13
  'onRoute',
14
14
  'onRegister',
15
+ 'onReady',
15
16
  'onClose'
16
17
  ]
17
18
  const {
@@ -22,6 +23,11 @@ const {
22
23
  }
23
24
  } = require('./errors')
24
25
 
26
+ const {
27
+ kChildren,
28
+ kHooks
29
+ } = require('./symbols')
30
+
25
31
  function Hooks () {
26
32
  this.onRequest = []
27
33
  this.preParsing = []
@@ -31,6 +37,7 @@ function Hooks () {
31
37
  this.onResponse = []
32
38
  this.onSend = []
33
39
  this.onError = []
40
+ this.onReady = []
34
41
  }
35
42
 
36
43
  Hooks.prototype.validate = function (hook, fn) {
@@ -56,9 +63,80 @@ function buildHooks (h) {
56
63
  hooks.onSend = h.onSend.slice()
57
64
  hooks.onResponse = h.onResponse.slice()
58
65
  hooks.onError = h.onError.slice()
66
+ hooks.onReady = []
59
67
  return hooks
60
68
  }
61
69
 
70
+ function hookRunnerApplication (hookName, boot, server, cb) {
71
+ const hooks = server[kHooks][hookName]
72
+ var i = 0
73
+ var c = 0
74
+
75
+ next()
76
+
77
+ function exit (err) {
78
+ if (err) {
79
+ cb(err)
80
+ return
81
+ }
82
+ cb()
83
+ }
84
+
85
+ function next (err) {
86
+ if (err) {
87
+ exit(err)
88
+ return
89
+ }
90
+
91
+ if (i === hooks.length && c === server[kChildren].length) {
92
+ if (i === 0 && c === 0) { // speed up start
93
+ exit()
94
+ } else {
95
+ boot(function manageTimeout (err, done) {
96
+ exit(err) // reply to the client's cb
97
+ done(err) // goahead with the avvio line
98
+ })
99
+ }
100
+ return
101
+ }
102
+
103
+ if (i === hooks.length && c < server[kChildren].length) {
104
+ const child = server[kChildren][c++]
105
+ hookRunnerApplication(hookName, boot, child, next)
106
+ return
107
+ }
108
+
109
+ boot(wrap(hooks[i++], server))
110
+ next()
111
+ }
112
+
113
+ function wrap (fn, server) {
114
+ return function (err, done) {
115
+ if (err) {
116
+ done(err)
117
+ return
118
+ }
119
+
120
+ if (fn.length === 1) {
121
+ try {
122
+ fn.call(server, done)
123
+ } catch (error) {
124
+ done(error)
125
+ }
126
+ return
127
+ }
128
+
129
+ const ret = fn.call(server)
130
+ if (ret && typeof ret.then === 'function') {
131
+ ret.then(done, done)
132
+ return
133
+ }
134
+
135
+ done(err) // auto done
136
+ }
137
+ }
138
+ }
139
+
62
140
  function hookRunner (functions, runner, request, reply, cb) {
63
141
  var i = 0
64
142
 
@@ -131,5 +209,6 @@ module.exports = {
131
209
  buildHooks,
132
210
  hookRunner,
133
211
  onSendHookRunner,
134
- hookIterator
212
+ hookIterator,
213
+ hookRunnerApplication
135
214
  }
package/lib/reply.js CHANGED
@@ -138,9 +138,30 @@ Reply.prototype.send = function (payload) {
138
138
 
139
139
  if (this[kReplySerializer] !== null) {
140
140
  payload = this[kReplySerializer](payload)
141
- } else if (hasContentType === false || contentType.indexOf('application/json') > -1) {
142
- if (hasContentType === false || contentType.indexOf('charset') === -1) {
141
+
142
+ // The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
143
+ } else if (hasContentType === false || contentType.indexOf('json') > -1) {
144
+ if (hasContentType === false) {
143
145
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
146
+ } else {
147
+ // If hasContentType === true, we have a JSON mimetype
148
+ if (contentType.indexOf('charset') === -1) {
149
+ // If we have simply application/json instead of a custom json mimetype
150
+ if (contentType.indexOf('/json') > -1) {
151
+ this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
152
+ } else {
153
+ const currContentType = this[kReplyHeaders]['content-type']
154
+ // We extract the custom mimetype part (e.g. 'hal+' from 'application/hal+json')
155
+ const customJsonType = currContentType.substring(
156
+ currContentType.indexOf('/'),
157
+ currContentType.indexOf('json') + 4
158
+ )
159
+
160
+ // We ensure we set the header to the proper JSON content-type if necessary
161
+ // (e.g. 'application/hal+json' instead of 'application/json')
162
+ this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON.replace('/json', customJsonType)
163
+ }
164
+ }
144
165
  }
145
166
  if (typeof payload !== 'string') {
146
167
  preserializeHook(this, payload)
package/lib/symbols.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const keys = {
4
+ kAvvioBoot: Symbol('fastify.avvioBoot'),
4
5
  kChildren: Symbol('fastify.children'),
5
6
  kBodyLimit: Symbol('fastify.bodyLimit'),
6
7
  kRoutePrefix: Symbol('fastify.routePrefix'),
package/lib/validation.js CHANGED
@@ -171,7 +171,9 @@ function buildSchemaCompiler (externalSchemas, options, cache) {
171
171
  coerceTypes: true,
172
172
  useDefaults: true,
173
173
  removeAdditional: true,
174
- allErrors: true,
174
+ // Explicitly set allErrors to `false`.
175
+ // When set to `true`, a DoS attack is possible.
176
+ allErrors: false,
175
177
  nullable: true
176
178
  }, options.customOptions, { cache }))
177
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "2.14.1",
3
+ "version": "2.15.3",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "typings": "fastify.d.ts",
@@ -143,7 +143,7 @@
143
143
  "dependencies": {
144
144
  "abstract-logging": "^2.0.0",
145
145
  "ajv": "^6.12.0",
146
- "avvio": "^6.3.1",
146
+ "avvio": "^6.5.0",
147
147
  "fast-json-stringify": "^1.18.0",
148
148
  "find-my-way": "^2.2.2",
149
149
  "flatstr": "^1.0.12",
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ const Fastify = require('../fastify')
4
+ const immediate = require('util').promisify(setImmediate)
5
+
6
+ module.exports = function asyncTest (t) {
7
+ t.test('async onReady should be called in order', t => {
8
+ t.plan(7)
9
+ const fastify = Fastify()
10
+
11
+ let order = 0
12
+
13
+ fastify.addHook('onReady', async function () {
14
+ await immediate()
15
+ t.equals(order++, 0, 'called in root')
16
+ t.equals(this.pluginName, fastify.pluginName, 'the this binding is the right instance')
17
+ })
18
+
19
+ fastify.register(async (childOne, o) => {
20
+ childOne.addHook('onReady', async function () {
21
+ await immediate()
22
+ t.equals(order++, 1, 'called in childOne')
23
+ t.equals(this.pluginName, childOne.pluginName, 'the this binding is the right instance')
24
+ })
25
+
26
+ childOne.register(async (childTwo, o) => {
27
+ childTwo.addHook('onReady', async function () {
28
+ await immediate()
29
+ t.equals(order++, 2, 'called in childTwo')
30
+ t.equals(this.pluginName, childTwo.pluginName, 'the this binding is the right instance')
31
+ })
32
+ })
33
+ })
34
+
35
+ return fastify.ready().then(() => { t.pass('ready') })
36
+ })
37
+
38
+ t.test('mix ready and onReady', t => {
39
+ t.plan(2)
40
+ const fastify = Fastify()
41
+ let order = 0
42
+
43
+ fastify.addHook('onReady', async function () {
44
+ await immediate()
45
+ order++
46
+ })
47
+
48
+ return fastify.ready()
49
+ .then(() => {
50
+ t.equals(order, 1)
51
+ return fastify.ready()
52
+ })
53
+ .then(() => {
54
+ t.equals(order, 1, 'ready hooks execute once')
55
+ })
56
+ })
57
+
58
+ t.test('listen and onReady order', async t => {
59
+ t.plan(9)
60
+
61
+ const fastify = Fastify()
62
+ let order = 0
63
+
64
+ fastify.register((instance, opts, next) => {
65
+ instance.ready(checkOrder.bind(null, 0))
66
+ instance.addHook('onReady', checkOrder.bind(null, 4))
67
+
68
+ instance.register((subinstance, opts, next) => {
69
+ subinstance.ready(checkOrder.bind(null, 1))
70
+ subinstance.addHook('onReady', checkOrder.bind(null, 5))
71
+
72
+ subinstance.register((realSubInstance, opts, next) => {
73
+ realSubInstance.ready(checkOrder.bind(null, 2))
74
+ realSubInstance.addHook('onReady', checkOrder.bind(null, 6))
75
+ next()
76
+ })
77
+ next()
78
+ })
79
+ next()
80
+ })
81
+
82
+ fastify.addHook('onReady', checkOrder.bind(null, 3))
83
+
84
+ await fastify.ready()
85
+ t.pass('trigger the onReady')
86
+ await fastify.listen(0)
87
+ t.pass('do not trigger the onReady')
88
+
89
+ await fastify.close()
90
+
91
+ function checkOrder (shouldbe) {
92
+ t.equals(order, shouldbe)
93
+ order++
94
+ }
95
+ })
96
+
97
+ t.test('multiple ready calls', async t => {
98
+ t.plan(11)
99
+
100
+ const fastify = Fastify()
101
+ let order = 0
102
+
103
+ fastify.register(async (instance, opts) => {
104
+ instance.ready(checkOrder.bind(null, 1))
105
+ instance.addHook('onReady', checkOrder.bind(null, 6))
106
+
107
+ await instance.register(async (subinstance, opts) => {
108
+ subinstance.ready(checkOrder.bind(null, 2))
109
+ subinstance.addHook('onReady', checkOrder.bind(null, 7))
110
+ })
111
+
112
+ t.equals(order, 0, 'ready and hooks not triggered yet')
113
+ order++
114
+ })
115
+
116
+ fastify.addHook('onReady', checkOrder.bind(null, 3))
117
+ fastify.addHook('onReady', checkOrder.bind(null, 4))
118
+ fastify.addHook('onReady', checkOrder.bind(null, 5))
119
+
120
+ await fastify.ready()
121
+ t.pass('trigger the onReady')
122
+
123
+ await fastify.ready()
124
+ t.pass('do not trigger the onReady')
125
+
126
+ await fastify.ready()
127
+ t.pass('do not trigger the onReady')
128
+
129
+ function checkOrder (shouldbe) {
130
+ t.equals(order, shouldbe)
131
+ order++
132
+ }
133
+ })
134
+
135
+ t.test('onReady should manage error in async', t => {
136
+ t.plan(4)
137
+ const fastify = Fastify()
138
+
139
+ fastify.addHook('onReady', function (done) {
140
+ t.pass('called in root')
141
+ done()
142
+ })
143
+
144
+ fastify.register(async (childOne, o) => {
145
+ childOne.addHook('onReady', async function () {
146
+ t.pass('called in childOne')
147
+ throw new Error('FAIL ON READY')
148
+ })
149
+
150
+ childOne.register(async (childTwo, o) => {
151
+ childTwo.addHook('onReady', async function () {
152
+ t.fail('should not be called')
153
+ })
154
+ })
155
+ })
156
+
157
+ fastify.ready(err => {
158
+ t.ok(err)
159
+ t.equals(err.message, 'FAIL ON READY')
160
+ })
161
+ })
162
+
163
+ t.test('onReady throw loading error', t => {
164
+ t.plan(1)
165
+ const fastify = Fastify()
166
+
167
+ try {
168
+ fastify.addHook('onReady', async function (done) {})
169
+ } catch (e) {
170
+ t.true(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.')
171
+ }
172
+ })
173
+ }
@@ -0,0 +1,236 @@
1
+ 'use strict'
2
+
3
+ const semver = require('semver')
4
+ const t = require('tap')
5
+ const Fastify = require('../fastify')
6
+
7
+ if (semver.gt(process.versions.node, '8.0.0')) {
8
+ require('./hooks.on-ready.async')(t)
9
+ } else {
10
+ t.test('async tests', t => {
11
+ t.plan(1)
12
+ t.pass('Skip because Node version < 8')
13
+ })
14
+ }
15
+
16
+ t.test('onReady should be called in order', t => {
17
+ t.plan(7)
18
+ const fastify = Fastify()
19
+
20
+ let order = 0
21
+
22
+ fastify.addHook('onReady', function (done) {
23
+ t.equals(order++, 0, 'called in root')
24
+ t.equals(this.pluginName, fastify.pluginName, 'the this binding is the right instance')
25
+ done()
26
+ })
27
+
28
+ fastify.register((childOne, o, next) => {
29
+ childOne.addHook('onReady', function (done) {
30
+ t.equals(order++, 1, 'called in childOne')
31
+ t.equals(this.pluginName, childOne.pluginName, 'the this binding is the right instance')
32
+ done()
33
+ })
34
+
35
+ childOne.register((childTwo, o, next) => {
36
+ childTwo.addHook('onReady', function () {
37
+ return new Promise((resolve) => {
38
+ setTimeout(resolve, 0)
39
+ })
40
+ .then(() => {
41
+ t.equals(order++, 2, 'called in childTwo')
42
+ t.equals(this.pluginName, childTwo.pluginName, 'the this binding is the right instance')
43
+ })
44
+ })
45
+ next()
46
+ })
47
+ next()
48
+ })
49
+
50
+ fastify.ready(err => t.error(err))
51
+ })
52
+
53
+ t.test('onReady should manage error in sync', t => {
54
+ t.plan(4)
55
+ const fastify = Fastify()
56
+
57
+ fastify.addHook('onReady', function (done) {
58
+ t.pass('called in root')
59
+ done()
60
+ })
61
+
62
+ fastify.register((childOne, o, next) => {
63
+ childOne.addHook('onReady', function (done) {
64
+ t.pass('called in childOne')
65
+ done(new Error('FAIL ON READY'))
66
+ })
67
+
68
+ childOne.register((childTwo, o, next) => {
69
+ childTwo.addHook('onReady', function () {
70
+ return Promise().resolve()
71
+ .then(() => {
72
+ t.fail('should not be called')
73
+ })
74
+ })
75
+ next()
76
+ })
77
+
78
+ next()
79
+ })
80
+
81
+ fastify.ready(err => {
82
+ t.ok(err)
83
+ t.equals(err.message, 'FAIL ON READY')
84
+ })
85
+ })
86
+
87
+ t.test('onReady should manage sync error', t => {
88
+ t.plan(4)
89
+ const fastify = Fastify()
90
+
91
+ fastify.addHook('onReady', function (done) {
92
+ t.pass('called in root')
93
+ done()
94
+ })
95
+
96
+ fastify.register((childOne, o, next) => {
97
+ childOne.addHook('onReady', function (done) {
98
+ t.pass('called in childOne')
99
+ throw new Error('FAIL UNWANTED SYNC EXCEPTION')
100
+ })
101
+
102
+ childOne.register((childTwo, o, next) => {
103
+ childTwo.addHook('onReady', function () {
104
+ t.fail('should not be called')
105
+ })
106
+ next()
107
+ })
108
+
109
+ next()
110
+ })
111
+
112
+ fastify.ready(err => {
113
+ t.ok(err)
114
+ t.equals(err.message, 'FAIL UNWANTED SYNC EXCEPTION')
115
+ })
116
+ })
117
+
118
+ t.test('onReady can not add decorators or application hooks', t => {
119
+ t.plan(3)
120
+ const fastify = Fastify()
121
+
122
+ fastify.addHook('onReady', function (done) {
123
+ t.pass('called in root')
124
+ fastify.decorate('test', () => {})
125
+
126
+ fastify.addHook('onReady', function () {
127
+ t.fail('it will be not called')
128
+ })
129
+ done()
130
+ })
131
+
132
+ fastify.addHook('onReady', function (done) {
133
+ t.ok(this.hasDecorator('test'))
134
+ done()
135
+ })
136
+
137
+ fastify.ready(err => { t.error(err) })
138
+ })
139
+
140
+ t.test('onReady cannot add lifecycle hooks', t => {
141
+ t.plan(4)
142
+ const fastify = Fastify()
143
+
144
+ fastify.addHook('onReady', function (done) {
145
+ t.pass('called in root')
146
+ try {
147
+ fastify.addHook('onRequest', (request, reply, done) => {})
148
+ } catch (error) {
149
+ t.ok(error)
150
+ t.equals(error.message, 'root plugin has already booted')
151
+ done(error)
152
+ }
153
+ })
154
+
155
+ fastify.addHook('onRequest', (request, reply, done) => {})
156
+ fastify.get('/', (r, reply) => reply.send('hello'))
157
+
158
+ fastify.ready((err) => { t.ok(err) })
159
+ })
160
+
161
+ t.test('onReady does not call done', t => {
162
+ t.plan(3)
163
+ const fastify = Fastify({ pluginTimeout: 500 })
164
+
165
+ fastify.addHook('onReady', function (done) {
166
+ t.pass('called in root')
167
+ // done() // don't call done to test timeout
168
+ })
169
+
170
+ fastify.ready(err => {
171
+ t.ok(err)
172
+ t.equal(err.code, 'ERR_AVVIO_READY_TIMEOUT')
173
+ })
174
+ })
175
+
176
+ t.test('ready chain order when no await', t => {
177
+ t.plan(3)
178
+ const fastify = Fastify({ })
179
+
180
+ let i = 0
181
+ fastify.ready(() => {
182
+ i++
183
+ t.equals(i, 1)
184
+ })
185
+ fastify.ready(() => {
186
+ i++
187
+ t.equals(i, 2)
188
+ })
189
+ fastify.ready(() => {
190
+ i++
191
+ t.equals(i, 3)
192
+ })
193
+ })
194
+
195
+ t.test('ready return the server with callback', t => {
196
+ t.plan(2)
197
+ const fastify = Fastify()
198
+
199
+ fastify.ready((err, instance) => {
200
+ t.error(err)
201
+ t.deepEquals(instance, fastify)
202
+ })
203
+ })
204
+
205
+ t.test('ready return the server with Promise', t => {
206
+ t.plan(1)
207
+ const fastify = Fastify()
208
+
209
+ fastify.ready()
210
+ .then(instance => { t.deepEquals(instance, fastify) })
211
+ .catch(err => { t.fail(err) })
212
+ })
213
+
214
+ t.test('ready return registered', t => {
215
+ t.plan(4)
216
+ const fastify = Fastify()
217
+
218
+ fastify.register((one, opts, next) => {
219
+ one.ready().then(itself => { t.deepEquals(itself, one) })
220
+ next()
221
+ })
222
+
223
+ fastify.register((two, opts, next) => {
224
+ two.ready().then(itself => { t.deepEquals(itself, two) })
225
+
226
+ two.register((twoDotOne, opts, next) => {
227
+ twoDotOne.ready().then(itself => { t.deepEquals(itself, twoDotOne) })
228
+ next()
229
+ })
230
+ next()
231
+ })
232
+
233
+ fastify.ready()
234
+ .then(instance => { t.deepEquals(instance, fastify) })
235
+ .catch(err => { t.fail(err) })
236
+ })
@@ -409,3 +409,18 @@ test('should throw error if callback specified and if ready errors', t => {
409
409
  t.strictEqual(err, error)
410
410
  })
411
411
  })
412
+
413
+ test('should support builder-style injection with non-ready app', (t) => {
414
+ t.plan(3)
415
+ const fastify = Fastify()
416
+ const payload = { hello: 'world' }
417
+
418
+ fastify.get('/', (req, reply) => {
419
+ reply.send(payload)
420
+ })
421
+ fastify.inject().get('/').end().then((res) => {
422
+ t.deepEqual(payload, JSON.parse(res.payload))
423
+ t.strictEqual(res.statusCode, 200)
424
+ t.strictEqual(res.headers['content-length'], '17')
425
+ })
426
+ })
@@ -544,6 +544,57 @@ test('plain string with content type application/json should NOT be serialized a
544
544
  })
545
545
  })
546
546
 
547
+ test('plain string with custom json content type should NOT be serialized as json', t => {
548
+ t.plan(16)
549
+
550
+ const fastify = require('../..')()
551
+
552
+ const customSamples = {
553
+ collectionjson: {
554
+ mimeType: 'application/vnd.collection+json',
555
+ sample: '{"collection":{"version":"1.0","href":"http://api.example.com/people/"}}'
556
+ },
557
+ hal: {
558
+ mimeType: 'application/hal+json',
559
+ sample: '{"_links":{"self":{"href":"https://api.example.com/people/1"}},"name":"John Doe"}'
560
+ },
561
+ jsonapi: {
562
+ mimeType: 'application/vnd.api+json',
563
+ sample: '{"data":{"type":"people","id":"1"}}'
564
+ },
565
+ jsonld: {
566
+ mimeType: 'application/ld+json',
567
+ sample: '{"@context":"https://json-ld.org/contexts/person.jsonld","name":"John Doe"}'
568
+ },
569
+ siren: {
570
+ mimeType: 'application/vnd.siren+json',
571
+ sample: '{"class":"person","properties":{"name":"John Doe"}}'
572
+ }
573
+ }
574
+
575
+ Object.keys(customSamples).forEach((path) => {
576
+ fastify.get(`/${path}`, function (req, reply) {
577
+ reply.type(customSamples[path].mimeType).send(customSamples[path].sample)
578
+ })
579
+ })
580
+
581
+ fastify.listen(0, err => {
582
+ t.error(err)
583
+ fastify.server.unref()
584
+
585
+ Object.keys(customSamples).forEach((path) => {
586
+ sget({
587
+ method: 'GET',
588
+ url: 'http://localhost:' + fastify.server.address().port + '/' + path
589
+ }, (err, response, body) => {
590
+ t.error(err)
591
+ t.strictEqual(response.headers['content-type'], customSamples[path].mimeType + '; charset=utf-8')
592
+ t.deepEqual(body.toString(), customSamples[path].sample)
593
+ })
594
+ })
595
+ })
596
+ })
597
+
547
598
  test('non-string with content type application/json SHOULD be serialized as json', t => {
548
599
  t.plan(4)
549
600
 
@@ -568,6 +619,57 @@ test('non-string with content type application/json SHOULD be serialized as json
568
619
  })
569
620
  })
570
621
 
622
+ test('non-string with custom json content type SHOULD be serialized as json', t => {
623
+ t.plan(16)
624
+
625
+ const fastify = require('../..')()
626
+
627
+ const customSamples = {
628
+ collectionjson: {
629
+ mimeType: 'application/vnd.collection+json',
630
+ sample: JSON.parse('{"collection":{"version":"1.0","href":"http://api.example.com/people/"}}')
631
+ },
632
+ hal: {
633
+ mimeType: 'application/hal+json',
634
+ sample: JSON.parse('{"_links":{"self":{"href":"https://api.example.com/people/1"}},"name":"John Doe"}')
635
+ },
636
+ jsonapi: {
637
+ mimeType: 'application/vnd.api+json',
638
+ sample: JSON.parse('{"data":{"type":"people","id":"1"}}')
639
+ },
640
+ jsonld: {
641
+ mimeType: 'application/ld+json',
642
+ sample: JSON.parse('{"@context":"https://json-ld.org/contexts/person.jsonld","name":"John Doe"}')
643
+ },
644
+ siren: {
645
+ mimeType: 'application/vnd.siren+json',
646
+ sample: JSON.parse('{"class":"person","properties":{"name":"John Doe"}}')
647
+ }
648
+ }
649
+
650
+ Object.keys(customSamples).forEach((path) => {
651
+ fastify.get(`/${path}`, function (req, reply) {
652
+ reply.type(customSamples[path].mimeType).send(customSamples[path].sample)
653
+ })
654
+ })
655
+
656
+ fastify.listen(0, err => {
657
+ t.error(err)
658
+ fastify.server.unref()
659
+
660
+ Object.keys(customSamples).forEach((path) => {
661
+ sget({
662
+ method: 'GET',
663
+ url: 'http://localhost:' + fastify.server.address().port + '/' + path
664
+ }, (err, response, body) => {
665
+ t.error(err)
666
+ t.strictEqual(response.headers['content-type'], customSamples[path].mimeType + '; charset=utf-8')
667
+ t.deepEqual(body.toString(), JSON.stringify(customSamples[path].sample))
668
+ })
669
+ })
670
+ })
671
+ })
672
+
571
673
  test('error object with a content type that is not application/json should work', t => {
572
674
  t.plan(6)
573
675
 
@@ -73,3 +73,84 @@ test('default 400 on request error with custom error handler', t => {
73
73
  })
74
74
  })
75
75
  })
76
+
77
+ test('error handler binding', t => {
78
+ t.plan(5)
79
+
80
+ const fastify = Fastify()
81
+
82
+ fastify.setErrorHandler(function (err, request, reply) {
83
+ t.strictEqual(this, fastify)
84
+ reply
85
+ .code(err.statusCode)
86
+ .type('application/json; charset=utf-8')
87
+ .send(err)
88
+ })
89
+
90
+ fastify.post('/', function (req, reply) {
91
+ reply.send({ hello: 'world' })
92
+ })
93
+
94
+ fastify.inject({
95
+ method: 'POST',
96
+ url: '/',
97
+ simulate: {
98
+ error: true
99
+ },
100
+ body: {
101
+ text: '12345678901234567890123456789012345678901234567890'
102
+ }
103
+ }, (err, res) => {
104
+ t.error(err)
105
+ t.strictEqual(res.statusCode, 400)
106
+ t.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
107
+ t.deepEqual(JSON.parse(res.payload), {
108
+ error: 'Bad Request',
109
+ message: 'Simulated',
110
+ statusCode: 400
111
+ })
112
+ })
113
+ })
114
+
115
+ test('encapsulated error handler binding', t => {
116
+ t.plan(7)
117
+
118
+ const fastify = Fastify()
119
+
120
+ fastify.register(function (app, opts, next) {
121
+ app.decorate('hello', 'world')
122
+ t.strictEqual(app.hello, 'world')
123
+ app.post('/', function (req, reply) {
124
+ reply.send({ hello: 'world' })
125
+ })
126
+ app.setErrorHandler(function (err, request, reply) {
127
+ t.strictEqual(this.hello, 'world')
128
+ reply
129
+ .code(err.statusCode)
130
+ .type('application/json; charset=utf-8')
131
+ .send(err)
132
+ })
133
+ next()
134
+ })
135
+
136
+ fastify.inject({
137
+ method: 'POST',
138
+ url: '/',
139
+ simulate: {
140
+ error: true
141
+ },
142
+ body: {
143
+ text: '12345678901234567890123456789012345678901234567890'
144
+ }
145
+ }, (err, res) => {
146
+ t.error(err)
147
+ t.strictEqual(res.statusCode, 400)
148
+ t.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
149
+ t.deepEqual(res.json(), {
150
+ error: 'Bad Request',
151
+ message: 'Simulated',
152
+ statusCode: 400
153
+ })
154
+ t.strictEqual(fastify.hello, undefined)
155
+ })
156
+ })
@@ -57,10 +57,10 @@ test('should fail immediately with invalid payload', t => {
57
57
  url: '/'
58
58
  }, (err, res) => {
59
59
  t.error(err)
60
- t.deepEqual(JSON.parse(res.payload), {
60
+ t.deepEqual(res.json(), {
61
61
  statusCode: 400,
62
62
  error: 'Bad Request',
63
- message: "body should have required property 'name', body should have required property 'work'"
63
+ message: "body should have required property 'name'"
64
64
  })
65
65
  t.strictEqual(res.statusCode, 400)
66
66
  })
@@ -216,19 +216,12 @@ test('should be able to attach validation to request', t => {
216
216
  }, (err, res) => {
217
217
  t.error(err)
218
218
 
219
- t.deepEqual(JSON.parse(res.payload), [{
219
+ t.deepEqual(res.json(), [{
220
220
  keyword: 'required',
221
221
  dataPath: '',
222
222
  schemaPath: '#/required',
223
223
  params: { missingProperty: 'name' },
224
224
  message: 'should have required property \'name\''
225
- },
226
- {
227
- keyword: 'required',
228
- dataPath: '',
229
- schemaPath: '#/required',
230
- params: { missingProperty: 'work' },
231
- message: 'should have required property \'work\''
232
225
  }])
233
226
  t.strictEqual(res.statusCode, 400)
234
227
  })
@@ -255,7 +248,7 @@ test('should respect when attachValidation is explicitly set to false', t => {
255
248
  t.deepEqual(JSON.parse(res.payload), {
256
249
  statusCode: 400,
257
250
  error: 'Bad Request',
258
- message: "body should have required property 'name', body should have required property 'work'"
251
+ message: "body should have required property 'name'"
259
252
  })
260
253
  t.strictEqual(res.statusCode, 400)
261
254
  })
@@ -285,7 +278,7 @@ test('Attached validation error should take precendence over setErrorHandler', t
285
278
  url: '/'
286
279
  }, (err, res) => {
287
280
  t.error(err)
288
- t.deepEqual(res.payload, "Attached: Error: body should have required property 'name', body should have required property 'work'")
281
+ t.deepEqual(res.payload, "Attached: Error: body should have required property 'name'")
289
282
  t.strictEqual(res.statusCode, 400)
290
283
  })
291
284
  })
@@ -320,7 +313,7 @@ test('should handle response validation error', t => {
320
313
  url: '/'
321
314
  }, (err, res) => {
322
315
  t.error(err)
323
- t.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"name is required!"}')
316
+ t.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"\\"name\\" is required!"}')
324
317
  })
325
318
  })
326
319
 
@@ -350,7 +343,7 @@ test('should handle response validation error with promises', t => {
350
343
  url: '/'
351
344
  }, (err, res) => {
352
345
  t.error(err)
353
- t.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"name is required!"}')
346
+ t.strictEqual(res.payload, '{"statusCode":500,"error":"Internal Server Error","message":"\\"name\\" is required!"}')
354
347
  })
355
348
  })
356
349
 
@@ -378,7 +371,7 @@ test('should return a defined output message parsing AJV errors', t => {
378
371
  url: '/'
379
372
  }, (err, res) => {
380
373
  t.error(err)
381
- t.strictEqual(res.payload, '{"statusCode":400,"error":"Bad Request","message":"body should have required property \'name\', body should have required property \'work\'"}')
374
+ t.strictEqual(res.payload, '{"statusCode":400,"error":"Bad Request","message":"body should have required property \'name\'"}')
382
375
  })
383
376
  })
384
377