fastify 5.6.2 → 5.7.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/README.md +34 -33
- package/SECURITY.md +45 -7
- package/SPONSORS.md +1 -1
- package/build/build-validation.js +1 -2
- package/build/sync-version.js +0 -1
- package/docs/Guides/Detecting-When-Clients-Abort.md +1 -1
- package/docs/Guides/Ecosystem.md +12 -15
- package/docs/Guides/Migration-Guide-V4.md +2 -1
- package/docs/Guides/Migration-Guide-V5.md +9 -0
- package/docs/Guides/Serverless.md +25 -7
- package/docs/Guides/Testing.md +2 -2
- package/docs/Reference/Decorators.md +4 -3
- package/docs/Reference/Encapsulation.md +2 -2
- package/docs/Reference/Logging.md +4 -0
- package/docs/Reference/Plugins.md +2 -2
- package/docs/Reference/Principles.md +2 -2
- package/docs/Reference/Reply.md +3 -2
- package/docs/Reference/Server.md +35 -4
- package/docs/Reference/TypeScript.md +2 -1
- package/docs/Reference/Validation-and-Serialization.md +7 -1
- package/examples/benchmark/webstream.js +27 -0
- package/fastify.d.ts +16 -21
- package/fastify.js +14 -9
- package/lib/config-validator.js +189 -223
- package/lib/error-handler.js +2 -5
- package/lib/error-status.js +14 -0
- package/lib/four-oh-four.js +2 -1
- package/lib/handle-request.js +6 -1
- package/lib/reply.js +53 -3
- package/lib/route.js +26 -12
- package/lib/schema-controller.js +2 -2
- package/lib/wrap-thenable.js +3 -0
- package/package.json +4 -4
- package/test/404s.test.js +69 -0
- package/test/diagnostics-channel/error-status.test.js +84 -0
- package/test/internals/schema-controller-perf.test.js +40 -0
- package/test/issue-4959.test.js +34 -9
- package/test/listen.1.test.js +9 -1
- package/test/logger/logging.test.js +38 -1
- package/test/router-options.test.js +169 -0
- package/test/server.test.js +4 -1
- package/test/types/fastify.test-d.ts +28 -7
- package/test/types/instance.test-d.ts +29 -21
- package/test/types/reply.test-d.ts +55 -4
- package/test/types/type-provider.test-d.ts +6 -6
- package/test/web-api.test.js +136 -0
- package/types/instance.d.ts +1 -1
- package/types/reply.d.ts +2 -2
- package/types/type-provider.d.ts +16 -0
- package/.vscode/settings.json +0 -22
- package/test/decorator-namespace.test._js_ +0 -30
|
@@ -16,7 +16,7 @@ t.test('logging', { timeout: 60000 }, async (t) => {
|
|
|
16
16
|
let localhost
|
|
17
17
|
let localhostForURL
|
|
18
18
|
|
|
19
|
-
t.plan(
|
|
19
|
+
t.plan(14)
|
|
20
20
|
|
|
21
21
|
t.before(async function () {
|
|
22
22
|
[localhost, localhostForURL] = await helper.getLoopbackHost()
|
|
@@ -282,6 +282,43 @@ t.test('logging', { timeout: 60000 }, async (t) => {
|
|
|
282
282
|
t.assert.strictEqual(stream.readableLength, 0)
|
|
283
283
|
})
|
|
284
284
|
|
|
285
|
+
await t.test('should log incoming request and outgoing response based on disableRequestLogging function', async (t) => {
|
|
286
|
+
const lines = [
|
|
287
|
+
'incoming request',
|
|
288
|
+
'request completed'
|
|
289
|
+
]
|
|
290
|
+
t.plan(lines.length)
|
|
291
|
+
|
|
292
|
+
const stream = split(JSON.parse)
|
|
293
|
+
const loggerInstance = pino(stream)
|
|
294
|
+
|
|
295
|
+
const fastify = Fastify({
|
|
296
|
+
disableRequestLogging: (request) => {
|
|
297
|
+
return request.url !== '/not-logged'
|
|
298
|
+
},
|
|
299
|
+
loggerInstance
|
|
300
|
+
})
|
|
301
|
+
t.after(() => fastify.close())
|
|
302
|
+
|
|
303
|
+
fastify.get('/logged', (req, reply) => {
|
|
304
|
+
return reply.code(200).send({})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
fastify.get('/not-logged', (req, reply) => {
|
|
308
|
+
return reply.code(200).send({})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
await fastify.ready()
|
|
312
|
+
|
|
313
|
+
await fastify.inject({ method: 'GET', url: '/not-logged' })
|
|
314
|
+
await fastify.inject({ method: 'GET', url: '/logged' })
|
|
315
|
+
|
|
316
|
+
for await (const [line] of on(stream, 'data')) {
|
|
317
|
+
t.assert.strictEqual(line.msg, lines.shift())
|
|
318
|
+
if (lines.length === 0) break
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
285
322
|
await t.test('defaults to info level', async (t) => {
|
|
286
323
|
const lines = [
|
|
287
324
|
{ req: { method: 'GET' }, msg: 'incoming request' },
|
|
@@ -447,6 +447,157 @@ test('Should honor disableRequestLogging option in frameworkErrors wrapper - FST
|
|
|
447
447
|
)
|
|
448
448
|
})
|
|
449
449
|
|
|
450
|
+
test('Should honor disableRequestLogging function in frameworkErrors wrapper - FST_ERR_BAD_URL', (t, done) => {
|
|
451
|
+
t.plan(4)
|
|
452
|
+
|
|
453
|
+
let logCallCount = 0
|
|
454
|
+
const logStream = split(JSON.parse)
|
|
455
|
+
|
|
456
|
+
const fastify = Fastify({
|
|
457
|
+
disableRequestLogging: (req) => {
|
|
458
|
+
// Disable logging for URLs containing 'silent'
|
|
459
|
+
return req.url.includes('silent')
|
|
460
|
+
},
|
|
461
|
+
frameworkErrors: function (err, req, res) {
|
|
462
|
+
res.send(`${err.message} - ${err.code}`)
|
|
463
|
+
},
|
|
464
|
+
logger: {
|
|
465
|
+
stream: logStream,
|
|
466
|
+
level: 'info'
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
fastify.get('/test/:id', (req, res) => {
|
|
471
|
+
res.send('{ hello: \'world\' }')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
logStream.on('data', (json) => {
|
|
475
|
+
if (json.msg === 'incoming request') {
|
|
476
|
+
logCallCount++
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// First request: URL does not contain 'silent', so logging should happen
|
|
481
|
+
fastify.inject(
|
|
482
|
+
{
|
|
483
|
+
method: 'GET',
|
|
484
|
+
url: '/test/%world'
|
|
485
|
+
},
|
|
486
|
+
(err, res) => {
|
|
487
|
+
t.assert.ifError(err)
|
|
488
|
+
t.assert.strictEqual(res.body, '\'/test/%world\' is not a valid url component - FST_ERR_BAD_URL')
|
|
489
|
+
|
|
490
|
+
// Second request: URL contains 'silent', so logging should be disabled
|
|
491
|
+
fastify.inject(
|
|
492
|
+
{
|
|
493
|
+
method: 'GET',
|
|
494
|
+
url: '/silent/%world'
|
|
495
|
+
},
|
|
496
|
+
(err2, res2) => {
|
|
497
|
+
t.assert.ifError(err2)
|
|
498
|
+
// Give time for any potential log events
|
|
499
|
+
setImmediate(() => {
|
|
500
|
+
// Only the first request should have logged
|
|
501
|
+
t.assert.strictEqual(logCallCount, 1)
|
|
502
|
+
done()
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('Should honor disableRequestLogging function in frameworkErrors wrapper - FST_ERR_ASYNC_CONSTRAINT', (t, done) => {
|
|
511
|
+
t.plan(4)
|
|
512
|
+
|
|
513
|
+
let logCallCount = 0
|
|
514
|
+
|
|
515
|
+
const constraint = {
|
|
516
|
+
name: 'secret',
|
|
517
|
+
storage: function () {
|
|
518
|
+
const secrets = {}
|
|
519
|
+
return {
|
|
520
|
+
get: (secret) => { return secrets[secret] || null },
|
|
521
|
+
set: (secret, store) => { secrets[secret] = store }
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
deriveConstraint: (req, ctx, done) => {
|
|
525
|
+
done(Error('kaboom'))
|
|
526
|
+
},
|
|
527
|
+
validate () { return true }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const logStream = split(JSON.parse)
|
|
531
|
+
|
|
532
|
+
const fastify = Fastify({
|
|
533
|
+
constraints: { secret: constraint },
|
|
534
|
+
disableRequestLogging: (req) => {
|
|
535
|
+
// Disable logging for URLs containing 'silent'
|
|
536
|
+
return req.url.includes('silent')
|
|
537
|
+
},
|
|
538
|
+
frameworkErrors: function (err, req, res) {
|
|
539
|
+
res.send(`${err.message} - ${err.code}`)
|
|
540
|
+
},
|
|
541
|
+
logger: {
|
|
542
|
+
stream: logStream,
|
|
543
|
+
level: 'info'
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
fastify.route({
|
|
548
|
+
method: 'GET',
|
|
549
|
+
url: '/',
|
|
550
|
+
constraints: { secret: 'alpha' },
|
|
551
|
+
handler: (req, reply) => {
|
|
552
|
+
reply.send({ hello: 'from alpha' })
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
fastify.route({
|
|
557
|
+
method: 'GET',
|
|
558
|
+
url: '/silent',
|
|
559
|
+
constraints: { secret: 'alpha' },
|
|
560
|
+
handler: (req, reply) => {
|
|
561
|
+
reply.send({ hello: 'from alpha' })
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
logStream.on('data', (json) => {
|
|
566
|
+
if (json.msg === 'incoming request') {
|
|
567
|
+
logCallCount++
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// First request: URL does not contain 'silent', so logging should happen
|
|
572
|
+
fastify.inject(
|
|
573
|
+
{
|
|
574
|
+
method: 'GET',
|
|
575
|
+
url: '/'
|
|
576
|
+
},
|
|
577
|
+
(err, res) => {
|
|
578
|
+
t.assert.ifError(err)
|
|
579
|
+
t.assert.strictEqual(res.body, 'Unexpected error from async constraint - FST_ERR_ASYNC_CONSTRAINT')
|
|
580
|
+
|
|
581
|
+
// Second request: URL contains 'silent', so logging should be disabled
|
|
582
|
+
fastify.inject(
|
|
583
|
+
{
|
|
584
|
+
method: 'GET',
|
|
585
|
+
url: '/silent'
|
|
586
|
+
},
|
|
587
|
+
(err2, res2) => {
|
|
588
|
+
t.assert.ifError(err2)
|
|
589
|
+
// Give time for any potential log events
|
|
590
|
+
setImmediate(() => {
|
|
591
|
+
// Only the first request should have logged
|
|
592
|
+
t.assert.strictEqual(logCallCount, 1)
|
|
593
|
+
done()
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
})
|
|
600
|
+
|
|
450
601
|
test('Should honor routerOptions.defaultRoute', async t => {
|
|
451
602
|
t.plan(3)
|
|
452
603
|
const fastify = Fastify({
|
|
@@ -895,3 +1046,21 @@ test('Should honor routerOptions.useSemicolonDelimiter over useSemicolonDelimite
|
|
|
895
1046
|
})
|
|
896
1047
|
t.assert.strictEqual(res.statusCode, 200)
|
|
897
1048
|
})
|
|
1049
|
+
|
|
1050
|
+
test('Should support extra find-my-way options', async t => {
|
|
1051
|
+
t.plan(1)
|
|
1052
|
+
// Use a real upstream option from find-my-way
|
|
1053
|
+
const fastify = Fastify({
|
|
1054
|
+
routerOptions: {
|
|
1055
|
+
buildPrettyMeta: (route) => {
|
|
1056
|
+
const cleanMeta = Object.assign({}, route.store)
|
|
1057
|
+
return cleanMeta
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
await fastify.ready()
|
|
1063
|
+
|
|
1064
|
+
// Ensure the option is preserved after validation
|
|
1065
|
+
t.assert.strictEqual(typeof fastify.initialConfig.routerOptions.buildPrettyMeta, 'function')
|
|
1066
|
+
})
|
package/test/server.test.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const dns = require('node:dns')
|
|
4
|
+
const { networkInterfaces } = require('node:os')
|
|
4
5
|
const { test } = require('node:test')
|
|
5
6
|
const Fastify = require('..')
|
|
6
7
|
const undici = require('undici')
|
|
7
8
|
const proxyquire = require('proxyquire')
|
|
8
9
|
|
|
10
|
+
const isIPv6Missing = !Object.values(networkInterfaces()).flat().some(({ family }) => family === 'IPv6')
|
|
11
|
+
|
|
9
12
|
test('listen should accept null port', async t => {
|
|
10
13
|
const fastify = Fastify()
|
|
11
14
|
t.after(() => fastify.close())
|
|
@@ -81,7 +84,7 @@ test('Test for hostname and port', async (t) => {
|
|
|
81
84
|
await fetch('http://localhost:8000/host')
|
|
82
85
|
})
|
|
83
86
|
|
|
84
|
-
test('Test for IPV6 port', async (t) => {
|
|
87
|
+
test('Test for IPV6 port', { skip: isIPv6Missing }, async (t) => {
|
|
85
88
|
t.plan(3)
|
|
86
89
|
const app = Fastify()
|
|
87
90
|
t.after(() => app.close())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorObject as AjvErrorObject } from 'ajv'
|
|
1
|
+
import Ajv, { ErrorObject as AjvErrorObject } from 'ajv'
|
|
2
2
|
import * as http from 'node:http'
|
|
3
3
|
import * as http2 from 'node:http2'
|
|
4
4
|
import * as https from 'node:https'
|
|
@@ -65,12 +65,12 @@ expectType<
|
|
|
65
65
|
>(fastify({ http2: true, https: {}, http2SessionTimeout: 1000 }))
|
|
66
66
|
expectType<LightMyRequestChain>(fastify({ http2: true, https: {} }).inject())
|
|
67
67
|
expectType<
|
|
68
|
-
FastifyInstance<
|
|
69
|
-
SafePromiseLike<FastifyInstance<
|
|
68
|
+
FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse> &
|
|
69
|
+
SafePromiseLike<FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse>>
|
|
70
70
|
>(fastify({ schemaController: {} }))
|
|
71
71
|
expectType<
|
|
72
|
-
FastifyInstance<
|
|
73
|
-
SafePromiseLike<FastifyInstance<
|
|
72
|
+
FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse> &
|
|
73
|
+
SafePromiseLike<FastifyInstance<http.Server, http.IncomingMessage, http.ServerResponse>>
|
|
74
74
|
>(
|
|
75
75
|
fastify({
|
|
76
76
|
schemaController: {
|
|
@@ -115,6 +115,7 @@ expectAssignable<FastifyInstance>(fastify({ pluginTimeout: 1000 }))
|
|
|
115
115
|
expectAssignable<FastifyInstance>(fastify({ bodyLimit: 100 }))
|
|
116
116
|
expectAssignable<FastifyInstance>(fastify({ maxParamLength: 100 }))
|
|
117
117
|
expectAssignable<FastifyInstance>(fastify({ disableRequestLogging: true }))
|
|
118
|
+
expectAssignable<FastifyInstance>(fastify({ disableRequestLogging: (req) => req.url?.includes('/health') ?? false }))
|
|
118
119
|
expectAssignable<FastifyInstance>(fastify({ requestIdLogLabel: 'request-id' }))
|
|
119
120
|
expectAssignable<FastifyInstance>(fastify({ onProtoPoisoning: 'error' }))
|
|
120
121
|
expectAssignable<FastifyInstance>(fastify({ onConstructorPoisoning: 'error' }))
|
|
@@ -232,12 +233,32 @@ expectAssignable<FastifyInstance>(fastify({
|
|
|
232
233
|
customOptions: {
|
|
233
234
|
removeAdditional: 'all'
|
|
234
235
|
},
|
|
235
|
-
plugins: [() =>
|
|
236
|
+
plugins: [(ajv: Ajv): Ajv => ajv]
|
|
237
|
+
}
|
|
238
|
+
}))
|
|
239
|
+
expectAssignable<FastifyInstance>(fastify({
|
|
240
|
+
ajv: {
|
|
241
|
+
plugins: [[(ajv: Ajv): Ajv => ajv, ['keyword1', 'keyword2']]]
|
|
242
|
+
}
|
|
243
|
+
}))
|
|
244
|
+
expectError(fastify({
|
|
245
|
+
ajv: {
|
|
246
|
+
customOptions: {
|
|
247
|
+
removeAdditional: 'all'
|
|
248
|
+
},
|
|
249
|
+
plugins: [
|
|
250
|
+
() => {
|
|
251
|
+
// error, plugins always return the Ajv instance fluently
|
|
252
|
+
}
|
|
253
|
+
]
|
|
236
254
|
}
|
|
237
255
|
}))
|
|
238
256
|
expectAssignable<FastifyInstance>(fastify({
|
|
239
257
|
ajv: {
|
|
240
|
-
|
|
258
|
+
onCreate: (ajvInstance) => {
|
|
259
|
+
expectType<Ajv>(ajvInstance)
|
|
260
|
+
return ajvInstance
|
|
261
|
+
}
|
|
241
262
|
}
|
|
242
263
|
}))
|
|
243
264
|
expectAssignable<FastifyInstance>(fastify({ frameworkErrors: () => { } }))
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { expectAssignable, expectError, expectNotDeprecated, expectType } from 'tsd'
|
|
1
|
+
import { expectAssignable, expectError, expectNotAssignable, expectNotDeprecated, expectType } from 'tsd'
|
|
2
2
|
import fastify, {
|
|
3
3
|
FastifyBaseLogger,
|
|
4
4
|
FastifyBodyParser,
|
|
5
5
|
FastifyError,
|
|
6
6
|
FastifyInstance,
|
|
7
|
+
FastifyRouterOptions,
|
|
7
8
|
RawReplyDefaultExpression,
|
|
8
9
|
RawRequestDefaultExpression,
|
|
9
10
|
RawServerDefault,
|
|
@@ -15,7 +16,7 @@ import { FastifyRequest } from '../../types/request'
|
|
|
15
16
|
import { FastifySchemaControllerOptions, FastifySchemaCompiler, FastifySerializerCompiler } from '../../types/schema'
|
|
16
17
|
import { AddressInfo } from 'node:net'
|
|
17
18
|
import { Bindings, ChildLoggerOptions } from '../../types/logger'
|
|
18
|
-
import { ConstraintStrategy } from 'find-my-way'
|
|
19
|
+
import { Config as FindMyWayConfig, ConstraintStrategy } from 'find-my-way'
|
|
19
20
|
import { FindMyWayVersion } from '../../types/instance'
|
|
20
21
|
|
|
21
22
|
const server = fastify()
|
|
@@ -92,12 +93,12 @@ interface ReplyPayload {
|
|
|
92
93
|
// typed sync error handler
|
|
93
94
|
server.setErrorHandler<CustomError, ReplyPayload>((error, request, reply) => {
|
|
94
95
|
expectType<CustomError>(error)
|
|
95
|
-
expectType<((payload
|
|
96
|
+
expectType<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>(reply.send)
|
|
96
97
|
})
|
|
97
98
|
// typed async error handler send
|
|
98
99
|
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
|
|
99
100
|
expectType<CustomError>(error)
|
|
100
|
-
expectType<((payload
|
|
101
|
+
expectType<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>(reply.send)
|
|
101
102
|
})
|
|
102
103
|
// typed async error handler return
|
|
103
104
|
server.setErrorHandler<CustomError, ReplyPayload>(async (error, request, reply) => {
|
|
@@ -323,7 +324,7 @@ type InitialConfig = Readonly<{
|
|
|
323
324
|
https?: boolean | Readonly<{ allowHTTP1: boolean }>,
|
|
324
325
|
ignoreTrailingSlash?: boolean,
|
|
325
326
|
ignoreDuplicateSlashes?: boolean,
|
|
326
|
-
disableRequestLogging?: boolean,
|
|
327
|
+
disableRequestLogging?: boolean | ((req: FastifyRequest) => boolean),
|
|
327
328
|
maxParamLength?: number,
|
|
328
329
|
onProtoPoisoning?: 'error' | 'remove' | 'ignore',
|
|
329
330
|
onConstructorPoisoning?: 'error' | 'remove' | 'ignore',
|
|
@@ -332,25 +333,32 @@ type InitialConfig = Readonly<{
|
|
|
332
333
|
requestIdLogLabel?: string,
|
|
333
334
|
http2SessionTimeout?: number,
|
|
334
335
|
useSemicolonDelimiter?: boolean,
|
|
335
|
-
routerOptions?:
|
|
336
|
-
allowUnsafeRegex?: boolean,
|
|
337
|
-
buildPrettyMeta?: (route: { [k: string]: unknown, store: { [k: string]: unknown } }) => object,
|
|
338
|
-
caseSensitive?: boolean,
|
|
339
|
-
constraints?: {
|
|
340
|
-
[name: string]: ConstraintStrategy<FindMyWayVersion<RawServerDefault>, unknown>
|
|
341
|
-
}
|
|
342
|
-
defaultRoute?: (req: FastifyRequest, res: FastifyReply) => void,
|
|
343
|
-
ignoreDuplicateSlashes?: boolean,
|
|
344
|
-
ignoreTrailingSlash?: boolean,
|
|
345
|
-
maxParamLength?: number,
|
|
346
|
-
onBadUrl?: (path: string, req: FastifyRequest, res: FastifyReply) => void,
|
|
347
|
-
querystringParser?: (str: string) => { [key: string]: unknown },
|
|
348
|
-
useSemicolonDelimiter?: boolean,
|
|
349
|
-
}
|
|
336
|
+
routerOptions?: FastifyRouterOptions<RawServerDefault>
|
|
350
337
|
}>
|
|
351
338
|
|
|
352
339
|
expectType<InitialConfig>(fastify().initialConfig)
|
|
353
340
|
|
|
341
|
+
const routerOptionsForFindMyWay = {} as FastifyRouterOptions<RawServerDefault>
|
|
342
|
+
expectAssignable<FindMyWayConfig<FindMyWayVersion<RawServerDefault>>>(routerOptionsForFindMyWay)
|
|
343
|
+
|
|
344
|
+
fastify({
|
|
345
|
+
routerOptions: {
|
|
346
|
+
defaultRoute: (req, res) => {
|
|
347
|
+
expectType<RawRequestDefaultExpression<RawServerDefault>>(req)
|
|
348
|
+
expectType<RawReplyDefaultExpression<RawServerDefault>>(res)
|
|
349
|
+
expectNotAssignable<FastifyReply>(res)
|
|
350
|
+
res.end('foo')
|
|
351
|
+
},
|
|
352
|
+
onBadUrl: (path, req, res) => {
|
|
353
|
+
expectType<string>(path)
|
|
354
|
+
expectType<RawRequestDefaultExpression<RawServerDefault>>(req)
|
|
355
|
+
expectType<RawReplyDefaultExpression<RawServerDefault>>(res)
|
|
356
|
+
expectNotAssignable<FastifyReply>(res)
|
|
357
|
+
res.end('foo')
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
|
|
354
362
|
expectType<FastifyBodyParser<string>>(server.defaultTextParser)
|
|
355
363
|
|
|
356
364
|
expectType<FastifyBodyParser<string>>(server.getDefaultJsonParser('ignore', 'error'))
|
|
@@ -562,7 +570,7 @@ expectError(server.decorateReply('typedTestReplyMethod', async function (x) {
|
|
|
562
570
|
const foo = server.getDecorator<string>('foo')
|
|
563
571
|
expectType<string>(foo)
|
|
564
572
|
|
|
565
|
-
const versionConstraintStrategy = {
|
|
573
|
+
const versionConstraintStrategy: ConstraintStrategy<FindMyWayVersion<RawServerDefault>> = {
|
|
566
574
|
name: 'version',
|
|
567
575
|
storage: () => ({
|
|
568
576
|
get: () => () => {},
|
|
@@ -16,14 +16,14 @@ const getHandler: RouteHandlerMethod = function (_request, reply) {
|
|
|
16
16
|
expectType<FastifyRequest<RouteGenericInterface, RawServerDefault, RawRequestDefaultExpression>>(reply.request)
|
|
17
17
|
expectType<<Code extends number>(statusCode: Code) => DefaultFastifyReplyWithCode<Code>>(reply.code)
|
|
18
18
|
expectType<<Code extends number>(statusCode: Code) => DefaultFastifyReplyWithCode<Code>>(reply.status)
|
|
19
|
-
expectType<(payload?: unknown) => FastifyReply>(reply.code(100 as number).send)
|
|
19
|
+
expectType<(...args: [payload?: unknown]) => FastifyReply>(reply.code(100 as number).send)
|
|
20
20
|
expectType<number>(reply.elapsedTime)
|
|
21
21
|
expectType<number>(reply.statusCode)
|
|
22
22
|
expectType<boolean>(reply.sent)
|
|
23
23
|
expectType<
|
|
24
24
|
(hints: Record<string, string | string[]>, callback?: (() => void) | undefined) => void
|
|
25
25
|
>(reply.writeEarlyHints)
|
|
26
|
-
expectType<((payload?: unknown) => FastifyReply)>(reply.send)
|
|
26
|
+
expectType<((...args: [payload?: unknown]) => FastifyReply)>(reply.send)
|
|
27
27
|
expectAssignable<(key: string, value: any) => FastifyReply>(reply.header)
|
|
28
28
|
expectAssignable<(values: { [key: string]: any }) => FastifyReply>(reply.headers)
|
|
29
29
|
expectAssignable<(key: string) => number | string | string[] | undefined>(reply.getHeader)
|
|
@@ -100,9 +100,29 @@ interface InvalidReplyHttpCodes {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
interface ReplyVoid {
|
|
104
|
+
Reply: void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ReplyUndefined {
|
|
108
|
+
Reply: undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Issue #5534 scenario: 204 No Content should allow empty send(), 201 Created should require payload
|
|
112
|
+
// Note: `204: undefined` gets converted to `unknown` via UndefinedToUnknown in type-provider.d.ts,
|
|
113
|
+
// meaning send() is optional but send({}) is also allowed. Use `void` instead of `undefined`
|
|
114
|
+
// if you want stricter "no payload allowed" semantics.
|
|
115
|
+
interface ReplyHttpCodesWithNoContent {
|
|
116
|
+
Reply: {
|
|
117
|
+
201: { id: string };
|
|
118
|
+
204: undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
const typedHandler: RouteHandler<ReplyPayload> = async (request, reply) => {
|
|
104
|
-
|
|
105
|
-
expectType<((payload
|
|
123
|
+
// When Reply type is specified, send() requires a payload argument
|
|
124
|
+
expectType<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>(reply.send)
|
|
125
|
+
expectType<((...args: [payload: ReplyPayload['Reply']]) => FastifyReply<ReplyPayload, RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>>)>(reply.code(100).send)
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
const server = fastify()
|
|
@@ -111,6 +131,10 @@ server.get('/typed', typedHandler)
|
|
|
111
131
|
server.get<ReplyPayload>('/get-generic-send', async function handler (request, reply) {
|
|
112
132
|
reply.send({ test: true })
|
|
113
133
|
})
|
|
134
|
+
// When Reply type is specified, send() requires a payload - calling without arguments should error
|
|
135
|
+
expectError(server.get<ReplyPayload>('/get-generic-send-missing-payload', async function handler (request, reply) {
|
|
136
|
+
reply.send()
|
|
137
|
+
}))
|
|
114
138
|
server.get<ReplyPayload>('/get-generic-return', async function handler (request, reply) {
|
|
115
139
|
return { test: false }
|
|
116
140
|
})
|
|
@@ -201,3 +225,30 @@ const httpHeaderHandler: RouteHandlerMethod = function (_request, reply) {
|
|
|
201
225
|
reply.headers({ 'x-fastify-test': 'test' })
|
|
202
226
|
reply.removeHeader('x-fastify-test')
|
|
203
227
|
}
|
|
228
|
+
|
|
229
|
+
// Test: send() without arguments is valid when no Reply type is specified (default unknown)
|
|
230
|
+
server.get('/get-no-type-send-empty', async function handler (request, reply) {
|
|
231
|
+
reply.send()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Test: send() without arguments is valid when Reply type is void
|
|
235
|
+
server.get<ReplyVoid>('/get-void-send-empty', async function handler (request, reply) {
|
|
236
|
+
reply.send()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Test: send() without arguments is valid when Reply type is undefined
|
|
240
|
+
server.get<ReplyUndefined>('/get-undefined-send-empty', async function handler (request, reply) {
|
|
241
|
+
reply.send()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// Issue #5534 scenario: HTTP status codes with 204 No Content
|
|
245
|
+
server.get<ReplyHttpCodesWithNoContent>('/get-http-codes-no-content', async function handler (request, reply) {
|
|
246
|
+
// 204 No Content - send() without payload is valid because Reply is undefined
|
|
247
|
+
reply.code(204).send()
|
|
248
|
+
// 201 Created - send() requires payload
|
|
249
|
+
reply.code(201).send({ id: '123' })
|
|
250
|
+
})
|
|
251
|
+
// 201 Created without payload should error
|
|
252
|
+
expectError(server.get<ReplyHttpCodesWithNoContent>('/get-http-codes-201-missing-payload', async function handler (request, reply) {
|
|
253
|
+
reply.code(201).send()
|
|
254
|
+
}))
|
|
@@ -479,9 +479,9 @@ expectAssignable(server.withTypeProvider<TypeBoxProvider>().get(
|
|
|
479
479
|
res.send('hello')
|
|
480
480
|
res.send(42)
|
|
481
481
|
res.send({ error: 'error' })
|
|
482
|
-
expectType<(payload
|
|
483
|
-
expectType<(payload
|
|
484
|
-
expectType<(payload
|
|
482
|
+
expectType<((...args: [payload: string]) => typeof res)>(res.code(200).send)
|
|
483
|
+
expectType<((...args: [payload: number]) => typeof res)>(res.code(400).send)
|
|
484
|
+
expectType<((...args: [payload: { error: string }]) => typeof res)>(res.code(500).send)
|
|
485
485
|
expectError<(payload?: unknown) => typeof res>(res.code(200).send)
|
|
486
486
|
}
|
|
487
487
|
))
|
|
@@ -711,9 +711,9 @@ expectAssignable(server.withTypeProvider<JsonSchemaToTsProvider>().get(
|
|
|
711
711
|
res.send('hello')
|
|
712
712
|
res.send(42)
|
|
713
713
|
res.send({ error: 'error' })
|
|
714
|
-
expectType<(payload
|
|
715
|
-
expectType<(payload
|
|
716
|
-
expectType<(payload
|
|
714
|
+
expectType<((...args: [payload: string]) => typeof res)>(res.code(200).send)
|
|
715
|
+
expectType<((...args: [payload: number]) => typeof res)>(res.code(400).send)
|
|
716
|
+
expectType<((...args: [payload: { [x: string]: unknown; error?: string }]) => typeof res)>(res.code(500).send)
|
|
717
717
|
expectError<(payload?: unknown) => typeof res>(res.code(200).send)
|
|
718
718
|
}
|
|
719
719
|
))
|
package/test/web-api.test.js
CHANGED
|
@@ -5,6 +5,7 @@ const Fastify = require('../fastify')
|
|
|
5
5
|
const fs = require('node:fs')
|
|
6
6
|
const { Readable } = require('node:stream')
|
|
7
7
|
const { fetch: undiciFetch } = require('undici')
|
|
8
|
+
const http = require('node:http')
|
|
8
9
|
|
|
9
10
|
test('should response with a ReadableStream', async (t) => {
|
|
10
11
|
t.plan(2)
|
|
@@ -330,3 +331,138 @@ test('allow to pipe with undici.fetch', async (t) => {
|
|
|
330
331
|
t.assert.strictEqual(response.statusCode, 200)
|
|
331
332
|
t.assert.deepStrictEqual(response.json(), { ok: true })
|
|
332
333
|
})
|
|
334
|
+
|
|
335
|
+
test('WebStream error before headers sent should trigger error handler', async (t) => {
|
|
336
|
+
t.plan(2)
|
|
337
|
+
|
|
338
|
+
const fastify = Fastify()
|
|
339
|
+
|
|
340
|
+
fastify.get('/', function (request, reply) {
|
|
341
|
+
const stream = new ReadableStream({
|
|
342
|
+
start (controller) {
|
|
343
|
+
controller.error(new Error('stream error'))
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
reply.send(stream)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
const response = await fastify.inject({ method: 'GET', path: '/' })
|
|
350
|
+
|
|
351
|
+
t.assert.strictEqual(response.statusCode, 500)
|
|
352
|
+
t.assert.strictEqual(response.json().message, 'stream error')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('WebStream error after headers sent should destroy response', (t, done) => {
|
|
356
|
+
t.plan(2)
|
|
357
|
+
|
|
358
|
+
const fastify = Fastify()
|
|
359
|
+
t.after(() => fastify.close())
|
|
360
|
+
|
|
361
|
+
fastify.get('/', function (request, reply) {
|
|
362
|
+
const stream = new ReadableStream({
|
|
363
|
+
start (controller) {
|
|
364
|
+
controller.enqueue('hello')
|
|
365
|
+
},
|
|
366
|
+
pull (controller) {
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
controller.error(new Error('stream error'))
|
|
369
|
+
}, 10)
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
reply.header('content-type', 'text/plain').send(stream)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
fastify.listen({ port: 0 }, err => {
|
|
376
|
+
t.assert.ifError(err)
|
|
377
|
+
|
|
378
|
+
let finished = false
|
|
379
|
+
http.get(`http://localhost:${fastify.server.address().port}`, (res) => {
|
|
380
|
+
res.on('close', () => {
|
|
381
|
+
if (!finished) {
|
|
382
|
+
finished = true
|
|
383
|
+
t.assert.ok('response closed')
|
|
384
|
+
done()
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
res.resume()
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('WebStream should cancel reader when response is destroyed', (t, done) => {
|
|
393
|
+
t.plan(2)
|
|
394
|
+
|
|
395
|
+
const fastify = Fastify()
|
|
396
|
+
t.after(() => fastify.close())
|
|
397
|
+
|
|
398
|
+
let readerCancelled = false
|
|
399
|
+
|
|
400
|
+
fastify.get('/', function (request, reply) {
|
|
401
|
+
const stream = new ReadableStream({
|
|
402
|
+
start (controller) {
|
|
403
|
+
controller.enqueue('hello')
|
|
404
|
+
},
|
|
405
|
+
pull (controller) {
|
|
406
|
+
return new Promise(() => {})
|
|
407
|
+
},
|
|
408
|
+
cancel () {
|
|
409
|
+
readerCancelled = true
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
reply.header('content-type', 'text/plain').send(stream)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
fastify.listen({ port: 0 }, err => {
|
|
416
|
+
t.assert.ifError(err)
|
|
417
|
+
|
|
418
|
+
const req = http.get(`http://localhost:${fastify.server.address().port}`, (res) => {
|
|
419
|
+
res.once('data', () => {
|
|
420
|
+
req.destroy()
|
|
421
|
+
setTimeout(() => {
|
|
422
|
+
t.assert.strictEqual(readerCancelled, true)
|
|
423
|
+
done()
|
|
424
|
+
}, 50)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('WebStream should warn when headers already sent', async (t) => {
|
|
431
|
+
t.plan(2)
|
|
432
|
+
|
|
433
|
+
let warnCalled = false
|
|
434
|
+
const spyLogger = {
|
|
435
|
+
level: 'warn',
|
|
436
|
+
fatal: () => { },
|
|
437
|
+
error: () => { },
|
|
438
|
+
warn: (msg) => {
|
|
439
|
+
if (typeof msg === 'string' && msg.includes('use res.writeHead in stream mode')) {
|
|
440
|
+
warnCalled = true
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
info: () => { },
|
|
444
|
+
debug: () => { },
|
|
445
|
+
trace: () => { },
|
|
446
|
+
child: () => spyLogger
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fastify = Fastify({ loggerInstance: spyLogger })
|
|
450
|
+
t.after(() => fastify.close())
|
|
451
|
+
|
|
452
|
+
fastify.get('/', function (request, reply) {
|
|
453
|
+
reply.raw.writeHead(200, { 'content-type': 'text/plain' })
|
|
454
|
+
const stream = new ReadableStream({
|
|
455
|
+
start (controller) {
|
|
456
|
+
controller.enqueue('hello')
|
|
457
|
+
controller.close()
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
reply.send(stream)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
await fastify.listen({ port: 0 })
|
|
464
|
+
|
|
465
|
+
const response = await fetch(`http://localhost:${fastify.server.address().port}/`)
|
|
466
|
+
t.assert.strictEqual(response.status, 200)
|
|
467
|
+
t.assert.strictEqual(warnCalled, true)
|
|
468
|
+
})
|