fastify 4.13.0 → 4.14.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.
@@ -648,5 +648,7 @@ section.
648
648
  Reusable workflows for use in the Fastify plugin
649
649
  - [`fast-maker`](https://github.com/imjuni/fast-maker) route configuration
650
650
  generator by directory structure.
651
+ - [`fastify-flux`](https://github.com/Jnig/fastify-flux) Tool for building
652
+ Fastify APIs using decorators and convert Typescript interface to JSON Schema.
651
653
  - [`simple-tjscli`](https://github.com/imjuni/simple-tjscli) CLI tool to
652
654
  generate JSON Schema from TypeScript interfaces.
@@ -19,6 +19,7 @@ are Request/Reply hooks and application hooks:
19
19
  - [onSend](#onsend)
20
20
  - [onResponse](#onresponse)
21
21
  - [onTimeout](#ontimeout)
22
+ - [onRequestAbort](#onrequestabort)
22
23
  - [Manage Errors from a hook](#manage-errors-from-a-hook)
23
24
  - [Respond to a request from a hook](#respond-to-a-request-from-a-hook)
24
25
  - [Application Hooks](#application-hooks)
@@ -267,6 +268,26 @@ service (if the `connectionTimeout` property is set on the Fastify instance).
267
268
  The `onTimeout` hook is executed when a request is timed out and the HTTP socket
268
269
  has been hanged up. Therefore, you will not be able to send data to the client.
269
270
 
271
+ ### onRequestAbort
272
+
273
+ ```js
274
+ fastify.addHook('onRequestAbort', (request, reply, done) => {
275
+ // Some code
276
+ done()
277
+ })
278
+ ```
279
+ Or `async/await`:
280
+ ```js
281
+ fastify.addHook('onRequestAbort', async (request, reply) => {
282
+ // Some code
283
+ await asyncMethod()
284
+ })
285
+ ```
286
+ The `onRequestAbort` hook is executed when a client closes the connection before
287
+ the entire request has been received. Therefore, you will not be able to send
288
+ data to the client.
289
+
290
+ **Notice:** client abort detection is not completely reliable. See: [`Detecting-When-Clients-Abort.md`](../Guides/Detecting-When-Clients-Abort.md)
270
291
 
271
292
  ### Manage Errors from a hook
272
293
  If you get an error during the execution of your hook, just pass it to `done()`
@@ -62,7 +62,7 @@ YAML workflow labels below:
62
62
  | OS | YAML Workflow Label | Package Manager | Node.js |
63
63
  |---------|------------------------|---------------------------|--------------|
64
64
  | Linux | `ubuntu-latest` | npm | 14,16,18 |
65
- | Linux | `ubuntu-18.04` | yarn,pnpm | 14,16,18 |
65
+ | Linux | `ubuntu-latest` | yarn,pnpm | 14,16,18 |
66
66
  | Windows | `windows-latest` | npm | 14,16,18 |
67
67
  | MacOS | `macos-latest` | npm | 14,16,18 |
68
68
 
@@ -382,7 +382,9 @@ fastify.addHook('onResponse', (req, reply, done) => {
382
382
  ```
383
383
 
384
384
  Please note that this setting will also disable an error log written by the
385
- default `onResponse` hook on reply callback errors.
385
+ default `onResponse` hook on reply callback errors. Other log messages
386
+ emitted by Fastify will stay enabled, like deprecation warnings and messages
387
+ emitted when requests are received while the server is closing.
386
388
 
387
389
  ### `serverFactory`
388
390
  <a id="custom-http-server"></a>
package/fastify.d.ts CHANGED
@@ -3,16 +3,16 @@ import * as http2 from 'http2'
3
3
  import * as https from 'https'
4
4
  import { Socket } from 'net'
5
5
 
6
- import { Options as AjvOptions, ValidatorCompiler } from '@fastify/ajv-compiler'
6
+ import { Options as AjvOptions, ValidatorFactory } from '@fastify/ajv-compiler'
7
7
  import { FastifyError } from '@fastify/error'
8
- import { Options as FJSOptions, SerializerCompiler } from '@fastify/fast-json-stringify-compiler'
8
+ import { Options as FJSOptions, SerializerFactory } from '@fastify/fast-json-stringify-compiler'
9
9
  import { ConstraintStrategy, HTTPVersion } from 'find-my-way'
10
10
  import { Chain as LightMyRequestChain, InjectOptions, Response as LightMyRequestResponse, CallbackFunc as LightMyRequestCallback } from 'light-my-request'
11
11
 
12
12
  import { FastifyBodyParser, FastifyContentTypeParser, AddContentTypeParser, hasContentTypeParser, getDefaultJsonParser, ProtoAction, ConstructorAction } from './types/content-type-parser'
13
13
  import { FastifyContext, FastifyContextConfig } from './types/context'
14
14
  import { FastifyErrorCodes } from './types/errors'
15
- import { DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler } from './types/hooks'
15
+ import { DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler } from './types/hooks'
16
16
  import { FastifyListenOptions, FastifyInstance, PrintRoutesOptions } from './types/instance'
17
17
  import { FastifyBaseLogger, FastifyLoggerInstance, FastifyLoggerOptions, PinoLoggerOptions, FastifyLogFn, LogLevel } from './types/logger'
18
18
  import { FastifyPluginCallback, FastifyPluginAsync, FastifyPluginOptions, FastifyPlugin } from './types/plugin'
@@ -20,7 +20,7 @@ import { FastifyRegister, FastifyRegisterOptions, RegisterOptions } from './type
20
20
  import { FastifyReply } from './types/reply'
21
21
  import { FastifyRequest, RequestGenericInterface } from './types/request'
22
22
  import { RouteHandler, RouteHandlerMethod, RouteOptions, RouteShorthandMethod, RouteShorthandOptions, RouteShorthandOptionsWithHandler, RouteGenericInterface } from './types/route'
23
- import { FastifySchema, FastifySchemaCompiler, FastifySchemaValidationError } from './types/schema'
23
+ import { FastifySchema, FastifySchemaCompiler, SchemaErrorDataVar, SchemaErrorFormatter } from './types/schema'
24
24
  import { FastifyServerFactory, FastifyServerFactoryHandler } from './types/serverFactory'
25
25
  import { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'
26
26
  import { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './types/utils'
@@ -28,7 +28,7 @@ import { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaul
28
28
  declare module '@fastify/error' {
29
29
  interface FastifyError {
30
30
  validation?: fastify.ValidationResult[];
31
- validationContext?: 'body' | 'headers' | 'parameters' | 'querystring';
31
+ validationContext?: SchemaErrorDataVar;
32
32
  }
33
33
  }
34
34
 
@@ -134,8 +134,8 @@ declare namespace fastify {
134
134
  getSchemas(): Record<string, unknown>;
135
135
  };
136
136
  compilersFactory?: {
137
- buildValidator?: ValidatorCompiler;
138
- buildSerializer?: SerializerCompiler;
137
+ buildValidator?: ValidatorFactory;
138
+ buildSerializer?: SerializerFactory;
139
139
  };
140
140
  };
141
141
  return503OnClosing?: boolean,
@@ -149,7 +149,7 @@ declare namespace fastify {
149
149
  res: FastifyReply<RawServer, RawRequestDefaultExpression<RawServer>, RawReplyDefaultExpression<RawServer>, RequestGeneric, FastifyContextConfig, SchemaCompiler, TypeProvider>
150
150
  ) => void,
151
151
  rewriteUrl?: (req: RawRequestDefaultExpression<RawServer>) => string,
152
- schemaErrorFormatter?: (errors: FastifySchemaValidationError[], dataVar: string) => Error,
152
+ schemaErrorFormatter?: SchemaErrorFormatter,
153
153
  /**
154
154
  * listener to error events emitted by client connections
155
155
  */
@@ -179,7 +179,7 @@ declare namespace fastify {
179
179
  FastifyError, // '@fastify/error'
180
180
  FastifySchema, FastifySchemaCompiler, // './types/schema'
181
181
  HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault, // './types/utils'
182
- DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, // './types/hooks'
182
+ DoneFuncWithErrOrRes, HookHandlerDoneFunction, RequestPayload, onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler, onRequestAbortHookHandler, onRequestAbortAsyncHookHandler, // './types/hooks'
183
183
  FastifyServerFactory, FastifyServerFactoryHandler, // './types/serverFactory'
184
184
  FastifyTypeProvider, FastifyTypeProviderDefault, // './types/type-provider'
185
185
  FastifyErrorCodes, // './types/errors'
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.13.0'
3
+ const VERSION = '4.14.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -588,6 +588,10 @@ function fastify (options) {
588
588
  if (fn.constructor.name === 'AsyncFunction' && fn.length !== 0) {
589
589
  throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
590
590
  }
591
+ } else if (name === 'onRequestAbort') {
592
+ if (fn.constructor.name === 'AsyncFunction' && fn.length !== 1) {
593
+ throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
594
+ }
591
595
  } else {
592
596
  if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) {
593
597
  throw new errorCodes.FST_ERR_HOOK_INVALID_ASYNC_HANDLER()
package/lib/context.js CHANGED
@@ -48,6 +48,7 @@ function Context ({
48
48
  this.preHandler = null
49
49
  this.onResponse = null
50
50
  this.preSerialization = null
51
+ this.onRequestAbort = null
51
52
  this.config = config
52
53
  this.errorHandler = errorHandler || server[kErrorHandler]
53
54
  this._middie = null
@@ -11,7 +11,8 @@ const {
11
11
  } = require('./symbols.js')
12
12
 
13
13
  const {
14
- FST_ERR_REP_INVALID_PAYLOAD_TYPE
14
+ FST_ERR_REP_INVALID_PAYLOAD_TYPE,
15
+ FST_ERR_FAILED_ERROR_SERIALIZATION
15
16
  } = require('./errors')
16
17
 
17
18
  const { getSchemaSerializer } = require('./schemas')
@@ -113,11 +114,7 @@ function fallbackErrorHandler (error, reply, cb) {
113
114
  // error is always FST_ERR_SCH_SERIALIZATION_BUILD because this is called from route/compileSchemasForSerialization
114
115
  reply.log.error({ err, statusCode: res.statusCode }, 'The serializer for the given status code failed')
115
116
  reply.code(500)
116
- payload = serializeError({
117
- error: statusCodes['500'],
118
- message: err.message,
119
- statusCode: 500
120
- })
117
+ payload = serializeError(new FST_ERR_FAILED_ERROR_SERIALIZATION(err.message, error.message))
121
118
  }
122
119
 
123
120
  if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {
@@ -2,15 +2,14 @@
2
2
  /* istanbul ignore file */
3
3
 
4
4
  'use strict'
5
-
6
5
 
7
6
 
8
7
  // eslint-disable-next-line
9
8
  const STR_ESCAPE = /[\u0000-\u001f\u0022\u005c\ud800-\udfff]|[\ud800-\udbff](?![\udc00-\udfff])|(?:[^\ud800-\udbff]|^)[\udc00-\udfff]/
10
9
 
11
10
  class Serializer {
12
- constructor (options = {}) {
13
- switch (options.rounding) {
11
+ constructor (options) {
12
+ switch (options && options.rounding) {
14
13
  case 'floor':
15
14
  this.parseInteger = Math.floor
16
15
  break
@@ -20,6 +19,7 @@ class Serializer {
20
19
  case 'round':
21
20
  this.parseInteger = Math.round
22
21
  break
22
+ case 'trunc':
23
23
  default:
24
24
  this.parseInteger = Math.trunc
25
25
  break
@@ -27,17 +27,28 @@ class Serializer {
27
27
  }
28
28
 
29
29
  asInteger (i) {
30
- if (typeof i === 'bigint') {
30
+ if (typeof i === 'number') {
31
+ if (i === Infinity || i === -Infinity) {
32
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
33
+ }
34
+ if (Number.isInteger(i)) {
35
+ return '' + i
36
+ }
37
+ if (Number.isNaN(i)) {
38
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
39
+ }
40
+ return this.parseInteger(i)
41
+ } else if (i === null) {
42
+ return '0'
43
+ } else if (typeof i === 'bigint') {
31
44
  return i.toString()
32
- } else if (Number.isInteger(i)) {
33
- return '' + i
34
45
  } else {
35
46
  /* eslint no-undef: "off" */
36
47
  const integer = this.parseInteger(i)
37
- if (Number.isNaN(integer) || !Number.isFinite(integer)) {
38
- throw new Error(`The value "${i}" cannot be converted to an integer.`)
39
- } else {
48
+ if (Number.isFinite(integer)) {
40
49
  return '' + integer
50
+ } else {
51
+ throw new Error(`The value "${i}" cannot be converted to an integer.`)
41
52
  }
42
53
  }
43
54
  }
@@ -91,23 +102,24 @@ class Serializer {
91
102
  }
92
103
 
93
104
  asString (str) {
94
- const quotes = '"'
95
- if (str instanceof Date) {
96
- return quotes + str.toISOString() + quotes
97
- } else if (str === null) {
98
- return quotes + quotes
99
- } else if (str instanceof RegExp) {
100
- str = str.source
101
- } else if (typeof str !== 'string') {
102
- str = str.toString()
105
+ if (typeof str !== 'string') {
106
+ if (str === null) {
107
+ return '""'
108
+ }
109
+ if (str instanceof Date) {
110
+ return '"' + str.toISOString() + '"'
111
+ }
112
+ if (str instanceof RegExp) {
113
+ str = str.source
114
+ } else {
115
+ str = str.toString()
116
+ }
103
117
  }
104
118
 
105
119
  // Fast escape chars check
106
120
  if (!STR_ESCAPE.test(str)) {
107
- return quotes + str + quotes
108
- }
109
-
110
- if (str.length < 42) {
121
+ return '"' + str + '"'
122
+ } else if (str.length < 42) {
111
123
  return this.asStringSmall(str)
112
124
  } else {
113
125
  return JSON.stringify(str)
@@ -151,81 +163,50 @@ class Serializer {
151
163
  }
152
164
 
153
165
 
154
-
155
- const serializer = new Serializer({"mode":"standalone"})
166
+ const serializer = new Serializer()
156
167
 
157
168
 
158
169
 
159
- function main (input) {
160
- let json = ''
161
- json += anonymous0(input)
162
- return json
163
- }
164
170
 
165
171
  function anonymous0 (input) {
166
172
  // #
167
173
 
168
- var obj = (input && typeof input.toJSON === 'function')
174
+ const obj = (input && typeof input.toJSON === 'function')
169
175
  ? input.toJSON()
170
176
  : input
171
177
 
172
- var json = '{'
173
- var addComma = false
178
+ let json = '{'
179
+ let addComma = false
174
180
 
175
181
  if (obj["statusCode"] !== undefined) {
176
-
177
- if (addComma) {
178
- json += ','
179
- } else {
180
- addComma = true
181
- }
182
-
183
- json += "\"statusCode\"" + ':'
182
+ !addComma && (addComma = true) || (json += ',')
183
+ json += "\"statusCode\":"
184
184
  json += serializer.asNumber(obj["statusCode"])
185
185
  }
186
186
 
187
187
  if (obj["code"] !== undefined) {
188
-
189
- if (addComma) {
190
- json += ','
191
- } else {
192
- addComma = true
193
- }
194
-
195
- json += "\"code\"" + ':'
188
+ !addComma && (addComma = true) || (json += ',')
189
+ json += "\"code\":"
196
190
  json += serializer.asString(obj["code"])
197
191
  }
198
192
 
199
193
  if (obj["error"] !== undefined) {
200
-
201
- if (addComma) {
202
- json += ','
203
- } else {
204
- addComma = true
205
- }
206
-
207
- json += "\"error\"" + ':'
194
+ !addComma && (addComma = true) || (json += ',')
195
+ json += "\"error\":"
208
196
  json += serializer.asString(obj["error"])
209
197
  }
210
198
 
211
199
  if (obj["message"] !== undefined) {
212
-
213
- if (addComma) {
214
- json += ','
215
- } else {
216
- addComma = true
217
- }
218
-
219
- json += "\"message\"" + ':'
200
+ !addComma && (addComma = true) || (json += ',')
201
+ json += "\"message\":"
220
202
  json += serializer.asString(obj["message"])
221
203
  }
222
204
 
223
- json += '}'
224
- return json
205
+ return json + '}'
225
206
  }
226
207
 
208
+ const main = anonymous0
209
+
227
210
 
228
-
229
211
 
230
212
  module.exports = main
231
-
package/lib/errors.js CHANGED
@@ -214,6 +214,10 @@ const codes = {
214
214
  'FST_ERR_BAD_TRAILER_VALUE',
215
215
  "Called reply.trailer('%s', fn) with an invalid type: %s. Expected a function."
216
216
  ),
217
+ FST_ERR_FAILED_ERROR_SERIALIZATION: createError(
218
+ 'FST_ERR_FAILED_ERROR_SERIALIZATION',
219
+ 'Failed to serialize an error. Error: %s. Original error: %s'
220
+ ),
217
221
  FST_ERR_MISSING_SERIALIZATION_FN: createError(
218
222
  'FST_ERR_MISSING_SERIALIZATION_FN',
219
223
  'Missing serialization function. Key "%s"'
package/lib/hooks.js CHANGED
@@ -15,7 +15,8 @@ const lifecycleHooks = [
15
15
  'preHandler',
16
16
  'onSend',
17
17
  'onResponse',
18
- 'onError'
18
+ 'onError',
19
+ 'onRequestAbort'
19
20
  ]
20
21
  const supportedHooks = lifecycleHooks.concat(applicationHooks)
21
22
  const {
@@ -46,6 +47,7 @@ function Hooks () {
46
47
  this.onRegister = []
47
48
  this.onReady = []
48
49
  this.onTimeout = []
50
+ this.onRequestAbort = []
49
51
  }
50
52
 
51
53
  Hooks.prototype.validate = function (hook, fn) {
@@ -74,6 +76,7 @@ function buildHooks (h) {
74
76
  hooks.onRoute = h.onRoute.slice()
75
77
  hooks.onRegister = h.onRegister.slice()
76
78
  hooks.onTimeout = h.onTimeout.slice()
79
+ hooks.onRequestAbort = h.onRequestAbort.slice()
77
80
  hooks.onReady = []
78
81
  return hooks
79
82
  }
@@ -246,6 +249,42 @@ function onSendHookRunner (functions, request, reply, payload, cb) {
246
249
  next()
247
250
  }
248
251
 
252
+ function onRequestAbortHookRunner (functions, runner, request, cb) {
253
+ let i = 0
254
+
255
+ function next (err) {
256
+ if (err || i === functions.length) {
257
+ cb(err, request)
258
+ return
259
+ }
260
+
261
+ let result
262
+ try {
263
+ result = runner(functions[i++], request, next)
264
+ } catch (error) {
265
+ next(error)
266
+ return
267
+ }
268
+ if (result && typeof result.then === 'function') {
269
+ result.then(handleResolve, handleReject)
270
+ }
271
+ }
272
+
273
+ function handleResolve () {
274
+ next()
275
+ }
276
+
277
+ function handleReject (err) {
278
+ if (!err) {
279
+ err = new FST_ERR_SEND_UNDEFINED_ERR()
280
+ }
281
+
282
+ cb(err, request)
283
+ }
284
+
285
+ next()
286
+ }
287
+
249
288
  function hookIterator (fn, request, reply, next) {
250
289
  if (reply.sent === true) return undefined
251
290
  return fn(request, reply, next)
@@ -256,6 +295,7 @@ module.exports = {
256
295
  buildHooks,
257
296
  hookRunner,
258
297
  onSendHookRunner,
298
+ onRequestAbortHookRunner,
259
299
  hookIterator,
260
300
  hookRunnerApplication,
261
301
  lifecycleHooks,
package/lib/route.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const FindMyWay = require('find-my-way')
4
4
  const Context = require('./context')
5
5
  const handleRequest = require('./handleRequest')
6
- const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks')
6
+ const { hookRunner, hookIterator, onRequestAbortHookRunner, lifecycleHooks } = require('./hooks')
7
7
  const { supportedMethods } = require('./httpMethods')
8
8
  const { normalizeSchema } = require('./schemas')
9
9
  const { parseHeadOnSendHandlers } = require('./headRoute')
@@ -303,7 +303,7 @@ function buildRouting (options) {
303
303
 
304
304
  // remove the head route created by fastify
305
305
  if (hasHEADHandler && !context[kRouteByFastify] && headHandler.store[kRouteByFastify]) {
306
- router.off(opts.method, opts.url, { constraints })
306
+ router.off('HEAD', opts.url, { constraints })
307
307
  }
308
308
 
309
309
  try {
@@ -394,8 +394,25 @@ function buildRouting (options) {
394
394
 
395
395
  // HTTP request entry point, the routing has already been executed
396
396
  function routeHandler (req, res, params, context, query) {
397
+ const id = genReqId(req)
398
+
399
+ const loggerBinding = {
400
+ [requestIdLogLabel]: id
401
+ }
402
+
403
+ const loggerOpts = {
404
+ level: context.logLevel
405
+ }
406
+
407
+ if (context.logSerializers) {
408
+ loggerOpts.serializers = context.logSerializers
409
+ }
410
+ const childLogger = logger.child(loggerBinding, loggerOpts)
411
+ childLogger[kDisableRequestLogging] = disableRequestLogging
412
+
397
413
  // TODO: The check here should be removed once https://github.com/nodejs/node/issues/43115 resolve in core.
398
414
  if (!validateHTTPVersion(req.httpVersion)) {
415
+ childLogger.info({ res: { statusCode: 505 } }, 'request aborted - invalid HTTP version')
399
416
  const message = '{"error":"HTTP Version Not Supported","message":"HTTP Version Not Supported","statusCode":505}'
400
417
  const headers = {
401
418
  'Content-Type': 'application/json',
@@ -424,6 +441,7 @@ function buildRouting (options) {
424
441
  }
425
442
  res.writeHead(503, headers)
426
443
  res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
444
+ childLogger.info({ res: { statusCode: 503 } }, 'request aborted - refusing to accept new requests as server is closing')
427
445
  return
428
446
  }
429
447
  }
@@ -445,22 +463,6 @@ function buildRouting (options) {
445
463
  req.headers[kRequestAcceptVersion] = undefined
446
464
  }
447
465
 
448
- const id = genReqId(req)
449
-
450
- const loggerBinding = {
451
- [requestIdLogLabel]: id
452
- }
453
-
454
- const loggerOpts = {
455
- level: context.logLevel
456
- }
457
-
458
- if (context.logSerializers) {
459
- loggerOpts.serializers = context.logSerializers
460
- }
461
- const childLogger = logger.child(loggerBinding, loggerOpts)
462
- childLogger[kDisableRequestLogging] = disableRequestLogging
463
-
464
466
  const request = new context.Request(id, params, req, query, childLogger, context)
465
467
  const reply = new context.Reply(res, request, childLogger)
466
468
 
@@ -484,6 +486,20 @@ function buildRouting (options) {
484
486
  runPreParsing(null, request, reply)
485
487
  }
486
488
 
489
+ if (context.onRequestAbort !== null) {
490
+ req.on('close', () => {
491
+ /* istanbul ignore else */
492
+ if (req.aborted) {
493
+ onRequestAbortHookRunner(
494
+ context.onRequestAbort,
495
+ hookIterator,
496
+ request,
497
+ handleOnRequestAbortHooksErrors.bind(null, reply)
498
+ )
499
+ }
500
+ })
501
+ }
502
+
487
503
  if (context.onTimeout !== null) {
488
504
  if (!request.raw.socket._meta) {
489
505
  request.raw.socket.on('timeout', handleTimeout)
@@ -493,6 +509,12 @@ function buildRouting (options) {
493
509
  }
494
510
  }
495
511
 
512
+ function handleOnRequestAbortHooksErrors (reply, err) {
513
+ if (err) {
514
+ reply.log.error({ err }, 'onRequestAborted hook failed')
515
+ }
516
+ }
517
+
496
518
  function handleTimeout () {
497
519
  const { context, request, reply } = this._meta
498
520
  hookRunner(
package/lib/symbols.js CHANGED
@@ -30,6 +30,7 @@ const keys = {
30
30
  kRequestValidateFns: Symbol('fastify.request.cache.validateFns'),
31
31
  kRequestPayloadStream: Symbol('fastify.RequestPayloadStream'),
32
32
  kRequestAcceptVersion: Symbol('fastify.RequestAcceptVersion'),
33
+ kRequestValidateWeakMap: Symbol('fastify.request.cache.validators'),
33
34
  // 404
34
35
  kFourOhFour: Symbol('fastify.404'),
35
36
  kCanSetNotFoundHandler: Symbol('fastify.canSetNotFoundHandler'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.13.0",
3
+ "version": "4.14.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -168,10 +168,10 @@
168
168
  "typescript": "^4.8.3",
169
169
  "undici": "^5.10.0",
170
170
  "vary": "^1.1.2",
171
- "yup": "^0.32.11"
171
+ "yup": "^1.0.0"
172
172
  },
173
173
  "dependencies": {
174
- "@fastify/ajv-compiler": "^3.3.1",
174
+ "@fastify/ajv-compiler": "^3.5.0",
175
175
  "@fastify/error": "^3.0.0",
176
176
  "@fastify/fast-json-stringify-compiler": "^4.1.0",
177
177
  "abstract-logging": "^2.0.1",
@@ -7,6 +7,7 @@ const test = t.test
7
7
  const Fastify = require('..')
8
8
  const { Client } = require('undici')
9
9
  const semver = require('semver')
10
+ const split = require('split2')
10
11
 
11
12
  test('close callback', t => {
12
13
  t.plan(4)
@@ -303,6 +304,40 @@ t.test('Current opened connection should not accept new incoming connections', t
303
304
  })
304
305
  })
305
306
 
307
+ t.test('rejected incoming connections should be logged', t => {
308
+ t.plan(2)
309
+ const stream = split(JSON.parse)
310
+ const fastify = Fastify({
311
+ forceCloseConnections: false,
312
+ logger: {
313
+ stream,
314
+ level: 'info'
315
+ }
316
+ })
317
+
318
+ const messages = []
319
+ stream.on('data', message => {
320
+ messages.push(message)
321
+ })
322
+ fastify.get('/', (req, reply) => {
323
+ fastify.close()
324
+ setTimeout(() => {
325
+ reply.send({ hello: 'world' })
326
+ }, 250)
327
+ })
328
+
329
+ fastify.listen({ port: 0 }, err => {
330
+ t.error(err)
331
+ const instance = new Client('http://localhost:' + fastify.server.address().port)
332
+ // initial request to trigger close
333
+ instance.request({ path: '/', method: 'GET' })
334
+ // subsequent request should be rejected
335
+ instance.request({ path: '/', method: 'GET' }).then(() => {
336
+ t.ok(messages.find(message => message.msg.includes('request aborted')))
337
+ })
338
+ })
339
+ })
340
+
306
341
  test('Cannot be reopened the closed server without listen callback', async t => {
307
342
  t.plan(2)
308
343
  const fastify = Fastify()
@@ -584,6 +584,18 @@ test('preHandler respond with a stream', t => {
584
584
  })
585
585
 
586
586
  test('Should log a warning if is an async function with `done`', t => {
587
+ t.test('2 arguments', t => {
588
+ t.plan(2)
589
+ const fastify = Fastify()
590
+
591
+ try {
592
+ fastify.addHook('onRequestAbort', async (req, done) => {})
593
+ } catch (e) {
594
+ t.ok(e.code, 'FST_ERR_HOOK_INVALID_ASYNC_HANDLER')
595
+ t.ok(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.')
596
+ }
597
+ })
598
+
587
599
  t.test('3 arguments', t => {
588
600
  t.plan(2)
589
601
  const fastify = Fastify()
@@ -11,6 +11,10 @@ const split = require('split2')
11
11
  const symbols = require('../lib/symbols.js')
12
12
  const payload = { hello: 'world' }
13
13
  const proxyquire = require('proxyquire')
14
+ const { promisify } = require('util')
15
+ const { connect } = require('net')
16
+
17
+ const sleep = promisify(setTimeout)
14
18
 
15
19
  process.removeAllListeners('warning')
16
20
 
@@ -3404,3 +3408,210 @@ test('registering invalid hooks should throw an error', async t => {
3404
3408
  })
3405
3409
  }, new Error('onSend hook should be a function, instead got [object Undefined]'))
3406
3410
  })
3411
+
3412
+ test('onRequestAbort should be triggered', t => {
3413
+ const fastify = Fastify()
3414
+ let order = 0
3415
+
3416
+ t.plan(3)
3417
+ t.teardown(() => fastify.close())
3418
+
3419
+ fastify.addHook('onRequestAbort', function (req, done) {
3420
+ t.equal(++order, 1, 'called in hook')
3421
+ done()
3422
+ })
3423
+
3424
+ fastify.route({
3425
+ method: 'GET',
3426
+ path: '/',
3427
+ async handler (request, reply) {
3428
+ await sleep(1000)
3429
+ return { hello: 'world' }
3430
+ },
3431
+ async onRequestAbort (req) {
3432
+ t.equal(++order, 2, 'called in route')
3433
+ }
3434
+ })
3435
+
3436
+ fastify.listen({ port: 0 }, err => {
3437
+ t.error(err)
3438
+
3439
+ const socket = connect(fastify.server.address().port)
3440
+
3441
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3442
+
3443
+ sleep(500).then(() => socket.destroy())
3444
+ })
3445
+ })
3446
+
3447
+ test('onRequestAbort should support encapsulation', t => {
3448
+ const fastify = Fastify()
3449
+ let order = 0
3450
+ let child
3451
+
3452
+ t.plan(6)
3453
+ t.teardown(() => fastify.close())
3454
+
3455
+ fastify.addHook('onRequestAbort', function (req, done) {
3456
+ t.equal(++order, 1, 'called in root')
3457
+ t.strictSame(this.pluginName, child.pluginName)
3458
+ done()
3459
+ })
3460
+
3461
+ fastify.register(async function (_child, _, done) {
3462
+ child = _child
3463
+
3464
+ fastify.addHook('onRequestAbort', async function (req) {
3465
+ t.equal(++order, 2, 'called in child')
3466
+ t.strictSame(this.pluginName, child.pluginName)
3467
+ })
3468
+
3469
+ child.route({
3470
+ method: 'GET',
3471
+ path: '/',
3472
+ async handler (request, reply) {
3473
+ await sleep(1000)
3474
+ return { hello: 'world' }
3475
+ },
3476
+ async onRequestAbort (_req) {
3477
+ t.equal(++order, 3, 'called in route')
3478
+ }
3479
+ })
3480
+
3481
+ done()
3482
+ })
3483
+
3484
+ fastify.listen({ port: 0 }, err => {
3485
+ t.error(err)
3486
+
3487
+ const socket = connect(fastify.server.address().port)
3488
+
3489
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3490
+
3491
+ sleep(500).then(() => socket.destroy())
3492
+ })
3493
+ })
3494
+
3495
+ test('onRequestAbort should handle errors / 1', t => {
3496
+ const fastify = Fastify()
3497
+
3498
+ t.plan(2)
3499
+ t.teardown(() => fastify.close())
3500
+
3501
+ fastify.addHook('onRequestAbort', function (req, done) {
3502
+ process.nextTick(() => t.pass())
3503
+ done(new Error('KABOOM!'))
3504
+ })
3505
+
3506
+ fastify.route({
3507
+ method: 'GET',
3508
+ path: '/',
3509
+ async handler (request, reply) {
3510
+ await sleep(1000)
3511
+ return { hello: 'world' }
3512
+ }
3513
+ })
3514
+
3515
+ fastify.listen({ port: 0 }, err => {
3516
+ t.error(err)
3517
+
3518
+ const socket = connect(fastify.server.address().port)
3519
+
3520
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3521
+
3522
+ sleep(500).then(() => socket.destroy())
3523
+ })
3524
+ })
3525
+
3526
+ test('onRequestAbort should handle errors / 2', t => {
3527
+ const fastify = Fastify()
3528
+
3529
+ t.plan(2)
3530
+ t.teardown(() => fastify.close())
3531
+
3532
+ fastify.addHook('onRequestAbort', function (req, done) {
3533
+ process.nextTick(() => t.pass())
3534
+ throw new Error('KABOOM!')
3535
+ })
3536
+
3537
+ fastify.route({
3538
+ method: 'GET',
3539
+ path: '/',
3540
+ async handler (request, reply) {
3541
+ await sleep(1000)
3542
+ return { hello: 'world' }
3543
+ }
3544
+ })
3545
+
3546
+ fastify.listen({ port: 0 }, err => {
3547
+ t.error(err)
3548
+
3549
+ const socket = connect(fastify.server.address().port)
3550
+
3551
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3552
+
3553
+ sleep(500).then(() => socket.destroy())
3554
+ })
3555
+ })
3556
+
3557
+ test('onRequestAbort should handle async errors / 1', t => {
3558
+ const fastify = Fastify()
3559
+
3560
+ t.plan(2)
3561
+ t.teardown(() => fastify.close())
3562
+
3563
+ fastify.addHook('onRequestAbort', async function (req) {
3564
+ process.nextTick(() => t.pass())
3565
+ throw new Error('KABOOM!')
3566
+ })
3567
+
3568
+ fastify.route({
3569
+ method: 'GET',
3570
+ path: '/',
3571
+ async handler (request, reply) {
3572
+ await sleep(1000)
3573
+ return { hello: 'world' }
3574
+ }
3575
+ })
3576
+
3577
+ fastify.listen({ port: 0 }, err => {
3578
+ t.error(err)
3579
+
3580
+ const socket = connect(fastify.server.address().port)
3581
+
3582
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3583
+
3584
+ sleep(500).then(() => socket.destroy())
3585
+ })
3586
+ })
3587
+
3588
+ test('onRequestAbort should handle async errors / 2', t => {
3589
+ const fastify = Fastify()
3590
+
3591
+ t.plan(2)
3592
+ t.teardown(() => fastify.close())
3593
+
3594
+ fastify.addHook('onRequestAbort', async function (req) {
3595
+ process.nextTick(() => t.pass())
3596
+ return Promise.reject()
3597
+ })
3598
+
3599
+ fastify.route({
3600
+ method: 'GET',
3601
+ path: '/',
3602
+ async handler (request, reply) {
3603
+ await sleep(1000)
3604
+ return { hello: 'world' }
3605
+ }
3606
+ })
3607
+
3608
+ fastify.listen({ port: 0 }, err => {
3609
+ t.error(err)
3610
+
3611
+ const socket = connect(fastify.server.address().port)
3612
+
3613
+ socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
3614
+
3615
+ sleep(500).then(() => socket.destroy())
3616
+ })
3617
+ })
@@ -28,15 +28,17 @@ before(async function () {
28
28
  [localhost, localhostForURL] = await helper.getLoopbackHost()
29
29
  })
30
30
 
31
- teardown(() => {
32
- files.forEach((file) => {
33
- try {
34
- fs.unlinkSync(file)
35
- } catch (e) {
36
- console.log(e)
37
- }
31
+ if (process.env.CI) {
32
+ teardown(() => {
33
+ files.forEach((file) => {
34
+ try {
35
+ fs.unlinkSync(file)
36
+ } catch (e) {
37
+ console.log(e)
38
+ }
39
+ })
38
40
  })
39
- })
41
+ }
40
42
 
41
43
  test('defaults to info level', t => {
42
44
  let fastify = null
@@ -1493,3 +1493,17 @@ test('exposeHeadRoute should not reuse the same route option', async t => {
1493
1493
  }
1494
1494
  })
1495
1495
  })
1496
+
1497
+ test('using fastify.all when a catchall is defined does not degrade performance', { timeout: 5000 }, async t => {
1498
+ t.plan(1)
1499
+
1500
+ const fastify = Fastify()
1501
+
1502
+ fastify.get('/*', async (_, reply) => reply.json({ ok: true }))
1503
+
1504
+ for (let i = 0; i < 100; i++) {
1505
+ fastify.all(`/${i}`, async (_, reply) => reply.json({ ok: true }))
1506
+ }
1507
+
1508
+ t.pass()
1509
+ })
@@ -833,8 +833,9 @@ test('do not crash if status code serializer errors', async t => {
833
833
  t.equal(res.statusCode, 500)
834
834
  t.same(res.json(), {
835
835
  statusCode: 500,
836
- error: 'Internal Server Error',
837
- message: '"code" is required!'
836
+ code: 'FST_ERR_FAILED_ERROR_SERIALIZATION',
837
+ message: 'Failed to serialize an error. Error: "code" is required!. ' +
838
+ 'Original error: querystring must have required property \'foo\''
838
839
  })
839
840
  })
840
841
 
@@ -240,7 +240,7 @@ expectAssignable<ValidationResult>(ajvErrorObject)
240
240
  expectAssignable<FastifyError['validation']>([ajvErrorObject])
241
241
  expectAssignable<FastifyError['validationContext']>('body')
242
242
  expectAssignable<FastifyError['validationContext']>('headers')
243
- expectAssignable<FastifyError['validationContext']>('parameters')
243
+ expectAssignable<FastifyError['validationContext']>('params')
244
244
  expectAssignable<FastifyError['validationContext']>('querystring')
245
245
 
246
246
  const routeGeneric: RouteGenericInterface = {}
@@ -114,6 +114,14 @@ server.addHook('onError', function (request, reply, error, done) {
114
114
  expectType<void>(done())
115
115
  })
116
116
 
117
+ server.addHook('onRequestAbort', function (request, done) {
118
+ expectType<FastifyInstance>(this)
119
+ expectType<FastifyRequest>(request)
120
+ expectAssignable<(err?: FastifyError) => void>(done)
121
+ expectAssignable<(err?: NodeJS.ErrnoException) => void>(done)
122
+ expectType<void>(done(new Error()))
123
+ })
124
+
117
125
  server.addHook('onRoute', function (opts) {
118
126
  expectType<FastifyInstance>(this)
119
127
  expectType<RouteOptions & { routePath: string; path: string; prefix: string}>(opts)
@@ -201,6 +209,11 @@ server.addHook('onError', async function (request, reply, error) {
201
209
  expectType<FastifyError>(error)
202
210
  })
203
211
 
212
+ server.addHook('onRequestAbort', async function (request) {
213
+ expectType<FastifyInstance>(this)
214
+ expectType<FastifyRequest>(request)
215
+ })
216
+
204
217
  server.addHook('onRegister', async (instance, opts) => {
205
218
  expectType<FastifyInstance>(instance)
206
219
  expectType<RegisterOptions & FastifyPluginOptions>(opts)
@@ -1,9 +1,8 @@
1
- import { expectAssignable, expectError } from 'tsd'
2
- import fastify, { FastifyInstance, FastifyRequest, FastifySchema } from '../../fastify'
3
- import { RouteGenericInterface } from '../../types/route'
4
- import { ContextConfigDefault } from '../../types/utils'
5
- import { FastifyReply } from '../../types/reply'
1
+ import { expectAssignable } from 'tsd'
2
+ import fastify, { FastifyInstance, FastifySchema } from '../../fastify'
6
3
  import Ajv from 'ajv'
4
+ import { StandaloneValidator } from '@fastify/ajv-compiler'
5
+ import { StandaloneSerializer } from '@fastify/fast-json-stringify-compiler'
7
6
 
8
7
  const server = fastify()
9
8
 
@@ -63,3 +62,36 @@ expectAssignable<FastifyInstance>(server.setValidatorCompiler<FastifySchema & {
63
62
  expectAssignable<FastifyInstance>(server.setSerializerCompiler<FastifySchema & { validate: string }>(
64
63
  () => data => JSON.stringify(data)
65
64
  ))
65
+
66
+ // https://github.com/fastify/ajv-compiler/issues/95
67
+ {
68
+ const factory = StandaloneValidator({
69
+ readMode: false,
70
+ storeFunction (routeOpts, schemaValidationCode) { }
71
+ })
72
+
73
+ const app = fastify({
74
+ jsonShorthand: false,
75
+ schemaController: {
76
+ compilersFactory: {
77
+ buildValidator: factory
78
+ }
79
+ }
80
+ })
81
+ }
82
+
83
+ {
84
+ const factory = StandaloneSerializer({
85
+ readMode: false,
86
+ storeFunction (routeOpts, schemaValidationCode) { }
87
+ })
88
+
89
+ const app = fastify({
90
+ jsonShorthand: false,
91
+ schemaController: {
92
+ compilersFactory: {
93
+ buildSerializer: factory
94
+ }
95
+ }
96
+ })
97
+ }
package/types/errors.d.ts CHANGED
@@ -28,6 +28,7 @@ export type FastifyErrorCodes = Record<
28
28
  'FST_ERR_BAD_STATUS_CODE'|
29
29
  'FST_ERR_BAD_TRAILER_NAME' |
30
30
  'FST_ERR_BAD_TRAILER_VALUE' |
31
+ 'FST_ERR_FAILED_ERROR_SERIALIZATION' |
31
32
  'FST_ERR_MISSING_SERIALIZATION_FN' |
32
33
  'FST_ERR_REQ_INVALID_VALIDATION_INVOCATION' |
33
34
  'FST_ERR_SCH_MISSING_ID' |
package/types/hooks.d.ts CHANGED
@@ -394,6 +394,44 @@ export interface onErrorAsyncHookHandler<
394
394
  ): Promise<unknown>;
395
395
  }
396
396
 
397
+ /**
398
+ * `onRequestAbort` is useful if you need to monitor the if the client aborts the request (if the `request.raw.aborted` property is set to `true`).
399
+ * The `onRequestAbort` hook is executed when a client closes the connection before the entire request has been received. Therefore, you will not be able to send data to the client.
400
+ * Notice: client abort detection is not completely reliable. See: https://github.com/fastify/fastify/blob/main/docs/Guides/Detecting-When-Clients-Abort.md
401
+ */
402
+ export interface onRequestAbortHookHandler<
403
+ RawServer extends RawServerBase = RawServerDefault,
404
+ RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
405
+ RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
406
+ RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
407
+ ContextConfig = ContextConfigDefault,
408
+ SchemaCompiler extends FastifySchema = FastifySchema,
409
+ TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
410
+ Logger extends FastifyBaseLogger = FastifyBaseLogger
411
+ > {
412
+ (
413
+ this: FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>,
414
+ request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider, ContextConfig, Logger>,
415
+ done: HookHandlerDoneFunction
416
+ ): void;
417
+ }
418
+
419
+ export interface onRequestAbortAsyncHookHandler<
420
+ RawServer extends RawServerBase = RawServerDefault,
421
+ RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
422
+ RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
423
+ RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
424
+ ContextConfig = ContextConfigDefault,
425
+ SchemaCompiler extends FastifySchema = FastifySchema,
426
+ TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
427
+ Logger extends FastifyBaseLogger = FastifyBaseLogger
428
+ > {
429
+ (
430
+ this: FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>,
431
+ request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler, TypeProvider, ContextConfig, Logger>,
432
+ ): Promise<unknown>;
433
+ }
434
+
397
435
  // Application Hooks
398
436
 
399
437
  /**
@@ -3,7 +3,7 @@ import { ConstraintStrategy, HTTPVersion } from 'find-my-way'
3
3
  import * as http from 'http'
4
4
  import { CallbackFunc as LightMyRequestCallback, Chain as LightMyRequestChain, InjectOptions, Response as LightMyRequestResponse } from 'light-my-request'
5
5
  import { AddContentTypeParser, ConstructorAction, FastifyBodyParser, getDefaultJsonParser, hasContentTypeParser, ProtoAction, removeAllContentTypeParsers, removeContentTypeParser } from './content-type-parser'
6
- import { onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler } from './hooks'
6
+ import { onCloseAsyncHookHandler, onCloseHookHandler, onErrorAsyncHookHandler, onErrorHookHandler, onReadyAsyncHookHandler, onReadyHookHandler, onRegisterHookHandler, onRequestAsyncHookHandler, onRequestHookHandler, onRequestAbortAsyncHookHandler, onRequestAbortHookHandler, onResponseAsyncHookHandler, onResponseHookHandler, onRouteHookHandler, onSendAsyncHookHandler, onSendHookHandler, onTimeoutAsyncHookHandler, onTimeoutHookHandler, preHandlerAsyncHookHandler, preHandlerHookHandler, preParsingAsyncHookHandler, preParsingHookHandler, preSerializationAsyncHookHandler, preSerializationHookHandler, preValidationAsyncHookHandler, preValidationHookHandler } from './hooks'
7
7
  import { FastifyBaseLogger } from './logger'
8
8
  import { FastifyRegister } from './register'
9
9
  import { FastifyReply } from './reply'
@@ -13,8 +13,8 @@ import {
13
13
  FastifySchema,
14
14
  FastifySchemaCompiler,
15
15
  FastifySchemaControllerOptions,
16
- FastifySchemaValidationError,
17
- FastifySerializerCompiler
16
+ FastifySerializerCompiler,
17
+ SchemaErrorFormatter
18
18
  } from './schema'
19
19
  import {
20
20
  FastifyTypeProvider,
@@ -393,6 +393,31 @@ export interface FastifyInstance<
393
393
  hook: onTimeoutAsyncHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>
394
394
  ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
395
395
 
396
+ /**
397
+ * `onRequestAbort` is useful if you need to monitor the if the client aborts the request (if the `request.raw.aborted` property is set to `true`).
398
+ * The `onRequestAbort` hook is executed when a client closes the connection before the entire request has been received. Therefore, you will not be able to send data to the client.
399
+ * Notice: client abort detection is not completely reliable. See: https://github.com/fastify/fastify/blob/main/docs/Guides/Detecting-When-Clients-Abort.md
400
+ */
401
+ addHook<
402
+ RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
403
+ ContextConfig = ContextConfigDefault,
404
+ SchemaCompiler extends FastifySchema = FastifySchema,
405
+ Logger extends FastifyBaseLogger = FastifyBaseLogger
406
+ >(
407
+ name: 'onRequestAbort',
408
+ hook: onRequestAbortHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>
409
+ ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
410
+
411
+ addHook<
412
+ RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
413
+ ContextConfig = ContextConfigDefault,
414
+ SchemaCompiler extends FastifySchema = FastifySchema,
415
+ Logger extends FastifyBaseLogger = FastifyBaseLogger
416
+ >(
417
+ name: 'onRequestAbort',
418
+ hook: onRequestAbortAsyncHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>
419
+ ): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
420
+
396
421
  /**
397
422
  * This hook is useful if you need to do some custom error logging or add some specific header in case of error.
398
423
  * It is not intended for changing the error, and calling reply.send will throw an exception.
@@ -530,7 +555,7 @@ export interface FastifyInstance<
530
555
  /*
531
556
  * Set the schema error formatter for all routes.
532
557
  */
533
- setSchemaErrorFormatter(errorFormatter: (errors: FastifySchemaValidationError[], dataVar: string) => Error): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
558
+ setSchemaErrorFormatter(errorFormatter: SchemaErrorFormatter): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
534
559
  /**
535
560
  * Add a content type parser
536
561
  */
package/types/route.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { FastifyInstance } from './instance'
2
2
  import { FastifyRequest, RequestGenericInterface } from './request'
3
3
  import { FastifyReply, ReplyGenericInterface } from './reply'
4
- import { FastifySchema, FastifySchemaCompiler, FastifySchemaValidationError, FastifySerializerCompiler } from './schema'
4
+ import { FastifySchema, FastifySchemaCompiler, FastifySerializerCompiler, SchemaErrorFormatter } from './schema'
5
5
  import { HTTPMethods, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, ContextConfigDefault } from './utils'
6
- import { preValidationHookHandler, preHandlerHookHandler, preSerializationHookHandler, onRequestHookHandler, preParsingHookHandler, onResponseHookHandler, onSendHookHandler, onErrorHookHandler, onTimeoutHookHandler } from './hooks'
6
+ import { preValidationHookHandler, preHandlerHookHandler, preSerializationHookHandler, onRequestHookHandler, preParsingHookHandler, onResponseHookHandler, onSendHookHandler, onErrorHookHandler, onTimeoutHookHandler, onRequestAbortHookHandler } from './hooks'
7
7
  import { FastifyError } from '@fastify/error'
8
8
  import { FastifyContext } from './context'
9
9
  import {
@@ -41,8 +41,7 @@ export interface RouteShorthandOptions<
41
41
  constraints?: { [name: string]: any },
42
42
  prefixTrailingSlash?: 'slash'|'no-slash'|'both';
43
43
  errorHandler?: (this: FastifyInstance, error: FastifyError, request: FastifyRequest, reply: FastifyReply) => void;
44
- // TODO: Change to actual type.
45
- schemaErrorFormatter?: (errors: FastifySchemaValidationError[], dataVar: string) => Error;
44
+ schemaErrorFormatter?: SchemaErrorFormatter;
46
45
 
47
46
  // hooks
48
47
  onRequest?: onRequestHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger> | onRequestHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>[];
@@ -54,6 +53,7 @@ export interface RouteShorthandOptions<
54
53
  onResponse?: onResponseHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger> | onResponseHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>[];
55
54
  onTimeout?: onTimeoutHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger> | onTimeoutHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>[];
56
55
  onError?: onErrorHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, SchemaCompiler, TypeProvider, Logger> | onErrorHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, SchemaCompiler, TypeProvider, Logger>[];
56
+ onRequestAbort?: onRequestAbortHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger> | onRequestAbortHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger>[];
57
57
  }
58
58
 
59
59
  /**
package/types/schema.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { ValidatorCompiler } from '@fastify/ajv-compiler'
2
- import { FastifyInstance, FastifyServerOptions } from '../fastify'
1
+ import { ValidatorFactory } from '@fastify/ajv-compiler'
2
+ import { SerializerFactory } from '@fastify/fast-json-stringify-compiler'
3
+ import { FastifyInstance } from '../fastify'
3
4
  /**
4
5
  * Schemas in Fastify follow the JSON-Schema standard. For this reason
5
6
  * we have opted to not ship strict schema based types. Instead we provide
@@ -50,7 +51,11 @@ export interface FastifySchemaControllerOptions{
50
51
  getSchemas(): Record<string, unknown>;
51
52
  };
52
53
  compilersFactory?: {
53
- buildValidator?: ValidatorCompiler;
54
- buildSerializer?: (externalSchemas: unknown, serializerOptsServerOption: FastifyServerOptions['serializerOpts']) => FastifySerializerCompiler<unknown>;
54
+ buildValidator?: ValidatorFactory;
55
+ buildSerializer?: SerializerFactory;
55
56
  };
56
57
  }
58
+
59
+ export type SchemaErrorDataVar = 'body' | 'headers' | 'params' | 'querystring'
60
+
61
+ export type SchemaErrorFormatter = (errors: FastifySchemaValidationError[], dataVar: SchemaErrorDataVar) => Error