fastify 4.7.0 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/Guides/Ecosystem.md +1 -1
- package/docs/Guides/Testing.md +50 -17
- package/docs/Reference/Errors.md +205 -60
- package/docs/Reference/Hooks.md +27 -0
- package/docs/Reference/Routes.md +54 -0
- package/docs/Reference/Server.md +3 -0
- package/fastify.d.ts +6 -0
- package/fastify.js +58 -25
- package/lib/contentTypeParser.js +20 -19
- package/lib/errors.js +39 -28
- package/lib/route.js +6 -1
- package/package.json +2 -2
- package/test/constrained-routes.test.js +114 -0
- package/test/content-parser.test.js +68 -2
- package/test/router-options.test.js +56 -2
- package/test/types/fastify.test-d.ts +5 -1
- package/types/errors.d.ts +53 -0
package/docs/Guides/Ecosystem.md
CHANGED
|
@@ -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/
|
|
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
|
package/docs/Guides/Testing.md
CHANGED
|
@@ -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
|
|
249
|
+
const { Client } = require('undici')
|
|
251
250
|
const buildFastify = require('./app')
|
|
252
251
|
|
|
253
|
-
tap.test('
|
|
254
|
-
t.plan(
|
|
252
|
+
tap.test('should work with undici', async t => {
|
|
253
|
+
t.plan(2)
|
|
255
254
|
|
|
256
255
|
const fastify = buildFastify()
|
|
257
256
|
|
|
258
|
-
|
|
257
|
+
await fastify.listen()
|
|
259
258
|
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
const client = new Client(
|
|
260
|
+
'http://localhost:' + fastify.server.address().port, {
|
|
261
|
+
keepAliveTimeout: 10,
|
|
262
|
+
keepAliveMaxTimeout: 10
|
|
263
|
+
}
|
|
264
|
+
)
|
|
262
265
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
package/docs/Reference/Errors.md
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
####
|
|
103
|
-
<a id="
|
|
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
|
-
|
|
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
|
-
####
|
|
132
|
-
<a id="
|
|
174
|
+
#### FST_ERR_CTP_INVALID_CONTENT_LENGTH
|
|
175
|
+
<a id="FST_ERR_CTP_INVALID_CONTENT_LENGTH"></a>
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
`buffer`.
|
|
177
|
+
Request body size did not match `Content-Length`.
|
|
136
178
|
|
|
137
|
-
####
|
|
138
|
-
<a id="
|
|
179
|
+
#### FST_ERR_CTP_EMPTY_JSON_BODY
|
|
180
|
+
<a id="FST_ERR_CTP_EMPTY_JSON_BODY"></a>
|
|
139
181
|
|
|
140
|
-
|
|
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
|
-
####
|
|
153
|
-
<a id="
|
|
199
|
+
#### FST_ERR_DEC_AFTER_START
|
|
200
|
+
<a id="FST_ERR_DEC_AFTER_START"></a>
|
|
154
201
|
|
|
155
|
-
The
|
|
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
|
-
####
|
|
168
|
-
<a id="
|
|
230
|
+
#### FST_ERR_REP_INVALID_PAYLOAD_TYPE
|
|
231
|
+
<a id="FST_ERR_REP_INVALID_PAYLOAD_TYPE"></a>
|
|
169
232
|
|
|
170
|
-
|
|
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
|
-
####
|
|
178
|
-
<a id="
|
|
240
|
+
#### FST_ERR_REP_SENT_VALUE
|
|
241
|
+
<a id="FST_ERR_REP_SENT_VALUE"></a>
|
|
179
242
|
|
|
180
|
-
|
|
243
|
+
The only possible value for `reply.sent` is `true`.
|
|
181
244
|
|
|
182
|
-
####
|
|
183
|
-
<a id="
|
|
245
|
+
#### FST_ERR_SEND_INSIDE_ONERR
|
|
246
|
+
<a id="FST_ERR_SEND_INSIDE_ONERR"></a>
|
|
184
247
|
|
|
185
|
-
|
|
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
|
-
####
|
|
193
|
-
<a id="
|
|
286
|
+
#### FST_ERR_SCH_ALREADY_PRESENT
|
|
287
|
+
<a id="FST_ERR_SCH_ALREADY_PRESENT"></a>
|
|
194
288
|
|
|
195
|
-
|
|
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
|
-
####
|
|
203
|
-
<a id="
|
|
301
|
+
#### FST_ERR_SCH_SERIALIZATION_BUILD
|
|
302
|
+
<a id="FST_ERR_SCH_SERIALIZATION_BUILD"></a>
|
|
204
303
|
|
|
205
|
-
|
|
304
|
+
The JSON schema provided for serialization of a route response is not valid.
|
|
206
305
|
|
|
207
|
-
####
|
|
208
|
-
<a id="
|
|
306
|
+
#### FST_ERR_HTTP2_INVALID_VERSION
|
|
307
|
+
<a id="FST_ERR_HTTP2_INVALID_VERSION"></a>
|
|
209
308
|
|
|
210
|
-
|
|
309
|
+
HTTP2 is available only from node >= 8.8.1.
|
|
211
310
|
|
|
212
|
-
|
|
213
|
-
|
|
311
|
+
#### FST_ERR_INIT_OPTS_INVALID
|
|
312
|
+
<a id="FST_ERR_INIT_OPTS_INVALID"></a>
|
|
214
313
|
|
|
215
|
-
|
|
314
|
+
Invalid initialization options.
|
|
216
315
|
|
|
217
|
-
|
|
218
|
-
|
|
316
|
+
#### FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE
|
|
317
|
+
<a id="FST_ERR_FORCE_CLOSE_CONNECTIONS_IDLE_NOT_AVAILABLE"></a>
|
|
219
318
|
|
|
220
|
-
|
|
319
|
+
Cannot set forceCloseConnections to `idle` as your HTTP server
|
|
320
|
+
does not support `closeIdleConnections` method.
|
|
221
321
|
|
|
222
|
-
<a name="
|
|
223
|
-
####
|
|
322
|
+
<a name="FST_ERR_DUPLICATED_ROUTE"></a>
|
|
323
|
+
#### FST_ERR_DUPLICATED_ROUTE
|
|
224
324
|
|
|
225
|
-
|
|
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="
|
|
238
|
-
####
|
|
382
|
+
<a name="FST_ERR_PLUGIN_TIMEOUT"></a>
|
|
383
|
+
#### FST_ERR_PLUGIN_TIMEOUT
|
|
239
384
|
|
|
240
|
-
|
|
385
|
+
Plugin did not start in time. Default timeout (in millis): `10000`
|
package/docs/Reference/Hooks.md
CHANGED
|
@@ -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
|
|
package/docs/Reference/Routes.md
CHANGED
|
@@ -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
|
package/docs/Reference/Server.md
CHANGED
|
@@ -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.
|
|
3
|
+
const VERSION = '4.8.1'
|
|
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
|
-
|
|
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
|
|
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/contentTypeParser.js
CHANGED
|
@@ -29,9 +29,10 @@ const {
|
|
|
29
29
|
|
|
30
30
|
function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning) {
|
|
31
31
|
this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning)
|
|
32
|
-
|
|
33
|
-
this.customParsers
|
|
34
|
-
this.customParsers
|
|
32
|
+
// using a map instead of a plain object to avoid prototype hijack attacks
|
|
33
|
+
this.customParsers = new Map()
|
|
34
|
+
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
|
|
35
|
+
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
|
|
35
36
|
this.parserList = ['application/json', 'text/plain']
|
|
36
37
|
this.parserRegExpList = []
|
|
37
38
|
this.cache = lru(100)
|
|
@@ -62,35 +63,35 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
if (contentTypeIsString && contentType === '*') {
|
|
65
|
-
this.customParsers
|
|
66
|
+
this.customParsers.set('', parser)
|
|
66
67
|
} else {
|
|
67
68
|
if (contentTypeIsString) {
|
|
68
69
|
this.parserList.unshift(contentType)
|
|
69
70
|
} else {
|
|
70
71
|
this.parserRegExpList.unshift(contentType)
|
|
71
72
|
}
|
|
72
|
-
this.customParsers
|
|
73
|
+
this.customParsers.set(contentType.toString(), parser)
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
ContentTypeParser.prototype.hasParser = function (contentType) {
|
|
77
|
-
return contentType
|
|
78
|
+
return this.customParsers.has(typeof contentType === 'string' ? contentType : contentType.toString())
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
ContentTypeParser.prototype.existingParser = function (contentType) {
|
|
81
|
-
if (contentType === 'application/json') {
|
|
82
|
-
return this.customParsers
|
|
82
|
+
if (contentType === 'application/json' && this.customParsers.has(contentType)) {
|
|
83
|
+
return this.customParsers.get(contentType).fn !== this[kDefaultJsonParse]
|
|
83
84
|
}
|
|
84
|
-
if (contentType === 'text/plain') {
|
|
85
|
-
return this.customParsers
|
|
85
|
+
if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
|
|
86
|
+
return this.customParsers.get(contentType).fn !== defaultPlainTextParser
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
return
|
|
89
|
+
return this.hasParser(contentType)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
ContentTypeParser.prototype.getParser = function (contentType) {
|
|
92
|
-
if (
|
|
93
|
-
return this.customParsers
|
|
93
|
+
if (this.hasParser(contentType)) {
|
|
94
|
+
return this.customParsers.get(contentType)
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
if (this.cache.has(contentType)) {
|
|
@@ -101,7 +102,7 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
101
102
|
for (var i = 0; i !== this.parserList.length; ++i) {
|
|
102
103
|
const parserName = this.parserList[i]
|
|
103
104
|
if (contentType.indexOf(parserName) !== -1) {
|
|
104
|
-
const parser = this.customParsers
|
|
105
|
+
const parser = this.customParsers.get(parserName)
|
|
105
106
|
this.cache.set(contentType, parser)
|
|
106
107
|
return parser
|
|
107
108
|
}
|
|
@@ -111,17 +112,17 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
111
112
|
for (var j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
112
113
|
const parserRegExp = this.parserRegExpList[j]
|
|
113
114
|
if (parserRegExp.test(contentType)) {
|
|
114
|
-
const parser = this.customParsers
|
|
115
|
+
const parser = this.customParsers.get(parserRegExp.toString())
|
|
115
116
|
this.cache.set(contentType, parser)
|
|
116
117
|
return parser
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
return this.customParsers
|
|
121
|
+
return this.customParsers.get('')
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
ContentTypeParser.prototype.removeAll = function () {
|
|
124
|
-
this.customParsers =
|
|
125
|
+
this.customParsers = new Map()
|
|
125
126
|
this.parserRegExpList = []
|
|
126
127
|
this.parserList = []
|
|
127
128
|
this.cache = lru(100)
|
|
@@ -130,7 +131,7 @@ ContentTypeParser.prototype.removeAll = function () {
|
|
|
130
131
|
ContentTypeParser.prototype.remove = function (contentType) {
|
|
131
132
|
if (!(typeof contentType === 'string' || contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
this.customParsers.delete(contentType.toString())
|
|
134
135
|
|
|
135
136
|
const parsers = typeof contentType === 'string' ? this.parserList : this.parserRegExpList
|
|
136
137
|
|
|
@@ -289,7 +290,7 @@ function Parser (asString, asBuffer, bodyLimit, fn) {
|
|
|
289
290
|
function buildContentTypeParser (c) {
|
|
290
291
|
const contentTypeParser = new ContentTypeParser()
|
|
291
292
|
contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
|
|
292
|
-
|
|
293
|
+
contentTypeParser.customParsers = new Map(c.customParsers.entries())
|
|
293
294
|
contentTypeParser.parserList = c.parserList.slice()
|
|
294
295
|
return contentTypeParser
|
|
295
296
|
}
|
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
|
|
267
|
+
* Avvio Errors
|
|
262
268
|
*/
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
newErr.cause = oldErr
|
|
291
|
+
function appendStackTrace (oldErr, newErr) {
|
|
292
|
+
newErr.cause = oldErr
|
|
289
293
|
|
|
290
|
-
|
|
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.
|
|
3
|
+
"version": "4.8.1",
|
|
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.
|
|
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
|
+
})
|
|
@@ -196,7 +196,7 @@ test('add', t => {
|
|
|
196
196
|
const contentTypeParser = fastify[keys.kContentTypeParser]
|
|
197
197
|
|
|
198
198
|
contentTypeParser.add('*', {}, first)
|
|
199
|
-
t.equal(contentTypeParser.customParsers
|
|
199
|
+
t.equal(contentTypeParser.customParsers.get('').fn, first)
|
|
200
200
|
})
|
|
201
201
|
|
|
202
202
|
t.end()
|
|
@@ -306,7 +306,7 @@ test('remove', t => {
|
|
|
306
306
|
|
|
307
307
|
contentTypeParser.remove('image/png')
|
|
308
308
|
|
|
309
|
-
t.same(
|
|
309
|
+
t.same(contentTypeParser.customParsers.size, 2)
|
|
310
310
|
})
|
|
311
311
|
|
|
312
312
|
t.end()
|
|
@@ -329,3 +329,69 @@ test('remove all should remove all existing parsers and reset cache', t => {
|
|
|
329
329
|
t.same(contentTypeParser.parserRegExpList.length, 0)
|
|
330
330
|
t.same(Object.keys(contentTypeParser.customParsers).length, 0)
|
|
331
331
|
})
|
|
332
|
+
|
|
333
|
+
test('Safeguard against malicious content-type / 1', async t => {
|
|
334
|
+
const badNames = Object.getOwnPropertyNames({}.__proto__) // eslint-disable-line
|
|
335
|
+
t.plan(badNames.length)
|
|
336
|
+
|
|
337
|
+
const fastify = Fastify()
|
|
338
|
+
|
|
339
|
+
fastify.post('/', async () => {
|
|
340
|
+
return 'ok'
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
for (const prop of badNames) {
|
|
344
|
+
const response = await fastify.inject({
|
|
345
|
+
method: 'POST',
|
|
346
|
+
path: '/',
|
|
347
|
+
headers: {
|
|
348
|
+
'content-type': prop
|
|
349
|
+
},
|
|
350
|
+
body: ''
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
t.same(response.statusCode, 415)
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('Safeguard against malicious content-type / 2', async t => {
|
|
358
|
+
t.plan(1)
|
|
359
|
+
|
|
360
|
+
const fastify = Fastify()
|
|
361
|
+
|
|
362
|
+
fastify.post('/', async () => {
|
|
363
|
+
return 'ok'
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const response = await fastify.inject({
|
|
367
|
+
method: 'POST',
|
|
368
|
+
path: '/',
|
|
369
|
+
headers: {
|
|
370
|
+
'content-type': '\\u0063\\u006fnstructor'
|
|
371
|
+
},
|
|
372
|
+
body: ''
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
t.same(response.statusCode, 415)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('Safeguard against malicious content-type / 3', async t => {
|
|
379
|
+
t.plan(1)
|
|
380
|
+
|
|
381
|
+
const fastify = Fastify()
|
|
382
|
+
|
|
383
|
+
fastify.post('/', async () => {
|
|
384
|
+
return 'ok'
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const response = await fastify.inject({
|
|
388
|
+
method: 'POST',
|
|
389
|
+
path: '/',
|
|
390
|
+
headers: {
|
|
391
|
+
'content-type': 'constructor; charset=utf-8'
|
|
392
|
+
},
|
|
393
|
+
body: ''
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
t.same(response.statusCode, 415)
|
|
397
|
+
})
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const test = require('tap').test
|
|
4
4
|
const Fastify = require('../')
|
|
5
|
-
const {
|
|
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>
|