fastify 5.8.5 → 5.10.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/PROJECT_CHARTER.md +1 -1
- package/README.md +7 -10
- package/SECURITY.md +1 -1
- package/SPONSORS.md +6 -4
- package/build/build-validation.js +2 -2
- package/docs/Guides/Database.md +0 -28
- package/docs/Guides/Delay-Accepting-Requests.md +1 -1
- package/docs/Guides/Ecosystem.md +23 -9
- package/docs/Guides/Getting-Started.md +2 -2
- package/docs/Guides/Migration-Guide-V4.md +2 -2
- package/docs/Guides/Migration-Guide-V5.md +1 -1
- package/docs/Guides/Plugins-Guide.md +1 -1
- package/docs/Guides/Prototype-Poisoning.md +3 -3
- package/docs/Guides/Serverless.md +8 -15
- package/docs/Guides/Style-Guide.md +1 -1
- package/docs/Guides/Write-Plugin.md +1 -1
- package/docs/Reference/Encapsulation.md +27 -26
- package/docs/Reference/Errors.md +12 -4
- package/docs/Reference/HTTP2.md +10 -10
- package/docs/Reference/Hooks.md +5 -5
- package/docs/Reference/Index.md +14 -16
- package/docs/Reference/LTS.md +12 -13
- package/docs/Reference/Lifecycle.md +9 -8
- package/docs/Reference/Logging.md +47 -39
- package/docs/Reference/Middleware.md +21 -25
- package/docs/Reference/Principles.md +2 -2
- package/docs/Reference/Reply.md +6 -1
- package/docs/Reference/Request.md +29 -18
- package/docs/Reference/Routes.md +5 -2
- package/docs/Reference/Server.md +138 -11
- package/docs/Reference/Type-Providers.md +53 -9
- package/docs/Reference/TypeScript.md +3 -3
- package/docs/Reference/Validation-and-Serialization.md +15 -2
- package/docs/Reference/Warnings.md +11 -6
- package/eslint.config.js +7 -2
- package/fastify.d.ts +13 -3
- package/fastify.js +60 -31
- package/lib/content-type-parser.js +2 -0
- package/lib/content-type.js +34 -1
- package/lib/context.js +0 -3
- package/lib/decorate.js +11 -3
- package/lib/error-handler.js +10 -28
- package/lib/error-serializer.js +59 -59
- package/lib/errors.js +23 -1
- package/lib/four-oh-four.js +22 -19
- package/lib/handle-request.js +12 -5
- package/lib/log-controller.js +169 -0
- package/lib/logger-factory.js +25 -4
- package/lib/plugin-override.js +2 -1
- package/lib/plugin-utils.js +5 -5
- package/lib/reply.js +96 -50
- package/lib/req-id-gen-factory.js +4 -1
- package/lib/request.js +16 -6
- package/lib/route.js +47 -41
- package/lib/schema-controller.js +1 -1
- package/lib/schemas.js +37 -30
- package/lib/symbols.js +4 -2
- package/lib/validation.js +10 -13
- package/lib/warnings.js +22 -4
- package/package.json +15 -17
- package/scripts/validate-ecosystem-links.js +1 -0
- package/test/bundler/esbuild/package.json +1 -1
- package/test/close-pipelining.test.js +1 -2
- package/test/content-type.test.js +20 -0
- package/test/custom-http-server.test.js +38 -0
- package/test/decorator-instance-properties.test.js +63 -0
- package/test/diagnostics-channel/async-error-handler.test.js +74 -0
- package/test/genReqId.test.js +24 -0
- package/test/hooks.test.js +94 -0
- package/test/http-methods/get.test.js +1 -1
- package/test/http2/plain.test.js +135 -0
- package/test/http2/secure-with-fallback.test.js +1 -1
- package/test/https/https.test.js +1 -2
- package/test/internals/errors.test.js +31 -1
- package/test/internals/logger.test.js +322 -0
- package/test/internals/plugin.test.js +3 -1
- package/test/internals/reply.test.js +35 -4
- package/test/internals/request.test.js +37 -10
- package/test/internals/schema-controller-perf.test.js +33 -0
- package/test/logger/logging.test.js +57 -1
- package/test/logger/options.test.js +38 -1
- package/test/reply-error.test.js +1 -1
- package/test/reply-trailers.test.js +70 -0
- package/test/request-media-type.test.js +105 -0
- package/test/route-prefix.test.js +34 -0
- package/test/router-options.test.js +222 -11
- package/test/schema-serialization.test.js +108 -0
- package/test/schema-validation.test.js +24 -0
- package/test/scripts/validate-ecosystem-links.test.js +40 -57
- package/test/stream.4.test.js +2 -2
- package/test/throw.test.js +14 -0
- package/test/trust-proxy.test.js +70 -30
- package/test/types/content-type-parser.tst.ts +70 -0
- package/test/types/{decorate-request-reply.test-d.ts → decorate-request-reply.tst.ts} +4 -4
- package/test/types/{dummy-plugin.ts → dummy-plugin.mts} +1 -1
- package/test/types/errors.tst.ts +91 -0
- package/test/types/fastify.tst.ts +351 -0
- package/test/types/hooks.tst.ts +578 -0
- package/test/types/instance.tst.ts +597 -0
- package/test/types/{logger.test-d.ts → logger.tst.ts} +61 -62
- package/test/types/plugin.tst.ts +96 -0
- package/test/types/register.tst.ts +245 -0
- package/test/types/reply.tst.ts +297 -0
- package/test/types/request.tst.ts +199 -0
- package/test/types/route.tst.ts +576 -0
- package/test/types/{schema.test-d.ts → schema.tst.ts} +22 -22
- package/test/types/{serverFactory.test-d.ts → serverFactory.tst.ts} +4 -4
- package/test/types/tsconfig.json +9 -0
- package/test/types/{type-provider.test-d.ts → type-provider.tst.ts} +256 -250
- package/test/types/using.tst.ts +14 -0
- package/types/errors.d.ts +4 -0
- package/types/instance.d.ts +2 -0
- package/types/logger.d.ts +22 -0
- package/types/request.d.ts +23 -2
- package/test/types/content-type-parser.test-d.ts +0 -72
- package/test/types/errors.test-d.ts +0 -90
- package/test/types/fastify.test-d.ts +0 -352
- package/test/types/hooks.test-d.ts +0 -550
- package/test/types/import.ts +0 -2
- package/test/types/instance.test-d.ts +0 -588
- package/test/types/plugin.test-d.ts +0 -97
- package/test/types/register.test-d.ts +0 -237
- package/test/types/reply.test-d.ts +0 -254
- package/test/types/request.test-d.ts +0 -188
- package/test/types/route.test-d.ts +0 -553
- package/test/types/using.test-d.ts +0 -17
package/test/http2/plain.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { test } = require('node:test')
|
|
4
|
+
const http2 = require('node:http2')
|
|
4
5
|
const Fastify = require('../..')
|
|
5
6
|
const h2url = require('h2url')
|
|
6
7
|
const msg = { hello: 'world' }
|
|
@@ -66,3 +67,137 @@ test('http2 plain test', async t => {
|
|
|
66
67
|
t.assert.strictEqual(JSON.parse(res.body).port, parseInt(host.split(':')[1]))
|
|
67
68
|
})
|
|
68
69
|
})
|
|
70
|
+
|
|
71
|
+
test('http2 large non-stream replies are sent completely', async t => {
|
|
72
|
+
const modes = ['buffer', 'string']
|
|
73
|
+
|
|
74
|
+
for (const mode of modes) {
|
|
75
|
+
await t.test(mode, async t => {
|
|
76
|
+
const fastify = Fastify({ http2: true })
|
|
77
|
+
const payload = mode === 'buffer'
|
|
78
|
+
? Buffer.alloc((64 * 1024) + 1, 'a')
|
|
79
|
+
: 'a'.repeat((64 * 1024) + 1)
|
|
80
|
+
const contentLength = Buffer.byteLength(payload)
|
|
81
|
+
|
|
82
|
+
fastify.get('/large', async (req, reply) => {
|
|
83
|
+
reply.header('content-type', 'application/octet-stream')
|
|
84
|
+
reply.header('content-length', contentLength)
|
|
85
|
+
|
|
86
|
+
return payload
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await fastify.listen({ port: 0, host: '127.0.0.1' })
|
|
90
|
+
|
|
91
|
+
const client = http2.connect(`http://127.0.0.1:${fastify.server.address().port}`)
|
|
92
|
+
t.after(() => {
|
|
93
|
+
client.close()
|
|
94
|
+
return fastify.close()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await new Promise((resolve, reject) => {
|
|
98
|
+
const large = client.request({
|
|
99
|
+
[http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
|
|
100
|
+
[http2.constants.HTTP2_HEADER_PATH]: '/large'
|
|
101
|
+
})
|
|
102
|
+
const chunks = []
|
|
103
|
+
|
|
104
|
+
large.on('error', reject)
|
|
105
|
+
large.on('data', chunk => {
|
|
106
|
+
chunks.push(chunk)
|
|
107
|
+
})
|
|
108
|
+
large.on('end', () => {
|
|
109
|
+
t.assert.strictEqual(Buffer.concat(chunks).length, contentLength)
|
|
110
|
+
resolve()
|
|
111
|
+
})
|
|
112
|
+
large.end()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('http2 large buffer replies can be cancelled without rejecting the next stream', async t => {
|
|
119
|
+
const fastify = Fastify({ http2: true })
|
|
120
|
+
const payload = Buffer.alloc(32 * 1024 * 1024, 'a')
|
|
121
|
+
let smallHit = false
|
|
122
|
+
|
|
123
|
+
fastify.get('/large', async (req, reply) => {
|
|
124
|
+
reply.header('content-type', 'application/octet-stream')
|
|
125
|
+
reply.header('content-length', payload.length)
|
|
126
|
+
|
|
127
|
+
return payload
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
fastify.get('/small', async () => {
|
|
131
|
+
smallHit = true
|
|
132
|
+
return 'ok'
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
await fastify.listen({ port: 0, host: '127.0.0.1' })
|
|
136
|
+
|
|
137
|
+
const client = http2.connect(`http://127.0.0.1:${fastify.server.address().port}`)
|
|
138
|
+
t.after(() => {
|
|
139
|
+
client.close()
|
|
140
|
+
return fastify.close()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
let cancelTimer
|
|
145
|
+
let requestTimer
|
|
146
|
+
let timeout
|
|
147
|
+
|
|
148
|
+
function cleanup () {
|
|
149
|
+
clearTimeout(cancelTimer)
|
|
150
|
+
clearTimeout(requestTimer)
|
|
151
|
+
clearTimeout(timeout)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const large = client.request({
|
|
155
|
+
[http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
|
|
156
|
+
[http2.constants.HTTP2_HEADER_PATH]: '/large'
|
|
157
|
+
})
|
|
158
|
+
let largeResponded = false
|
|
159
|
+
|
|
160
|
+
large.on('error', err => {
|
|
161
|
+
if (!largeResponded) {
|
|
162
|
+
cleanup()
|
|
163
|
+
reject(err)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
large.on('response', () => {
|
|
167
|
+
largeResponded = true
|
|
168
|
+
large.pause()
|
|
169
|
+
cancelTimer = setTimeout(() => {
|
|
170
|
+
large.close(http2.constants.NGHTTP2_CANCEL)
|
|
171
|
+
}, 100)
|
|
172
|
+
requestTimer = setTimeout(() => {
|
|
173
|
+
const small = client.request({
|
|
174
|
+
[http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
|
|
175
|
+
[http2.constants.HTTP2_HEADER_PATH]: '/small'
|
|
176
|
+
})
|
|
177
|
+
let body = ''
|
|
178
|
+
|
|
179
|
+
timeout = setTimeout(() => {
|
|
180
|
+
cleanup()
|
|
181
|
+
reject(new Error('timed out waiting for /small response'))
|
|
182
|
+
}, 3000)
|
|
183
|
+
|
|
184
|
+
small.setEncoding('utf8')
|
|
185
|
+
small.on('error', err => {
|
|
186
|
+
cleanup()
|
|
187
|
+
reject(err)
|
|
188
|
+
})
|
|
189
|
+
small.on('data', chunk => {
|
|
190
|
+
body += chunk
|
|
191
|
+
})
|
|
192
|
+
small.on('end', () => {
|
|
193
|
+
cleanup()
|
|
194
|
+
t.assert.strictEqual(body, 'ok')
|
|
195
|
+
t.assert.strictEqual(smallHit, true)
|
|
196
|
+
resolve()
|
|
197
|
+
})
|
|
198
|
+
small.end()
|
|
199
|
+
}, 200)
|
|
200
|
+
})
|
|
201
|
+
large.end()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
@@ -6,7 +6,7 @@ const h2url = require('h2url')
|
|
|
6
6
|
const msg = { hello: 'world' }
|
|
7
7
|
|
|
8
8
|
const { buildCertificate } = require('../build-certificate')
|
|
9
|
-
const { Agent } = require('undici')
|
|
9
|
+
const { Agent, fetch } = require('undici')
|
|
10
10
|
test.before(buildCertificate)
|
|
11
11
|
|
|
12
12
|
test('secure with fallback', async (t) => {
|
package/test/https/https.test.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { test } = require('node:test')
|
|
4
|
-
const { request } = require('undici')
|
|
4
|
+
const { request, Agent, fetch } = require('undici')
|
|
5
5
|
const Fastify = require('../..')
|
|
6
6
|
|
|
7
7
|
const { buildCertificate } = require('../build-certificate')
|
|
8
|
-
const { Agent } = require('undici')
|
|
9
8
|
test.before(buildCertificate)
|
|
10
9
|
|
|
11
10
|
test('https', async (t) => {
|
|
@@ -5,7 +5,7 @@ const errors = require('../../lib/errors')
|
|
|
5
5
|
const { readFileSync } = require('node:fs')
|
|
6
6
|
const { resolve } = require('node:path')
|
|
7
7
|
|
|
8
|
-
const expectedErrors =
|
|
8
|
+
const expectedErrors = 92
|
|
9
9
|
|
|
10
10
|
test(`should expose ${expectedErrors} errors`, t => {
|
|
11
11
|
t.plan(1)
|
|
@@ -630,6 +630,16 @@ test('FST_ERR_BAD_URL', t => {
|
|
|
630
630
|
t.assert.ok(error instanceof Error)
|
|
631
631
|
})
|
|
632
632
|
|
|
633
|
+
test('FST_ERR_MAX_PARAM_LENGTH', t => {
|
|
634
|
+
t.plan(5)
|
|
635
|
+
const error = new errors.FST_ERR_MAX_PARAM_LENGTH()
|
|
636
|
+
t.assert.strictEqual(error.name, 'FastifyError')
|
|
637
|
+
t.assert.strictEqual(error.code, 'FST_ERR_MAX_PARAM_LENGTH')
|
|
638
|
+
t.assert.strictEqual(error.message, "'%s' is exceeding the max param length")
|
|
639
|
+
t.assert.strictEqual(error.statusCode, 414)
|
|
640
|
+
t.assert.ok(error instanceof Error)
|
|
641
|
+
})
|
|
642
|
+
|
|
633
643
|
test('FST_ERR_ASYNC_CONSTRAINT', t => {
|
|
634
644
|
t.plan(5)
|
|
635
645
|
const error = new errors.FST_ERR_ASYNC_CONSTRAINT()
|
|
@@ -710,6 +720,16 @@ test('FST_ERR_ROUTE_METHOD_NOT_SUPPORTED', t => {
|
|
|
710
720
|
t.assert.ok(error instanceof Error)
|
|
711
721
|
})
|
|
712
722
|
|
|
723
|
+
test('FST_ERR_ROUTE_LOG_LEVEL_INVALID', t => {
|
|
724
|
+
t.plan(5)
|
|
725
|
+
const error = new errors.FST_ERR_ROUTE_LOG_LEVEL_INVALID()
|
|
726
|
+
t.assert.strictEqual(error.name, 'FastifyError')
|
|
727
|
+
t.assert.strictEqual(error.code, 'FST_ERR_ROUTE_LOG_LEVEL_INVALID')
|
|
728
|
+
t.assert.strictEqual(error.message, "Log level for '%s:%s' route must be a valid logger level. Received: '%s'")
|
|
729
|
+
t.assert.strictEqual(error.statusCode, 500)
|
|
730
|
+
t.assert.ok(error instanceof TypeError)
|
|
731
|
+
})
|
|
732
|
+
|
|
713
733
|
test('FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED', t => {
|
|
714
734
|
t.plan(5)
|
|
715
735
|
const error = new errors.FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED()
|
|
@@ -810,6 +830,16 @@ test('FST_ERR_PLUGIN_INVALID_ASYNC_HANDLER', t => {
|
|
|
810
830
|
t.assert.ok(error instanceof TypeError)
|
|
811
831
|
})
|
|
812
832
|
|
|
833
|
+
test('FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED', t => {
|
|
834
|
+
t.plan(5)
|
|
835
|
+
const error = new errors.FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED('my-dep', 'my-plugin')
|
|
836
|
+
t.assert.strictEqual(error.name, 'FastifyError')
|
|
837
|
+
t.assert.strictEqual(error.code, 'FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED')
|
|
838
|
+
t.assert.strictEqual(error.message, "The dependency 'my-dep' of plugin 'my-plugin' is not registered")
|
|
839
|
+
t.assert.strictEqual(error.statusCode, 500)
|
|
840
|
+
t.assert.ok(error instanceof Error)
|
|
841
|
+
})
|
|
842
|
+
|
|
813
843
|
test('FST_ERR_PLUGIN_CALLBACK_NOT_FN', t => {
|
|
814
844
|
t.plan(5)
|
|
815
845
|
const error = new errors.FST_ERR_PLUGIN_CALLBACK_NOT_FN()
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { test } = require('node:test')
|
|
4
4
|
const Fastify = require('../..')
|
|
5
5
|
const loggerUtils = require('../../lib/logger-factory')
|
|
6
|
+
const { createLogController, LogController } = require('../../lib/logger-factory')
|
|
6
7
|
const { serializers } = require('../../lib/logger-pino')
|
|
7
8
|
|
|
8
9
|
test('time resolution', t => {
|
|
@@ -142,6 +143,327 @@ test('The logger should error if both stream and file destination are given', t
|
|
|
142
143
|
}
|
|
143
144
|
})
|
|
144
145
|
|
|
146
|
+
test('LogController defaults', t => {
|
|
147
|
+
t.plan(3)
|
|
148
|
+
const dispatcher = new LogController()
|
|
149
|
+
t.assert.strictEqual(dispatcher.disableRequestLogging, false)
|
|
150
|
+
t.assert.strictEqual(dispatcher.requestIdLogLabel, 'reqId')
|
|
151
|
+
t.assert.strictEqual(dispatcher.isLogDisabled({}), false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('LogController with custom options', t => {
|
|
155
|
+
t.plan(2)
|
|
156
|
+
const dispatcher = new LogController({
|
|
157
|
+
disableRequestLogging: true,
|
|
158
|
+
requestIdLogLabel: 'traceId'
|
|
159
|
+
})
|
|
160
|
+
t.assert.strictEqual(dispatcher.disableRequestLogging, true)
|
|
161
|
+
t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('LogController with function disableRequestLogging', t => {
|
|
165
|
+
t.plan(2)
|
|
166
|
+
const dispatcher = new LogController({
|
|
167
|
+
disableRequestLogging: (req) => req.skip === true
|
|
168
|
+
})
|
|
169
|
+
t.assert.strictEqual(dispatcher.isLogDisabled({ skip: true }), true)
|
|
170
|
+
t.assert.strictEqual(dispatcher.isLogDisabled({ skip: false }), false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('requestCompleted should not log when logging is disabled (boolean)', t => {
|
|
174
|
+
t.plan(1)
|
|
175
|
+
const logController = new LogController({ disableRequestLogging: true })
|
|
176
|
+
const log = {
|
|
177
|
+
error: () => { t.assert.fail('error should not be called') },
|
|
178
|
+
info: () => { t.assert.fail('info should not be called') }
|
|
179
|
+
}
|
|
180
|
+
const request = {}
|
|
181
|
+
const reply = { request, log }
|
|
182
|
+
logController.requestCompleted(new Error('test'), request, reply)
|
|
183
|
+
t.assert.ok(true, 'logger was not called')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('requestCompleted should not log when logging is disabled (function)', t => {
|
|
187
|
+
t.plan(1)
|
|
188
|
+
const logController = new LogController({ disableRequestLogging: () => true })
|
|
189
|
+
const log = {
|
|
190
|
+
error: () => { t.assert.fail('error should not be called') },
|
|
191
|
+
info: () => { t.assert.fail('info should not be called') }
|
|
192
|
+
}
|
|
193
|
+
const request = {}
|
|
194
|
+
const reply = { request, log }
|
|
195
|
+
logController.requestCompleted(new Error('test'), request, reply)
|
|
196
|
+
t.assert.ok(true, 'logger was not called')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('requestCompleted should log error when err is present', t => {
|
|
200
|
+
t.plan(1)
|
|
201
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
202
|
+
const err = new Error('test')
|
|
203
|
+
const log = {
|
|
204
|
+
error: (data, msg) => {
|
|
205
|
+
t.assert.strictEqual(msg, 'request errored')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const request = {}
|
|
209
|
+
const reply = { request, log, elapsedTime: 42 }
|
|
210
|
+
logController.requestCompleted(err, request, reply)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('requestCompleted should log info when no error', t => {
|
|
214
|
+
t.plan(1)
|
|
215
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
216
|
+
const log = {
|
|
217
|
+
info: (data, msg) => {
|
|
218
|
+
t.assert.strictEqual(msg, 'request completed')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const request = {}
|
|
222
|
+
const reply = { request, log, elapsedTime: 42 }
|
|
223
|
+
logController.requestCompleted(null, request, reply)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('defaultErrorLog should not log when logging is disabled (boolean)', t => {
|
|
227
|
+
t.plan(1)
|
|
228
|
+
const logController = new LogController({ disableRequestLogging: true })
|
|
229
|
+
const log = {
|
|
230
|
+
info: () => { t.assert.fail('info should not be called') },
|
|
231
|
+
error: () => { t.assert.fail('error should not be called') }
|
|
232
|
+
}
|
|
233
|
+
const request = {}
|
|
234
|
+
const reply = { request, log, statusCode: 404 }
|
|
235
|
+
logController.defaultErrorLog(new Error('not found'), request, reply)
|
|
236
|
+
t.assert.ok(true, 'logger was not called')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('defaultErrorLog should not log when logging is disabled (function)', t => {
|
|
240
|
+
t.plan(1)
|
|
241
|
+
const logController = new LogController({ disableRequestLogging: () => true })
|
|
242
|
+
const log = {
|
|
243
|
+
info: () => { t.assert.fail('info should not be called') },
|
|
244
|
+
error: () => { t.assert.fail('error should not be called') }
|
|
245
|
+
}
|
|
246
|
+
const request = {}
|
|
247
|
+
const reply = { request, log, statusCode: 404 }
|
|
248
|
+
logController.defaultErrorLog(new Error('not found'), request, reply)
|
|
249
|
+
t.assert.ok(true, 'logger was not called')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('defaultErrorLog should log info for 4xx errors', t => {
|
|
253
|
+
t.plan(1)
|
|
254
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
255
|
+
const err = new Error('not found')
|
|
256
|
+
const log = {
|
|
257
|
+
info: (data, msg) => {
|
|
258
|
+
t.assert.strictEqual(msg, 'not found')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const request = {}
|
|
262
|
+
const reply = { request, log, statusCode: 404 }
|
|
263
|
+
logController.defaultErrorLog(err, request, reply)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('defaultErrorLog should log error for 5xx errors', t => {
|
|
267
|
+
t.plan(1)
|
|
268
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
269
|
+
const err = new Error('internal error')
|
|
270
|
+
const log = {
|
|
271
|
+
error: (data, msg) => {
|
|
272
|
+
t.assert.strictEqual(msg, 'internal error')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const request = {}
|
|
276
|
+
const reply = { request, log, statusCode: 500 }
|
|
277
|
+
logController.defaultErrorLog(err, request, reply)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('writeHeadError should not log when logging is disabled (boolean)', t => {
|
|
281
|
+
t.plan(1)
|
|
282
|
+
const logController = new LogController({ disableRequestLogging: true })
|
|
283
|
+
const log = {
|
|
284
|
+
warn: () => { t.assert.fail('warn should not be called') }
|
|
285
|
+
}
|
|
286
|
+
const request = {}
|
|
287
|
+
const reply = { request, log }
|
|
288
|
+
logController.writeHeadError(new Error('write head failed'), request, reply)
|
|
289
|
+
t.assert.ok(true, 'logger was not called')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('writeHeadError should not log when logging is disabled (function)', t => {
|
|
293
|
+
t.plan(1)
|
|
294
|
+
const logController = new LogController({ disableRequestLogging: () => true })
|
|
295
|
+
const log = {
|
|
296
|
+
warn: () => { t.assert.fail('warn should not be called') }
|
|
297
|
+
}
|
|
298
|
+
const request = {}
|
|
299
|
+
const reply = { request, log }
|
|
300
|
+
logController.writeHeadError(new Error('write head failed'), request, reply)
|
|
301
|
+
t.assert.ok(true, 'logger was not called')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('writeHeadError should log warn with error message', t => {
|
|
305
|
+
t.plan(2)
|
|
306
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
307
|
+
const error = new Error('write head failed')
|
|
308
|
+
const request = {}
|
|
309
|
+
const reply = {
|
|
310
|
+
request,
|
|
311
|
+
log: {
|
|
312
|
+
warn: (data, msg) => {
|
|
313
|
+
t.assert.strictEqual(msg, 'write head failed')
|
|
314
|
+
t.assert.strictEqual(data.err, error)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
logController.writeHeadError(error, request, reply)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('serializerError should not log when logging is disabled (boolean)', t => {
|
|
322
|
+
t.plan(1)
|
|
323
|
+
const logController = new LogController({ disableRequestLogging: true })
|
|
324
|
+
const log = {
|
|
325
|
+
error: () => { t.assert.fail('error should not be called') }
|
|
326
|
+
}
|
|
327
|
+
const request = {}
|
|
328
|
+
const reply = { request, log }
|
|
329
|
+
logController.serializerError(new Error('serializer failed'), request, reply, { statusCode: 500 })
|
|
330
|
+
t.assert.ok(true, 'logger was not called')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test('serializerError should not log when logging is disabled (function)', t => {
|
|
334
|
+
t.plan(1)
|
|
335
|
+
const logController = new LogController({ disableRequestLogging: () => true })
|
|
336
|
+
const log = {
|
|
337
|
+
error: () => { t.assert.fail('error should not be called') }
|
|
338
|
+
}
|
|
339
|
+
const request = {}
|
|
340
|
+
const reply = { request, log }
|
|
341
|
+
logController.serializerError(new Error('serializer failed'), request, reply, { statusCode: 500 })
|
|
342
|
+
t.assert.ok(true, 'logger was not called')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('serializerError should log error with status code', t => {
|
|
346
|
+
t.plan(2)
|
|
347
|
+
const logController = new LogController({ disableRequestLogging: false })
|
|
348
|
+
const err = new Error('serializer failed')
|
|
349
|
+
const request = {}
|
|
350
|
+
const reply = {
|
|
351
|
+
request,
|
|
352
|
+
log: {
|
|
353
|
+
error: (data, msg) => {
|
|
354
|
+
t.assert.strictEqual(msg, 'The serializer for the given status code failed')
|
|
355
|
+
t.assert.strictEqual(data.statusCode, 500)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
logController.serializerError(err, request, reply, { statusCode: 500 })
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('createLogController should use LogController instance directly', t => {
|
|
363
|
+
t.plan(2)
|
|
364
|
+
const custom = new LogController({ disableRequestLogging: true, requestIdLogLabel: 'traceId' })
|
|
365
|
+
const dispatcher = createLogController({ logController: custom })
|
|
366
|
+
t.assert.strictEqual(dispatcher, custom)
|
|
367
|
+
t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('createLogController should create default when no instance provided', t => {
|
|
371
|
+
t.plan(2)
|
|
372
|
+
const dispatcher = createLogController({ disableRequestLogging: false, requestIdLogLabel: 'reqId' })
|
|
373
|
+
t.assert.ok(dispatcher instanceof LogController)
|
|
374
|
+
t.assert.strictEqual(dispatcher.requestIdLogLabel, 'reqId')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
test('createLogController should throw on invalid type', t => {
|
|
378
|
+
t.plan(1)
|
|
379
|
+
t.assert.throws(() => {
|
|
380
|
+
createLogController({ disableRequestLogging: false, logController: 'bad' })
|
|
381
|
+
}, { code: 'FST_ERR_LOG_INVALID_LOG_CONTROLLER' })
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
test('createLogController should throw when plain object is provided', t => {
|
|
385
|
+
t.plan(1)
|
|
386
|
+
t.assert.throws(() => {
|
|
387
|
+
createLogController({ disableRequestLogging: false, logController: { incomingRequest: () => { } } })
|
|
388
|
+
}, { code: 'FST_ERR_LOG_INVALID_LOG_CONTROLLER' })
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('createLogController should accept null', t => {
|
|
392
|
+
t.plan(1)
|
|
393
|
+
const logController = createLogController({ disableRequestLogging: false, logController: null })
|
|
394
|
+
t.assert.ok(logController)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('LogController subclass should work', t => {
|
|
398
|
+
t.plan(2)
|
|
399
|
+
|
|
400
|
+
class MyDispatcher extends LogController {
|
|
401
|
+
constructor () {
|
|
402
|
+
super({ requestIdLogLabel: 'traceId' })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
incomingRequest (request, reply, metadata) {
|
|
406
|
+
t.assert.ok(true, 'custom incomingRequest called')
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const dispatcher = new MyDispatcher()
|
|
411
|
+
t.assert.strictEqual(dispatcher.requestIdLogLabel, 'traceId')
|
|
412
|
+
dispatcher.incomingRequest({})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
test('LogController subclass keeps defaults for non-overridden methods', t => {
|
|
416
|
+
t.plan(1)
|
|
417
|
+
|
|
418
|
+
class MyDispatcher extends LogController {
|
|
419
|
+
incomingRequest () { }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const dispatcher = new MyDispatcher()
|
|
423
|
+
const log = {
|
|
424
|
+
info: (data, msg) => {
|
|
425
|
+
t.assert.strictEqual(msg, 'request completed')
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const request = {}
|
|
429
|
+
const reply = { request, log, elapsedTime: 42 }
|
|
430
|
+
dispatcher.requestCompleted(null, request, reply)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('serviceUnavailable should log with logger and receive server', t => {
|
|
434
|
+
t.plan(2)
|
|
435
|
+
const dispatcher = new LogController()
|
|
436
|
+
const fakeLogger = {
|
|
437
|
+
info: (data, msg) => {
|
|
438
|
+
t.assert.deepStrictEqual(data, { res: { statusCode: 503 } })
|
|
439
|
+
t.assert.strictEqual(msg, 'request aborted - refusing to accept new requests as server is closing')
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const fakeServer = { name: 'test-server' }
|
|
443
|
+
dispatcher.serviceUnavailable(fakeLogger, fakeServer)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('serviceUnavailable subclass can use server', t => {
|
|
447
|
+
t.plan(1)
|
|
448
|
+
|
|
449
|
+
const fakeServer = { name: 'test-server' }
|
|
450
|
+
|
|
451
|
+
class MyDispatcher extends LogController {
|
|
452
|
+
serviceUnavailable (logger, server) {
|
|
453
|
+
t.assert.strictEqual(server, fakeServer)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const dispatcher = new MyDispatcher()
|
|
458
|
+
dispatcher.serviceUnavailable({}, fakeServer)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
test('LogController is exported from fastify', t => {
|
|
462
|
+
t.plan(2)
|
|
463
|
+
t.assert.ok(Fastify.LogController)
|
|
464
|
+
t.assert.strictEqual(Fastify.LogController, LogController)
|
|
465
|
+
})
|
|
466
|
+
|
|
145
467
|
test('The serializer prevent fails if the request socket is undefined', t => {
|
|
146
468
|
t.plan(1)
|
|
147
469
|
|
|
@@ -149,7 +149,7 @@ test('checkDependencies should check if the given dependency is present in the i
|
|
|
149
149
|
})
|
|
150
150
|
|
|
151
151
|
test('checkDependencies should check if the given dependency is present in the instance (errored)', t => {
|
|
152
|
-
t.plan(
|
|
152
|
+
t.plan(3)
|
|
153
153
|
|
|
154
154
|
fn[Symbol.for('plugin-meta')] = {
|
|
155
155
|
name: 'test-plugin',
|
|
@@ -164,6 +164,8 @@ test('checkDependencies should check if the given dependency is present in the i
|
|
|
164
164
|
t.assert.fail('should throw')
|
|
165
165
|
} catch (err) {
|
|
166
166
|
t.assert.strictEqual(err.message, "The dependency 'plugin' of plugin 'test-plugin' is not registered")
|
|
167
|
+
t.assert.strictEqual(err.code, 'FST_ERR_PLUGIN_DEPENDENCY_NOT_REGISTERED')
|
|
168
|
+
t.assert.strictEqual(err.name, 'FastifyError')
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
function fn () {}
|
|
@@ -5,6 +5,7 @@ const http = require('node:http')
|
|
|
5
5
|
const NotFound = require('http-errors').NotFound
|
|
6
6
|
const Request = require('../../lib/request')
|
|
7
7
|
const Reply = require('../../lib/reply')
|
|
8
|
+
const { LogController } = require('../../lib/log-controller')
|
|
8
9
|
const Fastify = require('../..')
|
|
9
10
|
const { Readable, Writable } = require('node:stream')
|
|
10
11
|
const {
|
|
@@ -13,7 +14,8 @@ const {
|
|
|
13
14
|
kReplySerializer,
|
|
14
15
|
kReplyIsError,
|
|
15
16
|
kReplySerializerDefault,
|
|
16
|
-
kRouteContext
|
|
17
|
+
kRouteContext,
|
|
18
|
+
kLogController
|
|
17
19
|
} = require('../../lib/symbols')
|
|
18
20
|
const fs = require('node:fs')
|
|
19
21
|
const path = require('node:path')
|
|
@@ -85,7 +87,14 @@ test('reply.send will logStream error and destroy the stream', t => {
|
|
|
85
87
|
warn: () => { }
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const
|
|
90
|
+
const fakeRequest = {
|
|
91
|
+
[kRouteContext]: {
|
|
92
|
+
onSend: null,
|
|
93
|
+
server: { [kLogController]: new LogController() }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const reply = new Reply(response, fakeRequest, log)
|
|
89
98
|
reply.send(payload)
|
|
90
99
|
payload.destroy(new Error('stream error'))
|
|
91
100
|
|
|
@@ -764,7 +773,7 @@ test('non-string with custom json\'s content-type SHOULD be serialized as json',
|
|
|
764
773
|
|
|
765
774
|
const result = await fetch(fastifyServer)
|
|
766
775
|
t.assert.ok(result.ok)
|
|
767
|
-
t.assert.strictEqual(result.headers.get('content-type'), 'application/json; version=2; charset=utf-8')
|
|
776
|
+
t.assert.strictEqual(result.headers.get('content-type'), 'application/json; version="2"; charset=utf-8')
|
|
768
777
|
t.assert.deepStrictEqual(await result.text(), JSON.stringify({ key: 'hello world!' }))
|
|
769
778
|
})
|
|
770
779
|
|
|
@@ -1149,7 +1158,7 @@ test('reply.hasHeader computes raw and fastify headers', async t => {
|
|
|
1149
1158
|
})
|
|
1150
1159
|
|
|
1151
1160
|
test('Reply should handle JSON content type with a charset', async t => {
|
|
1152
|
-
t.plan(
|
|
1161
|
+
t.plan(10)
|
|
1153
1162
|
|
|
1154
1163
|
const fastify = require('../../')()
|
|
1155
1164
|
|
|
@@ -1199,6 +1208,18 @@ test('Reply should handle JSON content type with a charset', async t => {
|
|
|
1199
1208
|
.send({ hello: 'world' })
|
|
1200
1209
|
})
|
|
1201
1210
|
|
|
1211
|
+
fastify.get('/upper-charset', function (req, reply) {
|
|
1212
|
+
reply
|
|
1213
|
+
.header('content-type', 'application/json; CHARSET=utf-8')
|
|
1214
|
+
.send({ hello: 'world' })
|
|
1215
|
+
})
|
|
1216
|
+
|
|
1217
|
+
fastify.get('/random-case', function (req, reply) {
|
|
1218
|
+
reply
|
|
1219
|
+
.header('content-type', 'ApPlIcAtIoN/JsOn; ChArSeT=utf-8')
|
|
1220
|
+
.send({ hello: 'world' })
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1202
1223
|
{
|
|
1203
1224
|
const res = await fastify.inject('/default')
|
|
1204
1225
|
t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
|
|
@@ -1233,6 +1254,16 @@ test('Reply should handle JSON content type with a charset', async t => {
|
|
|
1233
1254
|
t.assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-32')
|
|
1234
1255
|
}
|
|
1235
1256
|
|
|
1257
|
+
{
|
|
1258
|
+
const res = await fastify.inject('/upper-charset')
|
|
1259
|
+
t.assert.strictEqual(res.headers['content-type'], 'application/json; CHARSET=utf-8')
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
{
|
|
1263
|
+
const res = await fastify.inject('/random-case')
|
|
1264
|
+
t.assert.strictEqual(res.headers['content-type'], 'ApPlIcAtIoN/JsOn; ChArSeT=utf-8')
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1236
1267
|
{
|
|
1237
1268
|
const res = await fastify.inject('/no-space-type-utf32')
|
|
1238
1269
|
t.assert.strictEqual(res.headers['content-type'], 'application/json;charset=utf-32')
|