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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/SECURITY.md +20 -20
  4. package/build/build-validation.js +2 -0
  5. package/docs/Guides/Ecosystem.md +7 -59
  6. package/docs/Guides/Fluent-Schema.md +2 -1
  7. package/docs/Guides/Migration-Guide-V4.md +9 -8
  8. package/docs/Guides/Migration-Guide-V5.md +1 -1
  9. package/docs/Guides/Serverless.md +1 -1
  10. package/docs/Reference/ContentTypeParser.md +2 -1
  11. package/docs/Reference/Decorators.md +4 -2
  12. package/docs/Reference/Errors.md +5 -0
  13. package/docs/Reference/Hooks.md +85 -23
  14. package/docs/Reference/LTS.md +2 -2
  15. package/docs/Reference/Lifecycle.md +16 -1
  16. package/docs/Reference/Logging.md +11 -7
  17. package/docs/Reference/Middleware.md +3 -2
  18. package/docs/Reference/Reply.md +15 -8
  19. package/docs/Reference/Request.md +17 -1
  20. package/docs/Reference/Routes.md +15 -6
  21. package/docs/Reference/Server.md +135 -14
  22. package/docs/Reference/Type-Providers.md +5 -5
  23. package/docs/Reference/TypeScript.md +14 -10
  24. package/docs/Reference/Validation-and-Serialization.md +28 -1
  25. package/fastify.d.ts +1 -0
  26. package/fastify.js +4 -2
  27. package/lib/config-validator.js +324 -296
  28. package/lib/content-type-parser.js +1 -1
  29. package/lib/content-type.js +9 -3
  30. package/lib/context.js +5 -2
  31. package/lib/errors.js +12 -1
  32. package/lib/reply.js +23 -1
  33. package/lib/request.js +18 -1
  34. package/lib/route.js +45 -4
  35. package/lib/symbols.js +4 -0
  36. package/package.json +5 -5
  37. package/scripts/validate-ecosystem-links.js +179 -0
  38. package/test/content-parser.test.js +25 -1
  39. package/test/content-type.test.js +38 -0
  40. package/test/handler-timeout.test.js +367 -0
  41. package/test/internals/errors.test.js +2 -2
  42. package/test/internals/initial-config.test.js +2 -0
  43. package/test/request-error.test.js +41 -0
  44. package/test/router-options.test.js +42 -0
  45. package/test/scripts/validate-ecosystem-links.test.js +339 -0
  46. package/test/types/dummy-plugin.ts +2 -2
  47. package/test/types/fastify.test-d.ts +1 -0
  48. package/test/types/logger.test-d.ts +17 -18
  49. package/test/types/register.test-d.ts +2 -2
  50. package/test/types/reply.test-d.ts +2 -2
  51. package/test/types/request.test-d.ts +4 -3
  52. package/test/types/route.test-d.ts +6 -0
  53. package/test/types/type-provider.test-d.ts +1 -1
  54. package/test/web-api.test.js +75 -0
  55. package/types/errors.d.ts +2 -0
  56. package/types/logger.d.ts +1 -1
  57. package/types/request.d.ts +2 -0
  58. package/types/route.d.ts +35 -21
  59. 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 { FastifyPlugin } from '../../fastify'
1
+ import { FastifyPluginAsync } from '../../fastify'
2
2
 
3
3
  export interface DummyPluginOptions {
4
4
  foo?: number
5
5
  }
6
6
 
7
- declare const DummyPlugin: FastifyPlugin<DummyPluginOptions>
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
- FastifyLoggerInstance,
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<FastifyLoggerInstance>(fastify().log)
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, FastifyLoggerInstance>().log[logLevel as LogLevel]
20
+ fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]
22
21
  )
23
22
  expectType<void>(
24
- fastify<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>().log[logLevel as LogLevel]('')
23
+ fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]('')
25
24
  )
26
25
  expectType<void>(
27
- fastify<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>().log[logLevel as LogLevel]({})
26
+ fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]({})
28
27
  )
29
28
  expectType<void>(
30
- fastify<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>().log[logLevel as LogLevel]({ foo: 'bar' })
29
+ fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel]({ foo: 'bar' })
31
30
  )
32
31
  expectType<void>(
33
- fastify<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>().log[logLevel as LogLevel](new Error())
32
+ fastify<Server, IncomingMessage, ServerResponse, FastifyBaseLogger>().log[logLevel as LogLevel](new Error())
34
33
  )
35
34
  expectType<void>(
36
- fastify<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>().log[logLevel as LogLevel](new Foo())
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<FastifyLoggerInstance>(serverWithLogOptions.log)
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<FastifyLoggerInstance>(serverWithFileOption.log)
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<FastifyLoggerInstance>(childParent.child({}, { level: 'info' }))
270
- expectType<FastifyLoggerInstance>(childParent.child({}, { level: 'silent' }))
271
- expectType<FastifyLoggerInstance>(childParent.child({}, { redact: ['pass', 'pin'] }))
272
- expectType<FastifyLoggerInstance>(childParent.child({}, { serializers: { key: () => {} } }))
273
- expectType<FastifyLoggerInstance>(childParent.child({}, { level: 'info', redact: ['pass', 'pin'], serializers: { key: () => {} } }))
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, FastifyLoggerInstance, FastifyPluginAsync, FastifyPluginCallback, FastifyPluginOptions, RawServerDefault } from '../../fastify'
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
- FastifyLoggerInstance,
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 { FastifyLoggerInstance } from '../../types/logger'
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<FastifyLoggerInstance>(reply.log)
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 { FastifyLoggerInstance } from '../../types/logger'
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 FastifyLoggerInstance {
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<FastifyLoggerInstance>(request.log)
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 '@sinclair/typebox'
12
+ import { Type, TSchema, Static } from 'typebox'
13
13
  import { FromSchema, JSONSchema } from 'json-schema-to-ts'
14
14
 
15
15
  const server = fastify()
@@ -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 FastifyBaseLogger in the next major release. It seems that it is enough to have only FastifyBaseLogger.
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
  */
@@ -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