fastify 3.27.2 → 4.0.0-alpha.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 (116) hide show
  1. package/README.md +5 -4
  2. package/build/build-error-serializer.js +27 -0
  3. package/build/build-validation.js +47 -35
  4. package/docs/Migration-Guide-V4.md +12 -0
  5. package/docs/Reference/ContentTypeParser.md +4 -0
  6. package/docs/Reference/Errors.md +51 -6
  7. package/docs/Reference/Hooks.md +4 -7
  8. package/docs/Reference/LTS.md +5 -4
  9. package/docs/Reference/Reply.md +23 -22
  10. package/docs/Reference/Request.md +1 -3
  11. package/docs/Reference/Routes.md +17 -10
  12. package/docs/Reference/Server.md +48 -63
  13. package/docs/Reference/TypeScript.md +11 -13
  14. package/docs/Reference/Validation-and-Serialization.md +28 -53
  15. package/docs/Type-Providers.md +257 -0
  16. package/examples/hooks.js +1 -1
  17. package/examples/simple-stream.js +18 -0
  18. package/fastify.d.ts +34 -22
  19. package/fastify.js +37 -35
  20. package/lib/configValidator.js +902 -1023
  21. package/lib/contentTypeParser.js +6 -16
  22. package/lib/context.js +36 -10
  23. package/lib/decorate.js +3 -1
  24. package/lib/error-handler.js +158 -0
  25. package/lib/error-serializer.js +257 -0
  26. package/lib/errors.js +43 -9
  27. package/lib/fourOhFour.js +31 -20
  28. package/lib/handleRequest.js +10 -13
  29. package/lib/hooks.js +14 -9
  30. package/lib/pluginOverride.js +0 -3
  31. package/lib/pluginUtils.js +3 -2
  32. package/lib/reply.js +28 -157
  33. package/lib/request.js +13 -10
  34. package/lib/route.js +131 -138
  35. package/lib/schema-controller.js +2 -2
  36. package/lib/schemas.js +27 -1
  37. package/lib/server.js +219 -116
  38. package/lib/symbols.js +4 -3
  39. package/lib/validation.js +2 -1
  40. package/lib/warnings.js +2 -12
  41. package/lib/wrapThenable.js +4 -11
  42. package/package.json +31 -35
  43. package/test/404s.test.js +243 -110
  44. package/test/500s.test.js +2 -2
  45. package/test/async-await.test.js +13 -69
  46. package/test/content-parser.test.js +32 -0
  47. package/test/context-config.test.js +52 -0
  48. package/test/custom-http-server.test.js +14 -7
  49. package/test/custom-parser-async.test.js +0 -65
  50. package/test/custom-parser.test.js +54 -121
  51. package/test/decorator.test.js +1 -3
  52. package/test/delete.test.js +5 -5
  53. package/test/encapsulated-error-handler.test.js +50 -0
  54. package/test/esm/index.test.js +0 -14
  55. package/test/fastify-instance.test.js +4 -4
  56. package/test/fluent-schema.test.js +4 -4
  57. package/test/get.test.js +3 -3
  58. package/test/helper.js +18 -3
  59. package/test/hooks-async.test.js +14 -47
  60. package/test/hooks.on-ready.test.js +9 -4
  61. package/test/hooks.test.js +58 -99
  62. package/test/http2/closing.test.js +5 -11
  63. package/test/http2/unknown-http-method.test.js +3 -9
  64. package/test/https/custom-https-server.test.js +12 -6
  65. package/test/input-validation.js +2 -2
  66. package/test/internals/handleRequest.test.js +3 -40
  67. package/test/internals/initialConfig.test.js +33 -12
  68. package/test/internals/reply.test.js +245 -3
  69. package/test/internals/request.test.js +13 -7
  70. package/test/internals/server.test.js +88 -0
  71. package/test/listen.test.js +84 -1
  72. package/test/logger.test.js +80 -40
  73. package/test/maxRequestsPerSocket.test.js +6 -4
  74. package/test/middleware.test.js +2 -25
  75. package/test/nullable-validation.test.js +51 -14
  76. package/test/plugin.test.js +31 -5
  77. package/test/pretty-print.test.js +22 -10
  78. package/test/reply-error.test.js +123 -12
  79. package/test/request-error.test.js +2 -5
  80. package/test/route-hooks.test.js +17 -17
  81. package/test/route-prefix.test.js +2 -1
  82. package/test/route.test.js +204 -20
  83. package/test/router-options.test.js +1 -1
  84. package/test/schema-examples.test.js +11 -5
  85. package/test/schema-feature.test.js +24 -19
  86. package/test/schema-serialization.test.js +9 -9
  87. package/test/schema-special-usage.test.js +14 -81
  88. package/test/schema-validation.test.js +9 -9
  89. package/test/skip-reply-send.test.js +1 -1
  90. package/test/stream.test.js +23 -12
  91. package/test/throw.test.js +8 -5
  92. package/test/type-provider.test.js +20 -0
  93. package/test/types/fastify.test-d.ts +10 -18
  94. package/test/types/import.js +2 -0
  95. package/test/types/import.ts +1 -0
  96. package/test/types/instance.test-d.ts +35 -14
  97. package/test/types/logger.test-d.ts +44 -15
  98. package/test/types/route.test-d.ts +8 -2
  99. package/test/types/schema.test-d.ts +2 -39
  100. package/test/types/type-provider.test-d.ts +417 -0
  101. package/test/validation-error-handling.test.js +8 -8
  102. package/test/versioned-routes.test.js +28 -16
  103. package/test/wrapThenable.test.js +7 -6
  104. package/types/content-type-parser.d.ts +17 -8
  105. package/types/hooks.d.ts +102 -59
  106. package/types/instance.d.ts +124 -104
  107. package/types/logger.d.ts +18 -104
  108. package/types/plugin.d.ts +10 -4
  109. package/types/reply.d.ts +16 -11
  110. package/types/request.d.ts +10 -5
  111. package/types/route.d.ts +42 -31
  112. package/types/schema.d.ts +1 -1
  113. package/types/type-provider.d.ts +99 -0
  114. package/types/utils.d.ts +1 -1
  115. package/lib/schema-compilers.js +0 -12
  116. package/test/emit-warning.test.js +0 -166
