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.
- package/docs/Guides/Ecosystem.md +5 -0
- package/docs/Guides/Serverless.md +43 -1
- package/docs/Guides/Testing.md +135 -2
- package/docs/Guides/Write-Plugin.md +3 -1
- package/docs/Reference/Principles.md +78 -0
- package/docs/Reference/Request.md +2 -0
- package/docs/Reference/Routes.md +5 -0
- package/docs/Reference/Server.md +5 -2
- package/docs/Reference/Type-Providers.md +9 -2
- package/docs/Reference/TypeScript.md +21 -6
- package/fastify.d.ts +1 -1
- package/fastify.js +23 -8
- package/lib/contentTypeParser.js +1 -1
- package/lib/reply.js +2 -2
- package/lib/request.js +18 -8
- package/lib/route.js +0 -1
- package/lib/schema-controller.js +6 -6
- package/lib/schemas.js +2 -2
- package/lib/server.js +56 -30
- package/lib/symbols.js +1 -0
- package/package.json +9 -8
- package/test/buffer.test.js +51 -0
- package/test/close-pipelining.test.js +4 -1
- package/test/close.test.js +148 -6
- package/test/custom-http-server.test.js +13 -2
- package/test/genReqId.test.js +42 -0
- package/test/header-overflow.test.js +65 -0
- package/test/https/custom-https-server.test.js +2 -2
- package/test/inject.test.js +30 -0
- package/test/internals/reply.test.js +2 -2
- package/test/internals/request.test.js +60 -0
- package/test/router-options.test.js +221 -0
- package/test/types/fastify.test-d.ts +7 -1
- package/test/types/instance.test-d.ts +70 -0
- package/test/types/reply.test-d.ts +37 -4
- package/test/types/request.test-d.ts +1 -0
- package/test/types/route.test-d.ts +79 -3
- package/test/url-rewriting.test.js +35 -0
- package/test/versioned-routes.test.js +1 -1
- package/types/context.d.ts +2 -1
- package/types/instance.d.ts +24 -20
- package/types/reply.d.ts +11 -6
- package/types/request.d.ts +1 -0
- package/types/route.d.ts +5 -0
- package/types/utils.d.ts +24 -0
package/lib/schema-controller.js
CHANGED
|
@@ -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
|
|
103
|
+
* @param {object} serverOptions the fastify server options
|
|
104
104
|
*/
|
|
105
|
-
setupValidator (
|
|
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(),
|
|
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
|
|
116
|
+
* @param {object} serverOptions the fastify server options
|
|
117
117
|
*/
|
|
118
|
-
setupSerializer (
|
|
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(),
|
|
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|
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
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, '
|
|
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
|
})
|
package/test/close.test.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const net = require('net')
|
|
4
4
|
const http = require('http')
|
|
5
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
})
|
package/test/genReqId.test.js
CHANGED
|
@@ -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
|
|
19
|
+
t.ok(opts.serverFactory, 'it is called once for localhost')
|
|
20
20
|
|
|
21
21
|
const options = {
|
|
22
22
|
key: global.context.key,
|