fastify 4.0.2 → 4.0.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/README.md CHANGED
@@ -56,7 +56,7 @@ developer experience with the least overhead and a powerful plugin architecture.
56
56
  It is inspired by Hapi and Express and as far as we know, it is one of the
57
57
  fastest web frameworks in town.
58
58
 
59
- This branch refers to the upcoming Fastify v4 release. Check out the
59
+ This branch refers to the Fastify v4 release. Check out the
60
60
  [v3.x](https://github.com/fastify/fastify/tree/v3.x) branch for v3.
61
61
 
62
62
  ### Quick start
@@ -104,11 +104,11 @@ project as a dependency:
104
104
 
105
105
  Install with npm:
106
106
  ```sh
107
- npm i fastify@next
107
+ npm i fastify
108
108
  ```
109
109
  Install with yarn:
110
110
  ```sh
111
- yarn add fastify@next
111
+ yarn add fastify
112
112
  ```
113
113
 
114
114
  ### Example
@@ -346,6 +346,9 @@ section.
346
346
  minification and transformation of responses.
347
347
  - [`fastify-mongo-memory`](https://github.com/chapuletta/fastify-mongo-memory)
348
348
  Fastify MongoDB in Memory Plugin for testing support.
349
+ - [`fastify-mongodb-sanitizer`](https://github.com/KlemenKozelj/fastify-mongodb-sanitizer)
350
+ Fastify plugin that sanitizes client input to prevent
351
+ potential MongoDB query injection attacks.
349
352
  - [`fastify-mongoose-api`](https://github.com/jeka-kiselyov/fastify-mongoose-api)
350
353
  Fastify plugin to create REST API methods based on Mongoose MongoDB models.
351
354
  - [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose)
@@ -21,7 +21,7 @@ there and maintained. Use Fastify's [hooks](../Reference/Hooks.md) instead.
21
21
  ### Change of schema for multiple types
22
22
 
23
23
 
24
- Since Fastiy v4 has upgraded to Ajv v8. The "type" keywords with multiple types
24
+ Since Fastify v4 has upgraded to Ajv v8. The "type" keywords with multiple types
25
25
  (other than with "null") are prohibited. Read more
26
26
  ['here'](https://ajv.js.org/strict-mode.html#strict-types)
27
27
 
@@ -47,8 +47,8 @@ A "month" is defined as 30 consecutive days.
47
47
  | :------ | :----------- | :-------------- | :------------------- |
48
48
  | 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11 |
49
49
  | 2.0.0 | 2019-02-25 | 2021-01-31 | 6, 8, 10, 12, 14 |
50
- | 3.0.0 | 2020-07-07 | TBD | 10, 12, 14, 16, 18 |
51
- | 4.0.0 | TBD | TBD | 14, 16, 18 |
50
+ | 3.0.0 | 2020-07-07 | 2023-06-30 | 10, 12, 14, 16, 18 |
51
+ | 4.0.0 | 2022-06-08 | TBD | 14, 16, 18 |
52
52
 
53
53
  ### CI tested operating systems
54
54
  <a id="supported-os"></a>
@@ -65,7 +65,8 @@ $ npm i @fastify/type-provider-typebox
65
65
  ```
66
66
 
67
67
  ```typescript
68
- import { TypeBoxTypeProvider, Type } from '@fastify/type-provider-typebox'
68
+ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
69
+ import { Type } from '@sinclair/typebox'
69
70
 
70
71
  import fastify from 'fastify'
71
72
 
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.0.2'
3
+ const VERSION = '4.0.3'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -1,15 +1,183 @@
1
1
  // This file is autogenerated by build/build-error-serializer.js, do not edit
2
2
  /* istanbul ignore file */
3
3
 
4
- 'use strict'
4
+ 'use strict'
5
5
 
6
- const Serializer = require('fast-json-stringify/serializer')
7
- const buildAjv = require('fast-json-stringify/ajv')
6
+
7
+
8
+ class Serializer {
9
+ constructor (options = {}) {
10
+ switch (options.rounding) {
11
+ case 'floor':
12
+ this.parseInteger = Math.floor
13
+ break
14
+ case 'ceil':
15
+ this.parseInteger = Math.ceil
16
+ break
17
+ case 'round':
18
+ this.parseInteger = Math.round
19
+ break
20
+ default:
21
+ this.parseInteger = Math.trunc
22
+ break
23
+ }
24
+ }
8
25
 
9
- const serializer = new Serializer({"mode":"standalone"})
10
- const ajv = buildAjv({})
26
+ asAny (i) {
27
+ return JSON.stringify(i)
28
+ }
29
+
30
+ asNull () {
31
+ return 'null'
32
+ }
11
33
 
34
+ asInteger (i) {
35
+ if (typeof i === 'bigint') {
36
+ return i.toString()
37
+ } else if (Number.isInteger(i)) {
38
+ return '' + i
39
+ } else {
40
+ /* eslint no-undef: "off" */
41
+ const integer = this.parseInteger(i)
42
+ if (Number.isNaN(integer)) {
43
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
44
+ } else {
45
+ return '' + integer
46
+ }
47
+ }
48
+ }
49
+
50
+ asIntegerNullable (i) {
51
+ return i === null ? 'null' : this.asInteger(i)
52
+ }
53
+
54
+ asNumber (i) {
55
+ const num = Number(i)
56
+ if (Number.isNaN(num)) {
57
+ throw new Error(`The value "${i}" cannot be converted to a number.`)
58
+ } else {
59
+ return '' + num
60
+ }
61
+ }
62
+
63
+ asNumberNullable (i) {
64
+ return i === null ? 'null' : this.asNumber(i)
65
+ }
66
+
67
+ asBoolean (bool) {
68
+ return bool && 'true' || 'false' // eslint-disable-line
69
+ }
70
+
71
+ asBooleanNullable (bool) {
72
+ return bool === null ? 'null' : this.asBoolean(bool)
73
+ }
74
+
75
+ asDatetime (date, skipQuotes) {
76
+ const quotes = skipQuotes === true ? '' : '"'
77
+ if (date instanceof Date) {
78
+ return quotes + date.toISOString() + quotes
79
+ }
80
+ return this.asString(date, skipQuotes)
81
+ }
82
+
83
+ asDatetimeNullable (date, skipQuotes) {
84
+ return date === null ? 'null' : this.asDatetime(date, skipQuotes)
85
+ }
86
+
87
+ asDate (date, skipQuotes) {
88
+ const quotes = skipQuotes === true ? '' : '"'
89
+ if (date instanceof Date) {
90
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) + quotes
91
+ }
92
+ return this.asString(date, skipQuotes)
93
+ }
12
94
 
95
+ asDateNullable (date, skipQuotes) {
96
+ return date === null ? 'null' : this.asDate(date, skipQuotes)
97
+ }
98
+
99
+ asTime (date, skipQuotes) {
100
+ const quotes = skipQuotes === true ? '' : '"'
101
+ if (date instanceof Date) {
102
+ return quotes + new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(11, 19) + quotes
103
+ }
104
+ return this.asString(date, skipQuotes)
105
+ }
106
+
107
+ asTimeNullable (date, skipQuotes) {
108
+ return date === null ? 'null' : this.asTime(date, skipQuotes)
109
+ }
110
+
111
+ asString (str, skipQuotes) {
112
+ const quotes = skipQuotes === true ? '' : '"'
113
+ if (str instanceof Date) {
114
+ return quotes + str.toISOString() + quotes
115
+ } else if (str === null) {
116
+ return quotes + quotes
117
+ } else if (str instanceof RegExp) {
118
+ str = str.source
119
+ } else if (typeof str !== 'string') {
120
+ str = str.toString()
121
+ }
122
+ // If we skipQuotes it means that we are using it as test
123
+ // no need to test the string length for the render
124
+ if (skipQuotes) {
125
+ return str
126
+ }
127
+
128
+ if (str.length < 42) {
129
+ return this.asStringSmall(str)
130
+ } else {
131
+ return JSON.stringify(str)
132
+ }
133
+ }
134
+
135
+ asStringNullable (str) {
136
+ return str === null ? 'null' : this.asString(str)
137
+ }
138
+
139
+ // magically escape strings for json
140
+ // relying on their charCodeAt
141
+ // everything below 32 needs JSON.stringify()
142
+ // every string that contain surrogate needs JSON.stringify()
143
+ // 34 and 92 happens all the time, so we
144
+ // have a fast case for them
145
+ asStringSmall (str) {
146
+ const l = str.length
147
+ let result = ''
148
+ let last = 0
149
+ let found = false
150
+ let surrogateFound = false
151
+ let point = 255
152
+ // eslint-disable-next-line
153
+ for (var i = 0; i < l && point >= 32; i++) {
154
+ point = str.charCodeAt(i)
155
+ if (point >= 0xD800 && point <= 0xDFFF) {
156
+ // The current character is a surrogate.
157
+ surrogateFound = true
158
+ }
159
+ if (point === 34 || point === 92) {
160
+ result += str.slice(last, i) + '\\'
161
+ last = i
162
+ found = true
163
+ }
164
+ }
165
+
166
+ if (!found) {
167
+ result = str
168
+ } else {
169
+ result += str.slice(last)
170
+ }
171
+ return ((point < 32) || (surrogateFound === true)) ? JSON.stringify(str) : '"' + result + '"'
172
+ }
173
+ }
174
+
175
+
176
+
177
+ const serializer = new Serializer({"mode":"standalone"})
178
+
179
+
180
+
13
181
  function main (input) {
14
182
  let json = ''
15
183
  json += anonymous0(input)
@@ -81,5 +249,5 @@ const ajv = buildAjv({})
81
249
 
82
250
 
83
251
 
84
- module.exports = main
85
-
252
+ module.exports = main
253
+
package/lib/server.js CHANGED
@@ -41,7 +41,15 @@ function createServer (options, httpHandler) {
41
41
  listenOptions.cb = cb
42
42
  }
43
43
 
44
- const { host = 'localhost' } = listenOptions
44
+ // If we have a path specified, don't default host to 'localhost' so we don't end up listening
45
+ // on both path and host
46
+ // See https://github.com/fastify/fastify/issues/4007
47
+ let host
48
+ if (listenOptions.path == null) {
49
+ host = listenOptions.host ?? 'localhost'
50
+ } else {
51
+ host = listenOptions.host
52
+ }
45
53
  if (Object.prototype.hasOwnProperty.call(listenOptions, 'host') === false) {
46
54
  listenOptions.host = host
47
55
  }
@@ -11,9 +11,14 @@ function wrapThenable (thenable, reply) {
11
11
  return
12
12
  }
13
13
 
14
- // this is for async functions that
15
- // are using reply.send directly
16
- if (payload !== undefined || reply.sent === false) {
14
+ // this is for async functions that are using reply.send directly
15
+ //
16
+ // since wrap-thenable will be called when using reply.send directly
17
+ // without actual return. the response can be sent already or
18
+ // the request may be terminated during the reply. in this situation,
19
+ // it require an extra checking of request.aborted to see whether
20
+ // the request is killed by client.
21
+ if (payload !== undefined || (reply.sent === false && reply.raw.headersSent === false && reply.request.raw.aborted === false)) {
17
22
  // we use a try-catch internally to avoid adding a catch to another
18
23
  // promise, increase promise perf by 10%
19
24
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -146,6 +146,7 @@
146
146
  "eslint-plugin-n": "^15.2.0",
147
147
  "eslint-plugin-promise": "^6.0.0",
148
148
  "fast-json-body": "^1.1.0",
149
+ "fast-json-stringify": "^4.2.0",
149
150
  "fastify-plugin": "^3.0.1",
150
151
  "fluent-json-schema": "^3.1.0",
151
152
  "form-data": "^4.0.0",
@@ -169,7 +170,7 @@
169
170
  "split2": "^4.1.0",
170
171
  "standard": "^17.0.0-2",
171
172
  "tap": "^16.2.0",
172
- "tsd": "^0.20.0",
173
+ "tsd": "^0.21.0",
173
174
  "typescript": "^4.7.2",
174
175
  "undici": "^5.4.0",
175
176
  "x-xss-protection": "^2.0.0",
@@ -178,10 +179,9 @@
178
179
  "dependencies": {
179
180
  "@fastify/ajv-compiler": "^3.1.0",
180
181
  "@fastify/error": "^3.0.0",
181
- "@fastify/fast-json-stringify-compiler": "^3.0.0",
182
+ "@fastify/fast-json-stringify-compiler": "^3.0.1",
182
183
  "abstract-logging": "^2.0.1",
183
184
  "avvio": "^8.1.3",
184
- "fast-json-stringify": "^4.1.0",
185
185
  "find-my-way": "^6.3.0",
186
186
  "light-my-request": "^5.0.0",
187
187
  "pino": "^8.0.0",
@@ -196,14 +196,28 @@ if (os.platform() !== 'win32') {
196
196
  const fastify = Fastify()
197
197
  t.teardown(fastify.close.bind(fastify))
198
198
 
199
- const sockFile = path.join(os.tmpdir(), `${(Math.random().toString(16) + '0000000').substr(2, 8)}-server.sock`)
199
+ const sockFile = path.join(os.tmpdir(), `${(Math.random().toString(16) + '0000000').slice(2, 10)}-server.sock`)
200
200
  try {
201
201
  fs.unlinkSync(sockFile)
202
202
  } catch (e) { }
203
203
 
204
204
  fastify.listen({ path: sockFile }, (err, address) => {
205
205
  t.error(err)
206
- t.equal(sockFile, fastify.server.address())
206
+ t.strictSame(fastify.addresses(), [sockFile])
207
+ t.equal(address, sockFile)
208
+ })
209
+ })
210
+ } else {
211
+ test('listen on socket', t => {
212
+ t.plan(3)
213
+ const fastify = Fastify()
214
+ t.teardown(fastify.close.bind(fastify))
215
+
216
+ const sockFile = `\\\\.\\pipe\\${(Math.random().toString(16) + '0000000').slice(2, 10)}-server-sock`
217
+
218
+ fastify.listen({ path: sockFile }, (err, address) => {
219
+ t.error(err)
220
+ t.strictSame(fastify.addresses(), [sockFile])
207
221
  t.equal(address, sockFile)
208
222
  })
209
223
  })
@@ -741,3 +741,76 @@ test('reply.send handles aborted requests', t => {
741
741
  }, 1)
742
742
  })
743
743
  })
744
+
745
+ test('request terminated should not crash fastify', t => {
746
+ t.plan(10)
747
+
748
+ const spyLogger = {
749
+ level: 'error',
750
+ fatal: () => { },
751
+ error: () => {
752
+ t.fail('should not log an error')
753
+ },
754
+ warn: () => { },
755
+ info: () => { },
756
+ debug: () => { },
757
+ trace: () => { },
758
+ child: () => { return spyLogger }
759
+ }
760
+ const fastify = Fastify({
761
+ logger: spyLogger
762
+ })
763
+
764
+ fastify.get('/', async (req, reply) => {
765
+ const stream = new Readable()
766
+ stream._read = () => {}
767
+ reply.header('content-type', 'text/html; charset=utf-8')
768
+ reply.header('transfer-encoding', 'chunked')
769
+ stream.push('<h1>HTML</h1>')
770
+
771
+ reply.send(stream)
772
+
773
+ await new Promise((resolve) => { setTimeout(resolve, 6).unref() })
774
+
775
+ stream.push('<h1>should disply on second stream</h1>')
776
+ stream.push(null)
777
+ return reply
778
+ })
779
+
780
+ fastify.listen({ port: 0 }, err => {
781
+ t.error(err)
782
+ t.teardown(() => { fastify.close() })
783
+
784
+ const port = fastify.server.address().port
785
+ const http = require('http')
786
+ const req = http.get(`http://localhost:${port}`, function (res) {
787
+ const { statusCode, headers } = res
788
+ t.equal(statusCode, 200)
789
+ t.equal(headers['content-type'], 'text/html; charset=utf-8')
790
+ t.equal(headers['transfer-encoding'], 'chunked')
791
+ res.on('data', function (chunk) {
792
+ t.equal(chunk.toString(), '<h1>HTML</h1>')
793
+ })
794
+
795
+ setTimeout(() => {
796
+ req.destroy()
797
+
798
+ // the server is not crash, we can connect it
799
+ http.get(`http://localhost:${port}`, function (res) {
800
+ const { statusCode, headers } = res
801
+ t.equal(statusCode, 200)
802
+ t.equal(headers['content-type'], 'text/html; charset=utf-8')
803
+ t.equal(headers['transfer-encoding'], 'chunked')
804
+ let payload = ''
805
+ res.on('data', function (chunk) {
806
+ payload += chunk.toString()
807
+ })
808
+ res.on('end', function () {
809
+ t.equal(payload, '<h1>HTML</h1><h1>should disply on second stream</h1>')
810
+ t.pass('should end properly')
811
+ })
812
+ })
813
+ }, 1)
814
+ })
815
+ })
816
+ })