fastify 4.20.0 → 4.22.0

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.
Files changed (43) hide show
  1. package/README.md +2 -1
  2. package/docs/Guides/Ecosystem.md +5 -3
  3. package/docs/Guides/Getting-Started.md +1 -1
  4. package/docs/Reference/Hooks.md +3 -0
  5. package/docs/Reference/Server.md +199 -181
  6. package/docs/Reference/TypeScript.md +1 -1
  7. package/docs/Reference/Validation-and-Serialization.md +1 -1
  8. package/fastify.d.ts +7 -10
  9. package/fastify.js +3 -3
  10. package/lib/contentTypeParser.js +5 -2
  11. package/lib/error-serializer.js +31 -29
  12. package/lib/errors.js +1 -1
  13. package/lib/pluginOverride.js +10 -3
  14. package/lib/pluginUtils.js +13 -10
  15. package/lib/reply.js +16 -4
  16. package/lib/wrapThenable.js +4 -1
  17. package/package.json +7 -9
  18. package/test/async-await.test.js +1 -1
  19. package/test/bodyLimit.test.js +69 -0
  20. package/test/custom-http-server.test.js +2 -1
  21. package/test/https/custom-https-server.test.js +2 -1
  22. package/test/internals/errors.test.js +2 -2
  23. package/test/internals/plugin.test.js +17 -2
  24. package/test/internals/reply.test.js +33 -2
  25. package/test/plugin.test.js +26 -0
  26. package/test/serial/logger.0.test.js +6 -1
  27. package/test/stream.test.js +4 -4
  28. package/test/types/fastify.test-d.ts +8 -3
  29. package/test/types/hooks.test-d.ts +13 -0
  30. package/test/types/instance.test-d.ts +7 -2
  31. package/test/types/reply.test-d.ts +25 -0
  32. package/test/types/request.test-d.ts +1 -1
  33. package/test/types/type-provider.test-d.ts +82 -1
  34. package/test/wrapThenable.test.js +22 -0
  35. package/types/hooks.d.ts +104 -4
  36. package/types/instance.d.ts +52 -143
  37. package/types/reply.d.ts +8 -6
  38. package/types/request.d.ts +1 -1
  39. package/types/route.d.ts +6 -1
  40. package/types/schema.d.ts +1 -1
  41. package/types/tsconfig.eslint.json +2 -2
  42. package/types/type-provider.d.ts +2 -1
  43. package/types/utils.d.ts +9 -0
@@ -788,7 +788,7 @@ There are a couple supported import methods with the Fastify type system.
788
788
  Many type definitions share the same generic parameters; they are all
789
789
  documented, in detail, within this section.
790
790
 
791
- Most definitions depend on `@node/types` modules `http`, `https`, and `http2`
791
+ Most definitions depend on `@types/node` modules `http`, `https`, and `http2`
792
792
 
793
793
  ##### RawServer
794
794
  Underlying Node.js server type
