fastify 4.0.0-alpha.2 → 4.0.0-alpha.3

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.
@@ -8,6 +8,9 @@ section.
8
8
 
9
9
  #### [Core](#core)
10
10
 
11
+ - [`aws-lambda-fastify`](https://github.com/fastify/aws-lambda-fastify) allows you to
12
+ easily build serverless web applications/services and RESTful APIs using Fastify
13
+ on top of AWS Lambda and Amazon API Gateway.
11
14
  - [`fastify-accepts`](https://github.com/fastify/fastify-accepts) to have
12
15
  [accepts](https://www.npmjs.com/package/accepts) in your request object.
13
16
  - [`fastify-accepts-serializer`](https://github.com/fastify/fastify-accepts-serializer)
@@ -295,6 +298,8 @@ section.
295
298
  An error handling plugin for Fastify that uses enhanced HTTP errors.
296
299
  - [`fastify-https-redirect`](https://github.com/tomsvogel/fastify-https-redirect)
297
300
  Fastify plugin for auto-redirect from HTTP to HTTPS.
301
+ - [`fatify-impressions`](https://github.com/manju4ever/fastify-impressions) Fastify
302
+ plugin to track impressions of all the routes.
298
303
  - [`fastify-influxdb`](https://github.com/alex-ppg/fastify-influxdb) Fastify
299
304
  InfluxDB plugin connecting to an InfluxDB instance via the Influx default
300
305
  package.
@@ -334,6 +339,8 @@ section.
334
339
  - [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose)
335
340
  Fastify Mongoose plugin that connects to a MongoDB via the Mongoose plugin
336
341
  with support for Models.
342
+ - [`fastify-mqtt`](https://github.com/love-lena/fastify-mqtt) Plugin to share
343
+ [mqtt](https://www.npmjs.com/package/mqtt) client across Fastify.
337
344
  - [`fastify-msgpack`](https://github.com/kenriortega/fastify-msgpack) Fastify
338
345
  and MessagePack, together at last. Uses @msgpack/msgpack by default.
339
346
  - [`fastify-multer`](https://github.com/fox1t/fastify-multer) Multer is a plugin
@@ -484,6 +491,8 @@ section.
484
491
  waterline. Decorates Fastify with waterline models.
485
492
  - [`fastify-webpack-hmr`](https://github.com/lependu/fastify-webpack-hmr)
486
493
  Webpack hot module reloading plugin for Fastify.
494
+ - [`fastify-webpack-hot`](https://github.com/gajus/fastify-webpack-hot)
495
+ Webpack Hot Module Replacement for Fastify.
487
496
  - [`fastify-ws`](https://github.com/gj/fastify-ws) WebSocket integration for
488
497
  Fastify — with support for WebSocket lifecycle hooks instead of a single
489
498
  handler function. Built upon [ws](https://github.com/websockets/ws) and
@@ -13,6 +13,9 @@
13
13
  - [.getHeaders()](#getheaders)
14
14
  - [.removeHeader(key)](#removeheaderkey)
15
15
  - [.hasHeader(key)](#hasheaderkey)
16
+ - [.trailer(key, function)](#trailerkey-function)
17
+ - [.hasTrailer(key)](#hastrailerkey)
18
+ - [.removeTrailer(key)](#removetrailerkey)
16
19
  - [.redirect([code,] dest)](#redirectcode--dest)
17
20
  - [.callNotFound()](#callnotfound)
18
21
  - [.getResponseTime()](#getresponsetime)
@@ -47,6 +50,9 @@ object that exposes the following functions and properties:
47
50
  - `.getHeaders()` - Gets a shallow copy of all current response headers.
48
51
  - `.removeHeader(key)` - Remove the value of a previously set header.
49
52
  - `.hasHeader(name)` - Determine if a header has been set.
53
+ - `.trailer(key, function)` - Sets a response trailer.
54
+ - `.hasTrailer(key)` - Determine if a trailer has been set.
55
+ - `.removeTrailer(key)` - Remove the value of a previously set trailer.
50
56
  - `.type(value)` - Sets the header `Content-Type`.
51
57
  - `.redirect([code,] dest)` - Redirect to the specified url, the status code is
52
58
  optional (default to `302`).
@@ -199,6 +205,49 @@ reply.getHeader('x-foo') // undefined
199
205
 
200
206
  Returns a boolean indicating if the specified header has been set.
201
207
 
208
+ ### .trailer(key, function)
209
+ <a id="trailer"></a>
210
+
211
+ Sets a response trailer. Trailer usually used when you want some header that require heavy resources to be sent after the `data`, for example `Server-Timing`, `Etag`. It can ensure the client get the response data as soon as possible.
212
+
213
+ *Note: The header `Transfer-Encoding: chunked` will be added once you use the trailer. It is a hard requipment for using trailer in Node.js.*
214
+
215
+ *Note: Currently, the computation function only supports synchronous function. That means `async-await` and `promise` are not supported.*
216
+
217
+ ```js
218
+ reply.trailer('server-timing', function() {
219
+ return 'db;dur=53, app;dur=47.2'
220
+ })
221
+
222
+ const { createHash } = require('crypto')
223
+ // trailer function also recieve two argument
224
+ // @param {object} reply fastify reply
225
+ // @param {string|Buffer|null} payload payload that already sent, note that it will be null when stream is sent
226
+ reply.trailer('content-md5', function(reply, payload) {
227
+ const hash = createHash('md5')
228
+ hash.update(payload)
229
+ return hash.disgest('hex')
230
+ })
231
+ ```
232
+
233
+ ### .hasTrailer(key)
234
+ <a id="hasTrailer"></a>
235
+
236
+ Returns a boolean indicating if the specified trailer has been set.
237
+
238
+ ### .removeTrailer(key)
239
+ <a id="removeTrailer"></a>
240
+
241
+ Remove the value of a previously set trailer.
242
+ ```js
243
+ reply.trailer('server-timing', function() {
244
+ return 'db;dur=53, app;dur=47.2'
245
+ })
246
+ reply.removeTrailer('server-timing')
247
+ reply.getTrailer('server-timing') // undefined
248
+ ```
249
+
250
+
202
251
  ### .redirect([code ,] dest)
203
252
  <a id="redirect"></a>
204
253
 
@@ -261,6 +310,7 @@ Sets the content type for the response. This is a shortcut for
261
310
  ```js
262
311
  reply.type('text/html')
263
312
  ```
313
+ If the `Content-Type` has a JSON subtype, and the charset parameter is not set, `utf-8` will be used as the charset by default.
264
314
 
265
315
  ### .serializer(func)
266
316
  <a id="serializer"></a>
@@ -821,7 +821,7 @@ fastify.ready().then(() => {
821
821
  #### listen
822
822
  <a id="listen"></a>
823
823
 
824
- Starts the server and internally waits for the the `.ready()` event. The
824
+ Starts the server and internally waits for the `.ready()` event. The
825
825
  signature is `.listen([options][, callback])`. Both the `options` object and the
826
826
  `callback` parameters follow the [Node.js
827
827
  core][https://nodejs.org/api/net.html#serverlistenoptions-callback] parameter
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fastify = require('../fastify')({ logger: true })
4
4
  const jsonParser = require('fast-json-body')
5
- const qs = require('qs')
5
+ const querystring = require('querystring')
6
6
 
7
7
  // Handled by fastify
8
8
  // curl -X POST -d '{"hello":"world"}' -H'Content-type: application/json' http://localhost:3000/
@@ -22,7 +22,7 @@ fastify.addContentTypeParser('application/x-www-form-urlencoded', function (requ
22
22
  })
23
23
  payload.on('end', function () {
24
24
  try {
25
- const parsed = qs.parse(body)
25
+ const parsed = querystring.parse(body)
26
26
  done(null, parsed)
27
27
  } catch (e) {
28
28
  done(e)
@@ -31,6 +31,16 @@ fastify.addContentTypeParser('application/x-www-form-urlencoded', function (requ
31
31
  payload.on('error', done)
32
32
  })
33
33
 
34
+ // curl -X POST -d '{"hello":"world"}' -H'Content-type: application/vnd.custom+json' http://localhost:3000/
35
+ fastify.addContentTypeParser(/^application\/.+\+json$/, { parseAs: 'string' }, fastify.getDefaultJsonParser('error', 'ignore'))
36
+
37
+ // remove default json parser
38
+ // curl -X POST -d '{"hello":"world"}' -H'Content-type: application/json' http://localhost:3000/ is now no longer handled by fastify
39
+ fastify.removeContentTypeParser('application/json')
40
+
41
+ // This call would remove any content type parser
42
+ // fastify.removeAllContentTypeParsers()
43
+
34
44
  fastify
35
45
  .post('/', function (req, reply) {
36
46
  reply.send(req.body)
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '4.0.0-alpha.2'
3
+ const VERSION = '4.0.0-alpha.3'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('http')
package/lib/errors.js CHANGED
@@ -152,6 +152,14 @@ const codes = {
152
152
  'FST_ERR_BAD_STATUS_CODE',
153
153
  'Called reply with an invalid status code: %s'
154
154
  ),
155
+ FST_ERR_BAD_TRAILER_NAME: createError(
156
+ 'FST_ERR_BAD_TRAILER_NAME',
157
+ 'Called reply.trailer with an invalid header name: %s'
158
+ ),
159
+ FST_ERR_BAD_TRAILER_VALUE: createError(
160
+ 'FST_ERR_BAD_TRAILER_VALUE',
161
+ "Called reply.trailer('%s', fn) with an invalid type: %s. Expected a function."
162
+ ),
155
163
 
156
164
  /**
157
165
  * schemas
package/lib/reply.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  kReplySerializerDefault,
13
13
  kReplyIsError,
14
14
  kReplyHeaders,
15
+ kReplyTrailers,
15
16
  kReplyHasStatusCode,
16
17
  kReplyIsRunningOnErrorHook,
17
18
  kReplyNextErrorHandler,
@@ -35,7 +36,9 @@ const {
35
36
  FST_ERR_REP_ALREADY_SENT,
36
37
  FST_ERR_REP_SENT_VALUE,
37
38
  FST_ERR_SEND_INSIDE_ONERR,
38
- FST_ERR_BAD_STATUS_CODE
39
+ FST_ERR_BAD_STATUS_CODE,
40
+ FST_ERR_BAD_TRAILER_NAME,
41
+ FST_ERR_BAD_TRAILER_VALUE
39
42
  } = require('./errors')
40
43
  const warning = require('./warnings')
41
44
 
@@ -47,6 +50,7 @@ function Reply (res, request, log) {
47
50
  this[kReplyIsRunningOnErrorHook] = false
48
51
  this.request = request
49
52
  this[kReplyHeaders] = {}
53
+ this[kReplyTrailers] = null
50
54
  this[kReplyHasStatusCode] = false
51
55
  this[kReplyStartTime] = undefined
52
56
  this.log = log
@@ -158,22 +162,14 @@ Reply.prototype.send = function (payload) {
158
162
  if (hasContentType === false) {
159
163
  this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
160
164
  } else {
161
- // If hasContentType === true, we have a JSON mimetype
165
+ // If user doesn't set charset, we will set charset to utf-8
162
166
  if (contentType.indexOf('charset') === -1) {
163
- // If we have simply application/json instead of a custom json mimetype
164
- if (contentType.indexOf('/json') > -1) {
165
- this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON
167
+ const customContentType = contentType.trim()
168
+ if (customContentType.endsWith(';')) {
169
+ // custom content-type is ended with ';'
170
+ this[kReplyHeaders]['content-type'] = `${customContentType} charset=utf-8`
166
171
  } else {
167
- const currContentType = this[kReplyHeaders]['content-type']
168
- // We extract the custom mimetype part (e.g. 'hal+' from 'application/hal+json')
169
- const customJsonType = currContentType.substring(
170
- currContentType.indexOf('/'),
171
- currContentType.indexOf('json') + 4
172
- )
173
-
174
- // We ensure we set the header to the proper JSON content-type if necessary
175
- // (e.g. 'application/hal+json' instead of 'application/json')
176
- this[kReplyHeaders]['content-type'] = CONTENT_TYPE.JSON.replace('/json', customJsonType)
172
+ this[kReplyHeaders]['content-type'] = `${customContentType}; charset=utf-8`
177
173
  }
178
174
  }
179
175
  }
@@ -252,6 +248,47 @@ Reply.prototype.headers = function (headers) {
252
248
  return this
253
249
  }
254
250
 
251
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer#directives
252
+ // https://httpwg.org/specs/rfc7230.html#chunked.trailer.part
253
+ const INVALID_TRAILERS = new Set([
254
+ 'transfer-encoding',
255
+ 'content-length',
256
+ 'host',
257
+ 'cache-control',
258
+ 'max-forwards',
259
+ 'te',
260
+ 'authorization',
261
+ 'set-cookie',
262
+ 'content-encoding',
263
+ 'content-type',
264
+ 'content-range',
265
+ 'trailer'
266
+ ])
267
+
268
+ Reply.prototype.trailer = function (key, fn) {
269
+ key = key.toLowerCase()
270
+ if (INVALID_TRAILERS.has(key)) {
271
+ throw new FST_ERR_BAD_TRAILER_NAME(key)
272
+ }
273
+ if (typeof fn !== 'function') {
274
+ throw new FST_ERR_BAD_TRAILER_VALUE(key, typeof fn)
275
+ }
276
+ if (this[kReplyTrailers] === null) this[kReplyTrailers] = {}
277
+ this[kReplyTrailers][key] = fn
278
+ return this
279
+ }
280
+
281
+ Reply.prototype.hasTrailer = function (key) {
282
+ if (this[kReplyTrailers] === null) return false
283
+ return this[kReplyTrailers][key.toLowerCase()] !== undefined
284
+ }
285
+
286
+ Reply.prototype.removeTrailer = function (key) {
287
+ if (this[kReplyTrailers] === null) return this
288
+ this[kReplyTrailers][key.toLowerCase()] = undefined
289
+ return this
290
+ }
291
+
255
292
  Reply.prototype.code = function (code) {
256
293
  const intValue = parseInt(code)
257
294
  if (isNaN(intValue) || intValue < 100 || intValue > 600) {
@@ -404,6 +441,20 @@ function onSendEnd (reply, payload) {
404
441
  const req = reply.request
405
442
  const statusCode = res.statusCode
406
443
 
444
+ // we check if we need to update the trailers header and set it
445
+ if (reply[kReplyTrailers] !== null) {
446
+ const trailerHeaders = Object.keys(reply[kReplyTrailers])
447
+ let header = ''
448
+ for (const trailerName of trailerHeaders) {
449
+ if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
450
+ header += ' '
451
+ header += trailerName
452
+ }
453
+ // it must be chunked for trailer to work
454
+ reply.header('Transfer-Encoding', 'chunked')
455
+ reply.header('Trailer', header.trim())
456
+ }
457
+
407
458
  if (payload === undefined || payload === null) {
408
459
  // according to https://tools.ietf.org/html/rfc7230#section-3.3.2
409
460
  // we cannot send a content-length for 304 and 204, and all status code
@@ -414,6 +465,7 @@ function onSendEnd (reply, payload) {
414
465
  }
415
466
 
416
467
  res.writeHead(statusCode, reply[kReplyHeaders])
468
+ sendTrailer(payload, res, reply)
417
469
  // avoid ArgumentsAdaptorTrampoline from V8
418
470
  res.end(null, null, null)
419
471
  return
@@ -435,9 +487,12 @@ function onSendEnd (reply, payload) {
435
487
  }
436
488
 
437
489
  res.writeHead(statusCode, reply[kReplyHeaders])
438
-
490
+ // write payload first
491
+ res.write(payload)
492
+ // then send trailers
493
+ sendTrailer(payload, res, reply)
439
494
  // avoid ArgumentsAdaptorTrampoline from V8
440
- res.end(payload, null, null)
495
+ res.end(null, null, null)
441
496
  }
442
497
 
443
498
  function logStreamError (logger, err, res) {
@@ -454,6 +509,9 @@ function sendStream (payload, res, reply) {
454
509
  let sourceOpen = true
455
510
  let errorLogged = false
456
511
 
512
+ // set trailer when stream ended
513
+ sendStreamTrailer(payload, res, reply)
514
+
457
515
  eos(payload, { readable: true, writable: false }, function (err) {
458
516
  sourceOpen = false
459
517
  if (err != null) {
@@ -502,6 +560,22 @@ function sendStream (payload, res, reply) {
502
560
  payload.pipe(res)
503
561
  }
504
562
 
563
+ function sendTrailer (payload, res, reply) {
564
+ if (reply[kReplyTrailers] === null) return
565
+ const trailerHeaders = Object.keys(reply[kReplyTrailers])
566
+ const trailers = {}
567
+ for (const trailerName of trailerHeaders) {
568
+ if (typeof reply[kReplyTrailers][trailerName] !== 'function') continue
569
+ trailers[trailerName] = reply[kReplyTrailers][trailerName](reply, payload)
570
+ }
571
+ res.addTrailers(trailers)
572
+ }
573
+
574
+ function sendStreamTrailer (payload, res, reply) {
575
+ if (reply[kReplyTrailers] === null) return
576
+ payload.on('end', () => sendTrailer(null, res, reply))
577
+ }
578
+
505
579
  function onErrorHook (reply, error, cb) {
506
580
  if (reply.context.onError !== null && !reply[kReplyNextErrorHandler]) {
507
581
  reply[kReplyIsRunningOnErrorHook] = true
@@ -581,6 +655,7 @@ function buildReply (R) {
581
655
  this[kReplySerializer] = null
582
656
  this.request = request
583
657
  this[kReplyHeaders] = {}
658
+ this[kReplyTrailers] = null
584
659
  this[kReplyStartTime] = undefined
585
660
  this[kReplyEndTime] = undefined
586
661
  this.log = log
package/lib/server.js CHANGED
@@ -78,6 +78,7 @@ function createServer (options, httpHandler) {
78
78
  })
79
79
  })
80
80
  }
81
+ return listening
81
82
  }
82
83
 
83
84
  this.ready(listenCallback.call(this, server, listenOptions))
package/lib/symbols.js CHANGED
@@ -30,6 +30,7 @@ const keys = {
30
30
  kReplySerializer: Symbol('fastify.reply.serializer'),
31
31
  kReplyIsError: Symbol('fastify.reply.isError'),
32
32
  kReplyHeaders: Symbol('fastify.reply.headers'),
33
+ kReplyTrailers: Symbol('fastify.reply.trailers'),
33
34
  kReplyHasStatusCode: Symbol('fastify.reply.hasStatusCode'),
34
35
  kReplyHijacked: Symbol('fastify.reply.hijacked'),
35
36
  kReplyStartTime: Symbol('fastify.reply.startTime'),
package/lib/validation.js CHANGED
@@ -27,13 +27,14 @@ function compileSchemasForSerialization (context, compile) {
27
27
  }
28
28
 
29
29
  function compileSchemasForValidation (context, compile) {
30
- if (!context.schema) {
30
+ const { schema } = context
31
+ if (!schema) {
31
32
  return
32
33
  }
33
34
 
34
35
  const { method, url } = context.config || {}
35
36
 
36
- const headers = context.schema.headers
37
+ const headers = schema.headers
37
38
  if (headers && Object.getPrototypeOf(headers) !== Object.prototype) {
38
39
  // do not mess with non-literals, e.g. Joi schemas
39
40
  context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' })
@@ -54,16 +55,16 @@ function compileSchemasForValidation (context, compile) {
54
55
  context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
55
56
  }
56
57
 
57
- if (context.schema.body) {
58
- context[bodySchema] = compile({ schema: context.schema.body, method, url, httpPart: 'body' })
58
+ if (schema.body) {
59
+ context[bodySchema] = compile({ schema: schema.body, method, url, httpPart: 'body' })
59
60
  }
60
61
 
61
- if (context.schema.querystring) {
62
- context[querystringSchema] = compile({ schema: context.schema.querystring, method, url, httpPart: 'querystring' })
62
+ if (schema.querystring) {
63
+ context[querystringSchema] = compile({ schema: schema.querystring, method, url, httpPart: 'querystring' })
63
64
  }
64
65
 
65
- if (context.schema.params) {
66
- context[paramsSchema] = compile({ schema: context.schema.params, method, url, httpPart: 'params' })
66
+ if (schema.params) {
67
+ context[paramsSchema] = compile({ schema: schema.params, method, url, httpPart: 'params' })
67
68
  }
68
69
  }
69
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "4.0.0-alpha.2",
3
+ "version": "4.0.0-alpha.3",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -161,7 +161,7 @@
161
161
  "proxyquire": "^2.1.3",
162
162
  "pump": "^3.0.0",
163
163
  "self-cert": "^2.0.0",
164
- "send": "^0.17.2",
164
+ "send": "^0.18.0",
165
165
  "serve-static": "^1.14.1",
166
166
  "simple-get": "^4.0.0",
167
167
  "snazzy": "^9.0.0",