fastify 5.3.3 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +15 -15
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/SECURITY.md +158 -2
- package/build/build-validation.js +20 -1
- package/docs/Guides/Delay-Accepting-Requests.md +8 -5
- package/docs/Guides/Ecosystem.md +20 -5
- package/docs/Guides/Migration-Guide-V5.md +6 -10
- package/docs/Guides/Recommendations.md +1 -1
- package/docs/Reference/ContentTypeParser.md +1 -1
- package/docs/Reference/Errors.md +5 -3
- package/docs/Reference/Hooks.md +16 -20
- package/docs/Reference/Lifecycle.md +2 -2
- package/docs/Reference/Logging.md +3 -3
- package/docs/Reference/Middleware.md +1 -1
- package/docs/Reference/Reply.md +8 -8
- package/docs/Reference/Request.md +2 -2
- package/docs/Reference/Routes.md +7 -6
- package/docs/Reference/Server.md +341 -200
- package/docs/Reference/TypeScript.md +1 -3
- package/docs/Reference/Validation-and-Serialization.md +56 -4
- package/docs/Reference/Warnings.md +2 -1
- package/fastify.d.ts +4 -3
- package/fastify.js +47 -34
- package/lib/configValidator.js +196 -28
- package/lib/contentTypeParser.js +41 -48
- package/lib/error-handler.js +3 -3
- package/lib/errors.js +11 -0
- package/lib/handleRequest.js +13 -17
- package/lib/pluginOverride.js +3 -1
- package/lib/promise.js +23 -0
- package/lib/reply.js +24 -30
- package/lib/request.js +3 -10
- package/lib/route.js +37 -3
- package/lib/server.js +36 -35
- package/lib/symbols.js +1 -0
- package/lib/warnings.js +19 -1
- package/package.json +14 -10
- package/test/404s.test.js +226 -325
- package/test/allow-unsafe-regex.test.js +19 -48
- package/test/als.test.js +28 -40
- package/test/async-await.test.js +84 -128
- package/test/async_hooks.test.js +18 -37
- package/test/body-limit.test.js +90 -63
- package/test/buffer.test.js +22 -0
- package/test/build-certificate.js +1 -1
- package/test/case-insensitive.test.js +44 -65
- package/test/check.test.js +17 -21
- package/test/close-pipelining.test.js +24 -15
- package/test/constrained-routes.test.js +231 -0
- package/test/custom-http-server.test.js +7 -15
- package/test/custom-parser-async.test.js +17 -22
- package/test/custom-parser.0.test.js +267 -348
- package/test/custom-parser.1.test.js +141 -191
- package/test/custom-parser.2.test.js +34 -44
- package/test/custom-parser.3.test.js +56 -104
- package/test/custom-parser.4.test.js +106 -144
- package/test/custom-parser.5.test.js +56 -75
- package/test/custom-querystring-parser.test.js +51 -77
- package/test/decorator-namespace.test._js_ +3 -4
- package/test/decorator.test.js +76 -259
- package/test/delete.test.js +101 -110
- package/test/diagnostics-channel/404.test.js +7 -15
- package/test/diagnostics-channel/async-delay-request.test.js +7 -16
- package/test/diagnostics-channel/async-request.test.js +8 -16
- package/test/diagnostics-channel/error-request.test.js +7 -15
- package/test/diagnostics-channel/sync-delay-request.test.js +7 -16
- package/test/diagnostics-channel/sync-request-reply.test.js +9 -16
- package/test/diagnostics-channel/sync-request.test.js +9 -16
- package/test/fastify-instance.test.js +1 -1
- package/test/header-overflow.test.js +18 -29
- package/test/helper.js +139 -135
- package/test/hooks-async.test.js +259 -235
- package/test/hooks.test.js +951 -996
- package/test/http-methods/copy.test.js +14 -19
- package/test/http-methods/get.test.js +131 -143
- package/test/http-methods/head.test.js +53 -84
- package/test/http-methods/lock.test.js +31 -31
- package/test/http-methods/mkcalendar.test.js +45 -72
- package/test/http-methods/mkcol.test.js +5 -9
- package/test/http-methods/move.test.js +6 -10
- package/test/http-methods/propfind.test.js +34 -44
- package/test/http-methods/proppatch.test.js +23 -29
- package/test/http-methods/report.test.js +44 -69
- package/test/http-methods/search.test.js +67 -82
- package/test/http-methods/unlock.test.js +5 -9
- package/test/http2/closing.test.js +38 -20
- package/test/http2/secure-with-fallback.test.js +31 -28
- package/test/https/custom-https-server.test.js +9 -13
- package/test/https/https.test.js +56 -53
- package/test/input-validation.js +139 -150
- package/test/internals/errors.test.js +50 -1
- package/test/internals/handle-request.test.js +72 -65
- package/test/internals/promise.test.js +63 -0
- package/test/internals/reply.test.js +277 -496
- package/test/issue-4959.test.js +12 -3
- package/test/listen.4.test.js +31 -43
- package/test/nullable-validation.test.js +33 -46
- package/test/output-validation.test.js +24 -26
- package/test/plugin.1.test.js +40 -68
- package/test/plugin.2.test.js +108 -120
- package/test/plugin.3.test.js +50 -72
- package/test/plugin.4.test.js +124 -119
- package/test/promises.test.js +42 -63
- package/test/proto-poisoning.test.js +78 -97
- package/test/register.test.js +8 -18
- package/test/request-error.test.js +57 -146
- package/test/request-id.test.js +30 -49
- package/test/route-hooks.test.js +117 -101
- package/test/route-prefix.test.js +194 -133
- package/test/route-shorthand.test.js +9 -27
- package/test/route.1.test.js +74 -131
- package/test/route.8.test.js +9 -17
- package/test/router-options.test.js +450 -0
- package/test/schema-serialization.test.js +177 -154
- package/test/schema-special-usage.test.js +165 -132
- package/test/schema-validation.test.js +254 -218
- package/test/server.test.js +143 -5
- package/test/set-error-handler.test.js +58 -1
- package/test/skip-reply-send.test.js +64 -69
- package/test/stream.1.test.js +33 -50
- package/test/stream.4.test.js +18 -28
- package/test/stream.5.test.js +11 -19
- package/test/trust-proxy.test.js +32 -58
- package/test/types/errors.test-d.ts +13 -1
- package/test/types/fastify.test-d.ts +3 -0
- package/test/types/request.test-d.ts +1 -0
- package/test/types/type-provider.test-d.ts +55 -0
- package/test/url-rewriting.test.js +45 -62
- package/test/use-semicolon-delimiter.test.js +117 -59
- package/test/versioned-routes.test.js +39 -56
- package/types/errors.d.ts +11 -1
- package/types/hooks.d.ts +1 -1
- package/types/instance.d.ts +1 -1
- package/types/reply.d.ts +2 -2
- package/types/request.d.ts +1 -0
- package/.taprc +0 -7
package/lib/contentTypeParser.js
CHANGED
|
@@ -24,7 +24,8 @@ const {
|
|
|
24
24
|
FST_ERR_CTP_INVALID_MEDIA_TYPE,
|
|
25
25
|
FST_ERR_CTP_INVALID_CONTENT_LENGTH,
|
|
26
26
|
FST_ERR_CTP_EMPTY_JSON_BODY,
|
|
27
|
-
FST_ERR_CTP_INSTANCE_ALREADY_STARTED
|
|
27
|
+
FST_ERR_CTP_INSTANCE_ALREADY_STARTED,
|
|
28
|
+
FST_ERR_CTP_INVALID_JSON_BODY
|
|
28
29
|
} = require('./errors')
|
|
29
30
|
const { FSTSEC001 } = require('./warnings')
|
|
30
31
|
|
|
@@ -174,17 +175,18 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
|
|
|
174
175
|
const parser = this.getParser(contentType)
|
|
175
176
|
|
|
176
177
|
if (parser === undefined) {
|
|
177
|
-
if (request.is404) {
|
|
178
|
+
if (request.is404 === true) {
|
|
178
179
|
handler(request, reply)
|
|
179
|
-
|
|
180
|
-
reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType || undefined))
|
|
180
|
+
return
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
reply[kReplyIsError] = true
|
|
184
|
+
reply.send(new FST_ERR_CTP_INVALID_MEDIA_TYPE(contentType || undefined))
|
|
184
185
|
return
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
const resource = new AsyncResource('content-type-parser:run', request)
|
|
189
|
+
const done = resource.bind(onDone)
|
|
188
190
|
|
|
189
191
|
if (parser.asString === true || parser.asBuffer === true) {
|
|
190
192
|
rawBody(
|
|
@@ -194,48 +196,44 @@ ContentTypeParser.prototype.run = function (contentType, handler, request, reply
|
|
|
194
196
|
parser,
|
|
195
197
|
done
|
|
196
198
|
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
return
|
|
200
|
+
}
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
202
|
+
const result = parser.fn(request, request[kRequestPayloadStream], done)
|
|
203
|
+
if (result && typeof result.then === 'function') {
|
|
204
|
+
result.then(body => { done(null, body) }, done)
|
|
203
205
|
}
|
|
204
206
|
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
function onDone (error, body) {
|
|
208
|
+
resource.emitDestroy()
|
|
209
|
+
if (error != null) {
|
|
210
|
+
// We must close the connection as the client may
|
|
211
|
+
// send more data
|
|
212
|
+
reply.header('connection', 'close')
|
|
213
|
+
reply[kReplyIsError] = true
|
|
214
|
+
reply.send(error)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
request.body = body
|
|
218
|
+
handler(request, reply)
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
function rawBody (request, reply, options, parser, done) {
|
|
221
|
-
const asString = parser.asString
|
|
223
|
+
const asString = parser.asString === true
|
|
222
224
|
const limit = options.limit === null ? parser.bodyLimit : options.limit
|
|
223
225
|
const contentLength = Number(request.headers['content-length'])
|
|
224
226
|
|
|
225
227
|
if (contentLength > limit) {
|
|
226
|
-
|
|
227
|
-
// to send this data anyway
|
|
228
|
-
reply.header('connection', 'close')
|
|
229
|
-
reply.send(new FST_ERR_CTP_BODY_TOO_LARGE())
|
|
228
|
+
done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
|
|
230
229
|
return
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
let receivedLength = 0
|
|
234
|
-
let body = asString
|
|
235
|
-
|
|
233
|
+
let body = asString ? '' : []
|
|
236
234
|
const payload = request[kRequestPayloadStream] || request.raw
|
|
237
235
|
|
|
238
|
-
if (asString
|
|
236
|
+
if (asString) {
|
|
239
237
|
payload.setEncoding('utf8')
|
|
240
238
|
}
|
|
241
239
|
|
|
@@ -245,7 +243,7 @@ function rawBody (request, reply, options, parser, done) {
|
|
|
245
243
|
payload.resume()
|
|
246
244
|
|
|
247
245
|
function onData (chunk) {
|
|
248
|
-
receivedLength += chunk.length
|
|
246
|
+
receivedLength += asString ? Buffer.byteLength(chunk) : chunk.length
|
|
249
247
|
const { receivedEncodedLength = 0 } = payload
|
|
250
248
|
// The resulting body length must not exceed bodyLimit (see "zip bomb").
|
|
251
249
|
// The case when encoded length is larger than received length is rather theoretical,
|
|
@@ -254,11 +252,11 @@ function rawBody (request, reply, options, parser, done) {
|
|
|
254
252
|
payload.removeListener('data', onData)
|
|
255
253
|
payload.removeListener('end', onEnd)
|
|
256
254
|
payload.removeListener('error', onEnd)
|
|
257
|
-
|
|
255
|
+
done(new FST_ERR_CTP_BODY_TOO_LARGE(), undefined)
|
|
258
256
|
return
|
|
259
257
|
}
|
|
260
258
|
|
|
261
|
-
if (asString
|
|
259
|
+
if (asString) {
|
|
262
260
|
body += chunk
|
|
263
261
|
} else {
|
|
264
262
|
body.push(chunk)
|
|
@@ -270,37 +268,33 @@ function rawBody (request, reply, options, parser, done) {
|
|
|
270
268
|
payload.removeListener('end', onEnd)
|
|
271
269
|
payload.removeListener('error', onEnd)
|
|
272
270
|
|
|
273
|
-
if (err
|
|
271
|
+
if (err != null) {
|
|
274
272
|
if (!(typeof err.statusCode === 'number' && err.statusCode >= 400)) {
|
|
275
273
|
err.statusCode = 400
|
|
276
274
|
}
|
|
277
|
-
|
|
278
|
-
reply.code(err.statusCode).send(err)
|
|
275
|
+
done(err, undefined)
|
|
279
276
|
return
|
|
280
277
|
}
|
|
281
278
|
|
|
282
|
-
if (asString === true) {
|
|
283
|
-
receivedLength = Buffer.byteLength(body)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
279
|
if (!Number.isNaN(contentLength) && (payload.receivedEncodedLength || receivedLength) !== contentLength) {
|
|
287
|
-
|
|
288
|
-
reply.send(new FST_ERR_CTP_INVALID_CONTENT_LENGTH())
|
|
280
|
+
done(new FST_ERR_CTP_INVALID_CONTENT_LENGTH(), undefined)
|
|
289
281
|
return
|
|
290
282
|
}
|
|
291
283
|
|
|
292
|
-
if (asString
|
|
284
|
+
if (!asString) {
|
|
293
285
|
body = Buffer.concat(body)
|
|
294
286
|
}
|
|
295
287
|
|
|
296
288
|
const result = parser.fn(request, body, done)
|
|
297
289
|
if (result && typeof result.then === 'function') {
|
|
298
|
-
result.then(body => done(null, body), done)
|
|
290
|
+
result.then(body => { done(null, body) }, done)
|
|
299
291
|
}
|
|
300
292
|
}
|
|
301
293
|
}
|
|
302
294
|
|
|
303
295
|
function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
|
|
296
|
+
const parseOptions = { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning }
|
|
297
|
+
|
|
304
298
|
return defaultJsonParser
|
|
305
299
|
|
|
306
300
|
function defaultJsonParser (req, body, done) {
|
|
@@ -309,10 +303,9 @@ function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) {
|
|
|
309
303
|
return
|
|
310
304
|
}
|
|
311
305
|
try {
|
|
312
|
-
done(null, secureJsonParse(body,
|
|
313
|
-
} catch
|
|
314
|
-
|
|
315
|
-
done(err, undefined)
|
|
306
|
+
done(null, secureJsonParse(body, parseOptions))
|
|
307
|
+
} catch {
|
|
308
|
+
done(new FST_ERR_CTP_INVALID_JSON_BODY(), undefined)
|
|
316
309
|
}
|
|
317
310
|
}
|
|
318
311
|
}
|
package/lib/error-handler.js
CHANGED
|
@@ -39,7 +39,7 @@ function handleError (reply, error, cb) {
|
|
|
39
39
|
if (!reply.log[kDisableRequestLogging]) {
|
|
40
40
|
reply.log.warn(
|
|
41
41
|
{ req: reply.request, res: reply, err: error },
|
|
42
|
-
error
|
|
42
|
+
error?.message
|
|
43
43
|
)
|
|
44
44
|
}
|
|
45
45
|
reply.raw.writeHead(reply.raw.statusCode)
|
|
@@ -89,14 +89,14 @@ function defaultErrorHandler (error, request, reply) {
|
|
|
89
89
|
if (!reply.log[kDisableRequestLogging]) {
|
|
90
90
|
reply.log.info(
|
|
91
91
|
{ res: reply, err: error },
|
|
92
|
-
error
|
|
92
|
+
error?.message
|
|
93
93
|
)
|
|
94
94
|
}
|
|
95
95
|
} else {
|
|
96
96
|
if (!reply.log[kDisableRequestLogging]) {
|
|
97
97
|
reply.log.error(
|
|
98
98
|
{ req: request, res: reply, err: error },
|
|
99
|
-
error
|
|
99
|
+
error?.message
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
102
|
}
|
package/lib/errors.js
CHANGED
|
@@ -64,6 +64,12 @@ const codes = {
|
|
|
64
64
|
500,
|
|
65
65
|
TypeError
|
|
66
66
|
),
|
|
67
|
+
FST_ERR_ERROR_HANDLER_ALREADY_SET: createError(
|
|
68
|
+
'FST_ERR_ERROR_HANDLER_ALREADY_SET',
|
|
69
|
+
"Error Handler already set in this scope. Set 'allowErrorHandlerOverride: true' to allow overriding.",
|
|
70
|
+
500,
|
|
71
|
+
TypeError
|
|
72
|
+
),
|
|
67
73
|
|
|
68
74
|
/**
|
|
69
75
|
* ContentTypeParser
|
|
@@ -118,6 +124,11 @@ const codes = {
|
|
|
118
124
|
"Body cannot be empty when content-type is set to 'application/json'",
|
|
119
125
|
400
|
|
120
126
|
),
|
|
127
|
+
FST_ERR_CTP_INVALID_JSON_BODY: createError(
|
|
128
|
+
'FST_ERR_CTP_INVALID_JSON_BODY',
|
|
129
|
+
"Body is not valid JSON but content-type is set to 'application/json'",
|
|
130
|
+
400
|
|
131
|
+
),
|
|
121
132
|
FST_ERR_CTP_INSTANCE_ALREADY_STARTED: createError(
|
|
122
133
|
'FST_ERR_CTP_INSTANCE_ALREADY_STARTED',
|
|
123
134
|
'Cannot call "%s" when fastify instance is already started!',
|
package/lib/handleRequest.js
CHANGED
|
@@ -21,9 +21,7 @@ function handleRequest (err, request, reply) {
|
|
|
21
21
|
return
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const method = request.
|
|
25
|
-
const headers = request.headers
|
|
26
|
-
const context = request[kRouteContext]
|
|
24
|
+
const method = request.method
|
|
27
25
|
|
|
28
26
|
if (this[kSupportedHTTPMethods].bodyless.has(method)) {
|
|
29
27
|
handler(request, reply)
|
|
@@ -31,28 +29,26 @@ function handleRequest (err, request, reply) {
|
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
if (this[kSupportedHTTPMethods].bodywith.has(method)) {
|
|
32
|
+
const headers = request.headers
|
|
34
33
|
const contentType = headers['content-type']
|
|
35
|
-
const contentLength = headers['content-length']
|
|
36
|
-
const transferEncoding = headers['transfer-encoding']
|
|
37
34
|
|
|
38
35
|
if (contentType === undefined) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
const contentLength = headers['content-length']
|
|
37
|
+
const transferEncoding = headers['transfer-encoding']
|
|
38
|
+
const isEmptyBody = transferEncoding === undefined &&
|
|
39
|
+
(contentLength === undefined || contentLength === '0')
|
|
40
|
+
|
|
41
|
+
if (isEmptyBody) {
|
|
43
42
|
// Request has no body to parse
|
|
44
43
|
handler(request, reply)
|
|
45
|
-
} else {
|
|
46
|
-
context.contentTypeParser.run('', handler, request, reply)
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
if (contentLength === undefined && transferEncoding === undefined && method === 'OPTIONS') {
|
|
50
|
-
// OPTIONS can have a Content-Type header without a body
|
|
51
|
-
handler(request, reply)
|
|
52
44
|
return
|
|
53
45
|
}
|
|
54
|
-
|
|
46
|
+
|
|
47
|
+
request[kRouteContext].contentTypeParser.run('', handler, request, reply)
|
|
48
|
+
return
|
|
55
49
|
}
|
|
50
|
+
|
|
51
|
+
request[kRouteContext].contentTypeParser.run(contentType, handler, request, reply)
|
|
56
52
|
return
|
|
57
53
|
}
|
|
58
54
|
|
package/lib/pluginOverride.js
CHANGED
|
@@ -12,7 +12,8 @@ const {
|
|
|
12
12
|
kReply,
|
|
13
13
|
kRequest,
|
|
14
14
|
kFourOhFour,
|
|
15
|
-
kPluginNameChain
|
|
15
|
+
kPluginNameChain,
|
|
16
|
+
kErrorHandlerAlreadySet
|
|
16
17
|
} = require('./symbols.js')
|
|
17
18
|
|
|
18
19
|
const Reply = require('./reply')
|
|
@@ -57,6 +58,7 @@ module.exports = function override (old, fn, opts) {
|
|
|
57
58
|
// Track the plugin chain since the root instance.
|
|
58
59
|
// When an non-encapsulated plugin is added, the chain will be updated.
|
|
59
60
|
instance[kPluginNameChain] = [fnName]
|
|
61
|
+
instance[kErrorHandlerAlreadySet] = false
|
|
60
62
|
|
|
61
63
|
if (instance[kLogSerializers] || opts.logSerializers) {
|
|
62
64
|
instance[kLogSerializers] = Object.assign(Object.create(instance[kLogSerializers]), opts.logSerializers)
|
package/lib/promise.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { kTestInternals } = require('./symbols')
|
|
4
|
+
|
|
5
|
+
function withResolvers () {
|
|
6
|
+
let res, rej
|
|
7
|
+
const promise = new Promise((resolve, reject) => {
|
|
8
|
+
res = resolve
|
|
9
|
+
rej = reject
|
|
10
|
+
})
|
|
11
|
+
return { promise, resolve: res, reject: rej }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
// TODO(20.x): remove when node@20 is not supported
|
|
16
|
+
withResolvers: typeof Promise.withResolvers === 'function'
|
|
17
|
+
? Promise.withResolvers.bind(Promise) // Promise.withResolvers must bind to itself
|
|
18
|
+
/* c8 ignore next */
|
|
19
|
+
: withResolvers, // Tested using the kTestInternals
|
|
20
|
+
[kTestInternals]: {
|
|
21
|
+
withResolvers
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/reply.js
CHANGED
|
@@ -126,16 +126,16 @@ Reply.prototype.hijack = function () {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
Reply.prototype.send = function (payload) {
|
|
129
|
-
if (this[kReplyIsRunningOnErrorHook]
|
|
129
|
+
if (this[kReplyIsRunningOnErrorHook]) {
|
|
130
130
|
throw new FST_ERR_SEND_INSIDE_ONERR()
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (this.sent) {
|
|
133
|
+
if (this.sent === true) {
|
|
134
134
|
this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT(this.request.url, this.request.method) })
|
|
135
135
|
return this
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
if (
|
|
138
|
+
if (this[kReplyIsError] || payload instanceof Error) {
|
|
139
139
|
this[kReplyIsError] = false
|
|
140
140
|
onErrorHook(this, payload, onSendHook)
|
|
141
141
|
return this
|
|
@@ -162,8 +162,8 @@ Reply.prototype.send = function (payload) {
|
|
|
162
162
|
return this
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
if (payload
|
|
166
|
-
if (hasContentType
|
|
165
|
+
if (payload.buffer instanceof ArrayBuffer) {
|
|
166
|
+
if (!hasContentType) {
|
|
167
167
|
this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET
|
|
168
168
|
}
|
|
169
169
|
const payloadToSend = Buffer.isBuffer(payload) ? payload : Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)
|
|
@@ -171,7 +171,7 @@ Reply.prototype.send = function (payload) {
|
|
|
171
171
|
return this
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
if (hasContentType
|
|
174
|
+
if (!hasContentType && typeof payload === 'string') {
|
|
175
175
|
this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN
|
|
176
176
|
onSendHook(this, payload)
|
|
177
177
|
return this
|
|
@@ -182,26 +182,24 @@ Reply.prototype.send = function (payload) {
|
|
|
182
182
|
if (typeof payload !== 'string') {
|
|
183
183
|
preSerializationHook(this, payload)
|
|
184
184
|
return this
|
|
185
|
-
} else {
|
|
186
|
-
payload = this[kReplySerializer](payload)
|
|
187
185
|
}
|
|
186
|
+
payload = this[kReplySerializer](payload)
|
|
188
187
|
|
|
189
188
|
// The indexOf below also matches custom json mimetypes such as 'application/hal+json' or 'application/ld+json'
|
|
190
|
-
} else if (hasContentType
|
|
191
|
-
if (hasContentType
|
|
189
|
+
} else if (!hasContentType || contentType.indexOf('json') !== -1) {
|
|
190
|
+
if (!hasContentType) {
|
|
192
191
|
this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
|
|
193
|
-
} else {
|
|
192
|
+
} else if (contentType.indexOf('charset') === -1) {
|
|
194
193
|
// If user doesn't set charset, we will set charset to utf-8
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
|
|
202
|
-
}
|
|
194
|
+
const customContentType = contentType.trim()
|
|
195
|
+
if (customContentType.endsWith(';')) {
|
|
196
|
+
// custom content-type is ended with ';'
|
|
197
|
+
this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
|
|
198
|
+
} else {
|
|
199
|
+
this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
|
|
203
200
|
}
|
|
204
201
|
}
|
|
202
|
+
|
|
205
203
|
if (typeof payload !== 'string') {
|
|
206
204
|
preSerializationHook(this, payload)
|
|
207
205
|
return this
|
|
@@ -215,12 +213,8 @@ Reply.prototype.send = function (payload) {
|
|
|
215
213
|
|
|
216
214
|
Reply.prototype.getHeader = function (key) {
|
|
217
215
|
key = key.toLowerCase()
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
if (value === undefined && res.hasHeader(key)) {
|
|
221
|
-
value = res.getHeader(key)
|
|
222
|
-
}
|
|
223
|
-
return value
|
|
216
|
+
const value = this[kReplyHeaders][key]
|
|
217
|
+
return value !== undefined ? value : this.raw.getHeader(key)
|
|
224
218
|
}
|
|
225
219
|
|
|
226
220
|
Reply.prototype.getHeaders = function () {
|
|
@@ -315,12 +309,12 @@ Reply.prototype.removeTrailer = function (key) {
|
|
|
315
309
|
}
|
|
316
310
|
|
|
317
311
|
Reply.prototype.code = function (code) {
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
312
|
+
const statusCode = +code
|
|
313
|
+
if (!(statusCode >= 100 && statusCode <= 599)) {
|
|
320
314
|
throw new FST_ERR_BAD_STATUS_CODE(code || String(code))
|
|
321
315
|
}
|
|
322
316
|
|
|
323
|
-
this.raw.statusCode =
|
|
317
|
+
this.raw.statusCode = statusCode
|
|
324
318
|
this[kReplyHasStatusCode] = true
|
|
325
319
|
return this
|
|
326
320
|
}
|
|
@@ -500,11 +494,11 @@ function preSerializationHook (reply, payload) {
|
|
|
500
494
|
preSerializationHookEnd
|
|
501
495
|
)
|
|
502
496
|
} else {
|
|
503
|
-
preSerializationHookEnd(null,
|
|
497
|
+
preSerializationHookEnd(null, undefined, reply, payload)
|
|
504
498
|
}
|
|
505
499
|
}
|
|
506
500
|
|
|
507
|
-
function preSerializationHookEnd (err,
|
|
501
|
+
function preSerializationHookEnd (err, _request, reply, payload) {
|
|
508
502
|
if (err != null) {
|
|
509
503
|
onErrorHook(reply, err)
|
|
510
504
|
return
|
package/lib/request.js
CHANGED
|
@@ -188,19 +188,12 @@ Object.defineProperties(Request.prototype, {
|
|
|
188
188
|
exposeHeadRoute: context.exposeHeadRoute,
|
|
189
189
|
prefixTrailingSlash: context.prefixTrailingSlash,
|
|
190
190
|
handler: context.handler,
|
|
191
|
+
config: context.config,
|
|
192
|
+
schema: context.schema,
|
|
191
193
|
version
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
config: {
|
|
196
|
-
get: () => context.config
|
|
197
|
-
},
|
|
198
|
-
schema: {
|
|
199
|
-
get: () => context.schema
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
return Object.freeze(options)
|
|
196
|
+
return options
|
|
204
197
|
}
|
|
205
198
|
},
|
|
206
199
|
is404: {
|
package/lib/route.js
CHANGED
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
FST_ERR_HOOK_INVALID_ASYNC_HANDLER
|
|
30
30
|
} = require('./errors')
|
|
31
31
|
|
|
32
|
+
const { FSTDEP022 } = require('./warnings')
|
|
33
|
+
|
|
32
34
|
const {
|
|
33
35
|
kRoutePrefix,
|
|
34
36
|
kSupportedHTTPMethods,
|
|
@@ -52,6 +54,20 @@ const { buildErrorHandler } = require('./error-handler')
|
|
|
52
54
|
const { createChildLogger } = require('./logger-factory.js')
|
|
53
55
|
const { getGenReqId } = require('./reqIdGenFactory.js')
|
|
54
56
|
|
|
57
|
+
const routerKeys = [
|
|
58
|
+
'allowUnsafeRegex',
|
|
59
|
+
'buildPrettyMeta',
|
|
60
|
+
'caseSensitive',
|
|
61
|
+
'constraints',
|
|
62
|
+
'defaultRoute',
|
|
63
|
+
'ignoreDuplicateSlashes',
|
|
64
|
+
'ignoreTrailingSlash',
|
|
65
|
+
'maxParamLength',
|
|
66
|
+
'onBadUrl',
|
|
67
|
+
'querystringParser',
|
|
68
|
+
'useSemicolonDelimiter'
|
|
69
|
+
]
|
|
70
|
+
|
|
55
71
|
function buildRouting (options) {
|
|
56
72
|
const router = FindMyWay(options.config)
|
|
57
73
|
|
|
@@ -85,8 +101,8 @@ function buildRouting (options) {
|
|
|
85
101
|
|
|
86
102
|
globalExposeHeadRoutes = options.exposeHeadRoutes
|
|
87
103
|
disableRequestLogging = options.disableRequestLogging
|
|
88
|
-
ignoreTrailingSlash = options.ignoreTrailingSlash
|
|
89
|
-
ignoreDuplicateSlashes = options.ignoreDuplicateSlashes
|
|
104
|
+
ignoreTrailingSlash = options.routerOptions.ignoreTrailingSlash
|
|
105
|
+
ignoreDuplicateSlashes = options.routerOptions.ignoreDuplicateSlashes
|
|
90
106
|
return503OnClosing = Object.hasOwn(options, 'return503OnClosing') ? options.return503OnClosing : true
|
|
91
107
|
keepAliveConnections = fastifyArgs.keepAliveConnections
|
|
92
108
|
},
|
|
@@ -572,6 +588,24 @@ function runPreParsing (err, request, reply) {
|
|
|
572
588
|
}
|
|
573
589
|
}
|
|
574
590
|
|
|
591
|
+
function buildRouterOptions (options, defaultOptions) {
|
|
592
|
+
const routerOptions = options.routerOptions || Object.create(null)
|
|
593
|
+
|
|
594
|
+
const usedDeprecatedOptions = routerKeys.filter(key => Object.hasOwn(options, key))
|
|
595
|
+
|
|
596
|
+
if (usedDeprecatedOptions.length > 0) {
|
|
597
|
+
FSTDEP022(usedDeprecatedOptions.join(', '))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (const key of routerKeys) {
|
|
601
|
+
if (!Object.hasOwn(routerOptions, key)) {
|
|
602
|
+
routerOptions[key] = options[key] ?? defaultOptions[key]
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return routerOptions
|
|
607
|
+
}
|
|
608
|
+
|
|
575
609
|
/**
|
|
576
610
|
* Used within the route handler as a `net.Socket.close` event handler.
|
|
577
611
|
* The purpose is to remove a socket from the tracked sockets collection when
|
|
@@ -583,4 +617,4 @@ function removeTrackedSocket () {
|
|
|
583
617
|
|
|
584
618
|
function noop () { }
|
|
585
619
|
|
|
586
|
-
module.exports = { buildRouting, validateBodyLimitOption }
|
|
620
|
+
module.exports = { buildRouting, validateBodyLimitOption, buildRouterOptions }
|
package/lib/server.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
FST_ERR_REOPENED_SERVER,
|
|
15
15
|
FST_ERR_LISTEN_OPTIONS_INVALID
|
|
16
16
|
} = require('./errors')
|
|
17
|
+
const PonyPromise = require('./promise')
|
|
17
18
|
|
|
18
19
|
module.exports.createServer = createServer
|
|
19
20
|
|
|
@@ -41,10 +42,14 @@ function createServer (options, httpHandler) {
|
|
|
41
42
|
throw new FST_ERR_LISTEN_OPTIONS_INVALID('Invalid options.signal')
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// copy the current signal state
|
|
46
|
+
this[kState].aborted = listenOptions.signal.aborted
|
|
47
|
+
|
|
48
|
+
if (this[kState].aborted) {
|
|
49
|
+
return this.close()
|
|
46
50
|
} else {
|
|
47
51
|
const onAborted = () => {
|
|
52
|
+
this[kState].aborted = true
|
|
48
53
|
this.close()
|
|
49
54
|
}
|
|
50
55
|
listenOptions.signal.addEventListener('abort', onAborted, { once: true })
|
|
@@ -99,18 +104,18 @@ function createServer (options, httpHandler) {
|
|
|
99
104
|
if (cb === undefined) {
|
|
100
105
|
const listening = listenPromise.call(this, server, listenOptions)
|
|
101
106
|
return listening.then(address => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
resolve(address)
|
|
107
|
-
onListenHookRunner(this)
|
|
108
|
-
})
|
|
109
|
-
} else {
|
|
107
|
+
const { promise, resolve } = PonyPromise.withResolvers()
|
|
108
|
+
if (host === 'localhost') {
|
|
109
|
+
multipleBindings.call(this, server, httpHandler, options, listenOptions, () => {
|
|
110
|
+
this[kState].listening = true
|
|
110
111
|
resolve(address)
|
|
111
112
|
onListenHookRunner(this)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
113
|
+
})
|
|
114
|
+
} else {
|
|
115
|
+
resolve(address)
|
|
116
|
+
onListenHookRunner(this)
|
|
117
|
+
}
|
|
118
|
+
return promise
|
|
114
119
|
})
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -126,7 +131,7 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
|
|
|
126
131
|
|
|
127
132
|
// let's check if we need to bind additional addresses
|
|
128
133
|
dns.lookup(listenOptions.host, { all: true }, (dnsErr, addresses) => {
|
|
129
|
-
if (dnsErr) {
|
|
134
|
+
if (dnsErr || this[kState].aborted) {
|
|
130
135
|
// not blocking the main server listening
|
|
131
136
|
// this.log.warn('dns.lookup error:', dnsErr)
|
|
132
137
|
onListen()
|
|
@@ -239,35 +244,31 @@ function listenPromise (server, listenOptions) {
|
|
|
239
244
|
}
|
|
240
245
|
|
|
241
246
|
return this.ready().then(() => {
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
// skip listen when aborted during ready
|
|
248
|
+
if (this[kState].aborted) return
|
|
249
|
+
|
|
250
|
+
const { promise, resolve, reject } = PonyPromise.withResolvers()
|
|
251
|
+
|
|
252
|
+
const errEventHandler = (err) => {
|
|
253
|
+
cleanup()
|
|
254
|
+
this[kState].listening = false
|
|
255
|
+
reject(err)
|
|
256
|
+
}
|
|
257
|
+
const listeningEventHandler = () => {
|
|
258
|
+
cleanup()
|
|
259
|
+
this[kState].listening = true
|
|
260
|
+
resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
|
|
261
|
+
}
|
|
244
262
|
function cleanup () {
|
|
245
263
|
server.removeListener('error', errEventHandler)
|
|
246
264
|
server.removeListener('listening', listeningEventHandler)
|
|
247
265
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
cleanup()
|
|
251
|
-
this[kState].listening = false
|
|
252
|
-
reject(err)
|
|
253
|
-
}
|
|
254
|
-
server.once('error', errEventHandler)
|
|
255
|
-
})
|
|
256
|
-
const listeningEvent = new Promise((resolve, reject) => {
|
|
257
|
-
listeningEventHandler = () => {
|
|
258
|
-
cleanup()
|
|
259
|
-
this[kState].listening = true
|
|
260
|
-
resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
|
|
261
|
-
}
|
|
262
|
-
server.once('listening', listeningEventHandler)
|
|
263
|
-
})
|
|
266
|
+
server.once('error', errEventHandler)
|
|
267
|
+
server.once('listening', listeningEventHandler)
|
|
264
268
|
|
|
265
269
|
server.listen(listenOptions)
|
|
266
270
|
|
|
267
|
-
return
|
|
268
|
-
errEvent, // e.g invalid port range error is always emitted before the server listening
|
|
269
|
-
listeningEvent
|
|
270
|
-
])
|
|
271
|
+
return promise
|
|
271
272
|
})
|
|
272
273
|
}
|
|
273
274
|
|
package/lib/symbols.js
CHANGED
|
@@ -56,6 +56,7 @@ const keys = {
|
|
|
56
56
|
// This symbol is only meant to be used for fastify tests and should not be used for any other purpose
|
|
57
57
|
kTestInternals: Symbol('fastify.testInternals'),
|
|
58
58
|
kErrorHandler: Symbol('fastify.errorHandler'),
|
|
59
|
+
kErrorHandlerAlreadySet: Symbol('fastify.errorHandlerAlreadySet'),
|
|
59
60
|
kChildLoggerFactory: Symbol('fastify.childLoggerFactory'),
|
|
60
61
|
kHasBeenDecorated: Symbol('fastify.hasBeenDecorated'),
|
|
61
62
|
kKeepAliveConnections: Symbol('fastify.keepAliveConnections'),
|