fastify 4.18.0 → 4.19.1

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 (45) hide show
  1. package/docs/Guides/Ecosystem.md +5 -0
  2. package/docs/Guides/Serverless.md +43 -1
  3. package/docs/Guides/Testing.md +135 -2
  4. package/docs/Guides/Write-Plugin.md +3 -1
  5. package/docs/Reference/Principles.md +78 -0
  6. package/docs/Reference/Request.md +2 -0
  7. package/docs/Reference/Routes.md +5 -0
  8. package/docs/Reference/Server.md +5 -2
  9. package/docs/Reference/Type-Providers.md +9 -2
  10. package/docs/Reference/TypeScript.md +21 -6
  11. package/fastify.d.ts +1 -1
  12. package/fastify.js +23 -8
  13. package/lib/contentTypeParser.js +1 -1
  14. package/lib/reply.js +2 -2
  15. package/lib/request.js +18 -8
  16. package/lib/route.js +0 -1
  17. package/lib/schema-controller.js +6 -6
  18. package/lib/schemas.js +2 -2
  19. package/lib/server.js +56 -30
  20. package/lib/symbols.js +1 -0
  21. package/package.json +9 -8
  22. package/test/buffer.test.js +51 -0
  23. package/test/close-pipelining.test.js +4 -1
  24. package/test/close.test.js +148 -6
  25. package/test/custom-http-server.test.js +13 -2
  26. package/test/genReqId.test.js +42 -0
  27. package/test/header-overflow.test.js +65 -0
  28. package/test/https/custom-https-server.test.js +2 -2
  29. package/test/inject.test.js +30 -0
  30. package/test/internals/reply.test.js +2 -2
  31. package/test/internals/request.test.js +60 -0
  32. package/test/router-options.test.js +221 -0
  33. package/test/types/fastify.test-d.ts +7 -1
  34. package/test/types/instance.test-d.ts +70 -0
  35. package/test/types/reply.test-d.ts +37 -4
  36. package/test/types/request.test-d.ts +1 -0
  37. package/test/types/route.test-d.ts +79 -3
  38. package/test/url-rewriting.test.js +35 -0
  39. package/test/versioned-routes.test.js +1 -1
  40. package/types/context.d.ts +2 -1
  41. package/types/instance.d.ts +24 -20
  42. package/types/reply.d.ts +11 -6
  43. package/types/request.d.ts +1 -0
  44. package/types/route.d.ts +5 -0
  45. package/types/utils.d.ts +24 -0