@@ -397,7 +397,7 @@ configuration](https://github.com/fastify/ajv-compiler#ajv-configuration) is:
397
397
  {
398
398
  coerceTypes: 'array', // change data type of data to match type keyword
399
399
  useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
400
- removeAdditional: true, // remove additional properties
400
+ removeAdditional: true, // remove additional properties if additionalProperties is set to false, see: https://ajv.js.org/guide/modifying-data.html#removing-additional-properties
401
401
  uriResolver: require('fast-uri'),
402
402
  addUsedSchema: false,
403
403
  // Explicitly set allErrors to `false`.
package/fastify.d.ts CHANGED
@@ -20,15 +20,15 @@ import { FastifyRegister, FastifyRegisterOptions, RegisterOptions } from './type
20
20
  import { FastifyReply } from './types/reply'
21
21
  import { FastifyRequest, RequestGenericInterface } from './types/request'
22
22
  import { RouteHandler, RouteHandlerMethod, RouteOptions, RouteShorthandMethod, RouteShorthandOptions, RouteShorthandOptionsWithHandler, RouteGenericInterface } from './types/route'
23
- import { FastifySchema, FastifySchemaCompiler, SchemaErrorDataVar, SchemaErrorFormatter } from './types/schema'
23
+ import { FastifySchema, FastifySchemaCompiler, FastifySchemaValidationError, SchemaErrorDataVar, SchemaErrorFormatter } from './types/schema'
24
24
  import { FastifyServerFactory, FastifyServerFactoryHandler } from './types/serverFactory'
25
25
  import { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'
26
26
  import { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './types/utils'
27
27
 
28
28
  declare module '@fastify/error' {
29
29
  interface FastifyError {
30
- validation?: fastify.ValidationResult[];
31
30
  validationContext?: SchemaErrorDataVar;
31
+ validation?: FastifySchemaValidationError[];
32
32
  }
33
33
  }
34
34
 
@@ -162,13 +162,10 @@ declare namespace fastify {
162
162
  clientErrorHandler?: (error: ConnectionError, socket: Socket) => void,
163
163
  }
164
164
 
165
- export interface ValidationResult {
166
- keyword: string;
167
- instancePath: string;
168
- schemaPath: string;
169
- params: Record<string, string | string[]>;
170
- message?: string;
171
- }
165
+ /**
166
+ * @deprecated use {@link FastifySchemaValidationError}
167
+ */
168
+ export type ValidationResult = FastifySchemaValidationError;
172
169
 
173
170
  /* Export additional types */
174
171
  export type {
@@ -241,4 +238,4 @@ declare function fastify<
241
238
 
242
239
  // CJS export
243
240
  // const fastify = require('fastify')
244
- export = fastify
241
+ export = fastify
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.20.0'
3
+ const VERSION = '4.22.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -241,7 +241,7 @@ function fastify (options) {
241
241
  [kReply]: Reply.buildReply(Reply),
242
242
  [kRequest]: Request.buildRequest(Request, options.trustProxy),
243
243
  [kFourOhFour]: fourOhFour,
244
- [pluginUtils.registeredPlugins]: [],
244
+ [pluginUtils.kRegisteredPlugins]: [],
245
245
  [kPluginNameChain]: ['fastify'],
246
246
  [kAvvioBoot]: null,
247
247
  // routing method
@@ -312,7 +312,7 @@ function fastify (options) {
312
312
  close: null,
313
313
  printPlugins: null,
314
314
  hasPlugin: function (name) {
315
- return this[kPluginNameChain].includes(name)
315
+ return this[pluginUtils.kRegisteredPlugins].includes(name) || this[kPluginNameChain].includes(name)
316
316
  },
317
317
  // http server
318
318
  listen,
@@ -226,8 +226,11 @@ function rawBody (request, reply, options, parser, done) {
226
226
 
227
227
  function onData (chunk) {
228
228
  receivedLength += chunk.length
229
-
230
- if ((payload.receivedEncodedLength || receivedLength) > limit) {
229
+ const { receivedEncodedLength = 0 } = payload
230
+ // The resulting body length must not exceed bodyLimit (see "zip bomb").
231
+ // The case when encoded length is larger than received length is rather theoretical,
232
+ // unless the stream returned by preParsing hook is broken and reports wrong value.
233
+ if (receivedLength > limit || receivedEncodedLength > limit) {
231
234
  payload.removeListener('data', onData)
232
235
  payload.removeListener('end', onEnd)
233
236
  payload.removeListener('error', onEnd)
@@ -16,41 +16,43 @@
16
16
  ) {
17
17
 
18
18
 
19
- function anonymous0 (input) {
20
- // #
21
19
 
20
+ // #
21
+ function anonymous0 (input) {
22
22
  const obj = (input && typeof input.toJSON === 'function')
23
23
  ? input.toJSON()
24
24
  : input
25
25
 
26
- let json = '{'
27
- let addComma = false
26
+
27
+ let addComma = false
28
+ let json = '{'
29
+
30
+ if (obj["statusCode"] !== undefined) {
31
+ !addComma && (addComma = true) || (json += ',')
32
+ json += "\"statusCode\":"
33
+ json += serializer.asNumber(obj["statusCode"])
34
+ }
35
+
36
+ if (obj["code"] !== undefined) {
37
+ !addComma && (addComma = true) || (json += ',')
38
+ json += "\"code\":"
39
+ json += serializer.asString(obj["code"])
40
+ }
41
+
42
+ if (obj["error"] !== undefined) {
43
+ !addComma && (addComma = true) || (json += ',')
44
+ json += "\"error\":"
45
+ json += serializer.asString(obj["error"])
46
+ }
47
+
48
+ if (obj["message"] !== undefined) {
49
+ !addComma && (addComma = true) || (json += ',')
50
+ json += "\"message\":"
51
+ json += serializer.asString(obj["message"])
52
+ }
53
+
54
+ return json + '}'
28
55
 
29
- if (obj["statusCode"] !== undefined) {
30
- !addComma && (addComma = true) || (json += ',')
31
- json += "\"statusCode\":"
32
- json += serializer.asNumber(obj["statusCode"])
33
- }
34
-
35
- if (obj["code"] !== undefined) {
36
- !addComma && (addComma = true) || (json += ',')
37
- json += "\"code\":"
38
- json += serializer.asString(obj["code"])
39
- }
40
-
41
- if (obj["error"] !== undefined) {
42
- !addComma && (addComma = true) || (json += ',')
43
- json += "\"error\":"
44
- json += serializer.asString(obj["error"])
45
- }
46
-
47
- if (obj["message"] !== undefined) {
48
- !addComma && (addComma = true) || (json += ',')
49
- json += "\"message\":"
50
- json += serializer.asString(obj["message"])
51
- }
52
-
53
- return json + '}'
54
56
  }
55
57
 
56
58
  const main = anonymous0
package/lib/errors.js CHANGED
@@ -214,7 +214,7 @@ const codes = {
214
214
  ),
215
215
  FST_ERR_REP_ALREADY_SENT: createError(
216
216
  'FST_ERR_REP_ALREADY_SENT',
217
- 'Reply was already sent.'
217
+ 'Reply was already sent, did you forget to "return reply" in "%s" (%s)?'
218
218
  ),
219
219
  FST_ERR_REP_SENT_VALUE: createError(
220
220
  'FST_ERR_REP_SENT_VALUE',
@@ -27,9 +27,10 @@ const pluginUtils = require('./pluginUtils')
27
27
  module.exports = function override (old, fn, opts) {
28
28
  const shouldSkipOverride = pluginUtils.registerPlugin.call(old, fn)
29
29
 
30
+ const fnName = pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)
30
31
  if (shouldSkipOverride) {
31
32
  // after every plugin registration we will enter a new name
32
- old[kPluginNameChain].push(pluginUtils.getDisplayName(fn))
33
+ old[kPluginNameChain].push(fnName)
33
34
  return old
34
35
  }
35
36
 
@@ -48,8 +49,14 @@ module.exports = function override (old, fn, opts) {
48
49
  instance[kSchemaController] = SchemaController.buildSchemaController(old[kSchemaController])
49
50
  instance.getSchema = instance[kSchemaController].getSchema.bind(instance[kSchemaController])
50
51
  instance.getSchemas = instance[kSchemaController].getSchemas.bind(instance[kSchemaController])
51
- instance[pluginUtils.registeredPlugins] = Object.create(instance[pluginUtils.registeredPlugins])
52
- instance[kPluginNameChain] = [pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)]
52
+
53
+ // Track the registered and loaded plugins since the root instance.
54
+ // It does not track the current encapsulated plugin.
55
+ instance[pluginUtils.kRegisteredPlugins] = Object.create(instance[pluginUtils.kRegisteredPlugins])
56
+
57
+ // Track the plugin chain since the root instance.
58
+ // When an non-encapsulated plugin is added, the chain will be updated.
59
+ instance[kPluginNameChain] = [fnName]
53
60
 
54
61
  if (instance[kLogSerializers] || opts.logSerializers) {
55
62
  instance[kLogSerializers] = Object.assign(Object.create(instance[kLogSerializers]), opts.logSerializers)
@@ -2,7 +2,7 @@
2
2
 
3
3
  const semver = require('semver')
4
4
  const assert = require('assert')
5
- const registeredPlugins = Symbol.for('registered-plugin')
5
+ const kRegisteredPlugins = Symbol.for('registered-plugin')
6
6
  const {
7
7
  kTestInternals
8
8
  } = require('./symbols.js')
@@ -25,12 +25,15 @@ function getPluginName (func) {
25
25
  // let's see if this is a file, and in that case use that
26
26
  // this is common for plugins
27
27
  const cache = require.cache
28
- const keys = Object.keys(cache)
29
-
30
- for (let i = 0; i < keys.length; i++) {
31
- const key = keys[i]
32
- if (cache[key].exports === func) {
33
- return key
28
+ // cache is undefined inside SEA
29
+ if (cache) {
30
+ const keys = Object.keys(cache)
31
+
32
+ for (let i = 0; i < keys.length; i++) {
33
+ const key = keys[i]
34
+ if (cache[key].exports === func) {
35
+ return key
36
+ }
34
37
  }
35
38
  }
36
39
 
@@ -65,7 +68,7 @@ function checkDependencies (fn) {
65
68
 
66
69
  dependencies.forEach(dependency => {
67
70
  assert(
68
- this[registeredPlugins].indexOf(dependency) > -1,
71
+ this[kRegisteredPlugins].indexOf(dependency) > -1,
69
72
  `The dependency '${dependency}' of plugin '${meta.name}' is not registered`
70
73
  )
71
74
  })
@@ -128,7 +131,7 @@ function registerPluginName (fn) {
128
131
 
129
132
  const name = meta.name
130
133
  if (!name) return
131
- this[registeredPlugins].push(name)
134
+ this[kRegisteredPlugins].push(name)
132
135
  }
133
136
 
134
137
  function registerPlugin (fn) {
@@ -142,7 +145,7 @@ function registerPlugin (fn) {
142
145
  module.exports = {
143
146
  getPluginName,
144
147
  getFuncPreview,
145
- registeredPlugins,
148
+ kRegisteredPlugins,
146
149
  getDisplayName,
147
150
  registerPlugin
148
151
  }
package/lib/reply.js CHANGED
@@ -97,7 +97,7 @@ Object.defineProperties(Reply.prototype, {
97
97
 
98
98
  // We throw only if sent was overwritten from Fastify
99
99
  if (this.sent && this[kReplyHijacked]) {
100
- throw new FST_ERR_REP_ALREADY_SENT()
100
+ throw new FST_ERR_REP_ALREADY_SENT(this.request.url, this.request.method)
101
101
  }
102
102
 
103
103
  this[kReplyHijacked] = true
@@ -124,7 +124,7 @@ Reply.prototype.send = function (payload) {
124
124
  }
125
125
 
126
126
  if (this.sent) {
127
- this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT() }, 'Reply already sent')
127
+ this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT(this.request.url, this.request.method) })
128
128
  return this
129
129
  }
130
130
 
@@ -539,6 +539,18 @@ function wrapOnSendEnd (err, request, reply, payload) {
539
539
  }
540
540
  }
541
541
 
542
+ function safeWriteHead (reply, statusCode) {
543
+ const res = reply.raw
544
+ try {
545
+ res.writeHead(statusCode, reply[kReplyHeaders])
546
+ } catch (err) {
547
+ if (err.code === 'ERR_HTTP_HEADERS_SENT') {
548
+ reply.log.warn(`Reply was already sent, did you forget to "return reply" in the "${reply.request.raw.url}" (${reply.request.raw.method}) route?`)
549
+ }
550
+ throw err
551
+ }
552
+ }
553
+
542
554
  function onSendEnd (reply, payload) {
543
555
  const res = reply.raw
544
556
  const req = reply.request
@@ -569,7 +581,7 @@ function onSendEnd (reply, payload) {
569
581
  reply[kReplyHeaders]['content-length'] = '0'
570
582
  }
571
583
 
572
- res.writeHead(statusCode, reply[kReplyHeaders])
584
+ safeWriteHead(reply, statusCode)
573
585
  sendTrailer(payload, res, reply)
574
586
  return
575
587
  }
@@ -594,7 +606,7 @@ function onSendEnd (reply, payload) {
594
606
  }
595
607
  }
596
608
 
597
- res.writeHead(statusCode, reply[kReplyHeaders])
609
+ safeWriteHead(reply, statusCode)
598
610
  // write payload first
599
611
  res.write(payload)
600
612
  // then send trailers
@@ -18,7 +18,10 @@ function wrapThenable (thenable, reply) {
18
18
  // the request may be terminated during the reply. in this situation,
19
19
  // it require an extra checking of request.aborted to see whether
20
20
  // the request is killed by client.
21
- if (payload !== undefined || (reply.sent === false && reply.raw.headersSent === false && reply.request.raw.aborted === false)) {
21
+ // Most of the times aborted will be true when destroyed is true,
22
+ // however there is a race condition where the request is not
23
+ // aborted but only destroyed.
24
+ if (payload !== undefined || (reply.sent === false && reply.raw.headersSent === false && reply.request.raw.aborted === false && reply.request.raw.destroyed === false)) {
22
25
  // we use a try-catch internally to avoid adding a catch to another
23
26
  // promise, increase promise perf by 10%
24
27
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.20.0",
3
+ "version": "4.22.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -11,14 +11,14 @@
11
11
  "benchmark:parser": "npx concurrently -k -s first \"node ./examples/benchmark/parser.js\" \"npx autocannon -c 100 -d 30 -p 10 -i ./examples/benchmark/body.json -H \"content-type:application/jsoff\" -m POST localhost:3000/\"",
12
12
  "build:validation": "node build/build-error-serializer.js && node build/build-validation.js",
13
13
  "coverage": "npm run unit -- --coverage-report=html",
14
- "coverage:ci": "npm run unit -- --coverage-report=html --no-browser --no-check-coverage",
14
+ "coverage:ci": "c8 --reporter=lcov tap --coverage-report=html --no-browser --no-check-coverage",
15
15
  "coverage:ci-check-coverage": "c8 check-coverage --branches 100 --functions 100 --lines 100 --statements 100",
16
- "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause\"",
17
16
  "lint": "npm run lint:standard && npm run lint:typescript && npm run lint:markdown",
18
- "lint:fix": "standard --fix",
17
+ "lint:fix": "standard --fix && npm run lint:typescript:fix",
19
18
  "lint:markdown": "markdownlint-cli2",
20
19
  "lint:standard": "standard | snazzy",
21
20
  "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts",
21
+ "lint:typescript:fix": "npm run lint:typescript -- --fix",
22
22
  "prepublishOnly": "cross-env PREPUBLISH=true tap --no-check-coverage test/build/**.test.js && npm run test:validator:integrity",
23
23
  "test": "npm run lint && npm run unit && npm run test:typescript",
24
24
  "test:ci": "npm run unit -- --cov --coverage-report=lcovonly && npm run test:typescript",
@@ -134,11 +134,11 @@
134
134
  "homepage": "https://www.fastify.io/",
135
135
  "devDependencies": {
136
136
  "@fastify/pre-commit": "^2.0.2",
137
- "@sinclair/typebox": "^0.29.1",
137
+ "@sinclair/typebox": "^0.31.1",
138
138
  "@sinonjs/fake-timers": "^11.0.0",
139
139
  "@types/node": "^20.1.0",
140
- "@typescript-eslint/eslint-plugin": "^5.59.2",
141
- "@typescript-eslint/parser": "^5.59.2",
140
+ "@typescript-eslint/eslint-plugin": "^6.3.0",
141
+ "@typescript-eslint/parser": "^6.3.0",
142
142
  "ajv": "^8.12.0",
143
143
  "ajv-errors": "^3.0.0",
144
144
  "ajv-formats": "^2.1.1",
@@ -162,10 +162,8 @@
162
162
  "joi": "^17.9.2",
163
163
  "json-schema-to-ts": "^2.9.1",
164
164
  "JSONStream": "^1.3.5",
165
- "license-checker": "^25.0.1",
166
165
  "markdownlint-cli2": "^0.8.1",
167
166
  "proxyquire": "^2.1.3",
168
- "pump": "^3.0.0",
169
167
  "self-cert": "^2.0.0",
170
168
  "send": "^0.18.0",
171
169
  "simple-get": "^4.0.1",
@@ -124,7 +124,7 @@ test('ignore the result of the promise if reply.send is called beforehand (objec
124
124
  })
125
125
 
126
126
  test('server logs an error if reply.send is called and a value is returned via async/await', t => {
127
- const lines = ['incoming request', 'request completed', 'Reply already sent']
127
+ const lines = ['incoming request', 'request completed', 'Reply was already sent, did you forget to "return reply" in "/" (GET)?']
128
128
  t.plan(lines.length + 2)
129
129
 
130
130
  const splitStream = split(JSON.parse)
@@ -2,6 +2,7 @@
2
2
 
3
3
  const Fastify = require('..')
4
4
  const sget = require('simple-get').concat
5
+ const zlib = require('zlib')
5
6
  const t = require('tap')
6
7
  const test = t.test
7
8
 
@@ -45,6 +46,74 @@ test('bodyLimit', t => {
45
46
  })
46
47
  })
47
48
 
49
+ test('bodyLimit is applied to decoded content', t => {
50
+ t.plan(9)
51
+
52
+ const body = { x: 'x'.repeat(30000) }
53
+ const json = JSON.stringify(body)
54
+ const encoded = zlib.gzipSync(json)
55
+
56
+ const fastify = Fastify()
57
+
58
+ fastify.addHook('preParsing', async (req, reply, payload) => {
59
+ t.equal(req.headers['content-length'], `${encoded.length}`)
60
+ const unzip = zlib.createGunzip()
61
+ Object.defineProperty(unzip, 'receivedEncodedLength', {
62
+ get () {
63
+ return unzip.bytesWritten
64
+ }
65
+ })
66
+ payload.pipe(unzip)
67
+ return unzip
68
+ })
69
+
70
+ fastify.post('/body-limit-40k', {
71
+ bodyLimit: 40000,
72
+ onError: async (req, res, err) => {
73
+ t.fail('should not be called')
74
+ }
75
+ }, (request, reply) => {
76
+ reply.send({ x: request.body.x })
77
+ })
78
+
79
+ fastify.post('/body-limit-20k', {
80
+ bodyLimit: 20000,
81
+ onError: async (req, res, err) => {
82
+ t.equal(err.code, 'FST_ERR_CTP_BODY_TOO_LARGE')
83
+ t.equal(err.statusCode, 413)
84
+ }
85
+ }, (request, reply) => {
86
+ reply.send({ x: 'handler should not be called' })
87
+ })
88
+
89
+ fastify.inject({
90
+ method: 'POST',
91
+ url: '/body-limit-40k',
92
+ headers: {
93
+ 'content-encoding': 'gzip',
94
+ 'content-type': 'application/json'
95
+ },
96
+ payload: encoded
97
+ }, (err, res) => {
98
+ t.error(err)
99
+ t.equal(res.statusCode, 200)
100
+ t.same(res.json(), body)
101
+ })
102
+
103
+ fastify.inject({
104
+ method: 'POST',
105
+ url: '/body-limit-20k',
106
+ headers: {
107
+ 'content-encoding': 'gzip',
108
+ 'content-type': 'application/json'
109
+ },
110
+ payload: encoded
111
+ }, (err, res) => {
112
+ t.error(err)
113
+ t.equal(res.statusCode, 413)
114
+ })
115
+ })
116
+
48
117
  test('default request.routeOptions.bodyLimit should be 1048576', t => {
49
118
  t.plan(4)
50
119
  const fastify = Fastify()
@@ -10,8 +10,9 @@ const dns = require('dns').promises
10
10
 
11
11
  test('Should support a custom http server', async t => {
12
12
  const localAddresses = await dns.lookup('localhost', { all: true })
13
+ const minPlan = localAddresses.length - 1 || 1
13
14
 
14
- t.plan((localAddresses.length - 1) + 3)
15
+ t.plan(minPlan + 3)
15
16
 
16
17
  const serverFactory = (handler, opts) => {
17
18
  t.ok(opts.serverFactory, 'it is called once for localhost')
@@ -12,8 +12,9 @@ t.before(buildCertificate)
12
12
 
13
13
  test('Should support a custom https server', async t => {
14
14
  const localAddresses = await dns.lookup('localhost', { all: true })
15
+ const minPlan = localAddresses.length - 1 || 1
15
16
 
16
- t.plan((localAddresses.length - 1) + 3)
17
+ t.plan(minPlan + 3)
17
18
 
18
19
  const serverFactory = (handler, opts) => {
19
20
  t.ok(opts.serverFactory, 'it is called once for localhost')
@@ -339,10 +339,10 @@ test('FST_ERR_REP_INVALID_PAYLOAD_TYPE', t => {
339
339
 
340
340
  test('FST_ERR_REP_ALREADY_SENT', t => {
341
341
  t.plan(5)
342
- const error = new errors.FST_ERR_REP_ALREADY_SENT()
342
+ const error = new errors.FST_ERR_REP_ALREADY_SENT('/hello', 'GET')
343
343
  t.equal(error.name, 'FastifyError')
344
344
  t.equal(error.code, 'FST_ERR_REP_ALREADY_SENT')
345
- t.equal(error.message, 'Reply was already sent.')
345
+ t.equal(error.message, 'Reply was already sent, did you forget to "return reply" in "/hello" (GET)?')
346
346
  t.equal(error.statusCode, 500)
347
347
  t.ok(error instanceof Error)
348
348
  })
@@ -29,6 +29,21 @@ test('getPluginName should return plugin name if the file is cached', t => {
29
29
  t.equal(pluginName, expectedPluginName)
30
30
  })
31
31
 
32
+ test('getPluginName should not throw when require.cache is undefined', t => {
33
+ t.plan(1)
34
+ function example () {
35
+ console.log('is just an example')
36
+ }
37
+ const cache = require.cache
38
+ require.cache = undefined
39
+ t.teardown(() => {
40
+ require.cache = cache
41
+ })
42
+ const pluginName = pluginUtilsPublic.getPluginName(example)
43
+
44
+ t.equal(pluginName, 'example')
45
+ })
46
+
32
47
  test("getMeta should return the object stored with the 'plugin-meta' symbol", t => {
33
48
  t.plan(1)
34
49
 
@@ -122,7 +137,7 @@ test('checkDependencies should check if the given dependency is present in the i
122
137
  }
123
138
 
124
139
  function context () {}
125
- context[pluginUtilsPublic.registeredPlugins] = ['plugin']
140
+ context[pluginUtilsPublic.kRegisteredPlugins] = ['plugin']
126
141
 
127
142
  try {
128
143
  pluginUtils.checkDependencies.call(context, fn)
@@ -143,7 +158,7 @@ test('checkDependencies should check if the given dependency is present in the i
143
158
  }
144
159
 
145
160
  function context () {}
146
- context[pluginUtilsPublic.registeredPlugins] = []
161
+ context[pluginUtilsPublic.kRegisteredPlugins] = []
147
162
 
148
163
  try {
149
164
  pluginUtils.checkDependencies.call(context, fn)
@@ -1508,7 +1508,7 @@ test('should throw error when passing falsy value to reply.sent', t => {
1508
1508
  })
1509
1509
 
1510
1510
  test('should throw error when attempting to set reply.sent more than once', t => {
1511
- t.plan(4)
1511
+ t.plan(3)
1512
1512
  const fastify = Fastify()
1513
1513
 
1514
1514
  fastify.get('/', function (req, reply) {
@@ -1518,7 +1518,6 @@ test('should throw error when attempting to set reply.sent more than once', t =>
1518
1518
  t.fail('must throw')
1519
1519
  } catch (err) {
1520
1520
  t.equal(err.code, 'FST_ERR_REP_ALREADY_SENT')
1521
- t.equal(err.message, 'Reply was already sent.')
1522
1521
  }
1523
1522
  reply.raw.end()
1524
1523
  })
@@ -2088,3 +2087,35 @@ test('invalid response headers and custom error handler', async t => {
2088
2087
 
2089
2088
  await fastify.close()
2090
2089
  })
2090
+
2091
+ test('reply.send will intercept ERR_HTTP_HEADERS_SENT and log an error message', t => {
2092
+ t.plan(2)
2093
+
2094
+ const response = new Writable()
2095
+ Object.assign(response, {
2096
+ setHeader: () => {},
2097
+ hasHeader: () => false,
2098
+ getHeader: () => undefined,
2099
+ writeHead: () => {
2100
+ const err = new Error('kaboom')
2101
+ err.code = 'ERR_HTTP_HEADERS_SENT'
2102
+ throw err
2103
+ },
2104
+ write: () => {},
2105
+ headersSent: true
2106
+ })
2107
+
2108
+ const log = {
2109
+ warn: (msg) => {
2110
+ t.equal(msg, 'Reply was already sent, did you forget to "return reply" in the "/hello" (GET) route?')
2111
+ }
2112
+ }
2113
+
2114
+ const reply = new Reply(response, { [kRouteContext]: { onSend: null }, raw: { url: '/hello', method: 'GET' } }, log)
2115
+
2116
+ try {
2117
+ reply.send('')
2118
+ } catch (err) {
2119
+ t.equal(err.code, 'ERR_HTTP_HEADERS_SENT')
2120
+ }
2121
+ })
@@ -1247,3 +1247,29 @@ test('hasPlugin returns true when using no encapsulation', async t => {
1247
1247
 
1248
1248
  await fastify.ready()
1249
1249
  })
1250
+
1251
+ test('hasPlugin returns true when using encapsulation', async t => {
1252
+ t.plan(2)
1253
+
1254
+ const fastify = Fastify()
1255
+
1256
+ const pluginCallback = function (server, options, done) {
1257
+ done()
1258
+ }
1259
+ const pluginName = 'awesome-plugin'
1260
+ const plugin = fp(pluginCallback, { name: pluginName })
1261
+
1262
+ fastify.register(plugin)
1263
+
1264
+ fastify.register(async (server) => {
1265
+ t.ok(server.hasPlugin(pluginName))
1266
+ })
1267
+
1268
+ fastify.register(async function foo (server) {
1269
+ server.register(async function bar (server) {
1270
+ t.ok(server.hasPlugin(pluginName))
1271
+ })
1272
+ })
1273
+
1274
+ await fastify.ready()
1275
+ })
@@ -519,7 +519,12 @@ t.test('test log stream', (t) => {
519
519
  })
520
520
 
521
521
  t.test('reply.send logs an error if called twice in a row', async (t) => {
522
- const lines = ['incoming request', 'request completed', 'Reply already sent', 'Reply already sent']
522
+ const lines = [
523
+ 'incoming request',
524
+ 'request completed',
525
+ 'Reply was already sent, did you forget to "return reply" in "/" (GET)?',
526
+ 'Reply was already sent, did you forget to "return reply" in "/" (GET)?'
527
+ ]
523
528
  t.plan(lines.length + 1)
524
529
 
525
530
  const stream = split(JSON.parse)