fastify 4.7.0 → 4.8.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.
@@ -468,7 +468,7 @@ section.
468
468
  Fastify and protobufjs, together at last. Uses protobufjs by default.
469
469
  - [`fastify-qrcode`](https://github.com/chonla/fastify-qrcode) This plugin
470
470
  utilizes [qrcode](https://github.com/soldair/node-qrcode) to generate QR Code.
471
- - [`fastify-qs`](https://github.com/webdevium/fastify-qs) A plugin for Fastify
471
+ - [`fastify-qs`](https://github.com/vanodevium/fastify-qs) A plugin for Fastify
472
472
  that adds support for parsing URL query parameters with
473
473
  [qs](https://github.com/ljharb/qs).
474
474
  - [`fastify-racing`](https://github.com/metcoder95/fastify-racing) Fastify's
@@ -243,33 +243,66 @@ after initializing routes and plugins with `fastify.ready()`.
243
243
 
244
244
  Uses **app.js** from the previous example.
245
245
 
246
- **test-listen.js** (testing with
247
- [`Request`](https://www.npmjs.com/package/request))
246
+ **test-listen.js** (testing with [`undici`](https://www.npmjs.com/package/undici))
248
247
  ```js
249
248
  const tap = require('tap')
250
- const request = require('request')
249
+ const { Client } = require('undici')
251
250
  const buildFastify = require('./app')
252
251
 
253
- tap.test('GET `/` route', t => {
254
- t.plan(5)
252
+ tap.test('should work with undici', async t => {
253
+ t.plan(2)
255
254
 
256
255
  const fastify = buildFastify()
257
256
 
258
- t.teardown(() => fastify.close())
257
+ await fastify.listen()
259
258
 
260
- fastify.listen({ port: 0 }, (err) => {
261
- t.error(err)
259
+ const client = new Client(
260
+ 'http://localhost:' + fastify.server.address().port, {
261
+ keepAliveTimeout: 10,
262
+ keepAliveMaxTimeout: 10
263
+ }
264
+ )
262
265
 
263
- request({
264
- method: 'GET',
265
- url: 'http://localhost:' + fastify.server.address().port
266
- }, (err, response, body) => {
267
- t.error(err)
268
- t.equal(response.statusCode, 200)
269
- t.equal(response.headers['content-type'], 'application/json; charset=utf-8')
270
- t.same(JSON.parse(body), { hello: 'world' })
271
- })
266
+ t.teardown(() => {
267
+ fastify.close()
268
+ client.close()
272
269
  })
270
+
271
+ const response = await client.request({ method: 'GET', path: '/' })
272
+
273
+ t.equal(await response.body.text(), '{"hello":"world"}')
274
+ t.equal(response.statusCode, 200)
275
+ })
276
+ ```
277
+
278
+ Alternatively, starting with Node.js 18,
279
+ [`fetch`](https://nodejs.org/docs/latest-v18.x/api/globals.html#fetch)
280
+ may be used without requiring any extra dependencies:
281
+
282
+ **test-listen.js**
283
+ ```js
284
+ const tap = require('tap')
285
+ const buildFastify = require('./app')
286
+
287
+ tap.test('should work with fetch', async t => {
288
+ t.plan(3)
289
+
290
+ const fastify = buildFastify()
291
+
292
+ t.teardown(() => fastify.close())
293
+
294
+ await fastify.listen()
295
+
296
+ const response = await fetch(
297
+ 'http://localhost:' + fastify.server.address().port
298
+ )
299
+
300
+ t.equal(response.status, 200)
301
+ t.equal(
302
+ response.headers.get('content-type'),
303
+ 'application/json; charset=utf-8'
304
+ )
305
+ t.has(await response.json(), { hello: 'world' })
273
306
  })
274
307
  ```
275
308
 
@@ -83,15 +83,52 @@ Some things to consider in your custom error handler:
83
83
  ### Fastify Error Codes
84
84
  <a id="fastify-error-codes"></a>
85
85
 
86
- #### FST_ERR_BAD_URL
87
- <a id="FST_ERR_BAD_URL"></a>
88
-
89
- The router received an invalid url.
90
-
91
- <a name="FST_ERR_DUPLICATED_ROUTE"></a>
92
- #### FST_ERR_DUPLICATED_ROUTE
93
-
94
- The HTTP method already has a registered controller for that URL
86
+ You can access `errorCodes` for mapping:
87
+ ```js
88
+ // ESM
89
+ import { errorCodes } from 'fastify'
90
+
91
+ // CommonJs
92
+ const errorCodes = require('fastify').errorCodes
93
+ ```
94
+
95
+ For example:
96
+ ```js
97
+ const Fastify = require('./fastify')
98
+
99
+ // Instantiate the framework
100
+ const fastify = Fastify({
101
+ logger: true
102
+ })
103
+
104
+ // Declare a route
105
+ fastify.get('/', function (request, reply) {
106
+ reply.code('bad status code').send({ hello: 'world' })
107
+ })
108
+
109
+ fastify.setErrorHandler(function (error, request, reply) {
110
+ if (error instanceof Fastify.errorCodes.FST_ERR_BAD_STATUS_CODE) {
111
+ // Log error
112
+ this.log.error(error)
113
+ // Send error response
114
+ reply.status(500).send({ ok: false })
115
+ }
116
+ })
117
+
118
+ // Run the server!
119
+ fastify.listen({ port: 3000 }, function (err, address) {
120
+ if (err) {
121
+ fastify.log.error(err)
122
+ process.exit(1)
123
+ }
124
+ // Server is now listening on ${address}
125
+ })
126
+ ```
127
+
128
+ #### FST_ERR_NOT_FOUND
129
+ <a id="FST_ERR_NOT_FOUND"></a>
130
+
131
+ 404 Not Found.
95
132
 
96
133
  <a name="FST_ERR_CTP_ALREADY_PRESENT"></a>
97
134
  #### FST_ERR_CTP_ALREADY_PRESENT
@@ -99,130 +136,238 @@ The HTTP method already has a registered controller for that URL
99
136
 
100
137
  The parser for this content type was already registered.
101
138
 
102
- #### FST_ERR_CTP_BODY_TOO_LARGE
103
- <a id="FST_ERR_CTP_BODY_TOO_LARGE"></a>
104
-
105
- The request body is larger than the provided limit.
139
+ #### FST_ERR_CTP_INVALID_TYPE
140
+ <a id="FST_ERR_CTP_INVALID_TYPE"></a>
106
141
 
107
- This setting can be defined in the Fastify server instance:
108
- [`bodyLimit`](./Server.md#bodylimit)
142
+ The `Content-Type` should be a string.
109
143
 
110
144
  #### FST_ERR_CTP_EMPTY_TYPE
111
145
  <a id="FST_ERR_CTP_EMPTY_TYPE"></a>
112
146
 
113
147
  The content type cannot be an empty string.
114
148
 
115
- #### FST_ERR_CTP_INVALID_CONTENT_LENGTH
116
- <a id="FST_ERR_CTP_INVALID_CONTENT_LENGTH"></a>
117
-
118
- Request body size did not match Content-Length.
119
-
120
149
  #### FST_ERR_CTP_INVALID_HANDLER
121
150
  <a id="FST_ERR_CTP_INVALID_HANDLER"></a>
122
151
 
123
152
  An invalid handler was passed for the content type.
124
153
 
154
+ #### FST_ERR_CTP_INVALID_PARSE_TYPE
155
+ <a id="FST_ERR_CTP_INVALID_PARSE_TYPE"></a>
156
+
157
+ The provided parse type is not supported. Accepted values are `string` or
158
+ `buffer`.
159
+
160
+ #### FST_ERR_CTP_BODY_TOO_LARGE
161
+ <a id="FST_ERR_CTP_BODY_TOO_LARGE"></a>
162
+
163
+ The request body is larger than the provided limit.
164
+
165
+ This setting can be defined in the Fastify server instance:
166
+ [`bodyLimit`](./Server.md#bodylimit)
167
+
125
168
  #### FST_ERR_CTP_INVALID_MEDIA_TYPE
126
169
  <a id="FST_ERR_CTP_INVALID_MEDIA_TYPE"></a>
127
170
 
128
171
  The received media type is not supported (i.e. there is no suitable
129
172
  `Content-Type` parser for it).
130
173
 
131
- #### FST_ERR_CTP_INVALID_PARSE_TYPE
132
- <a id="FST_ERR_CTP_INVALID_PARSE_TYPE"></a>
174
+ #### FST_ERR_CTP_INVALID_CONTENT_LENGTH
175
+ <a id="FST_ERR_CTP_INVALID_CONTENT_LENGTH"></a>
133
176
 
134
- The provided parse type is not supported. Accepted values are `string` or
135
- `buffer`.
177
+ Request body size did not match `Content-Length`.
136
178
 
137
- #### FST_ERR_CTP_INVALID_TYPE
138
- <a id="FST_ERR_CTP_INVALID_TYPE"></a>
179
+ #### FST_ERR_CTP_EMPTY_JSON_BODY
180
+ <a id="FST_ERR_CTP_EMPTY_JSON_BODY"></a>
139
181
 
140
- The `Content-Type` should be a string.
182
+ Body cannot be empty when content-type is set to `application/json`.
141
183
 
142
184
  #### FST_ERR_DEC_ALREADY_PRESENT
143
185
  <a id="FST_ERR_DEC_ALREADY_PRESENT"></a>
144
186
 
145
187
  A decorator with the same name is already registered.
146
188
 
189
+ #### FST_ERR_DEC_DEPENDENCY_INVALID_TYPE
190
+ <a id="FST_ERR_DEC_DEPENDENCY_INVALID_TYPE"></a>
191
+
192
+ The dependencies of decorator must be of type `Array`.
193
+
147
194
  #### FST_ERR_DEC_MISSING_DEPENDENCY
148
195
  <a id="FST_ERR_DEC_MISSING_DEPENDENCY"></a>
149
196
 
150
197
  The decorator cannot be registered due to a missing dependency.
151
198
 
152
- #### FST_ERR_HOOK_INVALID_HANDLER
153
- <a id="FST_ERR_HOOK_INVALID_HANDLER"></a>
199
+ #### FST_ERR_DEC_AFTER_START
200
+ <a id="FST_ERR_DEC_AFTER_START"></a>
154
201
 
155
- The hook callback must be a function.
202
+ The decorator cannot be added after start.
156
203
 
157
204
  #### FST_ERR_HOOK_INVALID_TYPE
158
205
  <a id="FST_ERR_HOOK_INVALID_TYPE"></a>
159
206
 
160
207
  The hook name must be a string.
161
208
 
209
+ #### FST_ERR_HOOK_INVALID_HANDLER
210
+ <a id="FST_ERR_HOOK_INVALID_HANDLER"></a>
211
+
212
+ The hook callback must be a function.
213
+
214
+ #### FST_ERR_MISSING_MIDDLEWARE
215
+ <a id="FST_ERR_MISSING_MIDDLEWARE"></a>
216
+
217
+ You must register a plugin for handling middlewares,
218
+ visit [`Middleware`](./Middleware.md) for more info.
219
+
220
+ <a name="FST_ERR_HOOK_TIMEOUT"></a>
221
+ #### FST_ERR_HOOK_TIMEOUT
222
+
223
+ A callback for a hook timed out
224
+
162
225
  #### FST_ERR_LOG_INVALID_DESTINATION
163
226
  <a id="FST_ERR_LOG_INVALID_DESTINATION"></a>
164
227
 
165
228
  The logger accepts either a `'stream'` or a `'file'` as the destination.
166
229
 
167
- #### FST_ERR_PROMISE_NOT_FULFILLED
168
- <a id="FST_ERR_PROMISE_NOT_FULFILLED"></a>
230
+ #### FST_ERR_REP_INVALID_PAYLOAD_TYPE
231
+ <a id="FST_ERR_REP_INVALID_PAYLOAD_TYPE"></a>
169
232
 
170
- A promise may not be fulfilled with 'undefined' when statusCode is not 204.
233
+ Reply payload can be either a `string` or a `Buffer`.
171
234
 
172
235
  #### FST_ERR_REP_ALREADY_SENT
173
236
  <a id="FST_ERR_REP_ALREADY_SENT"></a>
174
237
 
175
238
  A response was already sent.
176
239
 
177
- #### FST_ERR_REP_INVALID_PAYLOAD_TYPE
178
- <a id="FST_ERR_REP_INVALID_PAYLOAD_TYPE"></a>
240
+ #### FST_ERR_REP_SENT_VALUE
241
+ <a id="FST_ERR_REP_SENT_VALUE"></a>
179
242
 
180
- Reply payload can be either a `string` or a `Buffer`.
243
+ The only possible value for `reply.sent` is `true`.
181
244
 
182
- #### FST_ERR_SCH_ALREADY_PRESENT
183
- <a id="FST_ERR_SCH_ALREADY_PRESENT"></a>
245
+ #### FST_ERR_SEND_INSIDE_ONERR
246
+ <a id="FST_ERR_SEND_INSIDE_ONERR"></a>
184
247
 
185
- A schema with the same `$id` already exists.
248
+ You cannot use `send` inside the `onError` hook.
249
+
250
+ #### FST_ERR_SEND_UNDEFINED_ERR
251
+ <a id="FST_ERR_SEND_UNDEFINED_ERR"></a>
252
+
253
+ Undefined error has occurred.
254
+
255
+ #### FST_ERR_BAD_STATUS_CODE
256
+ <a id="FST_ERR_BAD_STATUS_CODE"></a>
257
+
258
+ Called `reply` with an invalid status code.
259
+
260
+ #### FST_ERR_BAD_TRAILER_NAME
261
+ <a id="FST_ERR_BAD_TRAILER_NAME"></a>
262
+
263
+ Called `reply.trailer` with an invalid header name.
264
+
265
+ #### FST_ERR_BAD_TRAILER_VALUE
266
+ <a id="FST_ERR_BAD_TRAILER_VALUE"></a>
267
+
268
+ Called `reply.trailer` with an invalid type. Expected a function.
269
+
270
+ #### FST_ERR_MISSING_SERIALIZATION_FN
271
+ <a id="FST_ERR_MISSING_SERIALIZATION_FN"></a>
272
+
273
+ Missing serialization function.
274
+
275
+ #### FST_ERR_REQ_INVALID_VALIDATION_INVOCATION
276
+ <a id="FST_ERR_REQ_INVALID_VALIDATION_INVOCATION"></a>
277
+
278
+ Invalid validation invocation. Missing validation function for
279
+ HTTP part nor schema provided.
186
280
 
187
281
  #### FST_ERR_SCH_MISSING_ID
188
282
  <a id="FST_ERR_SCH_MISSING_ID"></a>
189
283
 
190
284
  The schema provided does not have `$id` property.
191
285
 
192
- #### FST_ERR_SCH_SERIALIZATION_BUILD
193
- <a id="FST_ERR_SCH_SERIALIZATION_BUILD"></a>
286
+ #### FST_ERR_SCH_ALREADY_PRESENT
287
+ <a id="FST_ERR_SCH_ALREADY_PRESENT"></a>
194
288
 
195
- The JSON schema provided for serialization of a route response is not valid.
289
+ A schema with the same `$id` already exists.
290
+
291
+ #### FST_ERR_SCH_DUPLICATE
292
+ <a id="FST_ERR_SCH_DUPLICATE"></a>
293
+
294
+ Schema with the same `$id` already present!
196
295
 
197
296
  #### FST_ERR_SCH_VALIDATION_BUILD
198
297
  <a id="FST_ERR_SCH_VALIDATION_BUILD"></a>
199
298
 
200
299
  The JSON schema provided for validation to a route is not valid.
201
300
 
202
- #### FST_ERR_SEND_INSIDE_ONERR
203
- <a id="FST_ERR_SEND_INSIDE_ONERR"></a>
301
+ #### FST_ERR_SCH_SERIALIZATION_BUILD
302
+ <a id="FST_ERR_SCH_SERIALIZATION_BUILD"></a>
204
303
 
205
- You cannot use `send` inside the `onError` hook.
304
+ The JSON schema provided for serialization of a route response is not valid.
206
305
 
207
- #### FST_ERR_SEND_UNDEFINED_ERR
208
- <a id="FST_ERR_SEND_UNDEFINED_ERR"></a>
306
+ #### FST_ERR_HTTP2_INVALID_VERSION
307
+ <a id="FST_ERR_HTTP2_INVALID_VERSION"></a>
209
308
 
210
- Undefined error has occurred.
309
+ HTTP2 is available only from node >= 8.8.1.
211
310
 
212
- <a name="FST_ERR_PLUGIN_NOT_VALID"></a>
213
- #### FST_ERR_PLUGIN_NOT_VALID
311
+ #### FST_ERR_INIT_OPTS_INVALID
312
+ <a id="FST_ERR_INIT_OPTS_INVALID"></a>
214
313
 
215
- Plugin must be a function or a promise.
314
+ Invalid initialization options.
216
315
 
217
- <a name="FST_ERR_PLUGIN_TIMEOUT"></a>
218
- #### FST_ERR_PLUGIN_TIMEOUT
316
+ #### FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE
317
+ <a id="FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE"></a>
219
318
 
220
- Plugin did not start in time. Default timeout (in millis): `10000`
319
+ Cannot set forceCloseConnections to `idle` as your HTTP server
320
+ does not support `closeIdleConnections` method.
221
321
 
222
- <a name="FST_ERR_HOOK_TIMEOUT"></a>
223
- #### FST_ERR_HOOK_TIMEOUT
322
+ <a name="FST_ERR_DUPLICATED_ROUTE"></a>
323
+ #### FST_ERR_DUPLICATED_ROUTE
224
324
 
225
- A callback for a hook timed out
325
+ The HTTP method already has a registered controller for that URL
326
+
327
+ #### FST_ERR_BAD_URL
328
+ <a id="FST_ERR_BAD_URL"></a>
329
+
330
+ The router received an invalid url.
331
+
332
+ ### FST_ERR_ASYNC_CONSTRAINT
333
+ <a id="FST_ERR_ASYNC_CONSTRAINT"></a>
334
+
335
+ The router received error when using asynchronous constraints.
336
+
337
+ #### FST_ERR_DEFAULT_ROUTE_INVALID_TYPE
338
+ <a id="FST_ERR_DEFAULT_ROUTE_INVALID_TYPE"></a>
339
+
340
+ The `defaultRoute` type should be a function.
341
+
342
+ #### FST_ERR_INVALID_URL
343
+ <a id="FST_ERR_INVALID_URL"></a>
344
+
345
+ URL must be a string.
346
+
347
+ #### FST_ERR_REOPENED_CLOSE_SERVER
348
+ <a id="FST_ERR_REOPENED_CLOSE_SERVER"></a>
349
+
350
+ Fastify has already been closed and cannot be reopened.
351
+
352
+ #### FST_ERR_REOPENED_SERVER
353
+ <a id="FST_ERR_REOPENED_SERVER"></a>
354
+
355
+ Fastify is already listening.
356
+
357
+ #### FST_ERR_PLUGIN_VERSION_MISMATCH
358
+ <a id="FST_ERR_PLUGIN_VERSION_MISMATCH"></a>
359
+
360
+ Installed Fastify plugin mismatched expected version.
361
+
362
+ <a name="FST_ERR_PLUGIN_CALLBACK_NOT_FN"></a>
363
+ #### FST_ERR_PLUGIN_CALLBACK_NOT_FN
364
+
365
+ Callback for a hook is not a function (mapped directly from `avvio`)
366
+
367
+ <a name="FST_ERR_PLUGIN_NOT_VALID"></a>
368
+ #### FST_ERR_PLUGIN_NOT_VALID
369
+
370
+ Plugin must be a function or a promise.
226
371
 
227
372
  <a name="FST_ERR_ROOT_PLG_BOOTED"></a>
228
373
  #### FST_ERR_ROOT_PLG_BOOTED
@@ -234,7 +379,7 @@ Root plugin has already booted (mapped directly from `avvio`)
234
379
 
235
380
  Impossible to load plugin because the parent (mapped directly from `avvio`)
236
381
 
237
- <a name="FST_ERR_PLUGIN_CALLBACK_NOT_FN"></a>
238
- #### FST_ERR_PLUGIN_CALLBACK_NOT_FN
382
+ <a name="FST_ERR_PLUGIN_TIMEOUT"></a>
383
+ #### FST_ERR_PLUGIN_TIMEOUT
239
384
 
240
- Callback for a hook is not a function (mapped directly from `avvio`)
385
+ Plugin did not start in time. Default timeout (in millis): `10000`
@@ -449,6 +449,33 @@ fastify.addHook('onRoute', (routeOptions) => {
449
449
  })
450
450
  ```
451
451
 
452
+ If you want to add more routes within an onRoute hook, you have to tag these
453
+ routes properly. If you don't, the hook will run into an infinite loop. The
454
+ recommended approach is shown below.
455
+
456
+ ```js
457
+ const kRouteAlreadyProcessed = Symbol('route-already-processed')
458
+
459
+ fastify.addHook('onRoute', function (routeOptions) {
460
+ const { url, method } = routeOptions
461
+
462
+ const isAlreadyProcessed = (routeOptions.custom && routeOptions.custom[kRouteAlreadyProcessed]) || false
463
+
464
+ if (!isAlreadyProcessed) {
465
+ this.route({
466
+ url,
467
+ method,
468
+ custom: {
469
+ [kRouteAlreadyProcessed]: true
470
+ },
471
+ handler: () => {}
472
+ })
473
+ }
474
+ })
475
+ ```
476
+
477
+ For more details, see this [issue](https://github.com/fastify/fastify/issues/4319).
478
+
452
479
  ### onRegister
453
480
  <a id="on-register"></a>
454
481
 
@@ -732,6 +732,60 @@ fastify.route({
732
732
  })
733
733
  ```
734
734
 
735
+ #### Asynchronous Custom Constraints
736
+
737
+ You can provide your custom constraints and the `constraint` criteria can be
738
+ fetched from other source, for example `database`. Usage of asynchronous custom
739
+ constraint should place at the last resort since it impacts the router
740
+ performance.
741
+
742
+ ```js
743
+ function databaseOperation(field, done) {
744
+ done(null, field)
745
+ }
746
+
747
+ const secret = {
748
+ // strategy name for referencing in the route handler `constraints` options
749
+ name: 'secret',
750
+ // storage factory for storing routes in the find-my-way route tree
751
+ storage: function () {
752
+ let handlers = {}
753
+ return {
754
+ get: (type) => { return handlers[type] || null },
755
+ set: (type, store) => { handlers[type] = store }
756
+ }
757
+ },
758
+ // function to get the value of the constraint from each incoming request
759
+ deriveConstraint: (req, ctx, done) => {
760
+ databaseOperation(req.headers['secret'], done)
761
+ },
762
+ // optional flag marking if handlers without constraints can match requests that have a value for this constraint
763
+ mustMatchWhenDerived: true
764
+ }
765
+ ```
766
+
767
+ > ## ⚠ Security Notice
768
+ > When using with asynchronous constraint. It is highly recommend never return error
769
+ > inside the callback. If the error is not preventable, it is recommended to provide
770
+ > a custom `frameworkErrors` handler to deal with it. Otherwise, you route selection
771
+ > may break or expose sensitive information to attackers.
772
+ >
773
+ > ```js
774
+ > const Fastify = require('fastify')
775
+ >
776
+ > const fastify = Fastify({
777
+ > frameworkErrors: function(err, res, res) {
778
+ > if(err instanceof Fastify.errorCodes.FST_ERR_ASYNC_CONSTRAINT) {
779
+ > res.code(400)
780
+ > return res.send("Invalid header provided")
781
+ > } else {
782
+ > res.send(err)
783
+ > }
784
+ > }
785
+ > })
786
+ > ```
787
+
788
+
735
789
  ### ⚠ HTTP version check
736
790
 
737
791
  Fastify will check the HTTP version of every request, based on configuration
@@ -731,6 +731,9 @@ const fastify = require('fastify')({
731
731
  if (error instanceof FST_ERR_BAD_URL) {
732
732
  res.code(400)
733
733
  return res.send("Provided url is not valid")
734
+ } else if(error instanceof FST_ERR_ASYNC_CONSTRAINT) {
735
+ res.code(400)
736
+ return res.send("Provided header is not valid")
734
737
  } else {
735
738
  res.send(err)
736
739
  }
package/fastify.d.ts CHANGED
@@ -20,6 +20,7 @@ import { SerializerCompiler } from '@fastify/fast-json-stringify-compiler'
20
20
  import { FastifySchema } from './types/schema'
21
21
  import { FastifyContextConfig } from './types/context'
22
22
  import { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'
23
+ import { FastifyErrorCodes } from './types/errors'
23
24
 
24
25
  /**
25
26
  * Fastify factory function for the standard fastify http, https, or http2 server instance.
@@ -61,6 +62,10 @@ declare function fastify<
61
62
  TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
62
63
  >(opts?: FastifyServerOptions<Server, Logger>): FastifyInstance<Server, Request, Reply, Logger, TypeProvider> & PromiseLike<FastifyInstance<Server, Request, Reply, Logger, TypeProvider>>
63
64
 
65
+ declare namespace fastify {
66
+ export const errorCodes: FastifyErrorCodes;
67
+ }
68
+
64
69
  export default fastify
65
70
 
66
71
  export type FastifyHttp2SecureOptions<
@@ -206,4 +211,5 @@ export { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaul
206
211
  export * from './types/hooks'
207
212
  export { FastifyServerFactory, FastifyServerFactoryHandler } from './types/serverFactory'
208
213
  export { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider'
214
+ export { FastifyErrorCodes } from './types/errors'
209
215
  export { fastify }
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.7.0'
3
+ const VERSION = '4.8.0'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
@@ -48,14 +48,19 @@ const getSecuredInitialConfig = require('./lib/initialConfigValidation')
48
48
  const override = require('./lib/pluginOverride')
49
49
  const warning = require('./lib/warnings')
50
50
  const noopSet = require('./lib/noop-set')
51
+ const {
52
+ appendStackTrace,
53
+ AVVIO_ERRORS_MAP,
54
+ ...errorCodes
55
+ } = require('./lib/errors')
56
+
51
57
  const { defaultInitOptions } = getSecuredInitialConfig
52
58
 
53
59
  const {
60
+ FST_ERR_ASYNC_CONSTRAINT,
54
61
  FST_ERR_BAD_URL,
55
- FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE,
56
- AVVIO_ERRORS_MAP,
57
- appendStackTrace
58
- } = require('./lib/errors')
62
+ FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE
63
+ } = errorCodes
59
64
 
60
65
  const { buildErrorHandler } = require('./lib/error-handler.js')
61
66
 
@@ -178,7 +183,7 @@ function fastify (options) {
178
183
  const fourOhFour = build404(options)
179
184
 
180
185
  // HTTP server and its handler
181
- const httpHandler = wrapRouting(router.routing, options)
186
+ const httpHandler = wrapRouting(router, options)
182
187
 
183
188
  // we need to set this before calling createServer
184
189
  options.http2SessionTimeout = initialConfig.http2SessionTimeout
@@ -662,6 +667,30 @@ function fastify (options) {
662
667
  res.end(body)
663
668
  }
664
669
 
670
+ function buildAsyncConstraintCallback (isAsync, req, res) {
671
+ if (isAsync === false) return undefined
672
+ return function onAsyncConstraintError (err) {
673
+ if (err) {
674
+ if (frameworkErrors) {
675
+ const id = genReqId(req)
676
+ const childLogger = logger.child({ reqId: id })
677
+
678
+ childLogger.info({ req }, 'incoming request')
679
+
680
+ const request = new Request(id, null, req, null, childLogger, onBadUrlContext)
681
+ const reply = new Reply(res, request, childLogger)
682
+ return frameworkErrors(new FST_ERR_ASYNC_CONSTRAINT(), request, reply)
683
+ }
684
+ const body = '{"error":"Internal Server Error","message":"Unexpected error from async constraint","statusCode":500}'
685
+ res.writeHead(500, {
686
+ 'Content-Type': 'application/json',
687
+ 'Content-Length': body.length
688
+ })
689
+ res.end(body)
690
+ }
691
+ }
692
+ }
693
+
665
694
  function setNotFoundHandler (opts, handler) {
666
695
  throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!')
667
696
 
@@ -718,8 +747,31 @@ function fastify (options) {
718
747
  opts.includeMeta = opts.includeHooks ? opts.includeMeta ? supportedHooks.concat(opts.includeMeta) : supportedHooks : opts.includeMeta
719
748
  return router.printRoutes(opts)
720
749
  }
750
+
751
+ function wrapRouting (router, { rewriteUrl, logger }) {
752
+ let isAsync
753
+ return function preRouting (req, res) {
754
+ // only call isAsyncConstraint once
755
+ if (isAsync === undefined) isAsync = router.isAsyncConstraint()
756
+ if (rewriteUrl) {
757
+ const originalUrl = req.url
758
+ const url = rewriteUrl(req)
759
+ if (originalUrl !== url) {
760
+ logger.debug({ originalUrl, url }, 'rewrite url')
761
+ if (typeof url === 'string') {
762
+ req.url = url
763
+ } else {
764
+ req.destroy(new Error(`Rewrite url for "${req.url}" needs to be of type "string" but received "${typeof url}"`))
765
+ }
766
+ }
767
+ }
768
+ router.routing(req, res, buildAsyncConstraintCallback(isAsync, req, res))
769
+ }
770
+ }
721
771
  }
722
772
 
773
+ fastify.errorCodes = errorCodes
774
+
723
775
  function validateSchemaErrorFormatter (schemaErrorFormatter) {
724
776
  if (typeof schemaErrorFormatter !== 'function') {
725
777
  throw new Error(`schemaErrorFormatter option should be a function, instead got ${typeof schemaErrorFormatter}`)
@@ -728,25 +780,6 @@ function validateSchemaErrorFormatter (schemaErrorFormatter) {
728
780
  }
729
781
  }
730
782
 
731
- function wrapRouting (httpHandler, { rewriteUrl, logger }) {
732
- if (!rewriteUrl) {
733
- return httpHandler
734
- }
735
- return function preRouting (req, res) {
736
- const originalUrl = req.url
737
- const url = rewriteUrl(req)
738
- if (originalUrl !== url) {
739
- logger.debug({ originalUrl, url }, 'rewrite url')
740
- if (typeof url === 'string') {
741
- req.url = url
742
- } else {
743
- req.destroy(new Error(`Rewrite url for "${req.url}" needs to be of type "string" but received "${typeof url}"`))
744
- }
745
- }
746
- httpHandler(req, res)
747
- }
748
- }
749
-
750
783
  /**
751
784
  * These export configurations enable JS and TS developers
752
785
  * to consumer fastify in whatever way best suits their needs.
package/lib/errors.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const createError = require('@fastify/error')
4
+
4
5
  const codes = {
5
6
  /**
6
7
  * Basic
@@ -225,6 +226,11 @@ const codes = {
225
226
  "'%s' is not a valid url component",
226
227
  400
227
228
  ),
229
+ FST_ERR_ASYNC_CONSTRAINT: createError(
230
+ 'FST_ERR_ASYNC_CONSTRAINT',
231
+ 'Unexpected error from async constraint',
232
+ 500
233
+ ),
228
234
  FST_ERR_DEFAULT_ROUTE_INVALID_TYPE: createError(
229
235
  'FST_ERR_DEFAULT_ROUTE_INVALID_TYPE',
230
236
  'The defaultRoute type should be a function',
@@ -258,37 +264,42 @@ const codes = {
258
264
  ),
259
265
 
260
266
  /**
261
- * Avvio Errors map
267
+ * Avvio Errors
262
268
  */
263
- AVVIO_ERRORS_MAP: {
264
- AVV_ERR_CALLBACK_NOT_FN: createError(
265
- 'FST_ERR_PLUGIN_CALLBACK_NOT_FN',
266
- 'fastify-plugin: %s'
267
- ),
268
- AVV_ERR_PLUGIN_NOT_VALID: createError(
269
- 'FST_ERR_PLUGIN_NOT_VALID',
270
- 'fastify-plugin: %s'
271
- ),
272
- AVV_ERR_ROOT_PLG_BOOTED: createError(
273
- 'FST_ERR_ROOT_PLG_BOOTED',
274
- 'fastify-plugin: %s'
275
- ),
276
- AVV_ERR_PARENT_PLG_LOADED: createError(
277
- 'FST_ERR_PARENT_PLUGIN_BOOTED',
278
- 'fastify-plugin: %s'
279
- ),
280
- AVV_ERR_READY_TIMEOUT: createError(
281
- 'FST_ERR_PLUGIN_TIMEOUT',
282
- 'fastify-plugin: %s'
283
- )
284
- },
269
+ FST_ERR_PLUGIN_CALLBACK_NOT_FN: createError(
270
+ 'FST_ERR_PLUGIN_CALLBACK_NOT_FN',
271
+ 'fastify-plugin: %s'
272
+ ),
273
+ FST_ERR_PLUGIN_NOT_VALID: createError(
274
+ 'FST_ERR_PLUGIN_NOT_VALID',
275
+ 'fastify-plugin: %s'
276
+ ),
277
+ FST_ERR_ROOT_PLG_BOOTED: createError(
278
+ 'FST_ERR_ROOT_PLG_BOOTED',
279
+ 'fastify-plugin: %s'
280
+ ),
281
+ FST_ERR_PARENT_PLUGIN_BOOTED: createError(
282
+ 'FST_ERR_PARENT_PLUGIN_BOOTED',
283
+ 'fastify-plugin: %s'
284
+ ),
285
+ FST_ERR_PLUGIN_TIMEOUT: createError(
286
+ 'FST_ERR_PLUGIN_TIMEOUT',
287
+ 'fastify-plugin: %s'
288
+ )
289
+ }
285
290
 
286
- // Util function
287
- appendStackTrace (oldErr, newErr) {
288
- newErr.cause = oldErr
291
+ function appendStackTrace (oldErr, newErr) {
292
+ newErr.cause = oldErr
289
293
 
290
- return newErr
291
- }
294
+ return newErr
292
295
  }
293
296
 
294
297
  module.exports = codes
298
+ module.exports.appendStackTrace = appendStackTrace
299
+ module.exports.AVVIO_ERRORS_MAP = {
300
+ AVV_ERR_CALLBACK_NOT_FN: codes.FST_ERR_PLUGIN_CALLBACK_NOT_FN,
301
+ AVV_ERR_PLUGIN_NOT_VALID: codes.FST_ERR_PLUGIN_NOT_VALID,
302
+ AVV_ERR_ROOT_PLG_BOOTED: codes.FST_ERR_ROOT_PLG_BOOTED,
303
+ AVV_ERR_PARENT_PLG_LOADED: codes.FST_ERR_PARENT_PLUGIN_BOOTED,
304
+ AVV_ERR_READY_TIMEOUT: codes.FST_ERR_PLUGIN_TIMEOUT
305
+ }
package/lib/route.js CHANGED
@@ -101,7 +101,8 @@ function buildRouting (options) {
101
101
  closeRoutes: () => { closing = true },
102
102
  printRoutes: router.prettyPrint.bind(router),
103
103
  addConstraintStrategy,
104
- hasConstraintStrategy
104
+ hasConstraintStrategy,
105
+ isAsyncConstraint
105
106
  }
106
107
 
107
108
  function addConstraintStrategy (strategy) {
@@ -113,6 +114,10 @@ function buildRouting (options) {
113
114
  return router.hasConstraintStrategy(strategyName)
114
115
  }
115
116
 
117
+ function isAsyncConstraint () {
118
+ return router.constrainer.asyncStrategiesInUse.size > 0
119
+ }
120
+
116
121
  // Convert shorthand to extended route declaration
117
122
  function prepareRoute ({ method, url, options, handler, isFastify }) {
118
123
  if (typeof url !== 'string') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -176,7 +176,7 @@
176
176
  "@fastify/fast-json-stringify-compiler": "^4.1.0",
177
177
  "abstract-logging": "^2.0.1",
178
178
  "avvio": "^8.2.0",
179
- "find-my-way": "^7.2.0",
179
+ "find-my-way": "^7.3.0",
180
180
  "light-my-request": "^5.6.1",
181
181
  "pino": "^8.5.0",
182
182
  "process-warning": "^2.0.0",
@@ -660,3 +660,117 @@ test('allows separate constrained and unconstrained HEAD routes', async (t) => {
660
660
 
661
661
  t.ok(true)
662
662
  })
663
+
664
+ test('allow async constraints', async (t) => {
665
+ t.plan(5)
666
+
667
+ const constraint = {
668
+ name: 'secret',
669
+ storage: function () {
670
+ const secrets = {}
671
+ return {
672
+ get: (secret) => { return secrets[secret] || null },
673
+ set: (secret, store) => { secrets[secret] = store }
674
+ }
675
+ },
676
+ deriveConstraint: (req, ctx, done) => {
677
+ done(null, req.headers['x-secret'])
678
+ },
679
+ validate () { return true }
680
+ }
681
+
682
+ const fastify = Fastify({ constraints: { secret: constraint } })
683
+
684
+ fastify.route({
685
+ method: 'GET',
686
+ url: '/',
687
+ constraints: { secret: 'alpha' },
688
+ handler: (req, reply) => {
689
+ reply.send({ hello: 'from alpha' })
690
+ }
691
+ })
692
+
693
+ fastify.route({
694
+ method: 'GET',
695
+ url: '/',
696
+ constraints: { secret: 'beta' },
697
+ handler: (req, reply) => {
698
+ reply.send({ hello: 'from beta' })
699
+ }
700
+ })
701
+
702
+ {
703
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'alpha' } })
704
+ t.same(JSON.parse(payload), { hello: 'from alpha' })
705
+ t.equal(statusCode, 200)
706
+ }
707
+ {
708
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'beta' } })
709
+ t.same(JSON.parse(payload), { hello: 'from beta' })
710
+ t.equal(statusCode, 200)
711
+ }
712
+ {
713
+ const { statusCode } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'gamma' } })
714
+ t.equal(statusCode, 404)
715
+ }
716
+ })
717
+
718
+ test('error in async constraints', async (t) => {
719
+ t.plan(8)
720
+
721
+ const constraint = {
722
+ name: 'secret',
723
+ storage: function () {
724
+ const secrets = {}
725
+ return {
726
+ get: (secret) => { return secrets[secret] || null },
727
+ set: (secret, store) => { secrets[secret] = store }
728
+ }
729
+ },
730
+ deriveConstraint: (req, ctx, done) => {
731
+ done(Error('kaboom'))
732
+ },
733
+ validate () { return true }
734
+ }
735
+
736
+ const fastify = Fastify({ constraints: { secret: constraint } })
737
+
738
+ fastify.route({
739
+ method: 'GET',
740
+ url: '/',
741
+ constraints: { secret: 'alpha' },
742
+ handler: (req, reply) => {
743
+ reply.send({ hello: 'from alpha' })
744
+ }
745
+ })
746
+
747
+ fastify.route({
748
+ method: 'GET',
749
+ url: '/',
750
+ constraints: { secret: 'beta' },
751
+ handler: (req, reply) => {
752
+ reply.send({ hello: 'from beta' })
753
+ }
754
+ })
755
+
756
+ {
757
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'alpha' } })
758
+ t.same(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
759
+ t.equal(statusCode, 500)
760
+ }
761
+ {
762
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'beta' } })
763
+ t.same(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
764
+ t.equal(statusCode, 500)
765
+ }
766
+ {
767
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/', headers: { 'X-Secret': 'gamma' } })
768
+ t.same(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
769
+ t.equal(statusCode, 500)
770
+ }
771
+ {
772
+ const { statusCode, payload } = await fastify.inject({ method: 'GET', path: '/' })
773
+ t.same(JSON.parse(payload), { error: 'Internal Server Error', message: 'Unexpected error from async constraint', statusCode: 500 })
774
+ t.equal(statusCode, 500)
775
+ }
776
+ })
@@ -2,7 +2,10 @@
2
2
 
3
3
  const test = require('tap').test
4
4
  const Fastify = require('../')
5
- const { FST_ERR_BAD_URL } = require('../lib/errors')
5
+ const {
6
+ FST_ERR_BAD_URL,
7
+ FST_ERR_ASYNC_CONSTRAINT
8
+ } = require('../lib/errors')
6
9
 
7
10
  test('Should honor ignoreTrailingSlash option', async t => {
8
11
  t.plan(4)
@@ -137,7 +140,7 @@ test('Should set is404 flag for unmatched paths', t => {
137
140
  })
138
141
  })
139
142
 
140
- test('Should honor frameworkErrors option', t => {
143
+ test('Should honor frameworkErrors option - FST_ERR_BAD_URL', t => {
141
144
  t.plan(3)
142
145
  const fastify = Fastify({
143
146
  frameworkErrors: function (err, req, res) {
@@ -165,3 +168,54 @@ test('Should honor frameworkErrors option', t => {
165
168
  }
166
169
  )
167
170
  })
171
+
172
+ test('Should honor frameworkErrors option - FST_ERR_ASYNC_CONSTRAINT', t => {
173
+ t.plan(3)
174
+
175
+ const constraint = {
176
+ name: 'secret',
177
+ storage: function () {
178
+ const secrets = {}
179
+ return {
180
+ get: (secret) => { return secrets[secret] || null },
181
+ set: (secret, store) => { secrets[secret] = store }
182
+ }
183
+ },
184
+ deriveConstraint: (req, ctx, done) => {
185
+ done(Error('kaboom'))
186
+ },
187
+ validate () { return true }
188
+ }
189
+
190
+ const fastify = Fastify({
191
+ frameworkErrors: function (err, req, res) {
192
+ if (err instanceof FST_ERR_ASYNC_CONSTRAINT) {
193
+ t.ok(true)
194
+ } else {
195
+ t.fail()
196
+ }
197
+ res.send(`${err.message} - ${err.code}`)
198
+ },
199
+ constraints: { secret: constraint }
200
+ })
201
+
202
+ fastify.route({
203
+ method: 'GET',
204
+ url: '/',
205
+ constraints: { secret: 'alpha' },
206
+ handler: (req, reply) => {
207
+ reply.send({ hello: 'from alpha' })
208
+ }
209
+ })
210
+
211
+ fastify.inject(
212
+ {
213
+ method: 'GET',
214
+ url: '/'
215
+ },
216
+ (err, res) => {
217
+ t.error(err)
218
+ t.equal(res.body, 'Unexpected error from async constraint - FST_ERR_ASYNC_CONSTRAINT')
219
+ }
220
+ )
221
+ })
@@ -9,7 +9,8 @@ import fastify, {
9
9
  LightMyRequestCallback,
10
10
  InjectOptions, FastifyBaseLogger,
11
11
  RouteGenericInterface,
12
- ValidationResult
12
+ ValidationResult,
13
+ FastifyErrorCodes
13
14
  } from '../../fastify'
14
15
  import { ErrorObject as AjvErrorObject } from 'ajv'
15
16
  import * as http from 'http'
@@ -240,3 +241,6 @@ expectType<unknown>(routeGeneric.Headers)
240
241
  expectType<unknown>(routeGeneric.Params)
241
242
  expectType<unknown>(routeGeneric.Querystring)
242
243
  expectType<unknown>(routeGeneric.Reply)
244
+
245
+ // ErrorCodes
246
+ expectType<FastifyErrorCodes>(fastify.errorCodes)
@@ -0,0 +1,53 @@
1
+ import { FastifyErrorConstructor } from '@fastify/error'
2
+
3
+ export type FastifyErrorCodes = Record<
4
+ 'FST_ERR_NOT_FOUND' |
5
+ 'FST_ERR_CTP_ALREADY_PRESENT' |
6
+ 'FST_ERR_CTP_INVALID_TYPE' |
7
+ 'FST_ERR_CTP_EMPTY_TYPE' |
8
+ 'FST_ERR_CTP_INVALID_HANDLER' |
9
+ 'FST_ERR_CTP_INVALID_PARSE_TYPE' |
10
+ 'FST_ERR_CTP_BODY_TOO_LARGE' |
11
+ 'FST_ERR_CTP_INVALID_MEDIA_TYPE' |
12
+ 'FST_ERR_CTP_INVALID_CONTENT_LENGTH' |
13
+ 'FST_ERR_CTP_EMPTY_JSON_BODY' |
14
+ 'FST_ERR_DEC_ALREADY_PRESENT' |
15
+ 'FST_ERR_DEC_DEPENDENCY_INVALID_TYPE' |
16
+ 'FST_ERR_DEC_MISSING_DEPENDENCY' |
17
+ 'FST_ERR_DEC_AFTER_START' |
18
+ 'FST_ERR_HOOK_INVALID_TYPE' |
19
+ 'FST_ERR_HOOK_INVALID_HANDLER' |
20
+ 'FST_ERR_MISSING_MIDDLEWARE' |
21
+ 'FST_ERR_HOOK_TIMEOUT' |
22
+ 'FST_ERR_LOG_INVALID_DESTINATION' |
23
+ 'FST_ERR_REP_INVALID_PAYLOAD_TYPE' |
24
+ 'FST_ERR_REP_ALREADY_SENT' |
25
+ 'FST_ERR_REP_SENT_VALUE'|
26
+ 'FST_ERR_SEND_INSIDE_ONERR'|
27
+ 'FST_ERR_SEND_UNDEFINED_ERR'|
28
+ 'FST_ERR_BAD_STATUS_CODE'|
29
+ 'FST_ERR_BAD_TRAILER_NAME' |
30
+ 'FST_ERR_BAD_TRAILER_VALUE' |
31
+ 'FST_ERR_MISSING_SERIALIZATION_FN' |
32
+ 'FST_ERR_REQ_INVALID_VALIDATION_INVOCATION' |
33
+ 'FST_ERR_SCH_MISSING_ID' |
34
+ 'FST_ERR_SCH_ALREADY_PRESENT' |
35
+ 'FST_ERR_SCH_DUPLICATE' |
36
+ 'FST_ERR_SCH_VALIDATION_BUILD' |
37
+ 'FST_ERR_SCH_SERIALIZATION_BUILD' |
38
+ 'FST_ERR_HTTP2_INVALID_VERSION' |
39
+ 'FST_ERR_INIT_OPTS_INVALID' |
40
+ 'FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE' |
41
+ 'FST_ERR_DUPLICATED_ROUTE' |
42
+ 'FST_ERR_BAD_URL' |
43
+ 'FST_ERR_DEFAULT_ROUTE_INVALID_TYPE' |
44
+ 'FST_ERR_INVALID_URL' |
45
+ 'FST_ERR_REOPENED_CLOSE_SERVER' |
46
+ 'FST_ERR_REOPENED_SERVER' |
47
+ 'FST_ERR_PLUGIN_VERSION_MISMATCH' |
48
+ 'FST_ERR_PLUGIN_CALLBACK_NOT_FN' |
49
+ 'FST_ERR_PLUGIN_NOT_VALID' |
50
+ 'FST_ERR_ROOT_PLG_BOOTED' |
51
+ 'FST_ERR_PARENT_PLUGIN_BOOTED' |
52
+ 'FST_ERR_PLUGIN_TIMEOUT'
53
+ , FastifyErrorConstructor>