@@ -100,28 +100,28 @@ class SchemaController {
100
100
  /**
101
101
  * This method will be called when a validator must be setup.
102
102
  * Do not setup the compiler more than once
103
- * @param {object} serverOptions: the fastify server option
103
+ * @param {object} serverOptions the fastify server options
104
104
  */
105
- setupValidator (serverOption) {
105
+ setupValidator (serverOptions) {
106
106
  const isReady = this.validatorCompiler !== undefined && !this.addedSchemas
107
107
  if (isReady) {
108
108
  return
109
109
  }
110
- this.validatorCompiler = this.getValidatorBuilder()(this.schemaBucket.getSchemas(), serverOption.ajv)
110
+ this.validatorCompiler = this.getValidatorBuilder()(this.schemaBucket.getSchemas(), serverOptions.ajv)
111
111
  }
112
112
 
113
113
  /**
114
114
  * This method will be called when a serializer must be setup.
115
115
  * Do not setup the compiler more than once
116
- * @param {object} serverOptions: the fastify server option
116
+ * @param {object} serverOptions the fastify server options
117
117
  */
118
- setupSerializer (serverOption) {
118
+ setupSerializer (serverOptions) {
119
119
  const isReady = this.serializerCompiler !== undefined && !this.addedSchemas
120
120
  if (isReady) {
121
121
  return
122
122
  }
123
123
 
124
- this.serializerCompiler = this.getSerializerBuilder()(this.schemaBucket.getSchemas(), serverOption.serializerOpts)
124
+ this.serializerCompiler = this.getSerializerBuilder()(this.schemaBucket.getSchemas(), serverOptions.serializerOpts)
125
125
  }
126
126
  }
127
127
 
package/lib/schemas.js CHANGED
@@ -147,8 +147,8 @@ function getSchemaAnyway (schema, jsonShorthand) {
147
147
  *
148
148
  * @param {object} context the request context
149
149
  * @param {number} statusCode the http status code
150
- * @param {string} contentType the reply content type
151
- * @returns {function|boolean} the right JSON Schema function to serialize
150
+ * @param {string} [contentType] the reply content type
151
+ * @returns {function|false} the right JSON Schema function to serialize
152
152
  * the reply or false if it is not set
153
153
  */
154
154
  function getSchemaSerializer (context, statusCode, contentType) {
package/lib/server.js CHANGED
@@ -61,7 +61,6 @@ function createServer (options, httpHandler) {
61
61
  if (Object.prototype.hasOwnProperty.call(listenOptions, 'host') === false) {
62
62
  listenOptions.host = host
63
63
  }
64
-
65
64
  if (host === 'localhost') {
66
65
  listenOptions.cb = (err, address) => {
67
66
  if (err) {
@@ -115,41 +114,68 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
115
114
  return
116
115
  }
117
116
 
117
+ const isMainServerListening = mainServer.listening && serverOpts.serverFactory
118
+
118
119
  let binding = 0
119
120
  let binded = 0
120
- const primaryAddress = mainServer.address()
121
- for (const adr of addresses) {
122
- if (adr.address !== primaryAddress.address) {
123
- binding++
124
- const secondaryOpts = Object.assign({}, listenOptions, {
125
- host: adr.address,
126
- port: primaryAddress.port,
127
- cb: (_ignoreErr) => {
128
- binded++
129
-
130
- /* istanbul ignore next: the else won't be taken unless listening fails */
131
- if (!_ignoreErr) {
132
- this[kServerBindings].push(secondaryServer)
121
+ if (!isMainServerListening) {
122
+ const primaryAddress = mainServer.address()
123
+ for (const adr of addresses) {
124
+ if (adr.address !== primaryAddress.address) {
125
+ binding++
126
+ const secondaryOpts = Object.assign({}, listenOptions, {
127
+ host: adr.address,
128
+ port: primaryAddress.port,
129
+ cb: (_ignoreErr) => {
130
+ binded++
131
+
132
+ /* istanbul ignore next: the else won't be taken unless listening fails */
133
+ if (!_ignoreErr) {
134
+ this[kServerBindings].push(secondaryServer)
135
+ // Due to the nature of the feature is not possible to know
136
+ // ahead of time the number of bindings that will be made.
137
+ // For instance, each binding is hooked to be closed at their own
138
+ // pace through the `onClose` hook.
139
+ // It also allows them to handle possible connections already
140
+ // attached to them if any.
141
+ this.onClose((instance, done) => {
142
+ if (instance[kState].listening) {
143
+ // No new TCP connections are accepted
144
+ // We swallow any error from the secondary
145
+ // server
146
+ secondaryServer.close(() => done())
147
+ /* istanbul ignore next: Cannot test this without Node.js core support */
148
+ if (serverOpts.forceCloseConnections === 'idle') {
149
+ // Not needed in Node 19
150
+ secondaryServer.closeIdleConnections()
151
+ /* istanbul ignore next: Cannot test this without Node.js core support */
152
+ } else if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections) {
153
+ secondaryServer.closeAllConnections()
154
+ }
155
+ } else {
156
+ done()
157
+ }
158
+ })
159
+ }
160
+
161
+ if (binded === binding) {
162
+ // regardless of the error, we are done
163
+ onListen()
164
+ }
133
165
  }
166
+ })
134
167
 
135
- if (binded === binding) {
136
- // regardless of the error, we are done
137
- onListen()
138
- }
139
- }
140
- })
141
-
142
- const secondaryServer = getServerInstance(serverOpts, httpHandler)
143
- const closeSecondary = () => { secondaryServer.close(() => {}) }
144
- secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade'))
145
- mainServer.on('unref', closeSecondary)
146
- mainServer.on('close', closeSecondary)
147
- mainServer.on('error', closeSecondary)
148
- this[kState].listening = false
149
- listenCallback.call(this, secondaryServer, secondaryOpts)()
168
+ const secondaryServer = getServerInstance(serverOpts, httpHandler)
169
+ const closeSecondary = () => { secondaryServer.close(() => { }) }
170
+ secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade'))
171
+ mainServer.on('unref', closeSecondary)
172
+ mainServer.on('close', closeSecondary)
173
+ mainServer.on('error', closeSecondary)
174
+ this[kState].listening = false
175
+ listenCallback.call(this, secondaryServer, secondaryOpts)()
176
+ }
150
177
  }
151
178
  }
152
-
153
179
  // no extra bindings are necessary
154
180
  if (binding === 0) {
155
181
  onListen()
package/lib/symbols.js CHANGED
@@ -30,6 +30,7 @@ const keys = {
30
30
  kRequestPayloadStream: Symbol('fastify.RequestPayloadStream'),
31
31
  kRequestAcceptVersion: Symbol('fastify.RequestAcceptVersion'),
32
32
  kRequestCacheValidateFns: Symbol('fastify.request.cache.validateFns'),
33
+ kRequestOriginalUrl: Symbol('fastify.request.originalUrl'),
33
34
  // 404
34
35
  kFourOhFour: Symbol('fastify.404'),
35
36
  kCanSetNotFoundHandler: Symbol('fastify.canSetNotFoundHandler'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.18.0",
3
+ "version": "4.19.1",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -10,7 +10,7 @@
10
10
  "benchmark": "npx concurrently -k -s first \"node ./examples/benchmark/simple.js\" \"npx autocannon -c 100 -d 30 -p 10 localhost:3000/\"",
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
- "coverage": "NODE_OPTIONS=no-network-family-autoselection npm run unit -- --cov --coverage-report=html",
13
+ "coverage": "cross-env NODE_OPTIONS=no-network-family-autoselection npm run unit -- --cov --coverage-report=html",
14
14
  "coverage:ci": "npm run unit -- --cov --coverage-report=html --no-browser --no-check-coverage",
15
15
  "coverage:ci-check-coverage": "nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100",
16
16
  "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause\"",
@@ -19,8 +19,8 @@
19
19
  "lint:markdown": "markdownlint-cli2",
20
20
  "lint:standard": "standard | snazzy",
21
21
  "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts",
22
- "prepublishOnly": "PREPUBLISH=true tap --no-check-coverage test/build/**.test.js && npm run test:validator:integrity",
23
- "test": "npm run lint && NODE_OPTIONS=no-network-family-autoselection npm run unit && npm run test:typescript",
22
+ "prepublishOnly": "cross-env PREPUBLISH=true tap --no-check-coverage test/build/**.test.js && npm run test:validator:integrity",
23
+ "test": "npm run lint && cross-env NODE_OPTIONS=no-network-family-autoselection npm run unit && npm run test:typescript",
24
24
  "test:ci": "npm run unit -- --cov --coverage-report=lcovonly && npm run test:typescript",
25
25
  "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
26
26
  "test:validator:integrity": "npm run build:validation && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/error-serializer.js && git diff --quiet --ignore-all-space --ignore-blank-lines --ignore-cr-at-eol lib/configValidator.js",
@@ -135,7 +135,7 @@
135
135
  "devDependencies": {
136
136
  "@fastify/pre-commit": "^2.0.2",
137
137
  "@sinclair/typebox": "^0.28.9",
138
- "@sinonjs/fake-timers": "^10.0.2",
138
+ "@sinonjs/fake-timers": "^11.0.0",
139
139
  "@types/node": "^20.1.0",
140
140
  "@typescript-eslint/eslint-plugin": "^5.59.2",
141
141
  "@typescript-eslint/parser": "^5.59.2",
@@ -145,11 +145,12 @@
145
145
  "ajv-i18n": "^4.2.0",
146
146
  "ajv-merge-patch": "^5.0.1",
147
147
  "branch-comparer": "^1.1.0",
148
+ "cross-env": "^7.0.3",
148
149
  "eslint": "^8.39.0",
149
150
  "eslint-config-standard": "^17.0.0",
150
151
  "eslint-import-resolver-node": "^0.3.7",
151
152
  "eslint-plugin-import": "^2.27.5",
152
- "eslint-plugin-n": "^15.7.0",
153
+ "eslint-plugin-n": "^16.0.1",
153
154
  "eslint-plugin-promise": "^6.1.1",
154
155
  "fast-json-body": "^1.1.0",
155
156
  "fastify-plugin": "^4.5.0",
@@ -161,7 +162,7 @@
161
162
  "json-schema-to-ts": "^2.9.1",
162
163
  "JSONStream": "^1.3.5",
163
164
  "license-checker": "^25.0.1",
164
- "markdownlint-cli2": "^0.7.1",
165
+ "markdownlint-cli2": "^0.8.1",
165
166
  "proxyquire": "^2.1.3",
166
167
  "pump": "^3.0.0",
167
168
  "self-cert": "^2.0.0",
@@ -181,10 +182,10 @@
181
182
  "@fastify/ajv-compiler": "^3.5.0",
182
183
  "@fastify/error": "^3.2.0",
183
184
  "@fastify/fast-json-stringify-compiler": "^4.3.0",
184
- "fast-json-stringify": "^5.7.0",
185
185
  "abstract-logging": "^2.0.1",
186
186
  "avvio": "^8.2.1",
187
187
  "fast-content-type-parse": "^1.0.0",
188
+ "fast-json-stringify": "^5.7.0",
188
189
  "find-my-way": "^7.6.0",
189
190
  "light-my-request": "^5.9.1",
190
191
  "pino": "^8.12.0",
@@ -0,0 +1,51 @@
1
+ const t = require('tap')
2
+ const test = t.test
3
+ const Fastify = require('..')
4
+
5
+ test('Buffer test', async t => {
6
+ const fastify = Fastify()
7
+ fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, fastify.getDefaultJsonParser('error', 'ignore'))
8
+
9
+ fastify.delete('/', async (request) => {
10
+ return request.body
11
+ })
12
+
13
+ test('should return 200 if the body is not empty', async t => {
14
+ t.plan(3)
15
+
16
+ const response = await fastify.inject({
17
+ method: 'DELETE',
18
+ url: '/',
19
+ payload: Buffer.from('{"hello":"world"}'),
20
+ headers: {
21
+ 'content-type': 'application/json'
22
+ }
23
+ })
24
+
25
+ t.error(response.error)
26
+ t.equal(response.statusCode, 200)
27
+ t.same(response.payload.toString(), '{"hello":"world"}')
28
+ })
29
+
30
+ test('should return 400 if the body is empty', async t => {
31
+ t.plan(3)
32
+
33
+ const response = await fastify.inject({
34
+ method: 'DELETE',
35
+ url: '/',
36
+ payload: Buffer.alloc(0),
37
+ headers: {
38
+ 'content-type': 'application/json'
39
+ }
40
+ })
41
+
42
+ t.error(response.error)
43
+ t.equal(response.statusCode, 400)
44
+ t.same(JSON.parse(response.payload.toString()), {
45
+ error: 'Bad Request',
46
+ code: 'FST_ERR_CTP_EMPTY_JSON_BODY',
47
+ message: 'Body cannot be empty when content-type is set to \'application/json\'',
48
+ statusCode: 400
49
+ })
50
+ })
51
+ })
@@ -87,13 +87,16 @@ test('Should close the socket abruptly - pipelining - return503OnClosing: false,
87
87
  })
88
88
 
89
89
  const responses = await Promise.allSettled([
90
+ instance.request({ path: '/', method: 'GET' }),
90
91
  instance.request({ path: '/', method: 'GET' }),
91
92
  instance.request({ path: '/', method: 'GET' }),
92
93
  instance.request({ path: '/', method: 'GET' })
93
94
  ])
95
+
94
96
  t.equal(responses[0].status, 'fulfilled')
95
- t.equal(responses[1].status, 'rejected')
97
+ t.equal(responses[1].status, 'fulfilled')
96
98
  t.equal(responses[2].status, 'rejected')
99
+ t.equal(responses[3].status, 'rejected')
97
100
 
98
101
  await instance.close()
99
102
  })
@@ -2,8 +2,7 @@
2
2
 
3
3
  const net = require('net')
4
4
  const http = require('http')
5
- const t = require('tap')
6
- const test = t.test
5
+ const { test } = require('tap')
7
6
  const Fastify = require('..')
8
7
  const { Client } = require('undici')
9
8
  const semver = require('semver')
@@ -205,7 +204,7 @@ test('Should return error while closing (callback) - injection', t => {
205
204
  })
206
205
 
207
206
  const isV19plus = semver.gte(process.version, '19.0.0')
208
- t.test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node >= v19.x', { skip: isV19plus }, t => {
207
+ test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node >= v19.x', { skip: isV19plus }, t => {
209
208
  const fastify = Fastify({
210
209
  return503OnClosing: false,
211
210
  forceCloseConnections: false
@@ -243,7 +242,7 @@ t.test('Current opened connection should continue to work after closing and retu
243
242
  })
244
243
  })
245
244
 
246
- t.test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node < v19.x', { skip: !isV19plus }, t => {
245
+ test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node < v19.x', { skip: !isV19plus }, t => {
247
246
  t.plan(4)
248
247
  const fastify = Fastify({
249
248
  return503OnClosing: false,
@@ -282,7 +281,7 @@ t.test('Current opened connection should NOT continue to work after closing and
282
281
  })
283
282
  })
284
283
 
285
- t.test('Current opened connection should not accept new incoming connections', t => {
284
+ test('Current opened connection should not accept new incoming connections', t => {
286
285
  t.plan(3)
287
286
  const fastify = Fastify({ forceCloseConnections: false })
288
287
  fastify.get('/', (req, reply) => {
@@ -304,7 +303,7 @@ t.test('Current opened connection should not accept new incoming connections', t
304
303
  })
305
304
  })
306
305
 
307
- t.test('rejected incoming connections should be logged', t => {
306
+ test('rejected incoming connections should be logged', t => {
308
307
  t.plan(2)
309
308
  const stream = split(JSON.parse)
310
309
  const fastify = Fastify({
@@ -449,6 +448,149 @@ test('shutsdown while keep-alive connections are active (non-async, idle, native
449
448
  })
450
449
  })
451
450
 
451
+ test('triggers on-close hook in the right order with multiple bindings', async t => {
452
+ const expectedOrder = [1, 2, 3]
453
+ const order = []
454
+ const fastify = Fastify()
455
+
456
+ t.plan(1)
457
+
458
+ // Follows LIFO
459
+ fastify.addHook('onClose', () => {
460
+ order.push(2)
461
+ })
462
+
463
+ fastify.addHook('onClose', () => {
464
+ order.push(1)
465
+ })
466
+
467
+ await fastify.listen({ port: 0 })
468
+
469
+ await new Promise((resolve, reject) => {
470
+ setTimeout(() => {
471
+ fastify.close(err => {
472
+ order.push(3)
473
+ t.match(order, expectedOrder)
474
+
475
+ if (err) t.error(err)
476
+ else resolve()
477
+ })
478
+ }, 2000)
479
+ })
480
+ })
481
+
482
+ test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - idle)', { skip: noSupport }, async t => {
483
+ const expectedPayload = { hello: 'world' }
484
+ const timeoutTime = 2 * 60 * 1000
485
+ const expectedOrder = [1, 2]
486
+ const order = []
487
+ const fastify = Fastify({ forceCloseConnections: 'idle' })
488
+
489
+ fastify.server.setTimeout(timeoutTime)
490
+ fastify.server.keepAliveTimeout = timeoutTime
491
+
492
+ fastify.get('/', async (req, reply) => {
493
+ await new Promise((resolve) => {
494
+ setTimeout(resolve, 1000)
495
+ })
496
+
497
+ return expectedPayload
498
+ })
499
+
500
+ fastify.addHook('onClose', () => {
501
+ order.push(1)
502
+ })
503
+
504
+ await fastify.listen({ port: 0 })
505
+ const addresses = fastify.addresses()
506
+ const testPlan = (addresses.length * 2) + 1
507
+
508
+ t.plan(testPlan)
509
+
510
+ for (const addr of addresses) {
511
+ const { family, address, port } = addr
512
+ const host = family === 'IPv6' ? `[${address}]` : address
513
+ const client = new Client(`http://${host}:${port}`, {
514
+ keepAliveTimeout: 1 * 60 * 1000
515
+ })
516
+
517
+ client.request({ path: '/', method: 'GET' })
518
+ .then((res) => res.body.json(), err => t.error(err))
519
+ .then(json => {
520
+ t.match(json, expectedPayload, 'should payload match')
521
+ t.notOk(client.closed, 'should client not be closed')
522
+ }, err => t.error(err))
523
+ }
524
+
525
+ await new Promise((resolve, reject) => {
526
+ setTimeout(() => {
527
+ fastify.close(err => {
528
+ order.push(2)
529
+ t.match(order, expectedOrder)
530
+
531
+ if (err) t.error(err)
532
+ else resolve()
533
+ })
534
+ }, 2000)
535
+ })
536
+ })
537
+
538
+ test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - true)', { skip: noSupport }, async t => {
539
+ const expectedPayload = { hello: 'world' }
540
+ const timeoutTime = 2 * 60 * 1000
541
+ const expectedOrder = [1, 2]
542
+ const order = []
543
+ const fastify = Fastify({ forceCloseConnections: true })
544
+
545
+ fastify.server.setTimeout(timeoutTime)
546
+ fastify.server.keepAliveTimeout = timeoutTime
547
+
548
+ fastify.get('/', async (req, reply) => {
549
+ await new Promise((resolve) => {
550
+ setTimeout(resolve, 1000)
551
+ })
552
+
553
+ return expectedPayload
554
+ })
555
+
556
+ fastify.addHook('onClose', () => {
557
+ order.push(1)
558
+ })
559
+
560
+ await fastify.listen({ port: 0 })
561
+ const addresses = fastify.addresses()
562
+ const testPlan = (addresses.length * 2) + 1
563
+
564
+ t.plan(testPlan)
565
+
566
+ for (const addr of addresses) {
567
+ const { family, address, port } = addr
568
+ const host = family === 'IPv6' ? `[${address}]` : address
569
+ const client = new Client(`http://${host}:${port}`, {
570
+ keepAliveTimeout: 1 * 60 * 1000
571
+ })
572
+
573
+ client.request({ path: '/', method: 'GET' })
574
+ .then((res) => res.body.json(), err => t.error(err))
575
+ .then(json => {
576
+ t.match(json, expectedPayload, 'should payload match')
577
+ t.notOk(client.closed, 'should client not be closed')
578
+ }, err => t.error(err))
579
+ }
580
+
581
+ await new Promise((resolve, reject) => {
582
+ setTimeout(() => {
583
+ fastify.close(err => {
584
+ order.push(2)
585
+ t.match(order, expectedOrder)
586
+
587
+ if (err) t.error(err)
588
+ else resolve()
589
+ })
590
+ }, 2000)
591
+ })
592
+ })
593
+
452
594
  test('shutsdown while keep-alive connections are active (non-async, custom)', t => {
453
595
  t.plan(5)
454
596
 
@@ -11,10 +11,10 @@ const dns = require('dns').promises
11
11
  test('Should support a custom http server', async t => {
12
12
  const localAddresses = await dns.lookup('localhost', { all: true })
13
13
 
14
- t.plan(localAddresses.length + 3)
14
+ t.plan((localAddresses.length - 1) + 3)
15
15
 
16
16
  const serverFactory = (handler, opts) => {
17
- t.ok(opts.serverFactory, 'it is called twice for every HOST interface')
17
+ t.ok(opts.serverFactory, 'it is called once for localhost')
18
18
 
19
19
  const server = http.createServer((req, res) => {
20
20
  req.custom = true
@@ -71,3 +71,14 @@ test('Should not allow forceCloseConnection=idle if the server does not support
71
71
  "Cannot set forceCloseConnections to 'idle' as your HTTP server does not support closeIdleConnections method"
72
72
  )
73
73
  })
74
+
75
+ test('Should accept user defined serverFactory and ignore secondary server creation', async t => {
76
+ const server = http.createServer(() => {})
77
+ t.teardown(() => new Promise(resolve => server.close(resolve)))
78
+ const app = await Fastify({
79
+ serverFactory: () => server
80
+ })
81
+ t.resolves(async () => {
82
+ await app.listen({ port: 0 })
83
+ })
84
+ })
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { Readable } = require('stream')
3
4
  const { test } = require('tap')
4
5
  const Fastify = require('..')
5
6
 
@@ -30,3 +31,44 @@ test('Should accept a custom genReqId function', t => {
30
31
  })
31
32
  })
32
33
  })
34
+
35
+ test('Custom genReqId function gets raw request as argument', t => {
36
+ t.plan(9)
37
+
38
+ const REQUEST_ID = 'REQ-1234'
39
+
40
+ const fastify = Fastify({
41
+ genReqId: function (req) {
42
+ t.notOk('id' in req)
43
+ t.notOk('raw' in req)
44
+ t.ok(req instanceof Readable)
45
+ // http.IncomingMessage does have `rawHeaders` property, but FastifyRequest does not
46
+ const index = req.rawHeaders.indexOf('x-request-id')
47
+ const xReqId = req.rawHeaders[index + 1]
48
+ t.equal(xReqId, REQUEST_ID)
49
+ t.equal(req.headers['x-request-id'], REQUEST_ID)
50
+ return xReqId
51
+ }
52
+ })
53
+
54
+ fastify.get('/', (req, reply) => {
55
+ t.equal(req.id, REQUEST_ID)
56
+ reply.send({ id: req.id })
57
+ })
58
+
59
+ fastify.listen({ port: 0 }, err => {
60
+ t.error(err)
61
+ fastify.inject({
62
+ method: 'GET',
63
+ headers: {
64
+ 'x-request-id': REQUEST_ID
65
+ },
66
+ url: 'http://localhost:' + fastify.server.address().port
67
+ }, (err, res) => {
68
+ t.error(err)
69
+ const payload = JSON.parse(res.payload)
70
+ t.equal(payload.id, REQUEST_ID)
71
+ fastify.close()
72
+ })
73
+ })
74
+ })
@@ -0,0 +1,65 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const test = t.test
5
+ const Fastify = require('..')
6
+ const sget = require('simple-get').concat
7
+
8
+ const maxHeaderSize = 1024
9
+
10
+ test('Should return 431 if request header fields are too large', t => {
11
+ t.plan(3)
12
+
13
+ const fastify = Fastify({ http: { maxHeaderSize } })
14
+ fastify.route({
15
+ method: 'GET',
16
+ url: '/',
17
+ handler: (_req, reply) => {
18
+ reply.send({ hello: 'world' })
19
+ }
20
+ })
21
+
22
+ fastify.listen({ port: 0 }, function (err) {
23
+ t.error(err)
24
+
25
+ sget({
26
+ method: 'GET',
27
+ url: 'http://localhost:' + fastify.server.address().port,
28
+ headers: {
29
+ 'Large-Header': 'a'.repeat(maxHeaderSize)
30
+ }
31
+ }, (err, res) => {
32
+ t.error(err)
33
+ t.equal(res.statusCode, 431)
34
+ })
35
+ })
36
+
37
+ t.teardown(() => fastify.close())
38
+ })
39
+
40
+ test('Should return 431 if URI is too long', t => {
41
+ t.plan(3)
42
+
43
+ const fastify = Fastify({ http: { maxHeaderSize } })
44
+ fastify.route({
45
+ method: 'GET',
46
+ url: '/',
47
+ handler: (_req, reply) => {
48
+ reply.send({ hello: 'world' })
49
+ }
50
+ })
51
+
52
+ fastify.listen({ port: 0 }, function (err) {
53
+ t.error(err)
54
+
55
+ sget({
56
+ method: 'GET',
57
+ url: 'http://localhost:' + fastify.server.address().port + `/${'a'.repeat(maxHeaderSize)}`
58
+ }, (err, res) => {
59
+ t.error(err)
60
+ t.equal(res.statusCode, 431)
61
+ })
62
+ })
63
+
64
+ t.teardown(() => fastify.close())
65
+ })
@@ -13,10 +13,10 @@ t.before(buildCertificate)
13
13
  test('Should support a custom https server', async t => {
14
14
  const localAddresses = await dns.lookup('localhost', { all: true })
15
15
 
16
- t.plan(localAddresses.length + 3)
16
+ t.plan((localAddresses.length - 1) + 3)
17
17
 
18
18
  const serverFactory = (handler, opts) => {
19
- t.ok(opts.serverFactory, 'it is called twice for every HOST interface')
19
+ t.ok(opts.serverFactory, 'it is called once for localhost')
20
20
 
21
21
  const options = {
22
22
  key: global.context.key,