fastify 5.7.4 → 5.8.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/LICENSE +1 -1
- package/README.md +1 -1
- package/SECURITY.md +20 -20
- package/build/build-validation.js +2 -0
- package/docs/Guides/Ecosystem.md +7 -59
- package/docs/Guides/Fluent-Schema.md +2 -1
- package/docs/Guides/Migration-Guide-V4.md +9 -8
- package/docs/Guides/Migration-Guide-V5.md +1 -1
- package/docs/Guides/Serverless.md +1 -1
- package/docs/Reference/ContentTypeParser.md +2 -1
- package/docs/Reference/Decorators.md +4 -2
- package/docs/Reference/Errors.md +5 -0
- package/docs/Reference/Hooks.md +85 -23
- package/docs/Reference/LTS.md +2 -2
- package/docs/Reference/Lifecycle.md +16 -1
- package/docs/Reference/Logging.md +11 -7
- package/docs/Reference/Middleware.md +3 -2
- package/docs/Reference/Reply.md +15 -8
- package/docs/Reference/Request.md +17 -1
- package/docs/Reference/Routes.md +15 -6
- package/docs/Reference/Server.md +135 -14
- package/docs/Reference/Type-Providers.md +5 -5
- package/docs/Reference/TypeScript.md +14 -10
- package/docs/Reference/Validation-and-Serialization.md +28 -1
- package/fastify.d.ts +1 -0
- package/fastify.js +4 -2
- package/lib/config-validator.js +324 -296
- package/lib/content-type-parser.js +1 -1
- package/lib/content-type.js +9 -3
- package/lib/context.js +5 -2
- package/lib/errors.js +12 -1
- package/lib/reply.js +23 -1
- package/lib/request.js +18 -1
- package/lib/route.js +45 -4
- package/lib/symbols.js +4 -0
- package/package.json +5 -5
- package/scripts/validate-ecosystem-links.js +179 -0
- package/test/content-parser.test.js +25 -1
- package/test/content-type.test.js +38 -0
- package/test/handler-timeout.test.js +367 -0
- package/test/internals/errors.test.js +2 -2
- package/test/internals/initial-config.test.js +2 -0
- package/test/request-error.test.js +41 -0
- package/test/router-options.test.js +42 -0
- package/test/scripts/validate-ecosystem-links.test.js +339 -0
- package/test/types/dummy-plugin.ts +2 -2
- package/test/types/fastify.test-d.ts +1 -0
- package/test/types/logger.test-d.ts +17 -18
- package/test/types/register.test-d.ts +2 -2
- package/test/types/reply.test-d.ts +2 -2
- package/test/types/request.test-d.ts +4 -3
- package/test/types/route.test-d.ts +6 -0
- package/test/types/type-provider.test-d.ts +1 -1
- package/test/web-api.test.js +75 -0
- package/types/errors.d.ts +2 -0
- package/types/logger.d.ts +1 -1
- package/types/request.d.ts +2 -0
- package/types/route.d.ts +35 -21
- package/types/tsconfig.eslint.json +0 -13
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert')
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const { MockAgent, setGlobalDispatcher, getGlobalDispatcher } = require('undici')
|
|
7
|
+
|
|
8
|
+
function loadValidateEcosystemLinksModule () {
|
|
9
|
+
const modulePath = require.resolve('../../scripts/validate-ecosystem-links')
|
|
10
|
+
delete require.cache[modulePath]
|
|
11
|
+
return require(modulePath)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('extractGitHubLinks', () => {
|
|
15
|
+
const { extractGitHubLinks } = loadValidateEcosystemLinksModule()
|
|
16
|
+
|
|
17
|
+
it('extracts simple GitHub repository links', () => {
|
|
18
|
+
const content = `
|
|
19
|
+
# Ecosystem
|
|
20
|
+
|
|
21
|
+
- [fastify-helmet](https://github.com/fastify/fastify-helmet) - Important security headers for Fastify
|
|
22
|
+
- [fastify-cors](https://github.com/fastify/fastify-cors) - CORS support
|
|
23
|
+
`
|
|
24
|
+
const links = extractGitHubLinks(content)
|
|
25
|
+
|
|
26
|
+
assert.strictEqual(links.length, 2)
|
|
27
|
+
assert.deepStrictEqual(links[0], {
|
|
28
|
+
name: 'fastify-helmet',
|
|
29
|
+
url: 'https://github.com/fastify/fastify-helmet',
|
|
30
|
+
owner: 'fastify',
|
|
31
|
+
repo: 'fastify-helmet'
|
|
32
|
+
})
|
|
33
|
+
assert.deepStrictEqual(links[1], {
|
|
34
|
+
name: 'fastify-cors',
|
|
35
|
+
url: 'https://github.com/fastify/fastify-cors',
|
|
36
|
+
owner: 'fastify',
|
|
37
|
+
repo: 'fastify-cors'
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('extracts links with different owner/repo combinations', () => {
|
|
42
|
+
const content = `
|
|
43
|
+
- [some-plugin](https://github.com/user123/awesome-plugin)
|
|
44
|
+
- [another-lib](https://github.com/org-name/lib-name)
|
|
45
|
+
`
|
|
46
|
+
const links = extractGitHubLinks(content)
|
|
47
|
+
|
|
48
|
+
assert.strictEqual(links.length, 2)
|
|
49
|
+
assert.strictEqual(links[0].owner, 'user123')
|
|
50
|
+
assert.strictEqual(links[0].repo, 'awesome-plugin')
|
|
51
|
+
assert.strictEqual(links[1].owner, 'org-name')
|
|
52
|
+
assert.strictEqual(links[1].repo, 'lib-name')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('handles links with hash fragments', () => {
|
|
56
|
+
const content = `
|
|
57
|
+
- [project](https://github.com/owner/repo#readme)
|
|
58
|
+
`
|
|
59
|
+
const links = extractGitHubLinks(content)
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(links.length, 1)
|
|
62
|
+
assert.strictEqual(links[0].repo, 'repo')
|
|
63
|
+
assert.strictEqual(links[0].url, 'https://github.com/owner/repo#readme')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('handles links with query parameters', () => {
|
|
67
|
+
const content = `
|
|
68
|
+
- [project](https://github.com/owner/repo?tab=readme)
|
|
69
|
+
`
|
|
70
|
+
const links = extractGitHubLinks(content)
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(links.length, 1)
|
|
73
|
+
assert.strictEqual(links[0].repo, 'repo')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('handles links with subpaths', () => {
|
|
77
|
+
const content = `
|
|
78
|
+
- [docs](https://github.com/owner/repo/tree/main/docs)
|
|
79
|
+
`
|
|
80
|
+
const links = extractGitHubLinks(content)
|
|
81
|
+
|
|
82
|
+
assert.strictEqual(links.length, 1)
|
|
83
|
+
assert.strictEqual(links[0].owner, 'owner')
|
|
84
|
+
assert.strictEqual(links[0].repo, 'repo')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('returns empty array for content with no GitHub links', () => {
|
|
88
|
+
const content = `
|
|
89
|
+
# No GitHub links here
|
|
90
|
+
|
|
91
|
+
Just some regular text and [a link](https://example.com).
|
|
92
|
+
`
|
|
93
|
+
const links = extractGitHubLinks(content)
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(links.length, 0)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('ignores non-GitHub links', () => {
|
|
99
|
+
const content = `
|
|
100
|
+
- [gitlab](https://gitlab.com/owner/repo)
|
|
101
|
+
- [github](https://github.com/owner/repo)
|
|
102
|
+
- [bitbucket](https://bitbucket.org/owner/repo)
|
|
103
|
+
`
|
|
104
|
+
const links = extractGitHubLinks(content)
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(links.length, 1)
|
|
107
|
+
assert.strictEqual(links[0].owner, 'owner')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('extracts multiple links from complex markdown', () => {
|
|
111
|
+
const content = `
|
|
112
|
+
## Category 1
|
|
113
|
+
|
|
114
|
+
Some description [inline link](https://github.com/a/b).
|
|
115
|
+
|
|
116
|
+
| Plugin | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| [plugin1](https://github.com/x/y) | Desc 1 |
|
|
119
|
+
| [plugin2](https://github.com/z/w) | Desc 2 |
|
|
120
|
+
`
|
|
121
|
+
const links = extractGitHubLinks(content)
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(links.length, 3)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('checkGitHubRepo', () => {
|
|
128
|
+
let originalDispatcher
|
|
129
|
+
let mockAgent
|
|
130
|
+
let originalFetch
|
|
131
|
+
let originalSetTimeout
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
delete process.env.GITHUB_TOKEN
|
|
135
|
+
originalDispatcher = getGlobalDispatcher()
|
|
136
|
+
mockAgent = new MockAgent()
|
|
137
|
+
mockAgent.disableNetConnect()
|
|
138
|
+
setGlobalDispatcher(mockAgent)
|
|
139
|
+
originalFetch = global.fetch
|
|
140
|
+
originalSetTimeout = global.setTimeout
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
afterEach(async () => {
|
|
144
|
+
global.fetch = originalFetch
|
|
145
|
+
global.setTimeout = originalSetTimeout
|
|
146
|
+
setGlobalDispatcher(originalDispatcher)
|
|
147
|
+
await mockAgent.close()
|
|
148
|
+
delete process.env.GITHUB_TOKEN
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('returns exists: true for status 200', async () => {
|
|
152
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
153
|
+
const mockPool = mockAgent.get('https://api.github.com')
|
|
154
|
+
mockPool.intercept({
|
|
155
|
+
path: '/repos/fastify/fastify',
|
|
156
|
+
method: 'HEAD'
|
|
157
|
+
}).reply(200)
|
|
158
|
+
|
|
159
|
+
const result = await checkGitHubRepo('fastify', 'fastify')
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(result.exists, true)
|
|
162
|
+
assert.strictEqual(result.status, 200)
|
|
163
|
+
assert.strictEqual(result.owner, 'fastify')
|
|
164
|
+
assert.strictEqual(result.repo, 'fastify')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('returns exists: false for status 404', async () => {
|
|
168
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
169
|
+
const mockPool = mockAgent.get('https://api.github.com')
|
|
170
|
+
mockPool.intercept({
|
|
171
|
+
path: '/repos/nonexistent/repo',
|
|
172
|
+
method: 'HEAD'
|
|
173
|
+
}).reply(404)
|
|
174
|
+
|
|
175
|
+
const result = await checkGitHubRepo('nonexistent', 'repo')
|
|
176
|
+
|
|
177
|
+
assert.strictEqual(result.exists, false)
|
|
178
|
+
assert.strictEqual(result.status, 404)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('returns invalid status for malformed owner or repository names', async () => {
|
|
182
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
183
|
+
let called = false
|
|
184
|
+
|
|
185
|
+
global.fetch = async () => {
|
|
186
|
+
called = true
|
|
187
|
+
return { status: 200 }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await checkGitHubRepo('owner/evil', 'repo', 1)
|
|
191
|
+
|
|
192
|
+
assert.strictEqual(called, false)
|
|
193
|
+
assert.strictEqual(result.exists, false)
|
|
194
|
+
assert.strictEqual(result.status, 'invalid')
|
|
195
|
+
assert.strictEqual(result.error, 'Invalid GitHub repository identifier')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('retries on rate limit responses', async () => {
|
|
199
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
200
|
+
let attempts = 0
|
|
201
|
+
|
|
202
|
+
global.setTimeout = (fn) => {
|
|
203
|
+
fn()
|
|
204
|
+
return 0
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
global.fetch = async () => {
|
|
208
|
+
attempts++
|
|
209
|
+
return {
|
|
210
|
+
status: attempts === 1 ? 403 : 200
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = await checkGitHubRepo('owner', 'repo', 1)
|
|
215
|
+
|
|
216
|
+
assert.strictEqual(attempts, 2)
|
|
217
|
+
assert.strictEqual(result.exists, true)
|
|
218
|
+
assert.strictEqual(result.status, 200)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('adds authorization header when GITHUB_TOKEN is set', async () => {
|
|
222
|
+
process.env.GITHUB_TOKEN = 'my-token'
|
|
223
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
224
|
+
let authorization
|
|
225
|
+
|
|
226
|
+
global.fetch = async (url, options) => {
|
|
227
|
+
authorization = options.headers.Authorization
|
|
228
|
+
return {
|
|
229
|
+
status: 200
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const result = await checkGitHubRepo('owner', 'repo')
|
|
234
|
+
|
|
235
|
+
assert.strictEqual(authorization, 'token my-token')
|
|
236
|
+
assert.strictEqual(result.exists, true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('handles network errors', async () => {
|
|
240
|
+
const { checkGitHubRepo } = loadValidateEcosystemLinksModule()
|
|
241
|
+
const mockPool = mockAgent.get('https://api.github.com')
|
|
242
|
+
mockPool.intercept({
|
|
243
|
+
path: '/repos/owner/repo',
|
|
244
|
+
method: 'HEAD'
|
|
245
|
+
}).replyWithError(new Error('Network error'))
|
|
246
|
+
|
|
247
|
+
const result = await checkGitHubRepo('owner', 'repo')
|
|
248
|
+
|
|
249
|
+
assert.strictEqual(result.exists, false)
|
|
250
|
+
assert.strictEqual(result.status, 'error')
|
|
251
|
+
assert.ok(result.error.length > 0)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('validateAllLinks', () => {
|
|
256
|
+
let originalReadFileSync
|
|
257
|
+
let originalFetch
|
|
258
|
+
let originalSetTimeout
|
|
259
|
+
let originalConsoleLog
|
|
260
|
+
let originalStdoutWrite
|
|
261
|
+
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
originalReadFileSync = fs.readFileSync
|
|
264
|
+
originalFetch = global.fetch
|
|
265
|
+
originalSetTimeout = global.setTimeout
|
|
266
|
+
originalConsoleLog = console.log
|
|
267
|
+
originalStdoutWrite = process.stdout.write
|
|
268
|
+
|
|
269
|
+
console.log = () => {}
|
|
270
|
+
process.stdout.write = () => true
|
|
271
|
+
|
|
272
|
+
global.setTimeout = (fn) => {
|
|
273
|
+
fn()
|
|
274
|
+
return 0
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
afterEach(() => {
|
|
279
|
+
fs.readFileSync = originalReadFileSync
|
|
280
|
+
global.fetch = originalFetch
|
|
281
|
+
global.setTimeout = originalSetTimeout
|
|
282
|
+
console.log = originalConsoleLog
|
|
283
|
+
process.stdout.write = originalStdoutWrite
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('validates links, deduplicates repositories and groups inaccessible links', async () => {
|
|
287
|
+
const { validateAllLinks } = loadValidateEcosystemLinksModule()
|
|
288
|
+
|
|
289
|
+
fs.readFileSync = () => `
|
|
290
|
+
- [repo one](https://github.com/owner/repo)
|
|
291
|
+
- [repo one duplicate](https://github.com/owner/repo)
|
|
292
|
+
- [repo two](https://github.com/another/project)
|
|
293
|
+
`
|
|
294
|
+
|
|
295
|
+
let requests = 0
|
|
296
|
+
global.fetch = async (url) => {
|
|
297
|
+
requests++
|
|
298
|
+
const pathname = new URL(url).pathname
|
|
299
|
+
|
|
300
|
+
if (pathname === '/repos/owner/repo') {
|
|
301
|
+
return { status: 404 }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (pathname === '/repos/another/project') {
|
|
305
|
+
return { status: 200 }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw new Error(`Unexpected url: ${url}`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await validateAllLinks()
|
|
312
|
+
|
|
313
|
+
assert.strictEqual(requests, 2)
|
|
314
|
+
assert.strictEqual(result.notFound.length, 1)
|
|
315
|
+
assert.strictEqual(result.found.length, 1)
|
|
316
|
+
assert.strictEqual(result.notFound[0].owner, 'owner')
|
|
317
|
+
assert.strictEqual(result.notFound[0].repo, 'repo')
|
|
318
|
+
assert.strictEqual(result.found[0].owner, 'another')
|
|
319
|
+
assert.strictEqual(result.found[0].repo, 'project')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('returns empty result when no GitHub links are present', async () => {
|
|
323
|
+
const { validateAllLinks } = loadValidateEcosystemLinksModule()
|
|
324
|
+
|
|
325
|
+
fs.readFileSync = () => '# Ecosystem\nNo links here.'
|
|
326
|
+
|
|
327
|
+
let requests = 0
|
|
328
|
+
global.fetch = async () => {
|
|
329
|
+
requests++
|
|
330
|
+
return { status: 200 }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const result = await validateAllLinks()
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(requests, 0)
|
|
336
|
+
assert.strictEqual(result.notFound.length, 0)
|
|
337
|
+
assert.strictEqual(result.found.length, 0)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FastifyPluginAsync } from '../../fastify'
|
|
2
2
|
|
|
3
3
|
export interface DummyPluginOptions {
|
|
4
4
|
foo?: number
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
declare const DummyPlugin:
|
|
7
|
+
declare const DummyPlugin: FastifyPluginAsync<DummyPluginOptions>
|
|
8
8
|
|
|
9
9
|
export default DummyPlugin
|
|
@@ -113,6 +113,7 @@ expectAssignable<FastifyInstance>(fastify({ forceCloseConnections: true }))
|
|
|
113
113
|
expectAssignable<FastifyInstance>(fastify({ keepAliveTimeout: 1000 }))
|
|
114
114
|
expectAssignable<FastifyInstance>(fastify({ pluginTimeout: 1000 }))
|
|
115
115
|
expectAssignable<FastifyInstance>(fastify({ bodyLimit: 100 }))
|
|
116
|
+
expectAssignable<FastifyInstance>(fastify({ handlerTimeout: 5000 }))
|
|
116
117
|
expectAssignable<FastifyInstance>(fastify({ maxParamLength: 100 }))
|
|
117
118
|
expectAssignable<FastifyInstance>(fastify({ disableRequestLogging: true }))
|
|
118
119
|
expectAssignable<FastifyInstance>(fastify({ disableRequestLogging: (req) => req.url?.includes('/health') ?? false }))
|
|
@@ -2,38 +2,37 @@ import { expectAssignable, expectDeprecated, expectError, expectNotAssignable, e
|
|
|
2
2
|
import fastify, {
|
|
3
3
|
FastifyLogFn,
|
|
4
4
|
LogLevel,
|
|
5
|
-
|
|
5
|
+
FastifyBaseLogger,
|
|
6
6
|
FastifyRequest,
|
|
7
|
-
FastifyReply
|
|
8
|
-
FastifyBaseLogger
|
|
7
|
+
FastifyReply
|
|
9
8
|
} from '../../fastify'
|
|
10
9
|
import { Server, IncomingMessage, ServerResponse } from 'node:http'
|
|
11
10
|
import * as fs from 'node:fs'
|
|
12
11
|
import P from 'pino'
|
|
13
|
-
import { ResSerializerReply } from '../../types/logger'
|
|
12
|
+
import { FastifyLoggerInstance, ResSerializerReply } from '../../types/logger'
|
|
14
13
|
|
|
15
|
-
expectType<
|
|
14
|
+
expectType<FastifyBaseLogger>(fastify().log)
|
|
16
15
|
|
|
17
16
|
class Foo {}
|
|
18
17
|
|
|
19
18
|
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(logLevel => {
|
|
20
19
|
expectType<FastifyLogFn>(
|
|
21
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
20
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]
|
|
22
21
|
)
|
|
23
22
|
expectType<void>(
|
|
24
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
23
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]('')
|
|
25
24
|
)
|
|
26
25
|
expectType<void>(
|
|
27
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
26
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]({})
|
|
28
27
|
)
|
|
29
28
|
expectType<void>(
|
|
30
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
29
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]({ foo: 'bar' })
|
|
31
30
|
)
|
|
32
31
|
expectType<void>(
|
|
33
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
32
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel](new Error())
|
|
34
33
|
)
|
|
35
34
|
expectType<void>(
|
|
36
|
-
fastify<Server, IncomingMessage, ServerResponse,
|
|
35
|
+
fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel](new Foo())
|
|
37
36
|
)
|
|
38
37
|
})
|
|
39
38
|
|
|
@@ -109,7 +108,7 @@ ServerResponse
|
|
|
109
108
|
}
|
|
110
109
|
})
|
|
111
110
|
|
|
112
|
-
expectType<
|
|
111
|
+
expectType<FastifyBaseLogger>(serverWithLogOptions.log)
|
|
113
112
|
|
|
114
113
|
const serverWithFileOption = fastify<
|
|
115
114
|
Server,
|
|
@@ -122,7 +121,7 @@ ServerResponse
|
|
|
122
121
|
}
|
|
123
122
|
})
|
|
124
123
|
|
|
125
|
-
expectType<
|
|
124
|
+
expectType<FastifyBaseLogger>(serverWithFileOption.log)
|
|
126
125
|
|
|
127
126
|
const serverAutoInferringTypes = fastify({
|
|
128
127
|
logger: {
|
|
@@ -266,11 +265,11 @@ expectDeprecated({} as FastifyLoggerInstance)
|
|
|
266
265
|
|
|
267
266
|
const childParent = fastify().log
|
|
268
267
|
// we test different option variant here
|
|
269
|
-
expectType<
|
|
270
|
-
expectType<
|
|
271
|
-
expectType<
|
|
272
|
-
expectType<
|
|
273
|
-
expectType<
|
|
268
|
+
expectType<FastifyBaseLogger>(childParent.child({}, { level: 'info' }))
|
|
269
|
+
expectType<FastifyBaseLogger>(childParent.child({}, { level: 'silent' }))
|
|
270
|
+
expectType<FastifyBaseLogger>(childParent.child({}, { redact: ['pass', 'pin'] }))
|
|
271
|
+
expectType<FastifyBaseLogger>(childParent.child({}, { serializers: { key: () => {} } }))
|
|
272
|
+
expectType<FastifyBaseLogger>(childParent.child({}, { level: 'info', redact: ['pass', 'pin'], serializers: { key: () => {} } }))
|
|
274
273
|
|
|
275
274
|
// no option pass
|
|
276
275
|
expectError(childParent.child())
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expectAssignable, expectError, expectType } from 'tsd'
|
|
2
2
|
import { IncomingMessage, Server, ServerResponse } from 'node:http'
|
|
3
3
|
import { Http2Server, Http2ServerRequest, Http2ServerResponse } from 'node:http2'
|
|
4
|
-
import fastify, { FastifyInstance, FastifyError,
|
|
4
|
+
import fastify, { FastifyInstance, FastifyError, FastifyBaseLogger, FastifyPluginAsync, FastifyPluginCallback, FastifyPluginOptions, RawServerDefault } from '../../fastify'
|
|
5
5
|
|
|
6
6
|
const testPluginCallback: FastifyPluginCallback = function (instance, opts, done) { }
|
|
7
7
|
const testPluginAsync: FastifyPluginAsync = async function (instance, opts) { }
|
|
@@ -97,7 +97,7 @@ type ServerWithTypeProvider = FastifyInstance<
|
|
|
97
97
|
Server,
|
|
98
98
|
IncomingMessage,
|
|
99
99
|
ServerResponse,
|
|
100
|
-
|
|
100
|
+
FastifyBaseLogger,
|
|
101
101
|
TestTypeProvider
|
|
102
102
|
>
|
|
103
103
|
const testPluginWithTypeProvider: FastifyPluginCallback<
|
|
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { expectAssignable, expectError, expectType } from 'tsd'
|
|
3
3
|
import fastify, { FastifyContextConfig, FastifyReply, FastifyRequest, FastifySchema, FastifyTypeProviderDefault, RawRequestDefaultExpression, RouteHandler, RouteHandlerMethod } from '../../fastify'
|
|
4
4
|
import { FastifyInstance } from '../../types/instance'
|
|
5
|
-
import {
|
|
5
|
+
import { FastifyBaseLogger } from '../../types/logger'
|
|
6
6
|
import { ResolveReplyTypeWithRouteGeneric } from '../../types/reply'
|
|
7
7
|
import { FastifyRouteConfig, RouteGenericInterface } from '../../types/route'
|
|
8
8
|
import { ContextConfigDefault, RawReplyDefaultExpression, RawServerDefault } from '../../types/utils'
|
|
@@ -12,7 +12,7 @@ type DefaultFastifyReplyWithCode<Code extends number> = FastifyReply<RouteGeneri
|
|
|
12
12
|
|
|
13
13
|
const getHandler: RouteHandlerMethod = function (_request, reply) {
|
|
14
14
|
expectType<RawReplyDefaultExpression>(reply.raw)
|
|
15
|
-
expectType<
|
|
15
|
+
expectType<FastifyBaseLogger>(reply.log)
|
|
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)
|
|
@@ -15,7 +15,7 @@ import fastify, {
|
|
|
15
15
|
SafePromiseLike
|
|
16
16
|
} from '../../fastify'
|
|
17
17
|
import { FastifyInstance } from '../../types/instance'
|
|
18
|
-
import {
|
|
18
|
+
import { FastifyBaseLogger } from '../../types/logger'
|
|
19
19
|
import { FastifyReply } from '../../types/reply'
|
|
20
20
|
import { FastifyRequest, RequestRouteOptions } from '../../types/request'
|
|
21
21
|
import { FastifyRouteConfig, RouteGenericInterface } from '../../types/route'
|
|
@@ -56,7 +56,7 @@ type CustomRequest = FastifyRequest<{
|
|
|
56
56
|
type HTTPRequestPart = 'body' | 'query' | 'querystring' | 'params' | 'headers'
|
|
57
57
|
type ExpectedGetValidationFunction = (input: { [key: string]: unknown }) => boolean
|
|
58
58
|
|
|
59
|
-
interface CustomLoggerInterface extends
|
|
59
|
+
interface CustomLoggerInterface extends FastifyBaseLogger {
|
|
60
60
|
foo: FastifyLogFn; // custom severity logger method
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -85,8 +85,9 @@ const getHandler: RouteHandler = function (request, _reply) {
|
|
|
85
85
|
|
|
86
86
|
expectType<RequestQuerystringDefault>(request.query)
|
|
87
87
|
expectType<string>(request.id)
|
|
88
|
-
expectType<
|
|
88
|
+
expectType<FastifyBaseLogger>(request.log)
|
|
89
89
|
expectType<RawRequestDefaultExpression['socket']>(request.socket)
|
|
90
|
+
expectType<AbortSignal>(request.signal)
|
|
90
91
|
expectType<Error & { validation: any; validationContext: string } | undefined>(request.validationError)
|
|
91
92
|
expectType<FastifyInstance>(request.server)
|
|
92
93
|
expectAssignable<(httpPart: HTTPRequestPart) => ExpectedGetValidationFunction>(request.getValidationFunction)
|
|
@@ -53,6 +53,12 @@ const routeHandlerWithReturnValue: RouteHandlerMethod = function (request, reply
|
|
|
53
53
|
return reply.send()
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const asyncPreHandler = async (request: FastifyRequest) => {
|
|
57
|
+
expectType<FastifyRequest>(request)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fastify().get('/', { preHandler: asyncPreHandler }, async () => 'this is an example')
|
|
61
|
+
|
|
56
62
|
fastify().get(
|
|
57
63
|
'/',
|
|
58
64
|
{ config: { foo: 'bar', bar: 100, includeMessage: true } },
|
|
@@ -9,7 +9,7 @@ import fastify, {
|
|
|
9
9
|
} from '../../fastify'
|
|
10
10
|
import { expectAssignable, expectError, expectType } from 'tsd'
|
|
11
11
|
import { IncomingHttpHeaders } from 'node:http'
|
|
12
|
-
import { Type, TSchema, Static } from '
|
|
12
|
+
import { Type, TSchema, Static } from 'typebox'
|
|
13
13
|
import { FromSchema, JSONSchema } from 'json-schema-to-ts'
|
|
14
14
|
|
|
15
15
|
const server = fastify()
|
package/test/web-api.test.js
CHANGED
|
@@ -500,6 +500,81 @@ test('WebStream should respect backpressure', async (t) => {
|
|
|
500
500
|
t.assert.ok(secondWriteAt >= drainEmittedAt)
|
|
501
501
|
})
|
|
502
502
|
|
|
503
|
+
test('WebStream should stop reading on drain after response destroy', async (t) => {
|
|
504
|
+
t.plan(2)
|
|
505
|
+
|
|
506
|
+
const fastify = Fastify()
|
|
507
|
+
t.after(() => fastify.close())
|
|
508
|
+
|
|
509
|
+
let cancelCalled = false
|
|
510
|
+
let resolveCancel
|
|
511
|
+
const cancelPromise = new Promise((resolve) => {
|
|
512
|
+
resolveCancel = resolve
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
fastify.get('/', function (request, reply) {
|
|
516
|
+
const raw = reply.raw
|
|
517
|
+
const originalWrite = raw.write.bind(raw)
|
|
518
|
+
let firstWrite = true
|
|
519
|
+
|
|
520
|
+
raw.write = function (chunk, encoding, cb) {
|
|
521
|
+
if (firstWrite) {
|
|
522
|
+
firstWrite = false
|
|
523
|
+
if (typeof cb === 'function') {
|
|
524
|
+
cb()
|
|
525
|
+
}
|
|
526
|
+
queueMicrotask(() => {
|
|
527
|
+
raw.destroy()
|
|
528
|
+
raw.emit('drain')
|
|
529
|
+
})
|
|
530
|
+
return false
|
|
531
|
+
}
|
|
532
|
+
return originalWrite(chunk, encoding, cb)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const stream = new ReadableStream({
|
|
536
|
+
start (controller) {
|
|
537
|
+
controller.enqueue(Buffer.from('chunk-1'))
|
|
538
|
+
},
|
|
539
|
+
pull (controller) {
|
|
540
|
+
controller.enqueue(Buffer.from('chunk-2'))
|
|
541
|
+
controller.close()
|
|
542
|
+
},
|
|
543
|
+
cancel () {
|
|
544
|
+
cancelCalled = true
|
|
545
|
+
resolveCancel()
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
reply.header('content-type', 'text/plain').send(stream)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
await new Promise((resolve, reject) => {
|
|
553
|
+
fastify.listen({ port: 0 }, err => {
|
|
554
|
+
if (err) return reject(err)
|
|
555
|
+
resolve()
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
await new Promise((resolve, reject) => {
|
|
560
|
+
const req = http.get(`http://localhost:${fastify.server.address().port}/`, (res) => {
|
|
561
|
+
res.once('close', resolve)
|
|
562
|
+
res.resume()
|
|
563
|
+
})
|
|
564
|
+
req.once('error', (err) => {
|
|
565
|
+
if (err.code === 'ECONNRESET') {
|
|
566
|
+
resolve()
|
|
567
|
+
} else {
|
|
568
|
+
reject(err)
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
await cancelPromise
|
|
574
|
+
t.assert.ok(true, 'response interrupted as expected')
|
|
575
|
+
t.assert.strictEqual(cancelCalled, true)
|
|
576
|
+
})
|
|
577
|
+
|
|
503
578
|
test('WebStream should warn when headers already sent', async (t) => {
|
|
504
579
|
t.plan(2)
|
|
505
580
|
|
package/types/errors.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type FastifyErrorCodes = Record<
|
|
|
75
75
|
'FST_ERR_ROUTE_METHOD_NOT_SUPPORTED' |
|
|
76
76
|
'FST_ERR_ROUTE_BODY_VALIDATION_SCHEMA_NOT_SUPPORTED' |
|
|
77
77
|
'FST_ERR_ROUTE_BODY_LIMIT_OPTION_NOT_INT' |
|
|
78
|
+
'FST_ERR_HANDLER_TIMEOUT' |
|
|
79
|
+
'FST_ERR_ROUTE_HANDLER_TIMEOUT_OPTION_NOT_INT' |
|
|
78
80
|
'FST_ERR_ROUTE_REWRITE_NOT_STR' |
|
|
79
81
|
'FST_ERR_REOPENED_CLOSE_SERVER' |
|
|
80
82
|
'FST_ERR_REOPENED_SERVER' |
|
package/types/logger.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface FastifyBaseLogger extends Pick<BaseLogger, 'level' | 'info' | '
|
|
|
28
28
|
child(bindings: Bindings, options?: ChildLoggerOptions): FastifyBaseLogger
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// TODO delete
|
|
31
|
+
// TODO delete FastifyLoggerInstance in the next major release. It seems that it is enough to have only FastifyBaseLogger.
|
|
32
32
|
/**
|
|
33
33
|
* @deprecated Use FastifyBaseLogger instead
|
|
34
34
|
*/
|
package/types/request.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface RequestRouteOptions<ContextConfig = ContextConfigDefault, Schem
|
|
|
25
25
|
// `url` can be `undefined` for instance when `request.is404` is true
|
|
26
26
|
url: string | undefined;
|
|
27
27
|
bodyLimit: number;
|
|
28
|
+
handlerTimeout: number;
|
|
28
29
|
attachValidation: boolean;
|
|
29
30
|
logLevel: string;
|
|
30
31
|
exposeHeadRoute: boolean;
|
|
@@ -82,6 +83,7 @@ export interface FastifyRequest<RouteGeneric extends RouteGenericInterface = Rou
|
|
|
82
83
|
readonly routeOptions: Readonly<RequestRouteOptions<ContextConfig, SchemaCompiler>>
|
|
83
84
|
readonly is404: boolean;
|
|
84
85
|
readonly socket: RawRequest['socket'];
|
|
86
|
+
readonly signal: AbortSignal;
|
|
85
87
|
|
|
86
88
|
getValidationFunction(httpPart: HTTPRequestPart): ValidationFunction
|
|
87
89
|
getValidationFunction(schema: { [key: string]: any }): ValidationFunction
|