@@ -3,7 +3,6 @@
3
3
  const t = require('tap')
4
4
  const Fastify = require('../..')
5
5
  const http2 = require('http2')
6
- const semver = require('semver')
7
6
  const { promisify } = require('util')
8
7
  const connect = promisify(http2.connect)
9
8
  const { once } = require('events')
@@ -37,8 +36,7 @@ t.test('http/2 request while fastify closing', t => {
37
36
  t.error(err)
38
37
  fastify.server.unref()
39
38
 
40
- // Skipped because there is likely a bug on Node 8.
41
- t.test('return 200', { skip: semver.lt(process.versions.node, '10.15.0') }, t => {
39
+ t.test('return 200', t => {
42
40
  const url = getUrl(fastify)
43
41
  const session = http2.connect(url, function () {
44
42
  this.request({
@@ -85,8 +83,7 @@ t.test('http/2 request while fastify closing - return503OnClosing: false', t =>
85
83
  t.error(err)
86
84
  fastify.server.unref()
87
85
 
88
- // Skipped because there is likely a bug on Node 8.
89
- t.test('return 200', { skip: semver.lt(process.versions.node, '10.15.0') }, t => {
86
+ t.test('return 200', t => {
90
87
  const url = getUrl(fastify)
91
88
  const session = http2.connect(url, function () {
92
89
  this.request({
@@ -115,8 +112,7 @@ t.test('http/2 request while fastify closing - return503OnClosing: false', t =>
115
112
  })
116
113
  })
117
114
 
118
- // Skipped because there is likely a bug on Node 8.
119
- t.test('http/2 closes successfully with async await', { skip: semver.lt(process.versions.node, '10.15.0') }, async t => {
115
+ t.test('http/2 closes successfully with async await', async t => {
120
116
  const fastify = Fastify({
121
117
  http2SessionTimeout: 100,
122
118
  http2: true
@@ -131,8 +127,7 @@ t.test('http/2 closes successfully with async await', { skip: semver.lt(process.
131
127
  await fastify.close()
132
128
  })
133
129
 
134
- // Skipped because there is likely a bug on Node 8.
135
- t.test('https/2 closes successfully with async await', { skip: semver.lt(process.versions.node, '10.15.0') }, async t => {
130
+ t.test('https/2 closes successfully with async await', async t => {
136
131
  const fastify = Fastify({
137
132
  http2SessionTimeout: 100,
138
133
  http2: true,
@@ -151,8 +146,7 @@ t.test('https/2 closes successfully with async await', { skip: semver.lt(process
151
146
  await fastify.close()
152
147
  })
153
148
 
154
- // Skipped because there is likely a bug on Node 8.
155
- t.test('http/2 server side session emits a timeout event', { skip: semver.lt(process.versions.node, '10.15.0') }, async t => {
149
+ t.test('http/2 server side session emits a timeout event', async t => {
156
150
  let _resolve
157
151
  const p = new Promise((resolve) => { _resolve = resolve })
158
152
 
@@ -6,15 +6,9 @@ const Fastify = require('../..')
6
6
  const h2url = require('h2url')
7
7
  const msg = { hello: 'world' }
8
8
 
9
- let fastify
10
- try {
11
- fastify = Fastify({
12
- http2: true
13
- })
14
- t.pass('http2 successfully loaded')
15
- } catch (e) {
16
- t.fail('http2 loading failed', e)
17
- }
9
+ const fastify = Fastify({
10
+ http2: true
11
+ })
18
12
 
19
13
  fastify.get('/', function (req, reply) {
20
14
  reply.code(200).send(msg)
@@ -5,15 +5,18 @@ const test = t.test
5
5
  const Fastify = require('../..')
6
6
  const https = require('https')
7
7
  const sget = require('simple-get').concat
8
+ const dns = require('dns').promises
8
9
 
9
10
  const { buildCertificate } = require('../build-certificate')
10
11
  t.before(buildCertificate)
11
12
 
12
- test('Should support a custom https server', t => {
13
- t.plan(6)
13
+ test('Should support a custom https server', async t => {
14
+ const localAddresses = await dns.lookup('localhost', { all: true })
15
+
16
+ t.plan(localAddresses.length + 3)
14
17
 
15
18
  const serverFactory = (handler, opts) => {
16
- t.ok(opts.serverFactory)
19
+ t.ok(opts.serverFactory, 'it is called twice for every HOST interface')
17
20
 
18
21
  const options = {
19
22
  key: global.context.key,
@@ -37,17 +40,20 @@ test('Should support a custom https server', t => {
37
40
  reply.send({ hello: 'world' })
38
41
  })
39
42
 
40
- fastify.listen(0, err => {
41
- t.error(err)
43
+ await fastify.listen(0)
42
44
 
45
+ await new Promise((resolve, reject) => {
43
46
  sget({
44
47
  method: 'GET',
45
48
  url: 'https://localhost:' + fastify.server.address().port,
46
49
  rejectUnauthorized: false
47
50
  }, (err, response, body) => {
48
- t.error(err)
51
+ if (err) {
52
+ return reject(err)
53
+ }
49
54
  t.equal(response.statusCode, 200)
50
55
  t.same(JSON.parse(body), { hello: 'world' })
56
+ resolve()
51
57
  })
52
58
  })
53
59
  })
@@ -3,7 +3,7 @@
3
3
 
4
4
  const sget = require('simple-get').concat
5
5
  const Ajv = require('ajv')
6
- const Joi = require('@hapi/joi')
6
+ const Joi = require('joi')
7
7
  const yup = require('yup')
8
8
 
9
9
  module.exports.payloadMethod = function (method, t) {
@@ -182,7 +182,7 @@ module.exports.payloadMethod = function (method, t) {
182
182
  t.equal(response.statusCode, 400)
183
183
  t.same(body, {
184
184
  error: 'Bad Request',
185
- message: 'body.hello should be integer',
185
+ message: 'body/hello must be integer',
186
186
  statusCode: 400
187
187
  })
188
188
  })
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { test } = require('tap')
4
- const semver = require('semver')
5
4
  const handleRequest = require('../../lib/handleRequest')
6
5
  const internals = require('../../lib/handleRequest')[Symbol.for('internals')]
7
6
  const Request = require('../../lib/request')
@@ -40,13 +39,8 @@ test('handleRequest function - invoke with error', t => {
40
39
  })
41
40
 
42
41
  test('handler function - invalid schema', t => {
43
- t.plan(2)
42
+ t.plan(1)
44
43
  const res = {}
45
- res.end = () => {
46
- t.equal(res.statusCode, 400)
47
- t.pass()
48
- }
49
- res.writeHead = () => {}
50
44
  res.log = { error: () => {}, info: () => {} }
51
45
  const context = {
52
46
  config: {
@@ -61,6 +55,7 @@ test('handler function - invalid schema', t => {
61
55
  }
62
56
  }
63
57
  },
58
+ errorHandler: { func: () => { t.pass('errorHandler called') } },
64
59
  handler: () => {},
65
60
  Reply,
66
61
  Request,
@@ -108,39 +103,7 @@ test('handler function - preValidationCallback with finished response', t => {
108
103
  t.plan(0)
109
104
  const res = {}
110
105
  // Be sure to check only `writableEnded` where is available
111
- if (semver.gte(process.versions.node, '12.9.0')) {
112
- res.writableEnded = true
113
- } else {
114
- res.writable = false
115
- res.finished = true
116
- }
117
- res.end = () => {
118
- t.fail()
119
- }
120
- res.writeHead = () => {}
121
- const context = {
122
- handler: (req, reply) => {
123
- t.fail()
124
- reply.send(undefined)
125
- },
126
- Reply,
127
- Request,
128
- preValidation: null,
129
- preHandler: [],
130
- onSend: [],
131
- onError: []
132
- }
133
- buildSchema(context, schemaValidator)
134
- internals.handler({}, new Reply(res, { context }))
135
- })
136
-
137
- test('handler function - preValidationCallback with finished response (< v12.9.0)', t => {
138
- t.plan(0)
139
- const res = {}
140
- // Be sure to check only `writableEnded` where is available
141
- res.writable = false
142
- res.finished = true
143
-
106
+ res.writableEnded = true
144
107
  res.end = () => {
145
108
  t.fail()
146
109
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { test, before } = require('tap')
4
4
  const Fastify = require('../..')
5
+ const helper = require('../helper')
5
6
  const http = require('http')
6
7
  const pino = require('pino')
7
8
  const split = require('split2')
@@ -9,10 +10,17 @@ const deepClone = require('rfdc')({ circles: true, proto: false })
9
10
  const { deepFreezeObject } = require('../../lib/initialConfigValidation').utils
10
11
 
11
12
  const { buildCertificate } = require('../build-certificate')
12
- before(buildCertificate)
13
13
 
14
14
  process.removeAllListeners('warning')
15
15
 
16
+ let localhost
17
+ let localhostForURL
18
+
19
+ before(async function () {
20
+ await buildCertificate();
21
+ [localhost, localhostForURL] = await helper.getLoopbackHost()
22
+ })
23
+
16
24
  test('Fastify.initialConfig is an object', t => {
17
25
  t.plan(1)
18
26
  t.type(Fastify().initialConfig, 'object')
@@ -23,7 +31,8 @@ test('without options passed to Fastify, initialConfig should expose default val
23
31
 
24
32
  const fastifyDefaultOptions = {
25
33
  connectionTimeout: 0,
26
- keepAliveTimeout: 5000,
34
+ keepAliveTimeout: 72000,
35
+ forceCloseConnections: false,
27
36
  maxRequestsPerSocket: 0,
28
37
  requestTimeout: 0,
29
38
  bodyLimit: 1024 * 1024,
@@ -37,7 +46,8 @@ test('without options passed to Fastify, initialConfig should expose default val
37
46
  pluginTimeout: 10000,
38
47
  requestIdHeader: 'request-id',
39
48
  requestIdLogLabel: 'reqId',
40
- http2SessionTimeout: 5000
49
+ http2SessionTimeout: 72000,
50
+ exposeHeadRoutes: true
41
51
  }
42
52
 
43
53
  t.same(Fastify().initialConfig, fastifyDefaultOptions)
@@ -81,7 +91,7 @@ test('Fastify.initialConfig should expose all options', t => {
81
91
  ignoreTrailingSlash: true,
82
92
  maxParamLength: 200,
83
93
  connectionTimeout: 0,
84
- keepAliveTimeout: 5000,
94
+ keepAliveTimeout: 72000,
85
95
  bodyLimit: 1049600,
86
96
  onProtoPoisoning: 'remove',
87
97
  serverFactory,
@@ -103,11 +113,11 @@ test('Fastify.initialConfig should expose all options', t => {
103
113
 
104
114
  const fastify = Fastify(options)
105
115
  t.equal(fastify.initialConfig.http2, true)
106
- t.equal(fastify.initialConfig.https, true)
116
+ t.equal(fastify.initialConfig.https, true, 'for security reason the key cert is hidden')
107
117
  t.equal(fastify.initialConfig.ignoreTrailingSlash, true)
108
118
  t.equal(fastify.initialConfig.maxParamLength, 200)
109
119
  t.equal(fastify.initialConfig.connectionTimeout, 0)
110
- t.equal(fastify.initialConfig.keepAliveTimeout, 5000)
120
+ t.equal(fastify.initialConfig.keepAliveTimeout, 72000)
111
121
  t.equal(fastify.initialConfig.bodyLimit, 1049600)
112
122
  t.equal(fastify.initialConfig.onProtoPoisoning, 'remove')
113
123
  t.equal(fastify.initialConfig.caseSensitive, true)
@@ -157,10 +167,19 @@ test('We must avoid shallow freezing and ensure that the whole object is freezed
157
167
  t.type(error, TypeError)
158
168
  t.equal(error.message, "Cannot assign to read only property 'allowHTTP1' of object '#<Object>'")
159
169
  t.ok(error.stack)
160
- t.pass()
170
+ t.same(fastify.initialConfig.https, {
171
+ allowHTTP1: true
172
+ }, 'key cert removed')
161
173
  }
162
174
  })
163
175
 
176
+ test('https value check', t => {
177
+ t.plan(1)
178
+
179
+ const fastify = Fastify({})
180
+ t.notOk(fastify.initialConfig.https)
181
+ })
182
+
164
183
  test('Return an error if options do not match the validation schema', t => {
165
184
  t.plan(6)
166
185
 
@@ -171,7 +190,7 @@ test('Return an error if options do not match the validation schema', t => {
171
190
  } catch (error) {
172
191
  t.type(error, Error)
173
192
  t.equal(error.name, 'FastifyError')
174
- t.equal(error.message, 'Invalid initialization options: \'["should be boolean"]\'')
193
+ t.equal(error.message, 'Invalid initialization options: \'["must be boolean"]\'')
175
194
  t.equal(error.code, 'FST_ERR_INIT_OPTS_INVALID')
176
195
  t.ok(error.stack)
177
196
  t.pass()
@@ -241,7 +260,8 @@ test('Should not have issues when passing stream options to Pino.js', t => {
241
260
  t.type(fastify, 'object')
242
261
  t.same(fastify.initialConfig, {
243
262
  connectionTimeout: 0,
244
- keepAliveTimeout: 5000,
263
+ keepAliveTimeout: 72000,
264
+ forceCloseConnections: false,
245
265
  maxRequestsPerSocket: 0,
246
266
  requestTimeout: 0,
247
267
  bodyLimit: 1024 * 1024,
@@ -255,7 +275,8 @@ test('Should not have issues when passing stream options to Pino.js', t => {
255
275
  pluginTimeout: 10000,
256
276
  requestIdHeader: 'request-id',
257
277
  requestIdLogLabel: 'reqId',
258
- http2SessionTimeout: 5000
278
+ http2SessionTimeout: 72000,
279
+ exposeHeadRoutes: true
259
280
  })
260
281
  } catch (error) {
261
282
  t.fail()
@@ -287,11 +308,11 @@ test('Should not have issues when passing stream options to Pino.js', t => {
287
308
  })
288
309
  })
289
310
 
290
- fastify.listen(0, err => {
311
+ fastify.listen(0, localhost, err => {
291
312
  t.error(err)
292
313
  fastify.server.unref()
293
314
 
294
- http.get('http://localhost:' + fastify.server.address().port)
315
+ http.get(`http://${localhostForURL}:${fastify.server.address().port}`)
295
316
  })
296
317
  })
297
318
 
@@ -15,6 +15,21 @@ const {
15
15
  kReplyIsError,
16
16
  kReplySerializerDefault
17
17
  } = require('../../lib/symbols')
18
+ const fs = require('fs')
19
+ const path = require('path')
20
+ const warning = require('../../lib/warnings')
21
+
22
+ const doGet = function (url) {
23
+ return new Promise((resolve, reject) => {
24
+ sget({ method: 'GET', url, followRedirects: false }, (err, response, body) => {
25
+ if (err) {
26
+ reject(err)
27
+ } else {
28
+ resolve({ response, body })
29
+ }
30
+ })
31
+ })
32
+ }
18
33
 
19
34
  test('Once called, Reply should return an object with methods', t => {
20
35
  t.plan(13)
@@ -37,7 +52,7 @@ test('Once called, Reply should return an object with methods', t => {
37
52
  t.equal(reply.request, request)
38
53
  })
39
54
 
40
- test('reply.send will logStream error and destroy the stream', { only: true }, t => {
55
+ test('reply.send will logStream error and destroy the stream', t => {
41
56
  t.plan(1)
42
57
  let destroyCalled
43
58
  const payload = new EventEmitter()
@@ -452,8 +467,6 @@ test('stream with content type should not send application/octet-stream', t => {
452
467
  t.plan(4)
453
468
 
454
469
  const fastify = require('../..')()
455
- const fs = require('fs')
456
- const path = require('path')
457
470
 
458
471
  const streamPath = path.join(__dirname, '..', '..', 'package.json')
459
472
  const stream = fs.createReadStream(streamPath)
@@ -477,6 +490,32 @@ test('stream with content type should not send application/octet-stream', t => {
477
490
  })
478
491
  })
479
492
 
493
+ test('stream without content type should not send application/octet-stream', t => {
494
+ t.plan(4)
495
+
496
+ const fastify = require('../..')()
497
+
498
+ const stream = fs.createReadStream(__filename)
499
+ const buf = fs.readFileSync(__filename)
500
+
501
+ fastify.get('/', function (req, reply) {
502
+ reply.send(stream)
503
+ })
504
+
505
+ fastify.listen(0, err => {
506
+ t.error(err)
507
+ fastify.server.unref()
508
+ sget({
509
+ method: 'GET',
510
+ url: 'http://localhost:' + fastify.server.address().port
511
+ }, (err, response, body) => {
512
+ t.error(err)
513
+ t.equal(response.headers['content-type'], undefined)
514
+ t.same(body, buf)
515
+ })
516
+ })
517
+ })
518
+
480
519
  test('stream using reply.raw.writeHead should return customize headers', t => {
481
520
  t.plan(6)
482
521
 
@@ -1301,6 +1340,34 @@ test('reply.header setting multiple cookies as multiple Set-Cookie headers', t =
1301
1340
  })
1302
1341
  })
1303
1342
 
1343
+ test('should emit deprecation warning when trying to modify the reply.sent property', t => {
1344
+ t.plan(4)
1345
+ const fastify = require('../..')()
1346
+
1347
+ const deprecationCode = 'FSTDEP010'
1348
+ warning.emitted.delete(deprecationCode)
1349
+
1350
+ process.removeAllListeners('warning')
1351
+ process.on('warning', onWarning)
1352
+ function onWarning (warning) {
1353
+ t.equal(warning.name, 'FastifyDeprecation')
1354
+ t.equal(warning.code, deprecationCode)
1355
+ }
1356
+
1357
+ fastify.get('/', (req, reply) => {
1358
+ reply.sent = true
1359
+
1360
+ reply.raw.end()
1361
+ })
1362
+
1363
+ fastify.inject('/', (err, res) => {
1364
+ t.error(err)
1365
+ t.pass()
1366
+
1367
+ process.removeListener('warning', onWarning)
1368
+ })
1369
+ })
1370
+
1304
1371
  test('should throw error when passing falsy value to reply.sent', t => {
1305
1372
  t.plan(4)
1306
1373
  const fastify = require('../..')()
@@ -1329,6 +1396,7 @@ test('should throw error when attempting to set reply.sent more than once', t =>
1329
1396
  reply.sent = true
1330
1397
  try {
1331
1398
  reply.sent = true
1399
+ t.fail('must throw')
1332
1400
  } catch (err) {
1333
1401
  t.equal(err.code, 'FST_ERR_REP_ALREADY_SENT')
1334
1402
  t.equal(err.message, 'Reply was already sent.')
@@ -1342,6 +1410,23 @@ test('should throw error when attempting to set reply.sent more than once', t =>
1342
1410
  })
1343
1411
  })
1344
1412
 
1413
+ test('should not throw error when attempting to set reply.sent if the underlining request was sent', t => {
1414
+ t.plan(3)
1415
+ const fastify = require('../..')()
1416
+
1417
+ fastify.get('/', function (req, reply) {
1418
+ reply.raw.end()
1419
+ t.doesNotThrow(() => {
1420
+ reply.sent = true
1421
+ })
1422
+ })
1423
+
1424
+ fastify.inject('/', (err, res) => {
1425
+ t.error(err)
1426
+ t.pass()
1427
+ })
1428
+ })
1429
+
1345
1430
  test('reply.getResponseTime() should return 0 before the timer is initialised on the reply by setting up response listeners', t => {
1346
1431
  t.plan(1)
1347
1432
  const response = { statusCode: 200 }
@@ -1727,3 +1812,160 @@ test('reply.then', t => {
1727
1812
  response.destroy(_err)
1728
1813
  })
1729
1814
  })
1815
+
1816
+ test('reply.sent should read from response.writableEnded if it is defined', t => {
1817
+ t.plan(1)
1818
+
1819
+ const reply = new Reply({ writableEnded: true }, {}, {})
1820
+
1821
+ t.equal(reply.sent, true)
1822
+ })
1823
+
1824
+ test('redirect to an invalid URL should not crash the server', async t => {
1825
+ const fastify = require('../..')()
1826
+ fastify.route({
1827
+ method: 'GET',
1828
+ url: '/redirect',
1829
+ handler: (req, reply) => {
1830
+ reply.log.warn = function mockWarn (obj, message) {
1831
+ t.equal(message, 'Invalid character in header content ["location"]')
1832
+ }
1833
+
1834
+ switch (req.query.useCase) {
1835
+ case '1':
1836
+ reply.redirect('/?key=a’b')
1837
+ break
1838
+
1839
+ case '2':
1840
+ reply.redirect(encodeURI('/?key=a’b'))
1841
+ break
1842
+
1843
+ default:
1844
+ reply.redirect('/?key=ab')
1845
+ break
1846
+ }
1847
+ }
1848
+ })
1849
+
1850
+ await fastify.listen(0)
1851
+
1852
+ {
1853
+ const { response, body } = await doGet(`http://localhost:${fastify.server.address().port}/redirect?useCase=1`)
1854
+ t.equal(response.statusCode, 500)
1855
+ t.same(JSON.parse(body), {
1856
+ statusCode: 500,
1857
+ code: 'ERR_INVALID_CHAR',
1858
+ error: 'Internal Server Error',
1859
+ message: 'Invalid character in header content ["location"]'
1860
+ })
1861
+ }
1862
+ {
1863
+ const { response } = await doGet(`http://localhost:${fastify.server.address().port}/redirect?useCase=2`)
1864
+ t.equal(response.statusCode, 302)
1865
+ t.equal(response.headers.location, '/?key=a%E2%80%99b')
1866
+ }
1867
+
1868
+ {
1869
+ const { response } = await doGet(`http://localhost:${fastify.server.address().port}/redirect?useCase=3`)
1870
+ t.equal(response.statusCode, 302)
1871
+ t.equal(response.headers.location, '/?key=ab')
1872
+ }
1873
+
1874
+ await fastify.close()
1875
+ })
1876
+
1877
+ test('invalid response headers should not crash the server', async t => {
1878
+ const fastify = require('../..')()
1879
+ fastify.route({
1880
+ method: 'GET',
1881
+ url: '/bad-headers',
1882
+ handler: (req, reply) => {
1883
+ reply.log.warn = function mockWarn (obj, message) {
1884
+ t.equal(message, 'Invalid character in header content ["smile-encoded"]', 'only the first invalid header is logged')
1885
+ }
1886
+
1887
+ reply.header('foo', '$')
1888
+ reply.header('smile-encoded', '\uD83D\uDE00')
1889
+ reply.header('smile', '😄')
1890
+ reply.header('bar', 'ƒ∂å')
1891
+
1892
+ reply.send({})
1893
+ }
1894
+ })
1895
+
1896
+ await fastify.listen(0)
1897
+
1898
+ const { response, body } = await doGet(`http://localhost:${fastify.server.address().port}/bad-headers`)
1899
+ t.equal(response.statusCode, 500)
1900
+ t.same(JSON.parse(body), {
1901
+ statusCode: 500,
1902
+ code: 'ERR_INVALID_CHAR',
1903
+ error: 'Internal Server Error',
1904
+ message: 'Invalid character in header content ["smile-encoded"]'
1905
+ })
1906
+
1907
+ await fastify.close()
1908
+ })
1909
+
1910
+ test('invalid response headers when sending back an error', async t => {
1911
+ const fastify = require('../..')()
1912
+ fastify.route({
1913
+ method: 'GET',
1914
+ url: '/bad-headers',
1915
+ handler: (req, reply) => {
1916
+ reply.log.warn = function mockWarn (obj, message) {
1917
+ t.equal(message, 'Invalid character in header content ["smile"]', 'only the first invalid header is logged')
1918
+ }
1919
+
1920
+ reply.header('smile', '😄')
1921
+ reply.send(new Error('user land error'))
1922
+ }
1923
+ })
1924
+
1925
+ await fastify.listen(0)
1926
+
1927
+ const { response, body } = await doGet(`http://localhost:${fastify.server.address().port}/bad-headers`)
1928
+ t.equal(response.statusCode, 500)
1929
+ t.same(JSON.parse(body), {
1930
+ statusCode: 500,
1931
+ code: 'ERR_INVALID_CHAR',
1932
+ error: 'Internal Server Error',
1933
+ message: 'Invalid character in header content ["smile"]'
1934
+ })
1935
+
1936
+ await fastify.close()
1937
+ })
1938
+
1939
+ test('invalid response headers and custom error handler', async t => {
1940
+ const fastify = require('../..')()
1941
+ fastify.route({
1942
+ method: 'GET',
1943
+ url: '/bad-headers',
1944
+ handler: (req, reply) => {
1945
+ reply.log.warn = function mockWarn (obj, message) {
1946
+ t.equal(message, 'Invalid character in header content ["smile"]', 'only the first invalid header is logged')
1947
+ }
1948
+
1949
+ reply.header('smile', '😄')
1950
+ reply.send(new Error('user land error'))
1951
+ }
1952
+ })
1953
+
1954
+ fastify.setErrorHandler(function (error, request, reply) {
1955
+ t.equal(error.message, 'user land error', 'custom error handler receives the error')
1956
+ reply.status(500).send({ ops: true })
1957
+ })
1958
+
1959
+ await fastify.listen(0)
1960
+
1961
+ const { response, body } = await doGet(`http://localhost:${fastify.server.address().port}/bad-headers`)
1962
+ t.equal(response.statusCode, 500)
1963
+ t.same(JSON.parse(body), {
1964
+ statusCode: 500,
1965
+ code: 'ERR_INVALID_CHAR',
1966
+ error: 'Internal Server Error',
1967
+ message: 'Invalid character in header content ["smile"]'
1968
+ })
1969
+
1970
+ await fastify.close()
1971
+ })