From d6b5354bc2e9b7519667642f78950978d6140b9c Mon Sep 17 00:00:00 2001 From: Sandro Martini Date: Mon, 13 May 2019 22:33:28 +0200 Subject: [PATCH 001/144] add tap into greenkeeper ignore list (#1643) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ed433965c57..42dd2647129 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "boom", "joi", "@types/node", + "tap", "tap-mocha-reporter" ] }, From 5a01a55dc64bf125e154e268e4c982426662ecd6 Mon Sep 17 00:00:00 2001 From: Andrey Chalkin Date: Wed, 15 May 2019 16:11:16 +0700 Subject: [PATCH 002/144] Added `fastify-decorators` to Ecosystem.md (#1645) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index fda4d0570c4..40568a15f45 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -58,6 +58,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-couchdb`](https://github.com/nigelhanlon/fastify-couchdb) Fastify plugin to add CouchDB support via [nano](https://github.com/apache/nano). - [`fastify-csrf`](https://github.com/Tarang11/fastify-csrf) A csrf plugin for Fastify. - [`fastify-datastore`](https://github.com/now-ims/now-fastify-datastore) Fastify plugin for [Google Cloud Datastore](https://cloud.google.com/nodejs/docs/reference/datastore/1.4.x/). +- [`fastify-decorators`](https://github.com/L2jLiga/fastify-decorators) Fastify plugin that provides the set of TypeScript decorators. - [`fastify-dynamodb`](https://github.com/matrus2/fastify-dynamodb) AWS DynamoDB plugin for Fastify. It exposes [AWS.DynamoDB.DocumentClient()](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html) object. - [`fastify-error-page`](https://github.com/hemerajs/fastify-error-page) Fastify plugin to print errors in structured HTML to the browser. - [`fastify-favicon`](https://github.com/smartiniOnGitHub/fastify-favicon) Fastify plugin to serve default favicon. From 0ce4f0d245388179049392b44360327151eac66e Mon Sep 17 00:00:00 2001 From: Daniil Kolesnik Date: Wed, 15 May 2019 18:44:28 -0400 Subject: [PATCH 003/144] Fix typo in azure-pipelines.yml (#1649) Windows + Yarn section name was `Windows_yarm` --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8f192cde268..61a2e839239 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,7 +24,7 @@ jobs: - template: build/azure-pipelines-yarn-template.yml parameters: - name: Windows_yarm + name: Windows_yarn vmImage: vs2017-win2016 - template: build/azure-pipelines-yarn-template.yml From a210899eb32ddc95b5b70b5c5e0119c4e6efed3f Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Thu, 16 May 2019 08:02:29 +0200 Subject: [PATCH 004/144] offboarding steps (#1646) --- CONTRIBUTING.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5af089f6370..b3b52323254 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,8 +53,22 @@ Welcome to the team! We are happy to have you. Before you start, please complete 2. Choose which team to join *(more than one is ok!)* based on how you want to help. 3. Open a pull request to [`fastify/fastify:master`](https://github.com/fastify/fastify/pulls) that adds your name, username, and email to the team you have choosen in the [README.md](./README.md) and [package.json](./package.json) *(if you are part of the core team)* files. The members lists are sorted alphabetically; make sure to add your name in the proper order. 4. Open a pull request to [`fastify/website:master`](https://github.com/fastify/website/pulls) adding yourself to the [team.yml](https://github.com/fastify/website/blob/master/src/website/data/team.yml) file. This list is also sorted alphabetically so make sure to add your name in the proper order. Use your GitHub profile icon for the `picture:` field. -5. The person that does the onboarding must add you to the [npm org](https://www.npmjs.com/org/fastify), so that you can help maintaining the official plugins. - +5. The person that does the onboarding must add you to the [npm org](https://www.npmjs.com/org/fastify), so that you can help maintaining the official plugins. + +### Offboarding Collaborators + +We are thankful to you and we are really glad to have worked with you. +We'll be really happy to see you here again if you want to come back, but for now the person that did the onboarding must: +1. Ask the collaborator if they want to stay or not. +1. If the collaborator can't work with us anymore, they should: + 1. Open a pull request to [`fastify/fastify:master`](https://github.com/fastify/fastify/pulls) and move themselves in the *Past Collaborators* section. + 2. Open a pull request to [`fastify/website:master`](https://github.com/fastify/website/pulls) and move themselves in the *Past Collaborators* section in the [team.yml](https://github.com/fastify/website/blob/master/src/website/data/team.yml) file. + +The person that did the onboarding must: +1. If the collaborator doesn't reply to the ping in reasonable time, open the pull requests described above. +2. Remove the collaborator from the Fastify teams on GitHub. +3. Remove the collaborator from the [npm org](https://www.npmjs.com/org/fastify). +4. Remove the collaborator from the Azure team. ----------------------------------------- From f627bbf5b35bfb9f5d91f8d3a83275f1f437e616 Mon Sep 17 00:00:00 2001 From: Alexander Kureniov Date: Thu, 16 May 2019 21:24:29 +0300 Subject: [PATCH 005/144] fix(types): fix register options (#1644) --- fastify.d.ts | 2 +- test/types/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 40b6015c5bf..20e30a3f527 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -280,7 +280,7 @@ declare namespace fastify { /** * Register options */ - interface RegisterOptions extends RouteShorthandOptions { + interface RegisterOptions { [key: string]: any, prefix?: string, } diff --git a/test/types/index.ts b/test/types/index.ts index c151f407545..ef3fb958dcb 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -308,7 +308,7 @@ server reply.send({ hello: 'world' }) }) done() - }, { prefix: 'v1', hello: 'world' }) + }, { prefix: 'v1', hello: 'world', schema: 'any string' }) .all('/all/no-opts', function (req, reply) { reply.send(req.headers) }) From 6e4e662bc51c8e5dd5aa960efb8575e183fd7e6a Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Thu, 16 May 2019 21:34:26 +0200 Subject: [PATCH 006/144] chore(package): update flatstr to version 1.0.12 (#1653) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42dd2647129..2f2ca0e3f71 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "avvio": "^6.1.1", "fast-json-stringify": "^1.15.0", "find-my-way": "^2.0.0", - "flatstr": "^1.0.9", + "flatstr": "^1.0.12", "light-my-request": "^3.2.0", "middie": "^4.0.1", "pino": "^5.11.1", From 481d8096c68cf829682cdb8716889774ba1ece92 Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Fri, 17 May 2019 15:30:41 +0200 Subject: [PATCH 007/144] Catches onRoute hooks errors (#1651) * Catches onRoute hooks errors * Fixes typo * Removes unneeded test --- fastify.js | 9 ++++++++- test/hooks.test.js | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/fastify.js b/fastify.js index 6d884443af4..932c2c4bb16 100644 --- a/fastify.js +++ b/fastify.js @@ -456,7 +456,14 @@ function build (options) { } // run 'onRoute' hooks - for (const hook of this[kGlobalHooks].onRoute) hook.call(this, opts) + for (const hook of this[kGlobalHooks].onRoute) { + try { + hook.call(this, opts) + } catch (error) { + done(error) + return + } + } const config = opts.config || {} config.url = url diff --git a/test/hooks.test.js b/test/hooks.test.js index 62ab40057c2..d16d90e28b1 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -564,6 +564,25 @@ test('onRoute hook should preserve handler function in options of shorthand rout }) }) +test('onRoute hook that throws should be caught ', t => { + t.plan(1) + const fastify = Fastify() + + fastify.register((instance, opts, next) => { + instance.addHook('onRoute', () => { + throw new Error('snap') + }) + instance.get('/', opts, function (req, reply) { + reply.send() + }) + next() + }) + + fastify.ready(err => { + t.ok(err) + }) +}) + test('onResponse hook should log request error', t => { t.plan(4) From d1dab14d93e74e61acb459f803156899817ae808 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Fri, 17 May 2019 20:55:12 +0200 Subject: [PATCH 008/144] add example repo link (#1648) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8f5532d1b6b..bb16885a994 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ matters to you. ## Ecosystem - [Core](https://github.com/fastify/fastify/blob/master/docs/Ecosystem.md#core) - Core plugins maintained by the _Fastify_ [team](#team). - [Community](https://github.com/fastify/fastify/blob/master/docs/Ecosystem.md#community) - Community supported plugins. +- [Live Examples](https://github.com/fastify/example) - Multirepo with a broad set of real working examples. ## Support - [Fastify help](https://github.com/fastify/help) From dd059016134f97fa8b8509e373dbc0b8194455a4 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 18 May 2019 12:39:00 -0400 Subject: [PATCH 009/144] chore(package): update send to version 0.17.0 (#1627) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f2ca0e3f71..ccc5c6e0703 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "proxyquire": "^2.1.0", "pump": "^3.0.0", "semver": "^6.0.0", - "send": "^0.16.2", + "send": "^0.17.0", "serve-static": "^1.13.2", "simple-get": "^3.0.3", "snazzy": "^8.0.0", From 1f3c0de979ed8e6edf2e30c9a8594d85d4299ee9 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 19 May 2019 06:13:14 -0400 Subject: [PATCH 010/144] Emit warning synchronously (#1657) Fixes #1656 --- fastify.js | 10 +++++----- test/route-hooks.test.js | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fastify.js b/fastify.js index 932c2c4bb16..30cf012eec2 100644 --- a/fastify.js +++ b/fastify.js @@ -415,6 +415,11 @@ function build (options) { validateBodyLimitOption(opts.bodyLimit) + if (opts.preHandler == null && opts.beforeHandler != null) { + beforeHandlerWarning() + opts.preHandler = opts.beforeHandler + } + const prefix = this[kRoutePrefix] this.after((notHandledErr, done) => { @@ -498,11 +503,6 @@ function build (options) { } } - if (opts.preHandler == null && opts.beforeHandler != null) { - beforeHandlerWarning() - opts.preHandler = opts.beforeHandler - } - const hooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization'] for (let hook of hooks) { diff --git a/test/route-hooks.test.js b/test/route-hooks.test.js index df62dd22b40..0afbf401988 100644 --- a/test/route-hooks.test.js +++ b/test/route-hooks.test.js @@ -288,7 +288,7 @@ testHook('preValidation') testHook('preParsing') test('preHandler backwards compatibility with beforeHandler option (should emit a warning)', t => { - t.plan(3) + t.plan(4) const fastify = Fastify() process.on('warning', warn => { @@ -296,6 +296,7 @@ test('preHandler backwards compatibility with beforeHandler option (should emit warn.message, 'The route option `beforeHandler` has been deprecated, use `preHandler` instead' ) + t.ok(warn.stack.indexOf(__filename) >= 0) }) fastify.post('/', { From d377ebe460bd045535ba272a3dfa9c44bf8ee763 Mon Sep 17 00:00:00 2001 From: Alex Otten Date: Sun, 19 May 2019 08:53:13 -0400 Subject: [PATCH 011/144] Make some spelling and grammar corrections to TS.md (#1640) --- docs/TypeScript.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/TypeScript.md b/docs/TypeScript.md index e88130cb498..e176a029647 100644 --- a/docs/TypeScript.md +++ b/docs/TypeScript.md @@ -2,11 +2,11 @@ ## TypeScript -Fastify is shipped with a typings file, but it still require to install `@types/node`, depending on the Node.js version that you are using. +Fastify is shipped with a typings file, but you may need to install `@types/node`, depending on the Node.js version you are using. ## Types support -We do care about the TypeScript community, but the framework is written in plain JavaScript and currently no one of the core team is a TypeScript user while only one of the collaborators is. -We do our best to have the typing updated with the latest version of the API, but *it can happen* that the typings are not in sync.
+We do care about the TypeScript community, and one of our core team members is currently reworking all types. +We do our best to have the typings updated with the latest version of the API, but *it can happen* that the typings are not in sync.
Luckly this is Open Source and you can contribute to fix them, we will be very happy to accept the fix and release it as soon as possible as a patch release. Checkout the [contributing](#contributing) rules! Plugins may or may not include typings. See [Plugin Types](#plugin-types) for more information. From 9e65ca4ed62fed5239b0719c961c79c0c073dd8b Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Sun, 19 May 2019 23:09:40 +0200 Subject: [PATCH 012/144] internal: moved route code (#1625) --- fastify.js | 405 +++++----------------------------------------- lib/fourOhFour.js | 7 +- lib/middleware.js | 38 +++++ lib/route.js | 374 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 457 insertions(+), 367 deletions(-) create mode 100644 lib/middleware.js create mode 100644 lib/route.js diff --git a/fastify.js b/fastify.js index 30cf012eec2..fd2a0b08d51 100644 --- a/fastify.js +++ b/fastify.js @@ -1,12 +1,9 @@ 'use strict' -const FindMyWay = require('find-my-way') const Avvio = require('avvio') const http = require('http') const querystring = require('querystring') -const Middie = require('middie') let lightMyRequest -const proxyAddr = require('proxy-addr') const { kChildren, @@ -23,28 +20,22 @@ const { kFourOhFour, kState, kOptions, - kGlobalHooks, - kDisableRequestLogging + kGlobalHooks } = require('./lib/symbols.js') const { createServer } = require('./lib/server') const Reply = require('./lib/reply') const Request = require('./lib/request') -const Context = require('./lib/context') const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS'] -const buildSchema = require('./lib/validation').build -const handleRequest = require('./lib/handleRequest') -const validation = require('./lib/validation') -const buildSchemaCompiler = validation.buildSchemaCompiler const decorator = require('./lib/decorate') const ContentTypeParser = require('./lib/contentTypeParser') -const { Hooks, hookRunner, hookIterator, buildHooks } = require('./lib/hooks') +const { Hooks, buildHooks } = require('./lib/hooks') const { Schemas, buildSchemas } = require('./lib/schemas') const { createLogger } = require('./lib/logger') const pluginUtils = require('./lib/pluginUtils') const reqIdGenFactory = require('./lib/reqIdGenFactory') +const { buildRouting, validateBodyLimitOption } = require('./lib/route') const build404 = require('./lib/fourOhFour') -const { beforeHandlerWarning } = require('./lib/warnings') const getSecuredInitialConfig = require('./lib/initialConfigValidation') const { defaultInitOptions } = getSecuredInitialConfig @@ -67,7 +58,6 @@ function build (options) { options.genReqId = options.logger.genReqId } - const trustProxy = options.trustProxy const modifyCoreObjects = options.modifyCoreObjects !== false const requestIdHeader = options.requestIdHeader || defaultInitOptions.requestIdHeader const querystringParser = options.querystringParser || querystring.parse @@ -83,27 +73,33 @@ function build (options) { options.logger = logger options.modifyCoreObjects = modifyCoreObjects options.genReqId = genReqId + options.requestIdHeader = requestIdHeader + options.querystringParser = querystringParser + options.requestIdLogLabel = requestIdLogLabel + options.modifyCoreObjects = modifyCoreObjects + options.disableRequestLogging = disableRequestLogging // Default router - const router = FindMyWay({ - defaultRoute: defaultRoute, - ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash, - maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength, - caseSensitive: options.caseSensitive, - versioning: options.versioning + const router = buildRouting({ + config: { + defaultRoute: defaultRoute, + ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash, + maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength, + caseSensitive: options.caseSensitive, + versioning: options.versioning + } }) // 404 router, used for handling encapsulated 404 handlers const fourOhFour = build404(options) // HTTP server and its handler - const httpHandler = router.lookup.bind(router) + const httpHandler = router.routing const { server, listen } = createServer(options, httpHandler) if (Number(process.version.match(/v(\d+)/)[1]) >= 6) { server.on('clientError', handleClientError) } const setupResponseListeners = Reply.setupResponseListeners - const proxyFn = getTrustProxyFn(options) const schemas = new Schemas() // Public API @@ -134,31 +130,35 @@ function build (options) { [pluginUtils.registeredPlugins]: [], // routes shorthand methods delete: function _delete (url, opts, handler) { - return prepareRoute.call(this, 'DELETE', url, opts, handler) + return router.prepareRoute.call(this, 'DELETE', url, opts, handler) }, get: function _get (url, opts, handler) { - return prepareRoute.call(this, 'GET', url, opts, handler) + return router.prepareRoute.call(this, 'GET', url, opts, handler) }, head: function _head (url, opts, handler) { - return prepareRoute.call(this, 'HEAD', url, opts, handler) + return router.prepareRoute.call(this, 'HEAD', url, opts, handler) }, patch: function _patch (url, opts, handler) { - return prepareRoute.call(this, 'PATCH', url, opts, handler) + return router.prepareRoute.call(this, 'PATCH', url, opts, handler) }, post: function _post (url, opts, handler) { - return prepareRoute.call(this, 'POST', url, opts, handler) + return router.prepareRoute.call(this, 'POST', url, opts, handler) }, put: function _put (url, opts, handler) { - return prepareRoute.call(this, 'PUT', url, opts, handler) + return router.prepareRoute.call(this, 'PUT', url, opts, handler) }, options: function _options (url, opts, handler) { - return prepareRoute.call(this, 'OPTIONS', url, opts, handler) + return router.prepareRoute.call(this, 'OPTIONS', url, opts, handler) }, all: function _all (url, opts, handler) { - return prepareRoute.call(this, supportedMethods, url, opts, handler) + return router.prepareRoute.call(this, supportedMethods, url, opts, handler) }, // extended route - route: route, + route: function _route (opts) { + // we need the fastify object that we are producing so we apply a lazy loading of the function, + // otherwise we should bind it after the declaration + return router.route.call(this, opts) + }, // expose logger instance log: logger, // hooks @@ -191,7 +191,7 @@ function build (options) { // fake http injection inject: inject, // pretty print of the registered routes - printRoutes: router.prettyPrint.bind(router), + printRoutes: router.printRoutes, // custom error handling setNotFoundHandler: setNotFoundHandler, setErrorHandler: setErrorHandler, @@ -237,11 +237,10 @@ function build (options) { avvio.override = override avvio.on('start', () => (fastify[kState].started = true)) // cache the closing value, since we are checking it in an hot path - var closing = false avvio.once('preReady', () => { fastify.onClose((instance, done) => { fastify[kState].closing = true - closing = true + router.closeRoutes() if (fastify[kState].listening) { instance.server.close(done) } else { @@ -254,302 +253,21 @@ function build (options) { fastify.setNotFoundHandler() fourOhFour.arrange404(fastify) - const schemaCache = new Map() - schemaCache.put = schemaCache.set + router.setup(options, { + avvio, + fourOhFour, + logger, + hasLogger, + setupResponseListeners, + throwIfAlreadyStarted + }) return fastify - // HTTP request entry point, the routing has already been executed - function routeHandler (req, res, params, context) { - if (closing === true) { - const headers = { - 'Content-Type': 'application/json', - 'Content-Length': '80' - } - if (req.httpVersionMajor !== 2) { - headers.Connection = 'close' - } - res.writeHead(503, headers) - res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}') - if (req.httpVersionMajor !== 2) { - // This is not needed in HTTP/2 - setImmediate(() => req.destroy()) - } - return - } - - req.id = req.headers[requestIdHeader] || genReqId(req) - req.originalUrl = req.url - var hostname = req.headers['host'] - var ip = req.connection.remoteAddress - var ips - - if (trustProxy) { - ip = proxyAddr(req, proxyFn) - ips = proxyAddr.all(req, proxyFn) - if (ip !== undefined && req.headers['x-forwarded-host']) { - hostname = req.headers['x-forwarded-host'] - } - } - - var childLogger = logger.child({ [requestIdLogLabel]: req.id, level: context.logLevel }) - childLogger[kDisableRequestLogging] = disableRequestLogging - - // added hostname, ip, and ips back to the Node req object to maintain backward compatibility - if (modifyCoreObjects) { - req.hostname = hostname - req.ip = ip - req.ips = ips - - req.log = res.log = childLogger - } - - if (disableRequestLogging === false) { - childLogger.info({ req }, 'incoming request') - } - - var queryPrefix = req.url.indexOf('?') - var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '') - var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname) - var reply = new context.Reply(res, context, request, childLogger) - - if (hasLogger === true || context.onResponse !== null) { - setupResponseListeners(reply) - } - - if (context.onRequest !== null) { - hookRunner( - context.onRequest, - hookIterator, - request, - reply, - middlewareCallback - ) - } else { - middlewareCallback(null, request, reply) - } - } - - function middlewareCallback (err, request, reply) { - if (reply.sent === true) return - if (err != null) { - reply.send(err) - return - } - - if (reply.context._middie !== null) { - reply.context._middie.run(request.raw, reply.res, reply) - } else { - onRunMiddlewares(null, null, null, reply) - } - } - - function onRunMiddlewares (err, req, res, reply) { - if (err != null) { - reply.send(err) - return - } - - if (reply.context.preParsing !== null) { - hookRunner( - reply.context.preParsing, - hookIterator, - reply.request, - reply, - handleRequest - ) - } else { - handleRequest(null, reply.request, reply) - } - } - function throwIfAlreadyStarted (msg) { if (fastify[kState].started) throw new Error(msg) } - // Convert shorthand to extended route declaration - function prepareRoute (method, url, options, handler) { - if (!handler && typeof options === 'function') { - handler = options - options = {} - } else if (handler && typeof handler === 'function') { - if (Object.prototype.toString.call(options) !== '[object Object]') { - throw new Error(`Options for ${method}:${url} route must be an object`) - } else if (options.handler) { - if (typeof options.handler === 'function') { - throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`) - } else { - throw new Error(`Handler for ${method}:${url} route must be a function`) - } - } - } - - options = Object.assign({}, options, { - method, - url, - handler: handler || (options && options.handler) - }) - - return route.call(this, options) - } - - // Route management - function route (opts) { - throwIfAlreadyStarted('Cannot add route when fastify instance is already started!') - - if (Array.isArray(opts.method)) { - for (var i = 0; i < opts.method.length; i++) { - if (supportedMethods.indexOf(opts.method[i]) === -1) { - throw new Error(`${opts.method[i]} method is not supported!`) - } - } - } else { - if (supportedMethods.indexOf(opts.method) === -1) { - throw new Error(`${opts.method} method is not supported!`) - } - } - - if (!opts.handler) { - throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`) - } - - validateBodyLimitOption(opts.bodyLimit) - - if (opts.preHandler == null && opts.beforeHandler != null) { - beforeHandlerWarning() - opts.preHandler = opts.beforeHandler - } - - const prefix = this[kRoutePrefix] - - this.after((notHandledErr, done) => { - var path = opts.url || opts.path - if (path === '/' && prefix.length > 0) { - switch (opts.prefixTrailingSlash) { - case 'slash': - afterRouteAdded.call(this, path, notHandledErr, done) - break - case 'no-slash': - afterRouteAdded.call(this, '', notHandledErr, done) - break - case 'both': - default: - afterRouteAdded.call(this, '', notHandledErr, done) - afterRouteAdded.call(this, path, notHandledErr, done) - } - } else if (path[0] === '/' && prefix.endsWith('/')) { - // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route' - afterRouteAdded.call(this, path.slice(1), notHandledErr, done) - } else { - afterRouteAdded.call(this, path, notHandledErr, done) - } - }) - - // chainable api - return this - - function afterRouteAdded (path, notHandledErr, done) { - const url = prefix + path - - opts.url = url - opts.path = url - opts.prefix = prefix - opts.logLevel = opts.logLevel || this[kLogLevel] - - if (opts.attachValidation == null) { - opts.attachValidation = false - } - - // run 'onRoute' hooks - for (const hook of this[kGlobalHooks].onRoute) { - try { - hook.call(this, opts) - } catch (error) { - done(error) - return - } - } - - const config = opts.config || {} - config.url = url - - const context = new Context( - opts.schema, - opts.handler.bind(this), - this[kReply], - this[kRequest], - this[kContentTypeParser], - config, - this._errorHandler, - opts.bodyLimit, - opts.logLevel, - opts.attachValidation - ) - - // TODO this needs to be refactored so that buildSchemaCompiler is - // not called for every single route. Creating a new one for every route - // is going to be very expensive. - if (opts.schema) { - try { - if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) { - const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true }) - this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, schemaCache)) - } - - buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas]) - } catch (error) { - done(error) - return - } - } - - const hooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization'] - - for (let hook of hooks) { - if (opts[hook]) { - if (Array.isArray(opts[hook])) { - opts[hook] = opts[hook].map(fn => fn.bind(this)) - } else { - opts[hook] = opts[hook].bind(this) - } - } - } - - try { - router.on(opts.method, url, { version: opts.version }, routeHandler, context) - } catch (err) { - done(err) - return - } - - // It can happen that a user register a plugin with some hooks/middlewares *after* - // the route registration. To be sure to load also that hooks/middlewares, - // we must listen for the avvio's preReady event, and update the context object accordingly. - avvio.once('preReady', () => { - const onResponse = this[kHooks].onResponse - const onSend = this[kHooks].onSend - const onError = this[kHooks].onError - - context.onSend = onSend.length ? onSend : null - context.onError = onError.length ? onError : null - context.onResponse = onResponse.length ? onResponse : null - - for (let hook of hooks) { - const toSet = this[kHooks][hook].concat(opts[hook] || []) - context[hook] = toSet.length ? toSet : null - } - - context._middie = buildMiddie(this[kMiddlewares]) - - // Must store the 404 Context in 'preReady' because it is only guaranteed to - // be available after all of the plugins and routes have been loaded. - fourOhFour.setContext(this, context) - }) - - done(notHandledErr) - } - } - // HTTP injection handling // If the server is not ready yet, this // utility will automatically force it. @@ -661,7 +379,7 @@ function build (options) { function setNotFoundHandler (opts, handler) { throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!') - fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, routeHandler, buildMiddie) + fourOhFour.setNotFoundHandler.call(this, opts, handler, avvio, router.routeHandler) } // wrapper that we expose to the user for schemas compiler handling @@ -679,47 +397,6 @@ function build (options) { this._errorHandler = func return this } - - function buildMiddie (middlewares) { - if (!middlewares.length) { - return null - } - - const middie = Middie(onRunMiddlewares) - for (var i = 0; i < middlewares.length; i++) { - middie.use.apply(middie, middlewares[i]) - } - - return middie - } -} - -function getTrustProxyFn (options) { - const tp = options.trustProxy - if (typeof tp === 'function') { - return tp - } - if (tp === true) { - // Support plain true/false - return function () { return true } - } - if (typeof tp === 'number') { - // Support trusting hop count - return function (a, i) { return i < tp } - } - if (typeof tp === 'string') { - // Support comma-separated tps - const vals = tp.split(',').map(it => it.trim()) - return proxyAddr.compile(vals) - } - return proxyAddr.compile(tp || []) -} - -function validateBodyLimitOption (bodyLimit) { - if (bodyLimit === undefined) return - if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) { - throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`) - } } // Function that runs the encapsulation magic. diff --git a/lib/fourOhFour.js b/lib/fourOhFour.js index e81e0dc2ee3..430663f6501 100644 --- a/lib/fourOhFour.js +++ b/lib/fourOhFour.js @@ -19,6 +19,7 @@ const { kHooks } = require('./symbols.js') const { beforeHandlerWarning } = require('./warnings') +const { buildMiddie } = require('./middleware') /** * Each fastify instance have a: @@ -58,7 +59,7 @@ function fourOhFour (options) { context[kFourOhFourContext] = _404Context } - function setNotFoundHandler (opts, handler, avvio, routeHandler, buildMiddie) { + function setNotFoundHandler (opts, handler, avvio, routeHandler) { // First initialization of the fastify root instance if (this[kCanSetNotFoundHandler] === undefined) { this[kCanSetNotFoundHandler] = true @@ -110,12 +111,12 @@ function fourOhFour (options) { } this.after((notHandledErr, done) => { - _setNotFoundHandler.call(this, prefix, opts, handler, avvio, routeHandler, buildMiddie) + _setNotFoundHandler.call(this, prefix, opts, handler, avvio, routeHandler) done(notHandledErr) }) } - function _setNotFoundHandler (prefix, opts, handler, avvio, routeHandler, buildMiddie) { + function _setNotFoundHandler (prefix, opts, handler, avvio, routeHandler) { const context = new Context( opts.schema, handler, diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 00000000000..6e66ccaa54e --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,38 @@ +'use strict' + +const Middie = require('middie') +const handleRequest = require('./handleRequest') +const { hookRunner, hookIterator } = require('./hooks') + +function onRunMiddlewares (err, req, res, reply) { + if (err != null) { + reply.send(err) + return + } + + if (reply.context.preParsing !== null) { + hookRunner( + reply.context.preParsing, + hookIterator, + reply.request, + reply, + handleRequest + ) + } else { + handleRequest(null, reply.request, reply) + } +} +module.exports.onRunMiddlewares = onRunMiddlewares + +module.exports.buildMiddie = function buildMiddie (middlewares) { + if (!middlewares.length) { + return null + } + + const middie = Middie(onRunMiddlewares) + for (var i = 0; i < middlewares.length; i++) { + middie.use.apply(middie, middlewares[i]) + } + + return middie +} diff --git a/lib/route.js b/lib/route.js new file mode 100644 index 00000000000..8cf41654ec5 --- /dev/null +++ b/lib/route.js @@ -0,0 +1,374 @@ +'use strict' + +const FindMyWay = require('find-my-way') +const proxyAddr = require('proxy-addr') +const Context = require('./context') +const { buildMiddie, onRunMiddlewares } = require('./middleware') +const { hookRunner, hookIterator } = require('./hooks') +const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS'] +const validation = require('./validation') +const buildSchema = validation.build +const { buildSchemaCompiler } = validation +const { beforeHandlerWarning } = require('./warnings') + +const { + kRoutePrefix, + kLogLevel, + kHooks, + kSchemas, + kSchemaCompiler, + kContentTypeParser, + kReply, + kRequest, + kMiddlewares, + kGlobalHooks, + kDisableRequestLogging +} = require('./symbols.js') + +function buildRouting (options) { + const router = FindMyWay(options.config) + + const schemaCache = new Map() + schemaCache.put = schemaCache.set + + let avvio + let fourOhFour + let trustProxy + let requestIdHeader + let querystringParser + let requestIdLogLabel + let logger + let hasLogger + let setupResponseListeners + let throwIfAlreadyStarted + let proxyFn + let modifyCoreObjects + let genReqId + let disableRequestLogging + + let closing = false + + return { + setup (options, fastifyArgs) { + avvio = fastifyArgs.avvio + fourOhFour = fastifyArgs.fourOhFour + logger = fastifyArgs.logger + hasLogger = fastifyArgs.hasLogger + setupResponseListeners = fastifyArgs.setupResponseListeners + throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted + + proxyFn = getTrustProxyFn(options) + trustProxy = options.trustProxy + requestIdHeader = options.requestIdHeader + querystringParser = options.querystringParser + requestIdLogLabel = options.requestIdLogLabel + modifyCoreObjects = options.modifyCoreObjects + genReqId = options.genReqId + disableRequestLogging = options.disableRequestLogging + }, + routing: router.lookup.bind(router), // router func to find the right handler to call + route, // configure a route in the fastify instance + prepareRoute, + routeHandler, + closeRoutes: () => { closing = true }, + printRoutes: router.prettyPrint.bind(router) + } + + // Convert shorthand to extended route declaration + function prepareRoute (method, url, options, handler) { + if (!handler && typeof options === 'function') { + handler = options + options = {} + } else if (handler && typeof handler === 'function') { + if (Object.prototype.toString.call(options) !== '[object Object]') { + throw new Error(`Options for ${method}:${url} route must be an object`) + } else if (options.handler) { + if (typeof options.handler === 'function') { + throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`) + } else { + throw new Error(`Handler for ${method}:${url} route must be a function`) + } + } + } + + options = Object.assign({}, options, { + method, + url, + handler: handler || (options && options.handler) + }) + + return route.call(this, options) + } + + // Route management + function route (opts) { + throwIfAlreadyStarted('Cannot add route when fastify instance is already started!') + + if (Array.isArray(opts.method)) { + for (var i = 0; i < opts.method.length; i++) { + if (supportedMethods.indexOf(opts.method[i]) === -1) { + throw new Error(`${opts.method[i]} method is not supported!`) + } + } + } else { + if (supportedMethods.indexOf(opts.method) === -1) { + throw new Error(`${opts.method} method is not supported!`) + } + } + + if (!opts.handler) { + throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`) + } + + validateBodyLimitOption(opts.bodyLimit) + + if (opts.preHandler == null && opts.beforeHandler != null) { + beforeHandlerWarning() + opts.preHandler = opts.beforeHandler + } + + const prefix = this[kRoutePrefix] + + this.after((notHandledErr, done) => { + var path = opts.url || opts.path + if (path === '/' && prefix.length > 0) { + switch (opts.prefixTrailingSlash) { + case 'slash': + afterRouteAdded.call(this, path, notHandledErr, done) + break + case 'no-slash': + afterRouteAdded.call(this, '', notHandledErr, done) + break + case 'both': + default: + afterRouteAdded.call(this, '', notHandledErr, done) + afterRouteAdded.call(this, path, notHandledErr, done) + } + } else if (path[0] === '/' && prefix.endsWith('/')) { + // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route' + afterRouteAdded.call(this, path.slice(1), notHandledErr, done) + } else { + afterRouteAdded.call(this, path, notHandledErr, done) + } + }) + + // chainable api + return this + + function afterRouteAdded (path, notHandledErr, done) { + const url = prefix + path + + opts.url = url + opts.path = url + opts.prefix = prefix + opts.logLevel = opts.logLevel || this[kLogLevel] + + if (opts.attachValidation == null) { + opts.attachValidation = false + } + + // run 'onRoute' hooks + for (const hook of this[kGlobalHooks].onRoute) { + try { + hook.call(this, opts) + } catch (error) { + done(error) + return + } + } + + const config = opts.config || {} + config.url = url + + const context = new Context( + opts.schema, + opts.handler.bind(this), + this[kReply], + this[kRequest], + this[kContentTypeParser], + config, + this._errorHandler, + opts.bodyLimit, + opts.logLevel, + opts.attachValidation + ) + + // TODO this needs to be refactored so that buildSchemaCompiler is + // not called for every single route. Creating a new one for every route + // is going to be very expensive. + if (opts.schema) { + try { + if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) { + const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true }) + this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, schemaCache)) + } + + buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas]) + } catch (error) { + done(error) + return + } + } + + const hooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization'] + + for (let hook of hooks) { + if (opts[hook]) { + if (Array.isArray(opts[hook])) { + opts[hook] = opts[hook].map(fn => fn.bind(this)) + } else { + opts[hook] = opts[hook].bind(this) + } + } + } + + try { + router.on(opts.method, url, { version: opts.version }, routeHandler, context) + } catch (err) { + done(err) + return + } + + // It can happen that a user register a plugin with some hooks/middlewares *after* + // the route registration. To be sure to load also that hooks/middlewares, + // we must listen for the avvio's preReady event, and update the context object accordingly. + avvio.once('preReady', () => { + const onResponse = this[kHooks].onResponse + const onSend = this[kHooks].onSend + const onError = this[kHooks].onError + + context.onSend = onSend.length ? onSend : null + context.onError = onError.length ? onError : null + context.onResponse = onResponse.length ? onResponse : null + + for (let hook of hooks) { + const toSet = this[kHooks][hook].concat(opts[hook] || []) + context[hook] = toSet.length ? toSet : null + } + + context._middie = buildMiddie(this[kMiddlewares]) + + // Must store the 404 Context in 'preReady' because it is only guaranteed to + // be available after all of the plugins and routes have been loaded. + fourOhFour.setContext(this, context) + }) + + done(notHandledErr) + } + } + + // HTTP request entry point, the routing has already been executed + function routeHandler (req, res, params, context) { + if (closing === true) { + const headers = { + 'Content-Type': 'application/json', + 'Content-Length': '80' + } + if (req.httpVersionMajor !== 2) { + headers.Connection = 'close' + } + res.writeHead(503, headers) + res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}') + if (req.httpVersionMajor !== 2) { + // This is not needed in HTTP/2 + setImmediate(() => req.destroy()) + } + return + } + + req.id = req.headers[requestIdHeader] || genReqId(req) + req.originalUrl = req.url + var hostname = req.headers['host'] + var ip = req.connection.remoteAddress + var ips + + if (trustProxy) { + ip = proxyAddr(req, proxyFn) + ips = proxyAddr.all(req, proxyFn) + if (ip !== undefined && req.headers['x-forwarded-host']) { + hostname = req.headers['x-forwarded-host'] + } + } + + var childLogger = logger.child({ [requestIdLogLabel]: req.id, level: context.logLevel }) + childLogger[kDisableRequestLogging] = disableRequestLogging + + // added hostname, ip, and ips back to the Node req object to maintain backward compatibility + if (modifyCoreObjects) { + req.hostname = hostname + req.ip = ip + req.ips = ips + + req.log = res.log = childLogger + } + + if (disableRequestLogging === false) { + childLogger.info({ req }, 'incoming request') + } + + var queryPrefix = req.url.indexOf('?') + var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '') + var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname) + var reply = new context.Reply(res, context, request, childLogger) + + if (hasLogger === true || context.onResponse !== null) { + setupResponseListeners(reply) + } + + if (context.onRequest !== null) { + hookRunner( + context.onRequest, + hookIterator, + request, + reply, + middlewareCallback + ) + } else { + middlewareCallback(null, request, reply) + } + } +} + +function validateBodyLimitOption (bodyLimit) { + if (bodyLimit === undefined) return + if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) { + throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`) + } +} + +function middlewareCallback (err, request, reply) { + if (reply.sent === true) return + if (err != null) { + reply.send(err) + return + } + + if (reply.context._middie !== null) { + reply.context._middie.run(request.raw, reply.res, reply) + } else { + onRunMiddlewares(null, null, null, reply) + } +} + +function getTrustProxyFn (options) { + const tp = options.trustProxy + if (typeof tp === 'function') { + return tp + } + if (tp === true) { + // Support plain true/false + return function () { return true } + } + if (typeof tp === 'number') { + // Support trusting hop count + return function (a, i) { return i < tp } + } + if (typeof tp === 'string') { + // Support comma-separated tps + const vals = tp.split(',').map(it => it.trim()) + return proxyAddr.compile(vals) + } + return proxyAddr.compile(tp || []) +} + +module.exports = { buildRouting, validateBodyLimitOption } From ff5bfddf3dcd828e77fef74de245b414384b63da Mon Sep 17 00:00:00 2001 From: Okan Sahin <39759830+mokimo@users.noreply.github.com> Date: Sun, 19 May 2019 23:10:20 +0200 Subject: [PATCH 013/144] docs(reply): clarify settings headers (#1642) --- docs/Reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Reply.md b/docs/Reply.md index 2cf8d61d3e5..5792d2eb037 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -77,7 +77,7 @@ For more information, see [`http.ServerResponse#setHeader`](https://nodejs.org/d ### .getHeader(key) Retrieves the value of a previously set header. ```js -reply.header('x-foo', 'foo') +reply.header('x-foo', 'foo') // setHeader: key, value reply.getHeader('x-foo') // 'foo' ``` From a6fa039939ee9f9317f9ef7739f265e45f2427b2 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 22 May 2019 09:22:02 +0200 Subject: [PATCH 014/144] Bumped v2.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ccc5c6e0703..01a41911256 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.3.0", + "version": "2.4.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 5d442c54970797a985eaad981abca2342eae23f9 Mon Sep 17 00:00:00 2001 From: Davlat Shavkatov Date: Wed, 22 May 2019 14:37:38 +0500 Subject: [PATCH 015/144] Add .git folder to NPM ignore (#1663) * Add .git folder to NPM ignore * Update .npmignore Co-Authored-By: Manuel Spigolon --- .npmignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.npmignore b/.npmignore index 6bc1af05e34..7f3bd63c002 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,7 @@ .editorconfig .gitattributes +.git +.DS_Store .gitignore .github .nyc_output From 712337d6be084b8fc6dca88923eb9cbe6c205b13 Mon Sep 17 00:00:00 2001 From: delvedor Date: Wed, 22 May 2019 11:38:38 +0200 Subject: [PATCH 016/144] Bumped v2.4.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01a41911256..1fb5664837f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.4.0", + "version": "2.4.1", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 1958c74a2bc2e96d3892254904f485985bc57330 Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Fri, 24 May 2019 15:29:37 +0200 Subject: [PATCH 017/144] Adds defaults to TS generics definitions (#1669) --- fastify.d.ts | 24 ++++++++++++------------ test/types/index.ts | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 20e30a3f527..05878ea47cc 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -69,9 +69,9 @@ declare namespace fastify { } type FastifyMiddleware< - HttpServer, - HttpRequest, - HttpResponse, + HttpServer = http.Server, + HttpRequest = http.IncomingMessage, + HttpResponse = http.ServerResponse, Query = DefaultQuery, Params = DefaultParams, Headers = DefaultHeaders, @@ -84,9 +84,9 @@ declare namespace fastify { ) => void type FastifyMiddlewareWithPayload< - HttpServer, - HttpRequest, - HttpResponse, + HttpServer = http.Server, + HttpRequest = http.IncomingMessage, + HttpResponse = http.ServerResponse, Query = DefaultQuery, Params = DefaultParams, Headers = DefaultHeaders, @@ -100,8 +100,8 @@ declare namespace fastify { ) => void type RequestHandler< - HttpRequest, - HttpResponse, + HttpRequest = http.IncomingMessage, + HttpResponse = http.ServerResponse, Query = DefaultQuery, Params = DefaultParams, Headers = DefaultHeaders, @@ -130,7 +130,7 @@ declare namespace fastify { * fastify's wrapped version of node.js IncomingMessage */ interface FastifyRequest< - HttpRequest, + HttpRequest = http.IncomingMessage, Query = DefaultQuery, Params = DefaultParams, Headers = DefaultHeaders, @@ -264,9 +264,9 @@ declare namespace fastify { * Route configuration options such as "url" and "method" */ interface RouteOptions< - HttpServer, - HttpRequest, - HttpResponse, + HttpServer = http.Server, + HttpRequest = http.IncomingMessage, + HttpResponse = http.ServerResponse, Query = DefaultQuery, Params = DefaultParams, Headers = DefaultHeaders, diff --git a/test/types/index.ts b/test/types/index.ts index ef3fb958dcb..8d1d490b426 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -604,3 +604,21 @@ const server2 = fastify() server2.close().then(() => {}) const server3 = fastify() server3.close(() => {}) + +{ + // tests generics default values + const routeOptions: fastify.RouteOptions = { + method: 'GET', + url: '/', + handler: function (req, reply) { reply.send({}) } + } + + const genericHandler: fastify.RequestHandler = (req, reply) => { reply.send(reply) } + + const middleware: fastify.FastifyMiddleware = function middleware (req, reply, done) { + this.addHook('onClose', function (instance, done) { + done() + }) + done() + } +} From 7f274d1be8ed27e32662a29a5940194b602086e8 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Fri, 31 May 2019 04:03:41 -0400 Subject: [PATCH 018/144] Update node types, typescript, and fix broken test (#1681) --- package.json | 4 ++-- test/types/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1fb5664837f..75bc1a25568 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "node": ">=6" }, "devDependencies": { - "@types/node": "^11.9.3", + "@types/node": "^11.13.12", "@typescript-eslint/eslint-plugin": "^1.4.2", "@typescript-eslint/parser": "^1.4.2", "JSONStream": "^1.3.5", @@ -127,7 +127,7 @@ "tap": "^12.5.2", "tap-mocha-reporter": "^3.0.7", "then-sleep": "^1.0.1", - "typescript": "^3.3.3333", + "typescript": "^3.5.1", "x-xss-protection": "^1.1.0" }, "dependencies": { diff --git a/test/types/index.ts b/test/types/index.ts index 8d1d490b426..1cd0ce47e7b 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -188,7 +188,7 @@ const schema: fastify.RouteSchema = { } } -const opts: fastify.RouteShorthandOptions = { +const opts: fastify.RouteShorthandOptions = { schema, preValidation: [ (request, reply, next) => { From fc52dc3813dec514b8bb374660818e0a52ec13b6 Mon Sep 17 00:00:00 2001 From: Alexander Kureniov Date: Tue, 4 Jun 2019 01:01:51 +0300 Subject: [PATCH 019/144] Add RBAC plugin to Ecosystem (#1692) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 40568a15f45..5d49c8d0f45 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -96,6 +96,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-openapi-glue`](https://github.com/seriousme/fastify-openapi-glue) Glue for Open Api specifications in Fastify, autogenerates routes based on an Open Api Specification - [`fastify-oracle`](https://github.com/cemremengu/fastify-oracle) Attaches an [`oracledb`](https://github.com/oracle/node-oracledb) connection pool to a Fastify server instance. - [`fastify-orientdb`](https://github.com/mahmed8003/fastify-orientdb) Fastify OrientDB connection plugin, with which you can share the OrientDB connection across every part of your server. +- [`fastify-rbac`](https://gitlab.com/m03geek/fastify-rbac) Fastify role-based access control plugin. - [`fastify-register-routes`](https://github.com/israeleriston/fastify-register-routes) Plugin to automatically load routes from a specified path and optionally limit loaded file names by a regular expression. - [`fastify-response-time`](https://github.com/lolo32/fastify-response-time) Add `X-Response-Time` header at each request for Fastify, in milliseconds. - [`fastify-rob-config`](https://github.com/jeromemacias/fastify-rob-config) Fastify Rob-Config integration. From ecea232ae596fd0eb06a5b38080e19e4414bd942 Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Tue, 4 Jun 2019 17:36:31 +0200 Subject: [PATCH 020/144] Removes double call to afterRouteAdded when ignoreTrailingSlash === true (#1675) * Removes double call to afterRouteAdded when ignoreTrailingSlash === true * Adds explaining comment when adding a prefixed plugin --- lib/route.js | 7 ++++++- test/route-prefix.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/route.js b/lib/route.js index 8cf41654ec5..b191fb2557f 100644 --- a/lib/route.js +++ b/lib/route.js @@ -45,6 +45,7 @@ function buildRouting (options) { let modifyCoreObjects let genReqId let disableRequestLogging + let ignoreTrailingSlash let closing = false @@ -65,6 +66,7 @@ function buildRouting (options) { modifyCoreObjects = options.modifyCoreObjects genReqId = options.genReqId disableRequestLogging = options.disableRequestLogging + ignoreTrailingSlash = options.ignoreTrailingSlash }, routing: router.lookup.bind(router), // router func to find the right handler to call route, // configure a route in the fastify instance @@ -142,7 +144,10 @@ function buildRouting (options) { case 'both': default: afterRouteAdded.call(this, '', notHandledErr, done) - afterRouteAdded.call(this, path, notHandledErr, done) + // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one. + if (ignoreTrailingSlash !== true) { + afterRouteAdded.call(this, path, notHandledErr, done) + } } } else if (path[0] === '/' && prefix.endsWith('/')) { // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route' diff --git a/test/route-prefix.test.js b/test/route-prefix.test.js index 7a0de1e3ee3..3c22fb8eaa1 100644 --- a/test/route-prefix.test.js +++ b/test/route-prefix.test.js @@ -459,6 +459,37 @@ test('matches both /prefix and /prefix/ with a / route - prefixTrailingSlash: " }) }) +test('returns 404 status code with /prefix/ and / route - prefixTrailingSlash: "both" (default), ignoreTrailingSlash: true', t => { + t.plan(2) + const fastify = Fastify({ + ignoreTrailingSlash: true + }) + + fastify.register(function (fastify, opts, next) { + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { + reply.send({ hello: 'world' }) + } + }) + + next() + }, { prefix: '/prefix/' }) + + fastify.inject({ + method: 'GET', + url: '/prefix//' + }, (err, res) => { + t.error(err) + t.same(JSON.parse(res.payload), { + 'error': 'Not Found', + 'message': 'Not Found', + 'statusCode': 404 + }) + }) +}) + test('matches only /prefix with a / route - prefixTrailingSlash: "no-slash", ignoreTrailingSlash: false', t => { t.plan(4) const fastify = Fastify({ From 41a94c39e491204e1057cb71ce625135bded26c3 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Fri, 7 Jun 2019 13:28:34 -0300 Subject: [PATCH 021/144] docs: fix req.body inside logger req (#1695) --- docs/Logging.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/Logging.md b/docs/Logging.md index 661a534d857..2deb223e07f 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -90,11 +90,10 @@ const fastify = require('fastify')({ url: req.url, path: req.path, parameters: req.parameters, - // Including the body and headers in the log could be in violation + // Including the headers in the log could be in violation // of privacy laws, e.g. GDPR. You should use the "redact" option to // remove sensitive fields. It could also leak authentication data in // the logs. - body: req.body, headers: req.headers }; } @@ -102,6 +101,19 @@ const fastify = require('fastify')({ } }); ``` +**Note**: The body not can serialize inside `req` method, because the request is serialized when we create the child logger. At that time, the body is not parsed yet. + +See a approach to log `req.body` + +```js +app.addHook('preHandler', function (req, reply, next) { + if (req.body) { + req.log.info({ body: req.body }, 'parsed body') + } + next() +}) +``` + *This option will be ignored by any logger other than Pino.* From bdc137e9ab320ed380b5c617647f4eb57aa8e840 Mon Sep 17 00:00:00 2001 From: Raghav Manikandan Date: Mon, 10 Jun 2019 03:31:19 +0530 Subject: [PATCH 022/144] Alias schema.query to schema.querystring (#1690) (#1694) --- docs/Fluent-Schema.md | 2 +- docs/Routes.md | 2 +- docs/Validation-and-Serialization.md | 2 +- lib/errors.js | 1 + lib/schemas.js | 12 +++++- test/internals/validation.test.js | 57 ++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/docs/Fluent-Schema.md b/docs/Fluent-Schema.md index 6a4fd4b000c..b50629f83cc 100644 --- a/docs/Fluent-Schema.md +++ b/docs/Fluent-Schema.md @@ -43,7 +43,7 @@ const headersJsonSchema = S.object() const schema = { body: bodyJsonSchema.valueOf(), - querystring: queryStringJsonSchema.valueOf(), + querystring: queryStringJsonSchema.valueOf(), // (or) query: queryStringJsonSchema.valueOf() params: paramsJsonSchema.valueOf(), headers: headersJsonSchema.valueOf() } diff --git a/docs/Routes.md b/docs/Routes.md index 4afeb31cd7d..afc5f00604a 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -16,7 +16,7 @@ They need to be in * `body`: validates the body of the request if it is a POST or a PUT. - * `querystring`: validates the querystring. This can be a complete JSON + * `querystring` or `query`: validates the querystring. This can be a complete JSON Schema object, with the property `type` of `object` and `properties` object of parameters, or simply the values of what would be contained in the `properties` object as shown below. * `params`: validates the params. diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 75497226707..054400b707f 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -14,7 +14,7 @@ Fastify uses a schema-based approach, and even if it is not mandatory we recomme ### Validation The route validation internally relies upon [Ajv](https://www.npmjs.com/package/ajv), which is a high-performance JSON schema validator. Validating the input is very easy: just add the fields that you need inside the route schema, and you are done! The supported validations are: - `body`: validates the body of the request if it is a POST or a PUT. -- `querystring`: validates the query string. This can be a complete JSON Schema object (with a `type` property of `'object'` and a `'properties'` object containing parameters) or a simpler variation in which the `type` and `properties` attributes are forgone and the query parameters are listed at the top level (see the example below). +- `querystring` or `query`: validates the query string. This can be a complete JSON Schema object (with a `type` property of `'object'` and a `'properties'` object containing parameters) or a simpler variation in which the `type` and `properties` attributes are forgone and the query parameters are listed at the top level (see the example below). - `params`: validates the route params. - `headers`: validates the request headers. diff --git a/lib/errors.js b/lib/errors.js index 036340c6b96..d01cca7039c 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -53,6 +53,7 @@ createError('FST_ERR_SEND_INSIDE_ONERR', 'You cannot use `send` inside the `onEr createError('FST_ERR_SCH_MISSING_ID', `Missing schema $id property`) createError('FST_ERR_SCH_ALREADY_PRESENT', `Schema with id '%s' already declared!`) createError('FST_ERR_SCH_NOT_PRESENT', `Schema with id '%s' does not exist!`) +createError('FST_ERR_SCH_DUPLICATE', `Schema with '%s' already present!`) /** * wrapThenable diff --git a/lib/schemas.js b/lib/schemas.js index a892ceb0a95..213baa5bdc9 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -6,7 +6,8 @@ const { codes: { FST_ERR_SCH_MISSING_ID, FST_ERR_SCH_ALREADY_PRESENT, - FST_ERR_SCH_NOT_PRESENT + FST_ERR_SCH_NOT_PRESENT, + FST_ERR_SCH_DUPLICATE } } = require('./errors') @@ -38,6 +39,15 @@ Schemas.prototype.resolve = function (id) { } Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { + // alias query to querystring schema + if (routeSchemas.query) { + // check if our schema has both querystring and query + if (routeSchemas.querystring) { + throw new FST_ERR_SCH_DUPLICATE('querystring') + } + routeSchemas.querystring = routeSchemas.query + } + // let's check if our schemas have a custom prototype for (const key of ['headers', 'querystring', 'params', 'body']) { if (typeof routeSchemas[key] === 'object' && Object.getPrototypeOf(routeSchemas[key]) !== Object.prototype) { diff --git a/test/internals/validation.test.js b/test/internals/validation.test.js index f33f0b14a0a..01a45a77c9c 100644 --- a/test/internals/validation.test.js +++ b/test/internals/validation.test.js @@ -74,6 +74,37 @@ test('build schema - payload schema', t => { t.is(typeof opts[symbols.bodySchema], 'function') }) +test('build schema - query schema', t => { + t.plan(2) + const opts = { + schema: { + query: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + } + validation.build(opts, schema => ajv.compile(schema), new Schemas()) + t.type(opts[symbols.querystringSchema].schema.type, 'string') + t.is(typeof opts[symbols.querystringSchema], 'function') +}) + +test('build schema - query schema abbreviated', t => { + t.plan(2) + const opts = { + schema: { + query: { + hello: { type: 'string' } + } + } + } + validation.build(opts, schema => ajv.compile(schema), new Schemas()) + t.type(opts[symbols.querystringSchema].schema.type, 'string') + t.is(typeof opts[symbols.querystringSchema], 'function') +}) + test('build schema - querystring schema', t => { t.plan(2) const opts = { @@ -105,6 +136,32 @@ test('build schema - querystring schema abbreviated', t => { t.is(typeof opts[symbols.querystringSchema], 'function') }) +test('build schema - must throw if querystring and query schema exist', t => { + t.plan(2) + try { + const opts = { + schema: { + query: { + type: 'object', + properties: { + hello: { type: 'string' } + } + }, + querystring: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + } + validation.build(opts, schema => ajv.compile(schema), new Schemas()) + } catch (err) { + t.is(err.code, 'FST_ERR_SCH_DUPLICATE') + t.is(err.message, 'FST_ERR_SCH_DUPLICATE: Schema with \'querystring\' already present!') + } +}) + test('build schema - params schema', t => { t.plan(1) const opts = { From 00d0e2397af127da88421ef23df789ce3ed611a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8F=9C?= Date: Mon, 10 Jun 2019 20:47:45 +0800 Subject: [PATCH 023/144] docs: fix typos (#1701) --- docs/Logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Logging.md b/docs/Logging.md index 2deb223e07f..85d246c51c9 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -71,7 +71,7 @@ const fastify = require('fastify')({ } }) ``` -For example, the response payload and headers could be logged using the approach below (even if it is *not* recommended*): +For example, the response payload and headers could be logged using the approach below (even if it is *not recommended*): ```js const fastify = require('fastify')({ From b53f0374de65bab4445a4a58bea75e7df1cd2594 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Tue, 11 Jun 2019 18:00:25 +0200 Subject: [PATCH 024/144] chore: greenkeeper ignore autocannon (#1696) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 75bc1a25568..ba2348fcb85 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ }, "greenkeeper": { "ignore": [ + "autocannon", "boom", "joi", "@types/node", From d8730a284dbb0563b216678851e30df43c46c65c Mon Sep 17 00:00:00 2001 From: Vincenzo Chianese Date: Tue, 11 Jun 2019 20:04:45 +0100 Subject: [PATCH 025/144] fix: use reply instead of original res (#1704) --- lib/context.js | 4 ++-- test/logger.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/context.js b/lib/context.js index 49e73f796b6..9b405d6ae38 100644 --- a/lib/context.js +++ b/lib/context.js @@ -27,9 +27,9 @@ function Context (schema, handler, Reply, Request, contentTypeParser, config, er function defaultErrorHandler (error, request, reply) { var res = reply.res if (res.statusCode >= 500) { - res.log.error({ req: reply.request.raw, res: res, err: error }, error && error.message) + reply.log.error({ req: reply.request.raw, res: res, err: error }, error && error.message) } else if (res.statusCode >= 400) { - res.log.info({ res: res, err: error }, error && error.message) + reply.log.info({ res: res, err: error }, error && error.message) } reply.send(error) } diff --git a/test/logger.test.js b/test/logger.test.js index b3367060047..f04c278ed9b 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -1072,6 +1072,38 @@ test('should not log the error if error handler is defined', t => { }) }) +test('should not rely on raw request to log errors', t => { + t.plan(7) + const stream = split(JSON.parse) + const fastify = Fastify({ + modifyCoreObjects: false, + logger: { + stream: stream, + level: 'info' + } + }) + fastify.get('/error', function (req, reply) { + t.ok(req.log) + reply.status(415).send(new Error('something happened')) + }) + fastify.listen(0, err => { + t.error(err) + fastify.server.unref() + http.get('http://localhost:' + fastify.server.address().port + '/error') + stream.once('data', listenAtLogLine => { + t.ok(listenAtLogLine, 'listen at log message is ok') + stream.once('data', line => { + t.equal(line.msg, 'incoming request', 'message is set') + stream.once('data', line => { + t.equal(line.level, 30, 'level is correct') + t.equal(line.msg, 'something happened', 'message is set') + t.deepEqual(line.res, { statusCode: 415 }, 'status code is set') + }) + }) + }) + }) +}) + test('should redact the authorization header if so specified', t => { t.plan(7) const stream = split(JSON.parse) From fda844820f8a581452aac9413a40a2d63220d859 Mon Sep 17 00:00:00 2001 From: hhwang39 Date: Tue, 11 Jun 2019 15:05:56 -0400 Subject: [PATCH 026/144] Added case sensitive option to definition file. (#1691) * Added case sensitive option to definition file. * Added casSensitive Option to test --- fastify.d.ts | 1 + test/types/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/fastify.d.ts b/fastify.d.ts index 05878ea47cc..025406598bd 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -178,6 +178,7 @@ declare namespace fastify { } type TrustProxyFunction = (addr: string, index: number) => boolean interface ServerOptions { + caseSensitive?: boolean, ignoreTrailingSlash?: boolean, bodyLimit?: number, pluginTimeout?: number, diff --git a/test/types/index.ts b/test/types/index.ts index 1cd0ce47e7b..34472b2551b 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -58,6 +58,7 @@ const cors = require('cors') // other simple options const otherServer = fastify({ + caseSensitive: false, ignoreTrailingSlash: true, bodyLimit: 1000, maxParamLength: 200, From 1ad7566f0c131a8fc110b979a2f2cf31b5a375a3 Mon Sep 17 00:00:00 2001 From: Glen Keane Date: Tue, 11 Jun 2019 20:07:44 +0100 Subject: [PATCH 027/144] add ability to get response time on reply (#1697) * add ability to get response time on reply * feedback * Update docs/Reply.md as per suggestion Co-Authored-By: Manuel Spigolon * update type of FastifyReply * add type test --- docs/Reply.md | 9 +++++++++ fastify.d.ts | 1 + lib/reply.js | 15 +++++++++++---- test/internals/reply.test.js | 30 +++++++++++++++++++++++++++++- test/types/index.ts | 4 ++++ 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/Reply.md b/docs/Reply.md index 5792d2eb037..e1b92e91a1b 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -10,6 +10,7 @@ - [.hasHeader(key)](#hasheaderkey) - [.redirect(dest)](#redirectdest) - [.callNotFound()](#callnotfound) + - [.getResponseTime()](#getresponsetime) - [.type(contentType)](#typecontenttype) - [.serializer(func)](#serializerfunc) - [.sent](#sent) @@ -109,6 +110,14 @@ Invokes the custom not found handler. reply.callNotFound() ``` + +### .getResponseTime() +Invokes the custom response time getter to calculate the amount of time passed since the request was started. + +```js +const milliseconds = reply.getResponseTime() +``` + ### .type(contentType) Sets the content type for the response. diff --git a/fastify.d.ts b/fastify.d.ts index 025406598bd..f74a54ed674 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -166,6 +166,7 @@ declare namespace fastify { getHeader(name: string): string | undefined hasHeader(name: string): boolean callNotFound(): void + getResponseTime(): number type(contentType: string): FastifyReply redirect(url: string): FastifyReply redirect(statusCode: number, url: string): FastifyReply diff --git a/lib/reply.js b/lib/reply.js index ca46c6ed4cd..02562dd7f11 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -222,6 +222,16 @@ Reply.prototype.callNotFound = function () { notFound(this) } +Reply.prototype.getResponseTime = function () { + var responseTime = 0 + + if (this[kReplyStartTime] !== undefined) { + responseTime = now() - this[kReplyStartTime] + } + + return responseTime +} + function preserializeHook (reply, payload) { if (reply.context.preSerialization !== null) { onSendHookRunner( @@ -460,11 +470,8 @@ function onResponseCallback (err, request, reply) { if (reply.log[kDisableRequestLogging]) { return } - var responseTime = 0 - if (reply[kReplyStartTime] !== undefined) { - responseTime = now() - reply[kReplyStartTime] - } + var responseTime = reply.getResponseTime() if (err != null) { reply.log.error({ diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index efa49b66e45..f9f819093c8 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -14,7 +14,7 @@ const { } = require('../../lib/symbols') test('Once called, Reply should return an object with methods', t => { - t.plan(12) + t.plan(13) const response = { res: 'res' } function context () {} function request () {} @@ -27,6 +27,7 @@ test('Once called, Reply should return an object with methods', t => { t.is(typeof reply.status, 'function') t.is(typeof reply.header, 'function') t.is(typeof reply.serialize, 'function') + t.is(typeof reply.getResponseTime, 'function') t.is(typeof reply[kReplyHeaders], 'object') t.strictEqual(reply.res, response) t.strictEqual(reply.context, context) @@ -977,3 +978,30 @@ test('should throw error when attempting to set reply.sent more than once', t => t.pass() }) }) + +test('reply.getResponseTime() should return 0 before the timer is initialised on the reply by setting up response listeners', t => { + t.plan(1) + const response = { statusCode: 200 } + const context = {} + const reply = new Reply(response, context, null) + t.equal(reply.getResponseTime(), 0) +}) + +test('reply.getResponseTime() should return a number greater than 0 after the timer is initialised on the reply by setting up response listeners', t => { + t.plan(1) + const fastify = require('../..')() + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { + reply.send('hello world') + } + }) + + fastify.addHook('onResponse', (req, reply) => { + t.true(reply.getResponseTime() > 0) + t.end() + }) + + fastify.inject({ method: 'GET', url: '/' }) +}) diff --git a/test/types/index.ts b/test/types/index.ts index 34472b2551b..c2506333001 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -346,6 +346,10 @@ server .get('/deprecatedpath/*', (req, reply) => { reply.callNotFound() }) + .get('/getResponseTime', function (req, reply) { + const milliseconds : number = reply.getResponseTime() + reply.send({ milliseconds }) + }) // Generics example interface Query { From 901911c553bea62a21799ec7df34d5e36d2bacd0 Mon Sep 17 00:00:00 2001 From: delvedor Date: Wed, 12 Jun 2019 12:16:37 +0200 Subject: [PATCH 028/144] Bumped v2.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba2348fcb85..1a1b5ff4cce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.4.1", + "version": "2.5.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 78c867f46ad17265f019dd8e84148a21bc4c33b6 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Mon, 24 Jun 2019 18:10:54 +0200 Subject: [PATCH 029/144] feature: custom serializator (#1706) --- docs/Server.md | 12 ++ fastify.d.ts | 5 + fastify.js | 10 ++ lib/context.js | 5 +- lib/reply.js | 14 +- lib/route.js | 4 +- lib/symbols.js | 1 + test/internals/reply.test.js | 247 +++++++++++++++++++++++++++++++++++ test/types/index.ts | 7 + 9 files changed, 300 insertions(+), 5 deletions(-) diff --git a/docs/Server.md b/docs/Server.md index dcd0c5652cc..8a768ddd8a3 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -585,6 +585,18 @@ Fake http injection (for testing purposes) [here](https://github.com/fastify/fas `fastify.addSchema(schemaObj)`, adds a shared schema to the Fastify instance. This allows you to reuse it everywhere in your application just by writing the schema id that you need.
To learn more, see [shared schema example](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#shared-schema) in the [Validation and Serialization](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) documentation. + +#### setReplySerializer +Set the reply serializer for all the routes. This will used as default if a [Reply.serializer(func)](https://github.com/fastify/fastify/blob/master/docs/Reply.md#serializerfunc) has not been set. The handler is fully encapsulated, so different plugins can set different error handlers. +Note: the function parameter is called only for status `2xx`. Checkout the [`setErrorHandler`](https://github.com/fastify/fastify/blob/master/docs/Server.md#seterrorhandler) for errors. + +```js +fastify.setReplySerializer(function (payload, statusCode){ + // serialize the payload with a sync function + return `my serialized ${statusCode} content: ${payload}` +}) +``` + #### setSchemaCompiler Set the schema compiler for all routes [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler). diff --git a/fastify.d.ts b/fastify.d.ts index f74a54ed674..94ca2d659bb 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -660,6 +660,11 @@ declare namespace fastify { */ setErrorHandler(handler: (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => void): void + /** + * Set a function that will be called whenever an error happens + */ + setReplySerializer(handler: (payload: string | object | Buffer | NodeJS.ReadableStream, statusCode: number) => string): void + /** * Set the schema compiler for all routes. */ diff --git a/fastify.js b/fastify.js index fd2a0b08d51..7a0f459fce4 100644 --- a/fastify.js +++ b/fastify.js @@ -13,6 +13,7 @@ const { kHooks, kSchemas, kSchemaCompiler, + kReplySerializerDefault, kContentTypeParser, kReply, kRequest, @@ -118,6 +119,7 @@ function build (options) { [kHooks]: new Hooks(), [kSchemas]: schemas, [kSchemaCompiler]: null, + [kReplySerializerDefault]: null, [kContentTypeParser]: new ContentTypeParser(bodyLimit, (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning)), [kReply]: Reply.buildReply(Reply), [kRequest]: Request.buildRequest(Request), @@ -167,6 +169,7 @@ function build (options) { addSchema: addSchema, getSchemas: schemas.getSchemas.bind(schemas), setSchemaCompiler: setSchemaCompiler, + setReplySerializer: setReplySerializer, // custom parsers addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser, hasContentTypeParser: ContentTypeParser.helpers.hasContentTypeParser, @@ -390,6 +393,13 @@ function build (options) { return this } + function setReplySerializer (replySerializer) { + throwIfAlreadyStarted('Cannot call "setReplySerializer" when fastify instance is already started!') + + this[kReplySerializerDefault] = replySerializer + return this + } + // wrapper that we expose to the user for configure the custom error handler function setErrorHandler (func) { throwIfAlreadyStarted('Cannot call "setErrorHandler" when fastify instance is already started!') diff --git a/lib/context.js b/lib/context.js index 9b405d6ae38..189779da7e5 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1,10 +1,10 @@ 'use strict' -const { kFourOhFourContext } = require('./symbols.js') +const { kFourOhFourContext, kReplySerializerDefault } = require('./symbols.js') // Objects that holds the context of every request // Every route holds an instance of this object. -function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, bodyLimit, logLevel, attachValidation) { +function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, bodyLimit, logLevel, attachValidation, replySerializer) { this.schema = schema this.handler = handler this.Reply = Reply @@ -22,6 +22,7 @@ function Context (schema, handler, Reply, Request, contentTypeParser, config, er this.logLevel = logLevel this[kFourOhFourContext] = null this.attachValidation = attachValidation + this[kReplySerializerDefault] = replySerializer } function defaultErrorHandler (error, request, reply) { diff --git a/lib/reply.js b/lib/reply.js index 02562dd7f11..c4a8c6ad5d0 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -11,6 +11,7 @@ const { kReplySentOverwritten, kReplyStartTime, kReplySerializer, + kReplySerializerDefault, kReplyIsError, kReplyHeaders, kReplyHasStatusCode, @@ -195,7 +196,11 @@ Reply.prototype.serialize = function (payload) { if (this[kReplySerializer] !== null) { return this[kReplySerializer](payload) } else { - return serialize(this.context, payload, this.res.statusCode) + if (this.context && this.context[kReplySerializerDefault]) { + return this.context[kReplySerializerDefault](payload, this.res.statusCode) + } else { + return serialize(this.context, payload, this.res.statusCode) + } } } @@ -252,7 +257,12 @@ function preserializeHookEnd (err, request, reply, payload) { return } - payload = serialize(reply.context, payload, reply.res.statusCode) + if (reply.context && reply.context[kReplySerializerDefault]) { + payload = reply.context[kReplySerializerDefault](payload, reply.res.statusCode) + } else { + payload = serialize(reply.context, payload, reply.res.statusCode) + } + flatstr(payload) onSendHook(reply, payload) diff --git a/lib/route.js b/lib/route.js index b191fb2557f..14eea3f2dc2 100644 --- a/lib/route.js +++ b/lib/route.js @@ -19,6 +19,7 @@ const { kSchemaCompiler, kContentTypeParser, kReply, + kReplySerializerDefault, kRequest, kMiddlewares, kGlobalHooks, @@ -195,7 +196,8 @@ function buildRouting (options) { this._errorHandler, opts.bodyLimit, opts.logLevel, - opts.attachValidation + opts.attachValidation, + this[kReplySerializerDefault] ) // TODO this needs to be refactored so that buildSchemaCompiler is diff --git a/lib/symbols.js b/lib/symbols.js index 73a1236408b..97a9cc56b89 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -8,6 +8,7 @@ const keys = { kHooks: Symbol('fastify.hooks'), kSchemas: Symbol('fastify.schemas'), kSchemaCompiler: Symbol('fastify.schemaCompiler'), + kReplySerializerDefault: Symbol('fastify.replySerializerDefault'), kContentTypeParser: Symbol('fastify.contentTypeParser'), kReply: Symbol('fastify.Reply'), kRequest: Symbol('fastify.Request'), diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index f9f819093c8..f4cc9726c8c 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -1005,3 +1005,250 @@ test('reply.getResponseTime() should return a number greater than 0 after the ti fastify.inject({ method: 'GET', url: '/' }) }) + +test('reply should use the custom serializer', t => { + t.plan(4) + const fastify = require('../..')() + fastify.setReplySerializer((payload, statusCode) => { + t.deepEqual(payload, { foo: 'bar' }) + t.equal(statusCode, 200) + payload.foo = 'bar bar' + return JSON.stringify(payload) + }) + + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { + reply.send({ foo: 'bar' }) + } + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"foo":"bar bar"}') + }) +}) + +test('reply should use the right serializer in encapsulated context', t => { + t.plan(9) + + const fastify = require('../..')() + fastify.setReplySerializer((payload) => { + t.deepEqual(payload, { foo: 'bar' }) + payload.foo = 'bar bar' + return JSON.stringify(payload) + }) + + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { reply.send({ foo: 'bar' }) } + }) + + fastify.register(function (instance, opts, next) { + instance.route({ + method: 'GET', + url: '/sub', + handler: (req, reply) => { reply.send({ john: 'doo' }) } + }) + instance.setReplySerializer((payload) => { + t.deepEqual(payload, { john: 'doo' }) + payload.john = 'too too' + return JSON.stringify(payload) + }) + next() + }) + + fastify.register(function (instance, opts, next) { + instance.route({ + method: 'GET', + url: '/sub', + handler: (req, reply) => { reply.send({ sweet: 'potato' }) } + }) + instance.setReplySerializer((payload) => { + t.deepEqual(payload, { sweet: 'potato' }) + payload.sweet = 'potato potato' + return JSON.stringify(payload) + }) + next() + }, { prefix: 'sub' }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"foo":"bar bar"}') + }) + + fastify.inject({ + method: 'GET', + url: '/sub' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"john":"too too"}') + }) + + fastify.inject({ + method: 'GET', + url: '/sub/sub' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"sweet":"potato potato"}') + }) +}) + +test('reply should use the right serializer in deep encapsulated context', t => { + t.plan(8) + + const fastify = require('../..')() + + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { reply.send({ foo: 'bar' }) } + }) + + fastify.register(function (instance, opts, next) { + instance.route({ + method: 'GET', + url: '/sub', + handler: (req, reply) => { reply.send({ john: 'doo' }) } + }) + instance.setReplySerializer((payload) => { + t.deepEqual(payload, { john: 'doo' }) + payload.john = 'too too' + return JSON.stringify(payload) + }) + + instance.register(function (subInstance, opts, next) { + subInstance.route({ + method: 'GET', + url: '/deep', + handler: (req, reply) => { reply.send({ john: 'deep' }) } + }) + subInstance.setReplySerializer((payload) => { + t.deepEqual(payload, { john: 'deep' }) + payload.john = 'deep deep' + return JSON.stringify(payload) + }) + next() + }) + next() + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"foo":"bar"}') + }) + + fastify.inject({ + method: 'GET', + url: '/sub' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"john":"too too"}') + }) + + fastify.inject({ + method: 'GET', + url: '/deep' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"john":"deep deep"}') + }) +}) + +test('reply should use the route serializer', t => { + t.plan(3) + + const fastify = require('../..')() + fastify.setReplySerializer(() => { + t.fail('this serializer should not be executed') + }) + + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { + reply + .serializer((payload) => { + t.deepEqual(payload, { john: 'doo' }) + payload.john = 'too too' + return JSON.stringify(payload) + }) + .send({ john: 'doo' }) + } + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, '{"john":"too too"}') + }) +}) + +test('cannot set the replySerializer when the server is running', t => { + t.plan(2) + + const fastify = require('../..')() + t.teardown(fastify.close.bind(fastify)) + + fastify.listen(err => { + t.error(err) + try { + fastify.setReplySerializer(() => {}) + t.fail('this serializer should not be setup') + } catch (e) { + t.is(e.message, 'Cannot call "setReplySerializer" when fastify instance is already started!') + } + }) +}) + +test('reply should not call the custom serializer for errors and not found', t => { + t.plan(9) + + const fastify = require('../..')() + fastify.setReplySerializer((payload, statusCode) => { + t.deepEqual(payload, { foo: 'bar' }) + t.equal(statusCode, 200) + return JSON.stringify(payload) + }) + + fastify.get('/', (req, reply) => { reply.send({ foo: 'bar' }) }) + fastify.get('/err', (req, reply) => { reply.send(new Error('an error')) }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.statusCode, 200) + t.strictEqual(res.payload, '{"foo":"bar"}') + }) + + fastify.inject({ + method: 'GET', + url: '/err' + }, (err, res) => { + t.error(err) + t.strictEqual(res.statusCode, 500) + }) + + fastify.inject({ + method: 'GET', + url: '/not-existing' + }, (err, res) => { + t.error(err) + t.strictEqual(res.statusCode, 404) + }) +}) diff --git a/test/types/index.ts b/test/types/index.ts index c2506333001..28b9e259353 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -445,6 +445,13 @@ server.setErrorHandler((err, request, reply) => { } }) +server.setReplySerializer((payload, statusCode) => { + if (statusCode === 201) { + return `Created ${payload}` + } + return JSON.stringify(payload) +}) + server.listen(3000, err => { if (err) throw err const address = server.server.address() From 02f4807e1d4746da66139175bf2b1d1d8a9bee46 Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Mon, 24 Jun 2019 19:49:38 +0200 Subject: [PATCH 030/144] Improve support for Fluent Schema (#1719) * Improve support for Fluent Schema * Updated test * Updated docs * Avoid code duplication --- docs/Fluent-Schema.md | 12 ++++------ lib/schemas.js | 6 ++++- lib/validation.js | 18 +++++++++++++++ package.json | 2 +- test/fluent-schema.js | 54 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/docs/Fluent-Schema.md b/docs/Fluent-Schema.md index b50629f83cc..d05985ac173 100644 --- a/docs/Fluent-Schema.md +++ b/docs/Fluent-Schema.md @@ -41,11 +41,12 @@ const paramsJsonSchema = S.object() const headersJsonSchema = S.object() .prop('x-foo', S.string().required()) +// note that there is no need to call `.valueOf()`! const schema = { - body: bodyJsonSchema.valueOf(), - querystring: queryStringJsonSchema.valueOf(), // (or) query: queryStringJsonSchema.valueOf() - params: paramsJsonSchema.valueOf(), - headers: headersJsonSchema.valueOf() + body: bodyJsonSchema, + querystring: queryStringJsonSchema, // (or) query: queryStringJsonSchema + params: paramsJsonSchema, + headers: headersJsonSchema } fastify.post('/the/url', { schema }, handler) @@ -69,20 +70,17 @@ const addressSchema = S.object() .prop('country').required() .prop('city').required() .prop('zipcode').required() - .valueOf() const commonSchemas = S.object() .id('https://fastify/demo') .definition('addressSchema', addressSchema) .definition('otherSchema', otherSchema) // you can add any schemas you need - .valueOf() fastify.addSchema(commonSchemas) const bodyJsonSchema = S.object() .prop('residence', S.ref('https://fastify/demo#address')).required() .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() - .valueOf() const schema = { body: bodyJsonSchema } diff --git a/lib/schemas.js b/lib/schemas.js index 213baa5bdc9..e6279b99990 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -1,6 +1,7 @@ 'use strict' const fastClone = require('rfdc')({ circles: false, proto: true }) +const kFluentSchema = Symbol.for('fluent-schema-object') const { codes: { @@ -18,7 +19,10 @@ function Schemas () { } Schemas.prototype.add = function (inputSchema) { - const schema = fastClone(inputSchema) + var schema = fastClone(inputSchema[kFluentSchema] + ? inputSchema.valueOf() + : inputSchema + ) const id = schema['$id'] if (id === undefined) { throw new FST_ERR_SCH_MISSING_ID() diff --git a/lib/validation.js b/lib/validation.js index bd45ffed64f..01ad49e2139 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -8,6 +8,7 @@ const querystringSchema = Symbol('querystring-schema') const paramsSchema = Symbol('params-schema') const responseSchema = Symbol('response-schema') const headersSchema = Symbol('headers-schema') +const kFluentSchema = Symbol.for('fluent-schema-object') function getValidatorForStatusCodeSchema (statusCodeDefinition, externalSchema) { return fastJsonStringify(statusCodeDefinition, { schema: externalSchema }) @@ -26,6 +27,7 @@ function build (context, compile, schemas) { return } + generateFluentSchema(context.schema) context.schema = schemas.resolveRefs(context.schema) const headers = context.schema.headers @@ -66,6 +68,22 @@ function build (context, compile, schemas) { } } +function generateFluentSchema (schema) { + ;['params', 'body', 'querystring', 'query', 'headers'].forEach(key => { + if (schema[key] && schema[key][kFluentSchema]) { + schema[key] = schema[key].valueOf() + } + }) + + if (schema.response) { + Object.keys(schema.response).forEach(code => { + if (schema.response[code][kFluentSchema]) { + schema.response[code] = schema.response[code].valueOf() + } + }) + } +} + function validateParam (validatorFunction, request, paramName) { var ret = validatorFunction && validatorFunction(request[paramName]) if (ret === false) return validatorFunction.errors diff --git a/package.json b/package.json index 1a1b5ff4cce..fb2bf210377 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", - "fluent-schema": "^0.7.0", + "fluent-schema": "^0.7.3", "form-data": "^2.3.3", "frameguard": "^3.0.0", "h2url": "^0.1.2", diff --git a/test/fluent-schema.js b/test/fluent-schema.js index 6ae7c5b68f7..87542a329b7 100644 --- a/test/fluent-schema.js +++ b/test/fluent-schema.js @@ -120,6 +120,60 @@ function fluentSchemaTest (t) { fastify.ready(t.error) }) + + test('Should call valueOf internally', t => { + t.plan(1) + + const fastify = new Fastify() + + const addressSchema = S.object() + .id('#address') + .prop('line1').required() + .prop('line2') + .prop('country').required() + .prop('city').required() + .prop('zipcode').required() + + const commonSchemas = S.object() + .id('https://fastify/demo') + .definition('addressSchema', addressSchema) + + fastify.addSchema(commonSchemas) + + fastify.route({ + method: 'POST', + url: '/query', + handler: () => {}, + schema: { + query: S.object().prop('hello', S.string()).required(), + body: S.object().prop('hello', S.string()).required(), + params: S.object().prop('hello', S.string()).required(), + headers: S.object().prop('hello', S.string()).required(), + response: { + 200: S.object().prop('hello', S.string()).required(), + 201: S.object().prop('hello', S.string()).required() + } + } + }) + + fastify.route({ + method: 'POST', + url: '/querystring', + handler: () => {}, + schema: { + querystring: S.object().prop('hello', S.string()).required(), + body: S.object().prop('hello', S.string()).required(), + params: S.object().prop('hello', S.string()).required(), + headers: S.object().prop('hello', S.string()).required(), + response: { + 200: S.object().prop('hello', S.string()).required(), + 201: S.object().prop('hello', S.string()).required() + } + } + }) + + fastify.ready(t.error) + }) } module.exports = fluentSchemaTest From b269bae0c557cb313b3f44407402b9a2217863be Mon Sep 17 00:00:00 2001 From: Adriano Raiano Date: Tue, 25 Jun 2019 08:35:33 +0200 Subject: [PATCH 031/144] allow nullable option in schemas #1709 (#1711) --- docs/Validation-and-Serialization.md | 8 +++-- lib/validation.js | 1 + test/nullable-validation.test.js | 52 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 test/nullable-validation.test.js diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 054400b707f..9983e33b3ae 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -31,7 +31,7 @@ const bodyJsonSchema = { maxItems: 3, items: { type: 'integer' } }, - nullableKey: { type: ['number', 'null'] }, + nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true } multipleTypesKey: { type: ['boolean', 'number'] }, multipleRestrictedTypesKey: { oneOf: [ @@ -251,7 +251,8 @@ Fastify's [baseline ajv configuration](https://github.com/epoberezkin/ajv#option removeAdditional: true, // remove additional properties useDefaults: true, // replace missing properties and items with the values from corresponding default keyword coerceTypes: true, // change data type of data to match type keyword - allErrors: true // check for all errors + allErrors: true, // check for all errors + nullable: true // support keyword "nullable" from Open API 3 specification. } ``` @@ -265,7 +266,8 @@ const ajv = new Ajv({ removeAdditional: true, useDefaults: true, coerceTypes: true, - allErrors: true + allErrors: true, + nullable: true, // any other options // ... }) diff --git a/lib/validation.js b/lib/validation.js index 01ad49e2139..b690f5aa4dc 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -170,6 +170,7 @@ function buildSchemaCompiler (externalSchemas, cache) { useDefaults: true, removeAdditional: true, allErrors: true, + nullable: true, cache }) diff --git a/test/nullable-validation.test.js b/test/nullable-validation.test.js new file mode 100644 index 00000000000..0283adc3f07 --- /dev/null +++ b/test/nullable-validation.test.js @@ -0,0 +1,52 @@ +'use strict' + +const t = require('tap') +const test = t.test +const Fastify = require('..') + +test('nullable string', t => { + t.plan(3) + const fastify = Fastify() + fastify.route({ + method: 'POST', + url: '/', + handler: (req, reply) => { + t.same(req.body.hello, null) + reply.code(200).send(req.body) + }, + schema: { + body: { + type: 'object', + properties: { + hello: { + type: 'string', + format: 'email', + nullable: true + } + } + }, + response: { + 200: { + type: 'object', + properties: { + hello: { + type: 'string', + format: 'email', + nullable: true + } + } + } + } + } + }) + fastify.inject({ + method: 'POST', + url: '/', + body: { + hello: null + } + }, (err, res) => { + t.error(err) + t.same(res.payload.hello, null) + }) +}) From e22131c65ee262aa358e2f03c041e560cb94d8ac Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 25 Jun 2019 12:58:17 +0200 Subject: [PATCH 032/144] Clarify that user-provided data relates to the schemas (#1722) --- docs/Validation-and-Serialization.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 9983e33b3ae..6d799438584 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -4,9 +4,10 @@ Fastify uses a schema-based approach, and even if it is not mandatory we recommend using [JSON Schema](http://json-schema.org/) to validate your routes and serialize your outputs. Internally, Fastify compiles the schema into a highly performant function. > ## ⚠ Security Notice +> Treat the schema definition as application code. > As both validation and serialization features dynamically evaluate -> code with `new Function()`, it is not safe to use them with -> user-provided data. See [Ajv](http://npm.im/ajv) and +> code with `new Function()`, it is not safe to use +> user-provided schemas. See [Ajv](http://npm.im/ajv) and > [fast-json-stringify](http://npm.im/fast-json-stringify) for more > details. From 1d268f91ed6492c162360a338cda1f48157b8b20 Mon Sep 17 00:00:00 2001 From: delvedor Date: Tue, 25 Jun 2019 19:51:04 +0200 Subject: [PATCH 033/144] Bumped v2.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb2bf210377..ee5e8857aa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.5.0", + "version": "2.6.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 8f1a016b3b1b58695a8efb7c7e434aa35182c4f0 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Thu, 27 Jun 2019 03:43:07 -0300 Subject: [PATCH 034/144] docs(Ecosystem): Add plugin community fastify-amqp (#1724) * docs(Ecosystem): Add plugin community fastify-amqp * docs(Ecosystem): Change order of fastify amqp --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 5d49c8d0f45..0c2fc4f8866 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -48,6 +48,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`apollo-server-fastify`](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-fastify) Run an [Apollo Server](https://github.com/apollographql/apollo-server) to serve GraphQL with Fastify. - [`arecibo`](https://github.com/nucleode/arecibo) Fastify ping responder for Kubernetes Liveness and Readiness Probes. - [`fastify-405`](https://github.com/Eomm/fastify-405) Fastify plugin that adds 405 HTTP status to your routes +- [`fastify-amqp`](https://github.com/RafaelGSS/fastify-amqp) Fastify AMQP connection plugin, to use with RabbitMQ or another connector. Just a wrapper to [`amqplib`](https://github.com/squaremo/amqp.node). - [`fastify-angular-universal`](https://github.com/exequiel09/fastify-angular-universal) Angular server-side rendering support using [`@angular/platform-server`](https://github.com/angular/angular/tree/master/packages/platform-server) for Fastify - [`fastify-babel`](https://github.com/cfware/fastify-babel) Fastify plugin for development servers which require babel transformations of JavaScript sources. - [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your routes to the console, so you definitely know which endpoints are available. From 0f15969e485a8d82551a155209db770dacf4bd83 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Thu, 27 Jun 2019 14:29:23 -0300 Subject: [PATCH 035/144] docs(Ecosystem): Removed fastify-nuxt community package (#1725) The package not found more. --- docs/Ecosystem.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 0c2fc4f8866..95f027f1878 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -92,7 +92,6 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-no-icon`](https://github.com/jsumners/fastify-no-icon) Plugin to eliminate thrown errors for `/favicon.ico` requests. - [`fastify-nodemailer`](https://github.com/lependu/fastify-nodemailer) Plugin to share [nodemailer](http://nodemailer.com) transporter across Fastify. - [`fastify-normalize-request-reply`](https://github.com/ericrglass/fastify-normalize-request-reply) Plugin to normalize the request and reply to the Express version 4.x request and response, which allows use of middleware, like swagger-stats, that was originally written for Express. -- [`fastify-nuxt`](https://github.com/lyquocnam/fastify-nuxt) VueJS server side rendering support for Fastify with [`NuxtJS`](https://nuxtjs.org/) - [`fastify-oas`](https://gitlab.com/m03geek/fastify-oas) Generates OpenAPI 3.0+ documentation from routes schemas for Fastify. - [`fastify-openapi-glue`](https://github.com/seriousme/fastify-openapi-glue) Glue for Open Api specifications in Fastify, autogenerates routes based on an Open Api Specification - [`fastify-oracle`](https://github.com/cemremengu/fastify-oracle) Attaches an [`oracledb`](https://github.com/oracle/node-oracledb) connection pool to a Fastify server instance. From 344ecc2170b24f031671c7375b23eb3adc960631 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Sat, 29 Jun 2019 00:31:09 -0700 Subject: [PATCH 036/144] docs: move Trivikram to past collaborators (#1727) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb16885a994..1381304e73e 100644 --- a/README.md +++ b/README.md @@ -204,12 +204,12 @@ Team members are listed in alphabetical order. ### Collaborators Great contributors on a specific area in the Fastify ecosystem will be invited to join this group by Lead Maintainers. -* [__Trivikram Kamat__](https://github.com/trivikr), , * [__Luciano Mammino__](https://github.com/lmammino), , * [__Evan Shortiss__](https://github.com/evanshortiss), , **Past Collaborators** * [__Çağatay Çalı__](https://github.com/cagataycali), , +* [__Trivikram Kamat__](https://github.com/trivikr), , ## Acknowledgements From 25d822bda09ddacbed1887bd075ac604dd5cfa4b Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Tue, 2 Jul 2019 13:01:22 +0200 Subject: [PATCH 037/144] docs(Ecosystem): Add plugin community fastify-reverse-routes (#1728) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 95f027f1878..a9d7140e1ec 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -99,6 +99,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-rbac`](https://gitlab.com/m03geek/fastify-rbac) Fastify role-based access control plugin. - [`fastify-register-routes`](https://github.com/israeleriston/fastify-register-routes) Plugin to automatically load routes from a specified path and optionally limit loaded file names by a regular expression. - [`fastify-response-time`](https://github.com/lolo32/fastify-response-time) Add `X-Response-Time` header at each request for Fastify, in milliseconds. +- [`fastify-reverse-routes`](https://github.com/dimonnwc3/fastify-reverse-routes) Fastify reverse routes plugin, allows to defined named routes and build path using name and parameters. - [`fastify-rob-config`](https://github.com/jeromemacias/fastify-rob-config) Fastify Rob-Config integration. - [`fastify-schema-constraint`](https://github.com/Eomm/fastify-schema-constraint) Choose the JSON schema to use based on request parameters. - [`fastify-sentry`](https://github.com/alex-ppg/fastify-sentry) Fastify plugin to add the Sentry SDK error handler to requests. From f31376f639b738f3c3237d18a0ca538e3089e2b1 Mon Sep 17 00:00:00 2001 From: Andrey Pechkurov <37772591+puzpuzpuz@users.noreply.github.com> Date: Fri, 5 Jul 2019 17:34:59 +0300 Subject: [PATCH 038/144] docs: Add cls-rtracer to Ecosystem (#1731) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index a9d7140e1ec..17eaec2049f 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -47,6 +47,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`apollo-server-fastify`](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-fastify) Run an [Apollo Server](https://github.com/apollographql/apollo-server) to serve GraphQL with Fastify. - [`arecibo`](https://github.com/nucleode/arecibo) Fastify ping responder for Kubernetes Liveness and Readiness Probes. +- [`cls-rtracer`](https://github.com/puzpuzpuz/cls-rtracer) Fastify middleware for CLS-based request id generation. An out-of-the-box solution for adding request ids into your logs. - [`fastify-405`](https://github.com/Eomm/fastify-405) Fastify plugin that adds 405 HTTP status to your routes - [`fastify-amqp`](https://github.com/RafaelGSS/fastify-amqp) Fastify AMQP connection plugin, to use with RabbitMQ or another connector. Just a wrapper to [`amqplib`](https://github.com/squaremo/amqp.node). - [`fastify-angular-universal`](https://github.com/exequiel09/fastify-angular-universal) Angular server-side rendering support using [`@angular/platform-server`](https://github.com/angular/angular/tree/master/packages/platform-server) for Fastify From 5ebf617b55fc8d552cbf062a84ff673ba10d2e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8F=9C?= Date: Tue, 9 Jul 2019 15:22:53 +0800 Subject: [PATCH 039/144] test: change describe for content-length.test.js (#1735) --- test/content-length.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/content-length.test.js b/test/content-length.test.js index ce3312a2e7a..3b30e3768a5 100644 --- a/test/content-length.test.js +++ b/test/content-length.test.js @@ -34,7 +34,7 @@ test('default 413 with bodyLimit option', t => { }) }) -test('default 413 with wrong content-length', t => { +test('default 400 with wrong content-length', t => { t.plan(4) const fastify = Fastify() @@ -102,7 +102,7 @@ test('custom 413 with bodyLimit option', t => { }) }) -test('custom 413 with wrong content-length', t => { +test('custom 400 with wrong content-length', t => { t.plan(4) const fastify = Fastify() From ca99ad6b08b4c7b9b32aa7bab6c3310724a2b7d7 Mon Sep 17 00:00:00 2001 From: Abhishek Nigam Date: Tue, 9 Jul 2019 19:06:09 +0530 Subject: [PATCH 040/144] Update Server.md (#1718) In the genReqId sention, it is mentioned in note, that the provided function for it will not be called if the "request-id" header is available. Hence, I think in the mentioned example, it should only return "i++" instead of "req.headers['request-id'] || i++"' as here the header here will not be present always. --- docs/Server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Server.md b/docs/Server.md index 8a768ddd8a3..c24276d4daa 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -230,7 +230,7 @@ Especially in distributed systems, you may want to override the default id gener ```js let i = 0 const fastify = require('fastify')({ - genReqId: function (req) { return req.headers['request-id'] || i++ } + genReqId: function (req) { return i++ } }) ``` From 093e18de2d37d559004f89d2c437478040913969 Mon Sep 17 00:00:00 2001 From: Chris Hawkes Date: Fri, 12 Jul 2019 16:14:12 -0400 Subject: [PATCH 041/144] fix typo Routes.md (#1743) --- docs/Routes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Routes.md b/docs/Routes.md index afc5f00604a..d94938d9abf 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -249,7 +249,7 @@ See the `prefixTrailingSlash` route option above to change this behaviour. ### Custom Log Level -It could happen that you need different log levels in your routes, with Fastify achieve this is very straightforward.
+It could happen that you need different log levels in your routes, Fastify achieves this in a very straightforward way.
You just need to pass the option `logLevel` to the plugin option or the route option with the [value](https://github.com/pinojs/pino/blob/master/docs/API.md#discussion-3) that you need. Be aware that if you set the `logLevel` at plugin level, also the [`setNotFoundHandler`](https://github.com/fastify/fastify/blob/master/docs/Server.md#setnotfoundhandler) and [`setErrorHandler`](https://github.com/fastify/fastify/blob/master/docs/Server.md#seterrorhandler) will be affected. From 5829bac2d7010820d5b2e92e965a827945e1193e Mon Sep 17 00:00:00 2001 From: Tommaso Allevi Date: Sun, 14 Jul 2019 20:49:08 +0200 Subject: [PATCH 042/144] Avoid return 503 after closing fastify (#1723) * Avoid return 503 after closing fastify * Allow ECONNRESET as error code * Destroy tcp connection when http request finishes * Add flag return503OnClosing. Default true * Add docs * Add return503OnClosing to typescript definition * Rework * Update test/http2/closing.js Co-Authored-By: Manuel Spigolon * Update test/http2/closing.js Co-Authored-By: Manuel Spigolon * Lint code --- docs/Server.md | 9 +++ fastify.d.ts | 3 +- fastify.js | 11 ++++ lib/route.js | 24 +++---- test/close-pipelining.test.js | 45 ++++++++++++- test/close.test.js | 81 +++++++++++++++++++---- test/http2/closing.js | 119 ++++++++++++++++++++++++---------- test/types/index.ts | 3 +- 8 files changed, 235 insertions(+), 60 deletions(-) diff --git a/docs/Server.md b/docs/Server.md index c24276d4daa..5f6d89631e5 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -352,6 +352,14 @@ fastify.get('/', (request, reply) => { }) ``` + +### `return503OnClosing` + +Returns 503 after calling `close` server method. +If `false`, the server routes the incoming request as usual. + ++ Default: `true` + ## Instance ### Server Methods @@ -516,6 +524,7 @@ Method to add routes to the server, it also has shorthand functions, check [here #### close `fastify.close(callback)`: call this function to close the server instance and run the [`'onClose'`](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#on-close) hook.
Calling `close` will also cause the server to respond to every new incoming request with a `503` error and destroy that request. +See [`return503OnClosing` flags](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-return-503-on-closing) for changing this behaviour. If it is called without any arguments, it will return a Promise: diff --git a/fastify.d.ts b/fastify.d.ts index 94ca2d659bb..df1cbb8508e 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -198,7 +198,8 @@ declare namespace fastify { }, deriveVersion(req: Object, ctx?: Context) : String, }, - modifyCoreObjects?: boolean + modifyCoreObjects?: boolean, + return503OnClosing?: boolean } interface ServerOptionsAsSecure extends ServerOptions { https: http2.SecureServerOptions diff --git a/fastify.js b/fastify.js index 7a0f459fce4..10229cfed81 100644 --- a/fastify.js +++ b/fastify.js @@ -245,6 +245,7 @@ function build (options) { fastify[kState].closing = true router.closeRoutes() if (fastify[kState].listening) { + // No new TCP connections are accepted instance.server.close(done) } else { done(null) @@ -282,6 +283,16 @@ function build (options) { } if (fastify[kState].started) { + if (fastify[kState].closing) { + // Force to return an error + const error = new Error('Server is closed') + if (cb) { + cb(error) + return + } else { + return Promise.reject(error) + } + } return lightMyRequest(httpHandler, opts, cb) } diff --git a/lib/route.js b/lib/route.js index 14eea3f2dc2..fc959a3ee50 100644 --- a/lib/route.js +++ b/lib/route.js @@ -47,6 +47,7 @@ function buildRouting (options) { let genReqId let disableRequestLogging let ignoreTrailingSlash + let return503OnClosing let closing = false @@ -68,6 +69,7 @@ function buildRouting (options) { genReqId = options.genReqId disableRequestLogging = options.disableRequestLogging ignoreTrailingSlash = options.ignoreTrailingSlash + return503OnClosing = options.hasOwnProperty('return503OnClosing') ? options.return503OnClosing : true }, routing: router.lookup.bind(router), // router func to find the right handler to call route, // configure a route in the fastify instance @@ -267,20 +269,20 @@ function buildRouting (options) { // HTTP request entry point, the routing has already been executed function routeHandler (req, res, params, context) { if (closing === true) { - const headers = { - 'Content-Type': 'application/json', - 'Content-Length': '80' - } if (req.httpVersionMajor !== 2) { - headers.Connection = 'close' + res.once('finish', () => req.destroy()) + res.setHeader('Connection', 'close') } - res.writeHead(503, headers) - res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}') - if (req.httpVersionMajor !== 2) { - // This is not needed in HTTP/2 - setImmediate(() => req.destroy()) + + if (return503OnClosing) { + const headers = { + 'Content-Type': 'application/json', + 'Content-Length': '80' + } + res.writeHead(503, headers) + res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}') + return } - return } req.id = req.headers[requestIdHeader] || genReqId(req) diff --git a/test/close-pipelining.test.js b/test/close-pipelining.test.js index ed0e722a097..2881ef1edef 100644 --- a/test/close-pipelining.test.js +++ b/test/close-pipelining.test.js @@ -28,8 +28,49 @@ test('Should return 503 while closing - pipelining', t => { t.strictEqual(statusCode, codes.shift()) }) - instance.on('done', () => t.end('Done')) - instance.on('reqError', () => t.deepEqual(codes.shift(), null)) + instance.on('done', () => { + t.strictEqual(codes.length, 0) + t.end('Done') + }) + instance.on('reqError', () => { + t.strictEqual(codes.shift(), undefined) + }) + instance.on('error', err => t.fail(err)) + }) +}) + +test('Should not return 503 while closing - pipelining - return503OnClosing', t => { + const fastify = Fastify({ + return503OnClosing: false + }) + + fastify.get('/', (req, reply) => { + fastify.close() + reply.send({ hello: 'world' }) + }) + + fastify.listen(0, err => { + t.error(err) + + const instance = autocannon({ + url: 'http://localhost:' + fastify.server.address().port, + pipelining: 1, + connections: 1, + amount: 10 + }) + + const codes = [200, 200] + instance.on('response', (client, statusCode) => { + t.strictEqual(statusCode, codes.shift()) + }) + + instance.on('done', () => { + t.strictEqual(codes.length, 0) + t.end('Done') + }) + instance.on('reqError', () => { + t.strictEqual(codes.shift(), undefined) + }) instance.on('error', err => t.fail(err)) }) }) diff --git a/test/close.test.js b/test/close.test.js index 49d13e52037..e7f767802ef 100644 --- a/test/close.test.js +++ b/test/close.test.js @@ -1,5 +1,6 @@ 'use strict' +const net = require('net') const t = require('tap') const test = t.test const Fastify = require('..') @@ -114,8 +115,8 @@ test('onClose should keep the context', t => { }) }) -test('Should return 503 while closing - injection', t => { - t.plan(8) +test('Should return error while closing - injection', t => { + t.plan(4) const fastify = Fastify() fastify.addHook('onClose', (instance, done) => { @@ -139,17 +140,73 @@ test('Should return 503 while closing - injection', t => { method: 'GET', url: '/' }, (err, res) => { - t.error(err) - t.strictEqual(res.statusCode, 503) - t.strictEqual(res.headers['content-type'], 'application/json') - t.strictEqual(res.headers['content-length'], '80') - t.strictEqual(res.headers['connection'], 'close') - t.deepEqual(JSON.parse(res.payload), { - error: 'Service Unavailable', - message: 'Service Unavailable', - statusCode: 503 - }) + t.ok(err) + t.equal(err.message, 'Server is closed') }) }, 100) }) }) + +t.test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false', t => { + const fastify = Fastify({ + return503OnClosing: false + }) + + fastify.get('/', (req, reply) => { + fastify.close() + reply.send({ hello: 'world' }) + }) + + fastify.listen(0, err => { + t.error(err) + + const port = fastify.server.address().port + const client = net.createConnection({ port: port }, () => { + client.write('GET / HTTP/1.1\r\n\r\n') + + client.once('data', data => { + t.match(data.toString(), /Connection:\s*keep-alive/i) + t.match(data.toString(), /200 OK/i) + + client.write('GET / HTTP/1.1\r\n\r\n') + + client.once('data', data => { + t.match(data.toString(), /Connection:\s*close/i) + t.match(data.toString(), /200 OK/i) + + // Test that fastify closes the TCP connection + client.once('close', () => { + t.end() + }) + }) + }) + }) + }) +}) + +t.test('Current opened connection should not accept new incoming connections', t => { + const fastify = Fastify() + + fastify.get('/', (req, reply) => { + fastify.close() + reply.send({ hello: 'world' }) + }) + + fastify.listen(0, err => { + t.error(err) + + const port = fastify.server.address().port + const client = net.createConnection({ port: port }, () => { + client.write('GET / HTTP/1.1\r\n\r\n') + + const newConnection = net.createConnection({ port: port }) + newConnection.on('error', err => { + t.ok(err) + t.ok(['ECONNREFUSED', 'ECONNRESET'].includes(err.code)) + + client.end() + t.end() + }) + }) + }) +}) diff --git a/test/http2/closing.js b/test/http2/closing.js index c69529cd8c0..576b1bad98c 100644 --- a/test/http2/closing.js +++ b/test/http2/closing.js @@ -1,48 +1,101 @@ 'use strict' const t = require('tap') -const test = t.test const Fastify = require('../..') const http2 = require('http2') const semver = require('semver') -let fastify -try { - fastify = Fastify({ - http2: true - }) - t.pass('http2 successfully loaded') -} catch (e) { - t.fail('http2 loading failed', e) -} - -fastify.listen(0, err => { - t.error(err) - fastify.server.unref() - - // Skipped because there is likely a bug on Node 8. - test('http/2 request while fastify closing', { skip: semver.lt(process.versions.node, '10.15.0') }, t => { - const url = `http://127.0.0.1:${fastify.server.address().port}` - const session = http2.connect(url, function () { - this.request({ - ':method': 'GET', - ':path': '/' - }).on('response', headers => { - t.strictEqual(headers[':status'], 503) - t.end() - this.destroy() - }).on('error', () => { +t.test('http/2 request while fastify closing', t => { + let fastify + try { + fastify = Fastify({ + http2: true + }) + t.pass('http2 successfully loaded') + } catch (e) { + t.fail('http2 loading failed', e) + } + + fastify.get('/', () => Promise.resolve({})) + + fastify.listen(0, err => { + t.error(err) + fastify.server.unref() + + // Skipped because there is likely a bug on Node 8. + t.test('return 200', { skip: semver.lt(process.versions.node, '10.15.0') }, t => { + const url = `http://127.0.0.1:${fastify.server.address().port}` + const session = http2.connect(url, function () { + this.request({ + ':method': 'GET', + ':path': '/' + }).on('response', headers => { + t.strictEqual(headers[':status'], 503) + t.end() + this.destroy() + }).on('error', () => { + // Nothing to do here, + // we are not interested in this error that might + // happen or not + }) + fastify.close() + }) + session.on('error', () => { // Nothing to do here, // we are not interested in this error that might // happen or not + t.end() }) - fastify.close() }) - session.on('error', () => { - // Nothing to do here, - // we are not interested in this error that might - // happen or not - t.end() + + t.end() + }) +}) + +t.test('http/2 request while fastify closing - return503OnClosing: false', t => { + let fastify + try { + fastify = Fastify({ + http2: true, + return503OnClosing: false }) + t.pass('http2 successfully loaded') + } catch (e) { + t.fail('http2 loading failed', e) + } + + fastify.get('/', () => Promise.resolve({})) + + fastify.listen(0, err => { + t.error(err) + fastify.server.unref() + + // Skipped because there is likely a bug on Node 8. + t.test('return 200', { skip: semver.lt(process.versions.node, '10.15.0') }, t => { + const url = `http://127.0.0.1:${fastify.server.address().port}` + const session = http2.connect(url, function () { + this.request({ + ':method': 'GET', + ':path': '/' + }).on('response', headers => { + t.strictEqual(headers[':status'], 200) + t.end() + this.destroy() + }).on('error', () => { + // Nothing to do here, + // we are not interested in this error that might + // happen or not + }) + fastify.close() + }) + session.on('error', () => { + // Nothing to do here, + // we are not interested in this error that might + // happen or not + t.end() + }) + }) + + t.end() }) }) diff --git a/test/types/index.ts b/test/types/index.ts index 28b9e259353..f02393fe9d2 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -63,7 +63,8 @@ const cors = require('cors') bodyLimit: 1000, maxParamLength: 200, querystringParser: (str: string) => ({ str: str, strArray: [str] }), - modifyCoreObjects: true + modifyCoreObjects: true, + return503OnClosing: true }) // custom types From 78562ce811fa760409a9f24ffd27de5302219167 Mon Sep 17 00:00:00 2001 From: kharandziuk Date: Tue, 16 Jul 2019 18:55:23 +0200 Subject: [PATCH 043/144] change code examples in Reply documentation (#1751) --- docs/Reply.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/Reply.md b/docs/Reply.md index e1b92e91a1b..e21090769a8 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -283,17 +283,15 @@ The type of the sent payload (after serialization and going through any [`onSend Fastify natively handles promises and supports async-await.
*Note that in the following examples we are not using reply.send.* ```js +const delay = promisify(setTimeout) + fastify.get('/promises', options, function (request, reply) { - return new Promise(function (resolve) { - setTimeout(resolve, 200, { hello: 'world' }) - }) + return delay(200).then(() => { return { hello: 'world' }}) }) fastify.get('/async-await', options, async function (request, reply) { - var res = await new Promise(function (resolve) { - setTimeout(resolve, 200, { hello: 'world' }) - }) - return res + await delay(200) + return { hello: 'world' } }) ``` From dad5c9875712cf4cafe38209a2a559d47793bb04 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 17 Jul 2019 16:44:20 +0200 Subject: [PATCH 044/144] tap --no-esm (#1752) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ee5e8857aa3..83f6d3ab2fb 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "lint": "npm run lint:standard && npm run lint:typescript", "lint:standard": "standard --verbose | snazzy", "lint:typescript": "standard --parser @typescript-eslint/parser --plugin @typescript-eslint/eslint-plugin test/types/*.ts fastify.d.ts", - "unit": "tap -J test/*.test.js test/*/*.test.js", - "unit:report": "tap -J test/*.test.js test/*/*.test.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap", + "unit": "tap --no-esm -J test/*.test.js test/*/*.test.js", + "unit:report": "tap --no-esm -J test/*.test.js test/*/*.test.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap", "unit:junit": "tap-mocha-reporter xunit < out.tap > test/junit-testresults.xml", "typescript": "tsc --project ./test/types/tsconfig.json", "test:report": "npm run lint && npm run unit:report && npm run typescript", From 1c8687c4cadf930e666fbd9a84a0809c5800bf5b Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Fri, 19 Jul 2019 04:35:33 -0300 Subject: [PATCH 045/144] docs(Hooks): Add payload inside example to prevent bugs (#1739) --- docs/Hooks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 57f3ffe3fc0..97b5f1c6c1c 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -339,7 +339,7 @@ Note: using an arrow function will break the binding of this to the Fastify inst ## Route level hooks -You can declare one or more custom `onRequest`, `preParsing`, `preValidation`, `preHandler` and `preSerialization` hook(s) that will be unique for the route. +You can declare one or more custom `onRequest`, `preParsing`, `preValidation`, `preHandler` and `preSerialization` hook(s) that will be **unique** for the route. If you do so, those hooks always be executed as last hook in their category.
This can be useful if you need to run the authentication, and the `preParsing` or `preValidation` hooks are exactly what you need for doing that. Multiple route-level hooks can also be specified as an array. @@ -367,7 +367,7 @@ fastify.addHook('preHandler', (request, reply, done) => { done() }) -fastify.addHook('preSerialization', (request, reply, done) => { +fastify.addHook('preSerialization', (request, reply, payload, done) => { // your code done() }) @@ -398,7 +398,7 @@ fastify.route({ // // this hook will always be executed after the shared `preHandler` hooks // done() // }], - preSerialization: (request, reply, payload, next) => { + preSerialization: (request, reply, payload, done) => { // manipulate the payload next(null, payload) }, From 421f214e55f0878317a22a331053ba8937498a28 Mon Sep 17 00:00:00 2001 From: Zoron Date: Sat, 20 Jul 2019 01:13:15 +0800 Subject: [PATCH 046/144] Fix typo Hooks.md (#1759) --- docs/Hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 97b5f1c6c1c..419cdb1842e 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -400,7 +400,7 @@ fastify.route({ // }], preSerialization: (request, reply, payload, done) => { // manipulate the payload - next(null, payload) + done(null, payload) }, handler: function (request, reply) { reply.send({ hello: 'world' }) From e842115bab8b4bf547422c0d79cedb3a641a78ab Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2019 17:09:40 +0200 Subject: [PATCH 047/144] =?UTF-8?q?Update=20standard=20to=20the=20latest?= =?UTF-8?q?=20version=20=F0=9F=9A=80=20(#1741)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(package): update standard to version 13.0.1 * Fix standard --- build/build-validation.js | 2 +- lib/decorate.js | 2 ++ lib/request.js | 4 ++-- lib/route.js | 4 ++-- package.json | 2 +- test/async-await.js | 2 +- test/decorator.test.js | 2 ++ test/hooks.test.js | 8 ++++---- test/input-validation.test.js | 2 +- test/internals/decorator.test.js | 2 ++ test/internals/initialConfig.test.js | 2 +- test/logger.test.js | 2 +- test/plugin.test.js | 2 ++ test/register.test.js | 2 ++ test/route-prefix.test.js | 6 +++--- test/shared-schemas.test.js | 6 +++--- test/versioned-routes.test.js | 6 +++--- 17 files changed, 33 insertions(+), 23 deletions(-) diff --git a/build/build-validation.js b/build/build-validation.js index 01eebf40e26..ff1a2df17f6 100644 --- a/build/build-validation.js +++ b/build/build-validation.js @@ -72,7 +72,7 @@ const schema = { const validate = ajv.compile(schema) -let moduleCode = `// This file is autogenerated by ${__filename.replace(__dirname, 'build')}, do not edit +const moduleCode = `// This file is autogenerated by ${__filename.replace(__dirname, 'build')}, do not edit /* istanbul ignore file */ // constant needed for customRule0 to work const self = {} diff --git a/lib/decorate.js b/lib/decorate.js index 191a5e0f3ff..e2ca39deebc 100644 --- a/lib/decorate.js +++ b/lib/decorate.js @@ -1,5 +1,7 @@ 'use strict' +/* eslint no-prototype-builtins: 0 */ + const { kReply, kRequest diff --git a/lib/request.js b/lib/request.js index d99061c0c38..69d1aa715ab 100644 --- a/lib/request.js +++ b/lib/request.js @@ -30,12 +30,12 @@ function buildRequest (R) { } Object.defineProperties(Request.prototype, { - 'req': { + req: { get: function () { return this.raw } }, - 'id': { + id: { get: function () { return this.raw.id } diff --git a/lib/route.js b/lib/route.js index fc959a3ee50..bb505f6147c 100644 --- a/lib/route.js +++ b/lib/route.js @@ -221,7 +221,7 @@ function buildRouting (options) { const hooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization'] - for (let hook of hooks) { + for (const hook of hooks) { if (opts[hook]) { if (Array.isArray(opts[hook])) { opts[hook] = opts[hook].map(fn => fn.bind(this)) @@ -250,7 +250,7 @@ function buildRouting (options) { context.onError = onError.length ? onError : null context.onResponse = onResponse.length ? onResponse : null - for (let hook of hooks) { + for (const hook of hooks) { const toSet = this[kHooks][hook].concat(opts[hook] || []) context[hook] = toSet.length ? toSet : null } diff --git a/package.json b/package.json index 83f6d3ab2fb..5f11bd9f888 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "simple-get": "^3.0.3", "snazzy": "^8.0.0", "split2": "^3.1.0", - "standard": "^12.0.1", + "standard": "^13.0.1", "tap": "^12.5.2", "tap-mocha-reporter": "^3.0.7", "then-sleep": "^1.0.1", diff --git a/test/async-await.js b/test/async-await.js index 50d3adf29cb..fa547be18a9 100644 --- a/test/async-await.js +++ b/test/async-await.js @@ -419,7 +419,7 @@ function asyncTest (t) { var fastify = null var stream = split(JSON.parse) - var payload = { 'hello': 'world' } + var payload = { hello: 'world' } try { fastify = Fastify({ logger: { diff --git a/test/decorator.test.js b/test/decorator.test.js index e795f23f6a2..d0bb4ad891e 100644 --- a/test/decorator.test.js +++ b/test/decorator.test.js @@ -1,5 +1,7 @@ 'use strict' +/* eslint no-prototype-builtins: 0 */ + const t = require('tap') const test = t.test const Fastify = require('..') diff --git a/test/hooks.test.js b/test/hooks.test.js index d16d90e28b1..7405ac42d25 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -1935,7 +1935,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { t.deepEqual(request.headers, { 'content-length': '17', 'content-type': 'application/json', - 'host': 'localhost:80', + host: 'localhost:80', 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) @@ -1949,7 +1949,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { t.deepEqual(request.headers, { 'content-length': '17', 'content-type': 'application/json', - 'host': 'localhost:80', + host: 'localhost:80', 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) @@ -1963,7 +1963,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { t.deepEqual(request.headers, { 'content-length': '17', 'content-type': 'application/json', - 'host': 'localhost:80', + host: 'localhost:80', 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) @@ -1977,7 +1977,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { t.deepEqual(request.headers, { 'content-length': '17', 'content-type': 'application/json', - 'host': 'localhost:80', + host: 'localhost:80', 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) diff --git a/test/input-validation.test.js b/test/input-validation.test.js index 45011b537ba..3e09bd1b001 100644 --- a/test/input-validation.test.js +++ b/test/input-validation.test.js @@ -27,7 +27,7 @@ test('case insensitive header validation', t => { method: 'GET', url: '/', headers: { - 'FooBar': 'Baz' + FooBar: 'Baz' } }, (err, res) => { t.error(err) diff --git a/test/internals/decorator.test.js b/test/internals/decorator.test.js index ee6b3a3aa3e..a7d0a930b6f 100644 --- a/test/internals/decorator.test.js +++ b/test/internals/decorator.test.js @@ -1,5 +1,7 @@ 'use strict' +/* eslint no-prototype-builtins: 0 */ + const t = require('tap') const test = t.test const decorator = require('../../lib/decorate') diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index 0e4949c7f9a..c0d648f1341 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -59,7 +59,7 @@ test('Fastify.initialConfig should expose all options', t => { } } - let options = { + const options = { http2: true, https: { key: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.key')), diff --git a/test/logger.test.js b/test/logger.test.js index f04c278ed9b..b37ed63dd2f 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -1143,7 +1143,7 @@ test('should redact the authorization header if so specified', t => { method: 'GET', url: 'http://localhost:' + fastify.server.address().port, headers: { - 'authorization': 'Bearer abcde' + authorization: 'Bearer abcde' } }, (err, response, body) => { t.error(err) diff --git a/test/plugin.test.js b/test/plugin.test.js index 87505eca629..8d4f2d17149 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -1,5 +1,7 @@ 'use strict' +/* eslint no-prototype-builtins: 0 */ + const t = require('tap') const test = t.test const Fastify = require('..') diff --git a/test/register.test.js b/test/register.test.js index 6c5550eab79..f03f6869e57 100644 --- a/test/register.test.js +++ b/test/register.test.js @@ -1,5 +1,7 @@ 'use strict' +/* eslint no-prototype-builtins: 0 */ + const t = require('tap') const test = t.test const sget = require('simple-get').concat diff --git a/test/route-prefix.test.js b/test/route-prefix.test.js index 3c22fb8eaa1..faa16392528 100644 --- a/test/route-prefix.test.js +++ b/test/route-prefix.test.js @@ -483,9 +483,9 @@ test('returns 404 status code with /prefix/ and / route - prefixTrailingSlash: " }, (err, res) => { t.error(err) t.same(JSON.parse(res.payload), { - 'error': 'Not Found', - 'message': 'Not Found', - 'statusCode': 404 + error: 'Not Found', + message: 'Not Found', + statusCode: 404 }) }) }) diff --git a/test/shared-schemas.test.js b/test/shared-schemas.test.js index 47a66e8bbd3..9a0f07740ac 100644 --- a/test/shared-schemas.test.js +++ b/test/shared-schemas.test.js @@ -565,7 +565,7 @@ test('Use shared schema and $ref with $id ($ref to $id)', t => { $id: '#address', type: 'object', properties: { - city: { 'type': 'string' } + city: { type: 'string' } } } }, @@ -624,7 +624,7 @@ test('Use shared schema and $ref with $id in response ($ref to $id)', t => { $id: '#address', type: 'object', properties: { - city: { 'type': 'string' } + city: { type: 'string' } } } }, @@ -1089,7 +1089,7 @@ test('Use shared schema and $ref to /definitions', t => { $id: '#otherId', type: 'object', properties: { - city: { 'type': 'string' } + city: { type: 'string' } } } }, diff --git a/test/versioned-routes.test.js b/test/versioned-routes.test.js index b155c08f702..e7d40480d6f 100644 --- a/test/versioned-routes.test.js +++ b/test/versioned-routes.test.js @@ -445,7 +445,7 @@ test('Should register a versioned route with custome versioning strategy', t => method: 'GET', url: '/', headers: { - 'Accept': 'application/vnd.example.api+json;version=2' + Accept: 'application/vnd.example.api+json;version=2' } }, (err, res) => { t.error(err) @@ -457,7 +457,7 @@ test('Should register a versioned route with custome versioning strategy', t => method: 'GET', url: '/', headers: { - 'Accept': 'application/vnd.example.api+json;version=3' + Accept: 'application/vnd.example.api+json;version=3' } }, (err, res) => { t.error(err) @@ -469,7 +469,7 @@ test('Should register a versioned route with custome versioning strategy', t => method: 'GET', url: '/', headers: { - 'Accept': 'application/vnd.example.api+json;version=4' + Accept: 'application/vnd.example.api+json;version=4' } }, (err, res) => { t.error(err) From 45f875008b649dded6408dcad60e40dd9ac88884 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 22 Jul 2019 20:12:23 +0200 Subject: [PATCH 048/144] Fixed linting --- lib/route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/route.js b/lib/route.js index bb505f6147c..838053707ca 100644 --- a/lib/route.js +++ b/lib/route.js @@ -69,7 +69,7 @@ function buildRouting (options) { genReqId = options.genReqId disableRequestLogging = options.disableRequestLogging ignoreTrailingSlash = options.ignoreTrailingSlash - return503OnClosing = options.hasOwnProperty('return503OnClosing') ? options.return503OnClosing : true + return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true }, routing: router.lookup.bind(router), // router func to find the right handler to call route, // configure a route in the fastify instance From eb7a8fc8ecf925c484e2b04c945e52ecd566141d Mon Sep 17 00:00:00 2001 From: Giacomo Gregoletto Date: Mon, 22 Jul 2019 20:15:40 +0200 Subject: [PATCH 049/144] docs(Ecosystem): Add fastify-no-additional-properties community plugin (#1762) * docs(Ecosystem): Add fastify-no-additional-properties community plugin * Update docs/Ecosystem.md Co-Authored-By: Manuel Spigolon --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 17eaec2049f..5a10786de10 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -90,6 +90,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose) Fastify Mongoose plugin that connects to a MongoDB via the Mongoose plugin with support for Models. - [`fastify-multer`](https://github.com/fox1t/multer) Multer is a plugin for handling multipart/form-data, which is primarily used for uploading files. - [`fastify-nats`](https://github.com/mahmed8003/fastify-nats) Plugin to share [NATS](http://nats.io) client across Fastify. +- [`fastify-no-additional-properties`](https://github.com/greguz/fastify-no-additional-properties) Add `additionalProperties: false` by default to your JSON Schemas. - [`fastify-no-icon`](https://github.com/jsumners/fastify-no-icon) Plugin to eliminate thrown errors for `/favicon.ico` requests. - [`fastify-nodemailer`](https://github.com/lependu/fastify-nodemailer) Plugin to share [nodemailer](http://nodemailer.com) transporter across Fastify. - [`fastify-normalize-request-reply`](https://github.com/ericrglass/fastify-normalize-request-reply) Plugin to normalize the request and reply to the Express version 4.x request and response, which allows use of middleware, like swagger-stats, that was originally written for Express. From fec034a16db3f2c56e3ba0b0f367211796ef7ffa Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Mon, 22 Jul 2019 20:41:47 +0200 Subject: [PATCH 050/144] security policy (#1738) * security.md * Apply suggestions from code review Co-Authored-By: James Sumners * add link * add plugins notice --- SECURITY.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..e31f0f032a7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Security Policy + +This document describes the management of vulnerabilities for the Fastify project and it's officials' plugins. + + +## Reporting vulnerabilities + +Individuals who find potential vulnerabilities in Fastify are invited to complete a vulnerability report via the dedicated HackerOne tool for Node.js modules: [https://hackerone.com/nodejs-ecosystem](https://hackerone.com/nodejs-ecosystem). + +### How to report a vulnerabiliy + +It is of the utmost importance that you read carefully [**HOW TO REPORT A VULNERABILIY**](https://github.com/nodejs/security-wg/blob/master/processes/third_party_vuln_process.md) written by the Security Working Group of Node.js. + + +## Handling vulnerability reports + +When a potential vulnerability is reported and confirmed the Fastify Core Team will intervene in the +`follow-up` stage of the process following the workflow and the timings described in the Security WG's document. + +### Vulnerabilities found outside this process + +âš  The Fastify project does not support any reporting outside the HackerOne process. + + +## The Fastify Core team + +The core team is responsible for the management of [this](https://github.com/nodejs/security-wg/blob/master/processes/third_party_vuln_process.md#handling-vulnerability-reports) process. + +Members of this team are expected to keep all information that they have privileged access to by being +on the team completely private to the team. This includes agreeing to not notify anyone outside the +team of issues that have not yet been disclosed publicly, including the existence of issues, +expectations of upcoming releases, and patching of any issues other than in the process of their work +as a member of the Fastify Core team. From e0fa8f9decf6361610706e8bac95e8591341cc5a Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Tue, 23 Jul 2019 09:33:09 -0300 Subject: [PATCH 051/144] docs: change the `next` to `done` inside documentation (#1756) --- docs/Decorators.md | 4 +- docs/Hooks.md | 72 ++++++++++++++-------------- docs/Logging.md | 4 +- docs/Plugins-Guide.md | 42 ++++++++-------- docs/Plugins.md | 20 ++++---- docs/Routes.md | 8 ++-- docs/Server.md | 36 +++++++------- docs/Validation-and-Serialization.md | 12 ++--- 8 files changed, 99 insertions(+), 99 deletions(-) diff --git a/docs/Decorators.md b/docs/Decorators.md index 2a6a1b6691c..de92d9e1a2b 100644 --- a/docs/Decorators.md +++ b/docs/Decorators.md @@ -137,9 +137,9 @@ As an example let's add a user property to the `Request` object: fastify.decorateRequest('user', '') // Update our property -fastify.addHook('preHandler', (req, reply, next) => { +fastify.addHook('preHandler', (req, reply, done) => { req.user = 'Bob Dylan' - next() + done() }) // And finally access it fastify.get('/', (req, reply) => { diff --git a/docs/Hooks.md b/docs/Hooks.md index 419cdb1842e..365f2f5c113 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -18,44 +18,44 @@ By using the hooks you can interact directly inside the lifecycle of Fastify. Th Example: ```js -fastify.addHook('onRequest', (request, reply, next) => { +fastify.addHook('onRequest', (request, reply, done) => { // some code - next() + done() }) -fastify.addHook('preParsing', (request, reply, next) => { +fastify.addHook('preParsing', (request, reply, done) => { // some code - next() + done() }) -fastify.addHook('preValidation', (request, reply, next) => { +fastify.addHook('preValidation', (request, reply, done) => { // some code - next() + done() }) -fastify.addHook('preHandler', (request, reply, next) => { +fastify.addHook('preHandler', (request, reply, done) => { // some code - next() + done() }) -fastify.addHook('preSerialization', (request, reply, payload, next) => { +fastify.addHook('preSerialization', (request, reply, payload, done) => { // some code - next() + done() }) -fastify.addHook('onError', (request, reply, error, next) => { +fastify.addHook('onError', (request, reply, error, done) => { // some code - next() + done() }) -fastify.addHook('onSend', (request, reply, payload, next) => { +fastify.addHook('onSend', (request, reply, payload, done) => { // some code - next() + done() }) -fastify.addHook('onResponse', (request, reply, next) => { +fastify.addHook('onResponse', (request, reply, done) => { // some code - next() + done() }) ``` Or `async/await` @@ -136,29 +136,29 @@ fastify.addHook('onResponse', async (request, reply) => { }) ``` -**Notice:** the `next` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `next` callback in this situation unexpected behavior may occur, e.g. duplicate invocation of handlers. +**Notice:** the `done` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `done` callback in this situation unexpected behavior may occur, e.g. duplicate invocation of handlers. **Notice:** in the `onRequest` and `preValidation` hooks, `request.body` will always be `null`, because the body parsing happens before the `preHandler` hook. [Request](https://github.com/fastify/fastify/blob/master/docs/Request.md) and [Reply](https://github.com/fastify/fastify/blob/master/docs/Reply.md) are the core Fastify objects.
-`next` is the function to continue with the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md). +`done` is the function to continue with the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md). It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md).
Hooks are affected by Fastify's encapsulation, and can thus be applied to selected routes. See the [Scopes](#scope) section for more information. -If you get an error during the execution of your hook, just pass it to `next()` and Fastify will automatically close the request and send the appropriate error code to the user. +If you get an error during the execution of your hook, just pass it to `done()` and Fastify will automatically close the request and send the appropriate error code to the user. ```js -fastify.addHook('onRequest', (request, reply, next) => { - next(new Error('some error')) +fastify.addHook('onRequest', (request, reply, done) => { + done(new Error('some error')) }) ``` If you want to pass a custom error code to the user, just use `reply.code()`: ```js -fastify.addHook('preHandler', (request, reply, next) => { +fastify.addHook('preHandler', (request, reply, done) => { reply.code(400) - next(new Error('some error')) + done(new Error('some error')) }) ``` @@ -169,13 +169,13 @@ fastify.addHook('preHandler', (request, reply, next) => { This hook is useful if you need to do some custom error logging or add some specific header in case of error.
It is not intended for changing the error, and calling `reply.send` will throw an exception.
This hook will be executed only after the `customErrorHandler` has been executed, and only if the `customErrorHandler` sends back an error to the user *(Note that the default `customErrorHandler` always send back the error to the user)*.
-**Notice:** unlike the other hooks, pass an error to the `next` function is not supported. +**Notice:** unlike the other hooks, pass an error to the `done` function is not supported. ```js -fastify.addHook('onError', (request, reply, error, next) => { +fastify.addHook('onError', (request, reply, error, done) => { // apm stands for Application Performance Monitoring apm.sendError(error) - next() + done() }) // Or async @@ -190,10 +190,10 @@ fastify.addHook('onError', async (request, reply, error) => { If you are using the `preSerialization` hook, you can change (or replace) the payload before it is serialized. For example: ```js -fastify.addHook('preSerialization', (request, reply, payload, next) => { +fastify.addHook('preSerialization', (request, reply, payload, done) => { var err = null; var newPayload = {wrapped: payload } - next(err, newPayload) + done(err, newPayload) }) // Or async @@ -209,10 +209,10 @@ Note: the hook is NOT called if the payload is a `string`, a `Buffer`, a `strea If you are using the `onSend` hook, you can change the payload. For example: ```js -fastify.addHook('onSend', (request, reply, payload, next) => { +fastify.addHook('onSend', (request, reply, payload, done) => { var err = null; var newPayload = payload.replace('some-text', 'some-new-text') - next(err, newPayload) + done(err, newPayload) }) // Or async @@ -225,10 +225,10 @@ fastify.addHook('onSend', async (request, reply, payload) => { You can also clear the payload to send a response with an empty body by replacing the payload with `null`: ```js -fastify.addHook('onSend', (request, reply, payload, next) => { +fastify.addHook('onSend', (request, reply, payload, done) => { reply.code(304) const newPayload = null - next(null, newPayload) + done(null, newPayload) }) ``` @@ -243,7 +243,7 @@ The `onResponse` hook is executed when a response has been sent, so you will not If needed, you can respond to a request before you reach the route handler. An example could be an authentication hook. If you are using `onRequest` or `preHandler` use `reply.send`; if you are using a middleware, `res.end`. ```js -fastify.addHook('onRequest', (request, reply, next) => { +fastify.addHook('onRequest', (request, reply, done) => { reply.send('early response') }) @@ -256,7 +256,7 @@ fastify.addHook('preHandler', async (request, reply) => { If you want to respond with a stream, you should avoid using an `async` function for the hook. If you must use an `async` function, your code will need to follow the pattern in [test/hooks-async.js](https://github.com/fastify/fastify/blob/94ea67ef2d8dce8a955d510cd9081aabd036fa85/test/hooks-async.js#L269-L275). ```js -fastify.addHook('onRequest', (request, reply, next) => { +fastify.addHook('onRequest', (request, reply, done) => { const stream = fs.createReadStream('some-file', 'utf8') reply.send(stream) }) @@ -330,9 +330,9 @@ fastify.addHook('onRegister', (instance) => { Except for [Application Hooks](#application-hooks), all hooks are encapsulated. This means that you can decide where your hooks should run by using `register` as explained in the [plugins guide](https://github.com/fastify/fastify/blob/master/docs/Plugins-Guide.md). If you pass a function, that function is bound to the right Fastify context and from there you have full access to the Fastify API. ```js -fastify.addHook('onRequest', function (request, reply, next) { +fastify.addHook('onRequest', function (request, reply, done) { const self = this // Fastify context - next() + done() }) ``` Note: using an arrow function will break the binding of this to the Fastify instance. diff --git a/docs/Logging.md b/docs/Logging.md index 85d246c51c9..3bc68f95bf2 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -106,11 +106,11 @@ const fastify = require('fastify')({ See a approach to log `req.body` ```js -app.addHook('preHandler', function (req, reply, next) { +app.addHook('preHandler', function (req, reply, done) { if (req.body) { req.log.info({ body: req.body }, 'parsed body') } - next() + done() }) ``` diff --git a/docs/Plugins-Guide.md b/docs/Plugins-Guide.md index ed81f37ede0..1223325346d 100644 --- a/docs/Plugins-Guide.md +++ b/docs/Plugins-Guide.md @@ -35,20 +35,20 @@ Fastify helps you a lot in this direction, because thanks to the encapsulation m *Let's return to how to correctly use `register`.*
As you probably know, the required plugins must expose a single function with the following signature ```js -module.exports = function (fastify, options, next) {} +module.exports = function (fastify, options, done) {} ``` -Where `fastify` is (pretty obvious) the encapsulated Fastify instance, `options` is the options object and `next` is the function you **must** call when your plugin is ready. +Where `fastify` is (pretty obvious) the encapsulated Fastify instance, `options` is the options object and `done` is the function you **must** call when your plugin is ready. Fastify's plugin model is fully reentrant and graph-based, it handles without any kind of problem asynchronous code and it guarantees the load order of the plugins, even the close order! *How?* Glad you asked, checkout [`avvio`](https://github.com/mcollina/avvio)! Fastify starts loading the plugin __after__ `.listen()`, `.inject()` or `.ready()` are called. -Inside a plugin you can do whatever you want, register routes, utilities (we'll see this in a moment) and do nested registers, just remember to call `next` when everything is set up! +Inside a plugin you can do whatever you want, register routes, utilities (we'll see this in a moment) and do nested registers, just remember to call `done` when everything is set up! ```js -module.exports = function (fastify, options, next) { +module.exports = function (fastify, options, done) { fastify.get('/plugin', (request, reply) => { reply.send({ hello: 'world' }) }) - next() + done() } ``` @@ -77,38 +77,38 @@ fastify.decorate('util', (a, b) => a + b) Now you can access your utility just by doing `fastify.util` whenever you need it, even inside your test.
And here's starts the magic; do you remember that few lines above we talked about encapsulation? Well, using `register` and `decorate` in conjunction enable exactly that, let me show you an example to clarify this: ```js -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { instance.decorate('util', (a, b) => a + b) console.log(instance.util('that is ', ' awesome')) - next() + done() }) -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { console.log(instance.util('that is ', ' awesome')) // this will throw an error - next() + done() }) ``` Inside the second register call `instance.util` will throw an error, because `util` exists only inside the first register context.
Let's step back for a moment and get deeper on this: when using the `register` api you will create a new context every time and this avoids situations like the one mentioned few line above. But pay attention, the encapsulation works only for the ancestors and the brothers, but not for the sons. ```js -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { instance.decorate('util', (a, b) => a + b) console.log(instance.util('that is ', ' awesome')) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { console.log(instance.util('that is ', ' awesome')) // this will not throw an error - next() + done() }) - next() + done() }) -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { console.log(instance.util('that is ', ' awesome')) // this will throw an error - next() + done() }) ``` *Take home message: if you need that an utility is available in every part of your application, pay attention that is declared at the root scope of your application. Otherwise you can use `fastify-plugin` utility as described [here](#distribution).* @@ -214,7 +214,7 @@ Now for every request you will run your utility, it is obvious that you can regi It can happen that you want a hook that must be executed just for a subset of routes, how can you do that? Yep, encapsulation! ```js -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { instance.decorate('util', (request, key, value) => { request[key] = value }) instance.addHook('preHandler', (request, reply, done) => { @@ -226,7 +226,7 @@ fastify.register((instance, opts, next) => { reply.send(request) }) - next() + done() }) fastify.get('/plugin2', (request, reply) => { @@ -260,10 +260,10 @@ Yes, I said that. But what I didn't tell you, is that you can tell to Fastify to const fp = require('fastify-plugin') const dbClient = require('db-client') -function dbPlugin (fastify, opts, next) { +function dbPlugin (fastify, opts, done) { dbClient.connect(opts.url, (err, conn) => { fastify.decorate('db', conn) - next() + done() }) } @@ -279,10 +279,10 @@ const fastify = require('fastify')() const fp = require('fastify-plugin') const dbClient = require('db-client') -function dbPlugin (fastify, opts, next) { +function dbPlugin (fastify, opts, done) { dbClient.connect(opts.url, (err, conn) => { fastify.decorate('db', conn) - next() + done() }) } diff --git a/docs/Plugins.md b/docs/Plugins.md index dc90037ef48..043bdf13da0 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -45,10 +45,10 @@ The `options` parameter can also be a `Function` which will be evaluated at the ```js const fp = require('fastify-plugin') -fastify.register(fp((fastify, opts, next) => { +fastify.register(fp((fastify, opts, done) => { fastify.decorate('foo_bar', { hello: 'world' }) - next() + done() })) // The opts argument of fastify-foo will be { hello: 'world' } @@ -100,24 +100,24 @@ await fastify.listen(3000) Creating a plugin is very easy, you just need to create a function that takes three parameters, the `fastify` instance, an options object and the next callback.
Example: ```js -module.exports = function (fastify, opts, next) { +module.exports = function (fastify, opts, done) { fastify.decorate('utility', () => {}) fastify.get('/', handler) - next() + done() } ``` You can also use `register` inside another `register`: ```js -module.exports = function (fastify, opts, next) { +module.exports = function (fastify, opts, done) { fastify.decorate('utility', () => {}) fastify.get('/', handler) fastify.register(require('./other-plugin')) - next() + done() } ``` Sometimes, you will need to know when the server is about to close, for example because you must close a connection to a database. To know when this is going to happen, you can use the [`'onClose'`](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#on-close) hook. @@ -136,18 +136,18 @@ We recommend to using the `fastify-plugin` module, because it solves this proble ```js const fp = require('fastify-plugin') -module.exports = fp(function (fastify, opts, next) { +module.exports = fp(function (fastify, opts, done) { fastify.decorate('utility', () => {}) - next() + done() }, '0.x') ``` Check the [`fastify-plugin`](https://github.com/fastify/fastify-plugin) documentation to know more about how use this module. If you don't use the `fastify-plugin` module, you can use the `'skip-override'` hidden property, but we do not recommend it. If in the future the Fastify API changes it will be a your responsibility update the module, while if you use `fastify-plugin`, you can be sure about backwards compatibility. ```js -function yourPlugin (fastify, opts, next) { +function yourPlugin (fastify, opts, done) { fastify.decorate('utility', () => {}) - next() + done() } yourPlugin[Symbol.for('skip-override')] = true module.exports = yourPlugin diff --git a/docs/Routes.md b/docs/Routes.md index d94938d9abf..590438d2734 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -216,16 +216,16 @@ fastify.listen(3000) ``` ```js // routes/v1/users.js -module.exports = function (fastify, opts, next) { +module.exports = function (fastify, opts, done) { fastify.get('/user', handler_v1) - next() + done() } ``` ```js // routes/v2/users.js -module.exports = function (fastify, opts, next) { +module.exports = function (fastify, opts, done) { fastify.get('/user', handler_v2) - next() + done() } ``` Fastify will not complain because you are using the same name for two different routes, because at compilation time it will handle the prefix automatically *(this also means that the performance will not be affected at all!)*. diff --git a/docs/Server.md b/docs/Server.md index 5f6d89631e5..b081440fe37 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -150,14 +150,14 @@ custom `onRequest` and `onResponse` hooks. ```js // Examples of hooks to replicate the disabled functionality. -fastify.addHook('onRequest', (req, reply, next) => { +fastify.addHook('onRequest', (req, reply, done) => { req.log.info({ url: req.req.url, id: req.id }, 'received request') - next() + done() }) -fastify.addHook('onResponse', (req, reply, next) => { +fastify.addHook('onResponse', (req, reply, done) => { req.log.info({ url: req.req.originalUrl, statusCode: res.res.statusCode }, 'request completed') - next() + done() }) ``` @@ -376,16 +376,16 @@ It is always executed before the method `fastify.ready`. ```js fastify - .register((instance, opts, next) => { + .register((instance, opts, done) => { console.log('Current plugin') - next() + done() }) .after(err => { console.log('After current plugin') }) - .register((instance, opts, next) => { + .register((instance, opts, done) => { console.log('Next plugin') - next() + done() }) .ready(err => { console.log('Everything has been loaded') @@ -560,24 +560,24 @@ The full path that will be prefixed to a route. Example: ```js -fastify.register(function (instance, opts, next) { +fastify.register(function (instance, opts, done) { instance.get('/foo', function (request, reply) { // Will log "prefix: /v1" request.log.info('prefix: %s', instance.prefix) reply.send({ prefix: instance.prefix }) }) - instance.register(function (instance, opts, next) { + instance.register(function (instance, opts, done) { instance.get('/bar', function (request, reply) { // Will log "prefix: /v1/v2" request.log.info('prefix: %s', instance.prefix) reply.send({ prefix: instance.prefix }) }) - next() + done() }, { prefix: '/v2' }) - next() + done() }, { prefix: '/v1' }) ``` @@ -623,24 +623,24 @@ You can also register a [`preValidation`](https://www.fastify.io/docs/latest/Hoo ```js fastify.setNotFoundHandler({ - preValidation: (req, reply, next) => { + preValidation: (req, reply, done) => { // your code - next() + done() }, - preHandler: (req, reply, next) => { + preHandler: (req, reply, done) => { // your code - next() + done() } }, function (request, reply) { // Default not found handler with preValidation and preHandler hooks }) -fastify.register(function (instance, options, next) { +fastify.register(function (instance, options, done) { instance.setNotFoundHandler(function (request, reply) { // Handle not found request without preValidation and preHandler hooks // to URLs that begin with '/v1' }) - next() + done() }, { prefix: '/v1' }) ``` diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 6d799438584..9d8704fea27 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -156,7 +156,7 @@ fastify.route({ handler: () => {} }) -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { /** * In children's scope can use schemas defined in upper scope like 'greetings'. @@ -180,7 +180,7 @@ fastify.register((instance, opts, next) => { handler: () => {} }) - next() + done() }) ``` @@ -220,16 +220,16 @@ The function `getSchemas` returns the shared schemas available in the selected s fastify.addSchema({ $id: 'one', my: 'hello' }) fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) }) -fastify.register((instance, opts, next) => { +fastify.register((instance, opts, done) => { instance.addSchema({ $id: 'two', my: 'ciao' }) instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) }) - instance.register((subinstance, opts, next) => { + instance.register((subinstance, opts, done) => { subinstance.addSchema({ $id: 'three', my: 'hola' }) subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) }) - next() + done() }) - next() + done() }) ``` This example will returns: From 9b233e8b803bc6e4767d0cc865df4af1d074273c Mon Sep 17 00:00:00 2001 From: Zoron Date: Thu, 25 Jul 2019 21:38:11 +0800 Subject: [PATCH 052/144] docs(Plugins): Fix typo (#1764) --- docs/Plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Plugins.md b/docs/Plugins.md index 043bdf13da0..f331ee9c3b2 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -97,7 +97,7 @@ await fastify.listen(3000) ``` ### Create a plugin -Creating a plugin is very easy, you just need to create a function that takes three parameters, the `fastify` instance, an options object and the next callback.
+Creating a plugin is very easy, you just need to create a function that takes three parameters, the `fastify` instance, an `options` object and the `done` callback.
Example: ```js module.exports = function (fastify, opts, done) { From b08414f8669c303e2a60a492ffe890efbd2c7048 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 25 Jul 2019 17:56:01 -0400 Subject: [PATCH 053/144] Fix bug regarding Joi schemas (#1768) --- lib/schemas.js | 9 ++++++++- test/route.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/schemas.js b/lib/schemas.js index e6279b99990..44c3ca4292e 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -59,6 +59,9 @@ Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { } } + // See issue https://github.com/fastify/fastify/issues/1767 + const cachedSchema = Object.assign({}, routeSchemas) + try { // this will work only for standard json schemas // other compilers such as Joi will fail @@ -72,9 +75,13 @@ Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { this.cleanId(routeSchemas) } } catch (err) { - // if we have failed because `resolve has thrown + // if we have failed because `resolve` has thrown // let's rethrow the error and let avvio handle it if (/FST_ERR_SCH_*/.test(err.code)) throw err + + // otherwise, the schema must not be a JSON schema + // so we let the user configured schemaCompiler handle it + return cachedSchema } if (routeSchemas.headers) { diff --git a/test/route.test.js b/test/route.test.js index 57d65ac391a..b29117252fe 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -3,6 +3,7 @@ const t = require('tap') const test = t.test const sget = require('simple-get').concat +const joi = require('joi') const Fastify = require('..') test('route', t => { @@ -255,3 +256,37 @@ test('handler function in options of shorthand route should works correctly', t t.deepEqual(JSON.parse(res.payload), { hello: 'world' }) }) }) + +test('does not mutate joi schemas', t => { + t.plan(4) + + const fastify = Fastify() + function schemaCompiler (schema) { + return function (data, opts) { + return joi.validate(data, schema) + } + } + + fastify.setSchemaCompiler(schemaCompiler) + + fastify.route({ + path: '/foo/:an_id', + method: 'GET', + schema: { + params: { an_id: joi.number() } + }, + handler (req, res) { + t.deepEqual(req.params, { an_id: 42 }) + res.send({ hello: 'world' }) + } + }) + + fastify.inject({ + method: 'GET', + url: '/foo/42' + }, (err, result) => { + t.error(err) + t.strictEqual(result.statusCode, 200) + t.deepEqual(JSON.parse(result.payload), { hello: 'world' }) + }) +}) From 4e4810fe0dde67306826856afdd0531ca27daaf0 Mon Sep 17 00:00:00 2001 From: delvedor Date: Sat, 27 Jul 2019 11:50:18 +0200 Subject: [PATCH 054/144] Bumped v2.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f11bd9f888..62e9c066a2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.6.0", + "version": "2.7.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From c1cf60486fd722c5ce07460ee3cc03ab81a25fbf Mon Sep 17 00:00:00 2001 From: Adriano Raiano Date: Sat, 27 Jul 2019 15:32:36 +0200 Subject: [PATCH 055/144] docs: simplify serverless usage with aws-lambda-fastify (#1748) --- docs/Serverless.md | 49 +++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/docs/Serverless.md b/docs/Serverless.md index 67f94e51079..0077d990f76 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -18,15 +18,15 @@ we do not test for such integration scenarios. The sample provided allows you to easily build serverless web applications/services and RESTful APIs using Fastify on top of AWS Lambda and Amazon API Gateway. -*Note: This is just one possible way.* +*Note: Using [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) is just one possible way.* ### app.js ```js const fastify = require('fastify'); -function init(serverFactory) { - const app = fastify({ serverFactory }); +function init() { + const app = fastify(); app.get('/', (request, reply) => reply.send({ hello: 'world' })); return app; } @@ -43,11 +43,9 @@ if (require.main !== module) { } ``` -You can simply wrap your initialization code by offering to inject an optional [serverFactory](https://www.fastify.io/docs/latest/Server/#serverfactory). - When executed in your lambda function we don't need to listen to a specific port, so we just export the wrapper function `init` in this case. -The [`lambda.js`](https://www.fastify.io/docs/latest/Server/#lambda.js) file will use this export. +The [`lambda.js`](https://www.fastify.io/docs/latest/Serverless/#lambda-js) file will use this export. When you execute your Fastify application like always, i.e. `node app.js` *(the detection for this could be `require.main === module`)*, @@ -56,31 +54,28 @@ you can normally listen to your port, so you can still run your Fastify function ### lambda.js ```js -const awsServerlessExpress = require('aws-serverless-express'); +const awsLambdaFastify = require('aws-lambda-fastify') const init = require('./app'); -let server; -const serverFactory = (handler) => { - server = awsServerlessExpress.createServer(handler); - return server; -} -const app = init(serverFactory); - -exports.handler = (event, context, callback) => { - context.callbackWaitsForEmptyEventLoop = false; - app.ready((e) => { - if (e) return console.error(e.stack || e); - awsServerlessExpress.proxy(server, event, context, 'CALLBACK', callback); - }); -}; +const proxy = awsLambdaFastify(init()) +// or +// const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] }) + +exports.handler = proxy; +// or +// exports.handler = (event, context, callback) => proxy(event, context, callback); +// or +// exports.handler = (event, context) => proxy(event, context); +// or +// exports.handler = async (event, context) => proxy(event, context); ``` -We define a custom `serverFactory` function, in which we create a new server with the help of [`aws-serverless-express`](https://github.com/awslabs/aws-serverless-express) -(make sure you install the dependency `npm i --save aws-serverless-express`). -Then we call the `init` function (imported from [`app.js`](https://www.fastify.io/docs/latest/Server/#app.js)) with the `serverFactory` function as the only parameter. -Finally inside the lambda `handler` function we wait for the Fastify app to be `ready` -and proxy all the incoming events (API Gateway requests) to the `proxy` function from [`aws-serverless-express`](https://github.com/awslabs/aws-serverless-express). - +We just require [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) +(make sure you install the dependency `npm i --save aws-lambda-fastify`) and our +[`app.js`](https://www.fastify.io/docs/latest/Serverless/#app-js) file and call the +exported `awsLambdaFastify` function with the `app` as the only parameter. +The resulting `proxy` function has the correct signature to be used as lambda `handler` function. +This way all the incoming events (API Gateway requests) are passed to the `proxy` function of [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify). ### Example From b856f07facb6667d8e34e6e352d76a0c53b452f9 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Tue, 30 Jul 2019 11:22:57 -0400 Subject: [PATCH 056/144] better typing for headers object (#1775) * better typing for headers object * Update fastify.d.ts Co-Authored-By: Tomas Della Vedova --- fastify.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastify.d.ts b/fastify.d.ts index df1cbb8508e..d62449ee1f8 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -316,7 +316,7 @@ declare namespace fastify { req: NodeJS.ReadableStream, res: http.ServerResponse }, - headers: object, + headers: Record, statusCode: number, statusMessage: string, payload: string, From 2996fd83042084a2ecccc38b6fa123ec87546eec Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 31 Jul 2019 09:59:03 +0200 Subject: [PATCH 057/144] Ensure we are not running the handler if reply.sent is true (#1778) Fixes #1733 Fixes #1776 --- lib/handleRequest.js | 4 +-- package.json | 26 +++++++------- test/hooks-async.js | 81 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/lib/handleRequest.js b/lib/handleRequest.js index 078f386d169..d037bf94a25 100644 --- a/lib/handleRequest.js +++ b/lib/handleRequest.js @@ -72,7 +72,7 @@ function handler (request, reply) { } function preValidationCallback (err, request, reply) { - if (reply.res.finished === true) return + if (reply.sent === true || reply.res.finished === true) return if (err != null) { reply.send(err) return @@ -103,7 +103,7 @@ function preValidationCallback (err, request, reply) { } function preHandlerCallback (err, request, reply) { - if (reply.res.finished === true) return + if (reply.sent || reply.res.finished === true) return if (err != null) { reply.send(err) return diff --git a/package.json b/package.json index 62e9c066a2c..74a8f995400 100644 --- a/package.json +++ b/package.json @@ -88,25 +88,25 @@ "node": ">=6" }, "devDependencies": { - "@types/node": "^11.13.12", - "@typescript-eslint/eslint-plugin": "^1.4.2", - "@typescript-eslint/parser": "^1.4.2", + "@types/node": "^11.13.18", + "@typescript-eslint/eslint-plugin": "^1.13.0", + "@typescript-eslint/parser": "^1.13.0", "JSONStream": "^1.3.5", "ajv-pack": "^0.3.1", "autocannon": "^3.2.0", "branch-comparer": "^0.4.0", - "concurrently": "^4.1.0", + "concurrently": "^4.1.1", "cors": "^2.8.5", - "coveralls": "^3.0.3", + "coveralls": "^3.0.5", "dns-prefetch-control": "^0.2.0", "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", "fluent-schema": "^0.7.3", - "form-data": "^2.3.3", + "form-data": "^2.5.0", "frameguard": "^3.0.0", "h2url": "^0.1.2", - "helmet": "^3.15.1", + "helmet": "^3.20.0", "hide-powered-by": "^1.0.0", "hsts": "^2.1.0", "http-errors": "^1.7.1", @@ -115,9 +115,9 @@ "license-checker": "^25.0.1", "lolex": "^4.0.1", "pre-commit": "^1.2.2", - "proxyquire": "^2.1.0", + "proxyquire": "^2.1.1", "pump": "^3.0.0", - "semver": "^6.0.0", + "semver": "^6.3.0", "send": "^0.17.0", "serve-static": "^1.13.2", "simple-get": "^3.0.3", @@ -127,19 +127,19 @@ "tap": "^12.5.2", "tap-mocha-reporter": "^3.0.7", "then-sleep": "^1.0.1", - "typescript": "^3.5.1", + "typescript": "^3.5.3", "x-xss-protection": "^1.1.0" }, "dependencies": { "abstract-logging": "^1.0.0", - "ajv": "^6.9.2", + "ajv": "^6.10.2", "avvio": "^6.1.1", "fast-json-stringify": "^1.15.0", "find-my-way": "^2.0.0", "flatstr": "^1.0.12", - "light-my-request": "^3.2.0", + "light-my-request": "^3.4.1", "middie": "^4.0.1", - "pino": "^5.11.1", + "pino": "^5.13.1", "proxy-addr": "^2.0.4", "readable-stream": "^3.1.1", "rfdc": "^1.1.2", diff --git a/test/hooks-async.js b/test/hooks-async.js index 2e52180cd43..61aade9f9b7 100644 --- a/test/hooks-async.js +++ b/test/hooks-async.js @@ -4,7 +4,8 @@ const split = require('split2') const sget = require('simple-get').concat const Fastify = require('..') const fs = require('fs') -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) +const { promisify } = require('util') +const sleep = promisify(setTimeout) function asyncHookTest (t) { const test = t.test @@ -231,6 +232,53 @@ function asyncHookTest (t) { }) }) + test('preValidation hooks should be able to block a request with async onSend', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('preValidation', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + await sleep(10) + t.equal(payload, 'hello') + }) + + fastify.addHook('preHandler', async (request, reply) => { + t.fail('we should not be here') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.post('/', { + schema: { + body: { + type: 'object', + properties: { + hello: { + type: 'string' + } + }, + required: ['hello'] + } + } + }, function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'POST' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) + }) + test('preSerialization hooks should be able to modify the payload', t => { t.plan(3) const fastify = Fastify() @@ -339,6 +387,37 @@ function asyncHookTest (t) { }) }) + test('preHandler hooks should be able to block a request (last hook) with a delay in onSend', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('preHandler', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + await sleep(10) + t.equal(payload, 'hello') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) + }) + test('onRequest respond with a stream', t => { t.plan(4) const fastify = Fastify() From 0713aeb4efd6104ff530ef15a5b16bd8e17d6916 Mon Sep 17 00:00:00 2001 From: Jinesh Shah Date: Wed, 31 Jul 2019 03:59:45 -0400 Subject: [PATCH 058/144] Added Google Cloud Run docs (#1770) * Added Google Cloud Run docs * Update docs/Serverless.md Co-Authored-By: Manuel Spigolon * Update docs/Serverless.md Co-Authored-By: Manuel Spigolon * Updated docs --- docs/Serverless.md | 106 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/docs/Serverless.md b/docs/Serverless.md index 0077d990f76..df5dd314cb2 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -2,6 +2,11 @@ Run serverless applications and REST APIs using your existing Fastify application. +### Contents + +- [AWS Lambda](#aws-lambda) +- [Google Cloud Run](#google-cloud-run) + ### Attention Readers: > Fastify is not designed to run on serverless environments. The Fastify framework is designed to make implementing a traditional HTTP/S server easy. @@ -12,7 +17,6 @@ it is possible to use Fastify in a serverless environment. Again, keep in mind that this is not Fastify's intended use case and we do not test for such integration scenarios. - ## AWS Lambda The sample provided allows you to easily build serverless web applications/services @@ -86,3 +90,103 @@ An example deployable with [claudia.js](https://claudiajs.com/tutorials/serverle - API Gateway doesn't support streams yet, so you're not able to handle [streams](https://www.fastify.io/docs/latest/Reply/#streams). - API Gateway has a timeout of 29 seconds, so it's important to provide a reply during this time. + +## Google Cloud Run + +Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. It's primary purpose is to provide an infrastucture-abstracted environment to run arbitrary containers. As a result, Fastify can be deployed to Google Cloud Run with little-to-no code changes from the way you would write your Fastify app normally. + +*Follow the steps below to deploy to Google Cloud Run if you are already familiar with gcloud or just follow their [quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)* + +### Adjust Fastfiy server + +In order for Fastify to properly listen for requests within the container, be sure to set the correct port and address: + +```js +function build() { + const fastify = Fastify({ trustProxy: true }) + return fastify +} + +async function start() { + // Google Cloud Run will set this environment variable for you, so + // you can also use it to detect if you are running in Cloud Run + const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined + + // You must listen on the port Cloud Run provides + const port = process.env.PORT || 3000 + + // You must listen on all IPV4 addresses in Cloud Run + const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined + + try { + const server = build() + const address = await server.listen(port, address) + console.log(`Listening on ${address}`) + } catch (err) { + console.error(err) + process.exit(1) + } +} + +module.exports = build + +if (require.main === module) { + start() +} +``` + +### Add a Dockerfile + +You can add any valid `Dockerfile` that packages and runs a Node app. A basic `Dockerfile` can be found in the official [gcloud docs](https://github.com/knative/docs/blob/2d654d1fd6311750cc57187a86253c52f273d924/docs/serving/samples/hello-world/helloworld-nodejs/Dockerfile). + +```Dockerfile +# Use the official Node.js 10 image. +# https://hub.docker.com/_/node +FROM node:10 + +# Create and change to the app directory. +WORKDIR /usr/src/app + +# Copy application dependency manifests to the container image. +# A wildcard is used to ensure both package.json AND package-lock.json are copied. +# Copying this separately prevents re-running npm install on every code change. +COPY package*.json ./ + +# Install production dependencies. +RUN npm install --only=production + +# Copy local code to the container image. +COPY . . + +# Run the web service on container startup. +CMD [ "npm", "start" ] +``` + +### Add a .dockerignore + +To keep build artifacts out of your container (which keeps it small and improves build times), add a `.dockerignore` file like the one below: + +```.dockerignore +Dockerfile +README.md +node_modules +npm-debug.log +``` + +### Submit build + +Next, submit your app to be built into a Docker image by running the following command (replacing `PROJECT-ID` and `APP-NAME` with your GCP project id and an app name: + +```bash +gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME +``` + +### Deploy Image + +After your image has built, you can deploy it with the following command: + +```bash +gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed +``` + +Your app will be accessible from the URL GCP provides. From fd781b7bb4703edd17abcd1da33031b8278f99c0 Mon Sep 17 00:00:00 2001 From: delvedor Date: Wed, 31 Jul 2019 10:08:56 +0200 Subject: [PATCH 059/144] Bumped v2.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74a8f995400..645f89b84a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.7.0", + "version": "2.7.1", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 5035eca77efdb922f42f92c13b9b75e4e9b1fcc0 Mon Sep 17 00:00:00 2001 From: Zoron Date: Sat, 3 Aug 2019 17:43:27 +0800 Subject: [PATCH 060/144] docs(Serverless): add missing punctuation marks (#1783) --- docs/Serverless.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Serverless.md b/docs/Serverless.md index df5dd314cb2..14ab0b3f3ea 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -95,7 +95,7 @@ An example deployable with [claudia.js](https://claudiajs.com/tutorials/serverle Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. It's primary purpose is to provide an infrastucture-abstracted environment to run arbitrary containers. As a result, Fastify can be deployed to Google Cloud Run with little-to-no code changes from the way you would write your Fastify app normally. -*Follow the steps below to deploy to Google Cloud Run if you are already familiar with gcloud or just follow their [quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)* +*Follow the steps below to deploy to Google Cloud Run if you are already familiar with gcloud or just follow their [quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)*. ### Adjust Fastfiy server @@ -175,7 +175,7 @@ npm-debug.log ### Submit build -Next, submit your app to be built into a Docker image by running the following command (replacing `PROJECT-ID` and `APP-NAME` with your GCP project id and an app name: +Next, submit your app to be built into a Docker image by running the following command (replacing `PROJECT-ID` and `APP-NAME` with your GCP project id and an app name): ```bash gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME From 53bac6dcfc277a67334441d10fdc7f6fa3c0feaf Mon Sep 17 00:00:00 2001 From: James Sumners Date: Sat, 3 Aug 2019 12:03:52 -0400 Subject: [PATCH 061/144] Add toStrinTag to errors (#1785) --- lib/errors.js | 1 + test/internals/errors.test.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/errors.js b/lib/errors.js index d01cca7039c..21ce5805a81 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -95,6 +95,7 @@ function createError (code, message, statusCode = 500, Base = Error) { this.message = `${this.code}: ${this.message}` this.statusCode = statusCode || undefined } + FastifyError.prototype[Symbol.toStringTag] = 'Error' inherits(FastifyError, Base) diff --git a/test/internals/errors.test.js b/test/internals/errors.test.js index e53747fe072..57654585795 100644 --- a/test/internals/errors.test.js +++ b/test/internals/errors.test.js @@ -94,3 +94,11 @@ test('Create error with different base', t => { t.equal(err.statusCode, 500) t.ok(err.stack) }) + +test('Error has appropriate string tag', t => { + t.plan(1) + const NewError = createError('CODE', 'foo') + const err = new NewError() + const str = Object.prototype.toString.call(err) + t.equal(str, '[object Error]') +}) From 11ed5d7e04f7868907b9098013b033252ab32c72 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Mon, 5 Aug 2019 23:00:10 +0700 Subject: [PATCH 062/144] [ts] Add `handler` to `RouteShorthandOptions` (#1788) * [ts] Add `handler` to `RouteShorthandOptions` * [ts] make `handler` prop optional (incase already defined in opts) * update tests --- fastify.d.ts | 17 +++++++++-------- test/types/index.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index d62449ee1f8..bb4a0bb6243 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -256,6 +256,7 @@ declare namespace fastify { preSerialization?: FastifyMiddlewareWithPayload | Array> + handler?: RequestHandler schemaCompiler?: SchemaCompiler bodyLimit?: number logLevel?: string @@ -362,7 +363,7 @@ declare namespace fastify { get( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -379,7 +380,7 @@ declare namespace fastify { put( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -396,7 +397,7 @@ declare namespace fastify { patch( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -413,7 +414,7 @@ declare namespace fastify { post( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -430,7 +431,7 @@ declare namespace fastify { head( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -447,7 +448,7 @@ declare namespace fastify { delete( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -464,7 +465,7 @@ declare namespace fastify { options( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** @@ -481,7 +482,7 @@ declare namespace fastify { all( url: string, opts: RouteShorthandOptions, - handler: RequestHandler, + handler?: RequestHandler, ): FastifyInstance /** diff --git a/test/types/index.ts b/test/types/index.ts index f02393fe9d2..17d61369d1f 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -209,6 +209,10 @@ const opts: fastify.RouteShorthandOptions = { + ...opts, + handler (req, reply) { reply.send({ hello: 'route' }) } +} // Chaining and route definitions server @@ -250,6 +254,7 @@ server reply.header('Content-Type', 'application/json').code(200) reply.send({ hello: 'world' }) }) + .get('/optsWithHandler', optsWithHandler) .get('/status', function (req, reply) { reply.status(204).send() }) @@ -279,17 +284,21 @@ server .headers({ 'Content-Type': 'application/json' }) .send({ hello: 'world' }) }) + .post('/optsWithHandler', optsWithHandler) .head('/', {}, function (req, reply) { reply.send() }) + .head('/optsWithHandler', optsWithHandler) .delete('/', opts, function (req, reply) { reply.send({ hello: 'world' }) }) + .delete('/optsWithHandler', optsWithHandler) .patch('/:id', opts, function (req, reply) { req.log.info(`incoming id is ${req.params.id}`) reply.send({ hello: 'world' }) }) + .patch('/optsWithHandler', optsWithHandler) .route({ method: ['GET', 'POST', 'PUT'], url: '/multi-route', @@ -317,6 +326,7 @@ server .all('/all/with-opts', opts, function (req, reply) { reply.send(req.headers) }) + .all('/optsWithHandler', optsWithHandler) .route({ method: 'GET', url: '/headers', From ca1bc0636e7ddbd80cfbb928410ed48bfae3fcf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=BChl?= Date: Fri, 9 Aug 2019 12:48:12 +0300 Subject: [PATCH 063/144] typo: typescript-server.ts (#1791) --- examples/typescript-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/typescript-server.ts b/examples/typescript-server.ts index aa9fad3f4f2..b0324815034 100644 --- a/examples/typescript-server.ts +++ b/examples/typescript-server.ts @@ -3,7 +3,7 @@ * Most type annotations in this file are not strictly necessary but are * included for this example. * - * To run this example exectute the following commands to install typescript, + * To run this example execute the following commands to install typescript, * transpile the code, and start the server: * * npm i -g typescript From 124e7eea7af4048b008abf9c495bb581ec25061b Mon Sep 17 00:00:00 2001 From: Francis Brito Date: Sat, 10 Aug 2019 12:13:35 -0400 Subject: [PATCH 064/144] Add request property to reply documentation (#1792) * typescript: add incoming request property to fastify reply type * docs(reply): add request to list of exposed reply properties (#1734) --- docs/Reply.md | 3 ++- fastify.d.ts | 1 + test/types/index.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/Reply.md b/docs/Reply.md index e21090769a8..c3856f266e7 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -43,7 +43,8 @@ and properties: - `.send(payload)` - Sends the payload to the user, could be a plain text, a buffer, JSON, stream, or an Error object. - `.sent` - A boolean value that you can use if you need to know if `send` has already been called. - `.res` - The [`http.ServerResponse`](https://nodejs.org/dist/latest/docs/api/http.html#http_class_http_serverresponse) from Node core. -- `.log` - the logger instance of the incoming request +- `.log` - The logger instance of the incoming request. +- `.request` - The incoming request. ```js fastify.get('/', options, function (request, reply) { diff --git a/fastify.d.ts b/fastify.d.ts index bb4a0bb6243..dd8ffa0c5a1 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -176,6 +176,7 @@ declare namespace fastify { sent: boolean res: HttpResponse context: FastifyContext + request: FastifyRequest } type TrustProxyFunction = (addr: string, index: number) => boolean interface ServerOptions { diff --git a/test/types/index.ts b/test/types/index.ts index 17d61369d1f..e645b30934f 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -645,3 +645,11 @@ server3.close(() => {}) done() } } + +type TestReplyDecoration = (this: fastify.FastifyReply) => void + +const server4 = fastify() +const testReplyDecoration: TestReplyDecoration = function () { + console.log('can access request from reply decorator', this.request.id) +} +server4.decorateReply('test-request-accessible-from-reply', testReplyDecoration) From 5c7da212e893360334c5f980db4fd4b232253188 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Sat, 10 Aug 2019 14:01:40 -0400 Subject: [PATCH 065/144] Use .isFluentSchema instead of symbol to check for fluent-schema (#1794) * Use .isFluentSchema instead of symbol to check for fluent-schema * Add back symbol check for backcompat --- lib/schemas.js | 2 +- lib/validation.js | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/schemas.js b/lib/schemas.js index 44c3ca4292e..e88ee1d66d0 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -19,7 +19,7 @@ function Schemas () { } Schemas.prototype.add = function (inputSchema) { - var schema = fastClone(inputSchema[kFluentSchema] + var schema = fastClone((inputSchema.isFluentSchema || inputSchema[kFluentSchema]) ? inputSchema.valueOf() : inputSchema ) diff --git a/lib/validation.js b/lib/validation.js index b690f5aa4dc..697eda02a17 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -70,14 +70,14 @@ function build (context, compile, schemas) { function generateFluentSchema (schema) { ;['params', 'body', 'querystring', 'query', 'headers'].forEach(key => { - if (schema[key] && schema[key][kFluentSchema]) { + if (schema[key] && (schema[key].isFluentSchema || schema[key][kFluentSchema])) { schema[key] = schema[key].valueOf() } }) if (schema.response) { Object.keys(schema.response).forEach(code => { - if (schema.response[code][kFluentSchema]) { + if (schema.response[code].isFluentSchema || schema.response[code][kFluentSchema]) { schema.response[code] = schema.response[code].valueOf() } }) diff --git a/package.json b/package.json index 645f89b84a0..95c5c00bf47 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", - "fluent-schema": "^0.7.3", + "fluent-schema": "^0.7.4", "form-data": "^2.5.0", "frameguard": "^3.0.0", "h2url": "^0.1.2", From fb68541db9efe8fc25a43e395ab249703f499172 Mon Sep 17 00:00:00 2001 From: kivi Date: Tue, 13 Aug 2019 10:49:15 +0200 Subject: [PATCH 066/144] fix inverted if in serverless example (#1797) in one example of the serverless documentation: (require.main === module) was inverted --- docs/Serverless.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Serverless.md b/docs/Serverless.md index 14ab0b3f3ea..4e3098c72d3 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -35,7 +35,7 @@ function init() { return app; } -if (require.main !== module) { +if (require.main === module) { // called directly i.e. "node app" init().listen(3000, (err) => { if (err) console.error(err); From 9c940b72b38ea61dd04bb2851963bb96e584858a Mon Sep 17 00:00:00 2001 From: Jemar Jones Date: Fri, 16 Aug 2019 05:54:36 -0400 Subject: [PATCH 067/144] Ensure that header properties are not duplicated in schema (#1806) --- lib/validation.js | 1 + test/internals/validation.test.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/validation.js b/lib/validation.js index 697eda02a17..fea6b863f1b 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -44,6 +44,7 @@ function build (context, compile, schemas) { headersSchemaLowerCase.required = headersSchemaLowerCase.required.map(h => h.toLowerCase()) } if (headers.properties) { + headersSchemaLowerCase.properties = {} Object.keys(headers.properties).forEach(k => { headersSchemaLowerCase.properties[k.toLowerCase()] = headers.properties[k] }) diff --git a/test/internals/validation.test.js b/test/internals/validation.test.js index 01a45a77c9c..ea589f77fac 100644 --- a/test/internals/validation.test.js +++ b/test/internals/validation.test.js @@ -226,3 +226,21 @@ test('build schema - headers are not lowercased in case of custom object', t => return () => {} }, new Schemas()) }) + +test('build schema - uppercased headers are not included', t => { + t.plan(1) + const opts = { + schema: { + headers: { + type: 'object', + properties: { + 'Content-Type': { type: 'string' } + } + } + } + } + validation.build(opts, schema => { + t.notOk('Content-Type' in schema.properties, 'uppercase does not exist') + return () => {} + }, new Schemas()) +}) From 1568dff519fcd0164c22187bf7e24456d83c75a5 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Fri, 16 Aug 2019 17:15:56 -0300 Subject: [PATCH 068/144] docs(Hooks): Add each Hook to a title (#1801) --- docs/Hooks.md | 270 ++++++++++++++++++++++++-------------------------- 1 file changed, 129 insertions(+), 141 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 365f2f5c113..410f9debb60 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -4,61 +4,40 @@ Hooks are registered with the `fastify.addHook` method and allow you to listen to specific events in the application or request/response lifecycle. You have to register a hook before the event is triggered otherwise the event is lost. -## Request/Response Hooks - By using the hooks you can interact directly inside the lifecycle of Fastify. There are seven different Hooks that you can use *(in order of execution)*: -- `'onRequest'` -- `'preParsing'` -- `'preValidation'` -- `'preHandler'` -- `'preSerialization'` -- `'onError'` -- `'onSend'` -- `'onResponse'` - -Example: -```js -fastify.addHook('onRequest', (request, reply, done) => { - // some code - done() -}) -fastify.addHook('preParsing', (request, reply, done) => { - // some code - done() -}) +- [Request/Reply Hooks](#requestreply-hooks) + - [onRequest](#onRequest) + - [preParsing](#preParsing) + - [preValidation](#preValidation) + - [preHandler](#preHandler) + - [preSerialization](#preSerialization) + - [onError](#onError) + - [onSend](#onSend) + - [onResponse](#onResponse) +- [Application Hooks](#application-hooks) + - [onClose](#onclose) + - [onRoute](#onroute) + - [onRegister](#onregister) -fastify.addHook('preValidation', (request, reply, done) => { - // some code - done() -}) - -fastify.addHook('preHandler', (request, reply, done) => { - // some code - done() -}) +**Notice:** the `done` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `done` callback in this situation unexpected behavior may occur, e.g. duplicate invocation of handlers. -fastify.addHook('preSerialization', (request, reply, payload, done) => { - // some code - done() -}) +## Request/Reply Hooks -fastify.addHook('onError', (request, reply, error, done) => { - // some code - done() -}) +[Request](https://github.com/fastify/fastify/blob/master/docs/Request.md) and [Reply](https://github.com/fastify/fastify/blob/master/docs/Reply.md) are the core Fastify objects.
+`done` is the function to continue with the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md). -fastify.addHook('onSend', (request, reply, payload, done) => { - // some code - done() -}) +It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md).
+Hooks are affected by Fastify's encapsulation, and can thus be applied to selected routes. See the [Scopes](#scope) section for more information. -fastify.addHook('onResponse', (request, reply, done) => { +### onRequest +```js +fastify.addHook('onRequest', (request, reply, done) => { // some code done() }) ``` -Or `async/await` +Or `async/await`: ```js fastify.addHook('onRequest', async (request, reply) => { // some code @@ -69,28 +48,20 @@ fastify.addHook('onRequest', async (request, reply) => { } return }) +``` -fastify.addHook('preParsing', async (request, reply) => { - // some code - await asyncMethod() - // error occurred - if (err) { - throw new Error('some errors occurred.') - } - return -}) +**Notice:** in the [onRequest](#onRequest) hook, `request.body` will always be `null`, because the body parsing happens before the [preHandler](#preHandler) hook. -fastify.addHook('preValidation', async (request, reply) => { +### preParsing +```js +fastify.addHook('preParsing', (request, reply, done) => { // some code - await asyncMethod() - // error occurred - if (err) { - throw new Error('some errors occurred.') - } - return + done() }) - -fastify.addHook('preHandler', async (request, reply) => { +``` +Or `async/await`: +```js +fastify.addHook('preParsing', async (request, reply) => { // some code await asyncMethod() // error occurred @@ -99,23 +70,17 @@ fastify.addHook('preHandler', async (request, reply) => { } return }) - -fastify.addHook('preSerialization', async (request, reply, payload) => { +``` +### preValidation +```js +fastify.addHook('preValidation', (request, reply, done) => { // some code - await asyncMethod() - // error occurred - if (err) { - throw new Error('some errors occurred.') - } - return payload -}) - -fastify.addHook('onError', async (request, reply, error) => { - // useful for custom error logging - // you should not use this hook to update the error + done() }) - -fastify.addHook('onSend', async (request, reply, payload) => { +``` +Or `async/await`: +```js +fastify.addHook('preValidation', async (request, reply) => { // some code await asyncMethod() // error occurred @@ -124,8 +89,19 @@ fastify.addHook('onSend', async (request, reply, payload) => { } return }) +``` +**Notice:** in the [preValidation](#preValidation) hook, `request.body` will always be `null`, because the body parsing happens before the [preHandler](#preHandler) hook. -fastify.addHook('onResponse', async (request, reply) => { +### preHandler +```js +fastify.addHook('preHandler', (request, reply, done) => { + // some code + done() +}) +``` +Or `async/await`: +```js +fastify.addHook('preHandler', async (request, reply) => { // some code await asyncMethod() // error occurred @@ -135,77 +111,46 @@ fastify.addHook('onResponse', async (request, reply) => { return }) ``` +### preSerialization -**Notice:** the `done` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `done` callback in this situation unexpected behavior may occur, e.g. duplicate invocation of handlers. - -**Notice:** in the `onRequest` and `preValidation` hooks, `request.body` will always be `null`, because the body parsing happens before the `preHandler` hook. - -[Request](https://github.com/fastify/fastify/blob/master/docs/Request.md) and [Reply](https://github.com/fastify/fastify/blob/master/docs/Reply.md) are the core Fastify objects.
-`done` is the function to continue with the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md). - -It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md).
-Hooks are affected by Fastify's encapsulation, and can thus be applied to selected routes. See the [Scopes](#scope) section for more information. - -If you get an error during the execution of your hook, just pass it to `done()` and Fastify will automatically close the request and send the appropriate error code to the user. +If you are using the `preSerialization` hook, you can change (or replace) the payload before it is serialized. For example: ```js -fastify.addHook('onRequest', (request, reply, done) => { - done(new Error('some error')) +fastify.addHook('preSerialization', (request, reply, payload, done) => { + var err = null; + var newPayload = { wrapped: payload } + done(err, newPayload) }) ``` - -If you want to pass a custom error code to the user, just use `reply.code()`: +Or `async/await` ```js -fastify.addHook('preHandler', (request, reply, done) => { - reply.code(400) - done(new Error('some error')) +fastify.addHook('preSerialization', async (request, reply, payload) => { + return {wrapped: payload } }) ``` -*The error will be handled by [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md#errors).* - -#### The `onError` Hook - -This hook is useful if you need to do some custom error logging or add some specific header in case of error.
-It is not intended for changing the error, and calling `reply.send` will throw an exception.
-This hook will be executed only after the `customErrorHandler` has been executed, and only if the `customErrorHandler` sends back an error to the user *(Note that the default `customErrorHandler` always send back the error to the user)*.
-**Notice:** unlike the other hooks, pass an error to the `done` function is not supported. +Note: the hook is NOT called if the payload is a `string`, a `Buffer`, a `stream`, or `null`. +### onError ```js fastify.addHook('onError', (request, reply, error, done) => { - // apm stands for Application Performance Monitoring - apm.sendError(error) + // some code done() }) - -// Or async -fastify.addHook('onError', async (request, reply, error) => { - // apm stands for Application Performance Monitoring - apm.sendError(error) -}) ``` - -#### The `preSerialization` Hook - -If you are using the `preSerialization` hook, you can change (or replace) the payload before it is serialized. For example: - +Or `async/await`: ```js -fastify.addHook('preSerialization', (request, reply, payload, done) => { - var err = null; - var newPayload = {wrapped: payload } - done(err, newPayload) -}) - -// Or async -fastify.addHook('preSerialization', async (request, reply, payload) => { - return {wrapped: payload } +fastify.addHook('onError', async (request, reply, error) => { + // useful for custom error logging + // you should not use this hook to update the error }) ``` +This hook is useful if you need to do some custom error logging or add some specific header in case of error.
+It is not intended for changing the error, and calling `reply.send` will throw an exception.
+This hook will be executed only after the `customErrorHandler` has been executed, and only if the `customErrorHandler` sends back an error to the user *(Note that the default `customErrorHandler` always send back the error to the user)*.
+**Notice:** unlike the other hooks, pass an error to the `done` function is not supported. -Note: the hook is NOT called if the payload is a `string`, a `Buffer`, a `stream`, or `null`. - -#### The `onSend` Hook - +### onSend If you are using the `onSend` hook, you can change the payload. For example: ```js @@ -214,8 +159,9 @@ fastify.addHook('onSend', (request, reply, payload, done) => { var newPayload = payload.replace('some-text', 'some-new-text') done(err, newPayload) }) - -// Or async +``` +Or `async/await`: +```js fastify.addHook('onSend', async (request, reply, payload) => { var newPayload = payload.replace('some-text', 'some-new-text') return newPayload @@ -236,9 +182,50 @@ fastify.addHook('onSend', (request, reply, payload, done) => { Note: If you change the payload, you may only change it to a `string`, a `Buffer`, a `stream`, or `null`. -#### The `onResponse` Hook + +### onResponse +```js + +fastify.addHook('onResponse', (request, reply, done) => { + // some code + done() +}) +``` +Or `async/await`: +```js +fastify.addHook('onResponse', async (request, reply) => { + // some code + await asyncMethod() + // error occurred + if (err) { + throw new Error('some errors occurred.') + } + return +}) +``` + The `onResponse` hook is executed when a response has been sent, so you will not be able to send more data to the client, however you can use this hook to send some data to an external service or elaborate some statistics. +### Manage Errors from a hook +If you get an error during the execution of your hook, just pass it to `done()` and Fastify will automatically close the request and send the appropriate error code to the user. + +```js +fastify.addHook('onRequest', (request, reply, done) => { + done(new Error('some error')) +}) +``` + +If you want to pass a custom error code to the user, just use `reply.code()`: +```js +fastify.addHook('preHandler', (request, reply, done) => { + reply.code(400) + done(new Error('some error')) +}) +``` + +*The error will be handled by [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md#errors).* + + ### Respond to a request from a hook If needed, you can respond to a request before you reach the route handler. An example could be an authentication hook. If you are using `onRequest` or `preHandler` use `reply.send`; if you are using a middleware, `res.end`. @@ -266,12 +253,13 @@ fastify.addHook('onRequest', (request, reply, done) => { You are able to hook into the application-lifecycle as well. It's important to note that these hooks aren't fully encapsulated. The `this` inside the hooks are encapsulated but the handlers can respond to an event outside the encapsulation boundaries. -- `'onClose'` -- `'onRoute'` -- `'onRegister'` +- [onClose](#onclose) +- [onRoute](#onroute) +- [onRegister](#onregister) -**'onClose'**
+ +### onClose Triggered when `fastify.close()` is invoked to stop the server. It is useful when [plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins.md) need a "shutdown" event, such as a connection to a database.
The first argument is the Fastify instance, the second one the `done` callback. ```js @@ -281,7 +269,7 @@ fastify.addHook('onClose', (instance, done) => { }) ``` -**'onRoute'**
+### onRoute Triggered when a new route is registered. Listeners are passed a `routeOptions` object as the sole parameter. The interface is synchronous, and, as such, the listeners do not get passed a callback. ```js fastify.addHook('onRoute', (routeOptions) => { @@ -295,7 +283,7 @@ fastify.addHook('onRoute', (routeOptions) => { }) ``` -**'onRegister'**
+### onRegister Triggered when a new plugin function is registered, and a new encapsulation context is created, the hook will be executed **before** the plugin code.
This hook can be useful if you are developing a plugin that needs to know when a plugin context is formed, and you want to operate in that specific context.
**Note:** This hook will not be called if a plugin is wrapped inside [`fastify-plugin`](https://github.com/fastify/fastify-plugin). @@ -339,9 +327,9 @@ Note: using an arrow function will break the binding of this to the Fastify inst ## Route level hooks -You can declare one or more custom `onRequest`, `preParsing`, `preValidation`, `preHandler` and `preSerialization` hook(s) that will be **unique** for the route. +You can declare one or more custom [onRequest](#onRequest), [preParsing](#preParsing), [preValidation](#preValidation), [preHandler](#preHandler) and [preSerialization](#preSerialization) hook(s) that will be **unique** for the route. If you do so, those hooks always be executed as last hook in their category.
-This can be useful if you need to run the authentication, and the `preParsing` or `preValidation` hooks are exactly what you need for doing that. +This can be useful if you need to run the authentication, and the [preParsing](#preParsing) or [preValidation](#preValidation) hooks are exactly what you need for doing that. Multiple route-level hooks can also be specified as an array. Let's make an example: From 0d2fdd701f9601bf0def9a018e06a371e821e8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D1=96=D0=B9=20=D0=9E=D1=80=D1=94?= =?UTF-8?q?=D1=85=D0=BE=D0=B2?= Date: Mon, 19 Aug 2019 16:00:32 +0300 Subject: [PATCH 069/144] Docs(typescript) and other typos (#1811) * docs(TypeScript): fix order of parameters according to type definitions https://github.com/andriyor/fastify/blob/124e7eea7af4048b008abf9c495bb581ec25061b/fastify.d.ts#L364 * docs: fix typos --- docs/Errors.md | 2 +- docs/Server.md | 2 +- docs/Serverless.md | 2 +- docs/TypeScript.md | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Errors.md b/docs/Errors.md index 4d4d701b18c..7cd61479591 100644 --- a/docs/Errors.md +++ b/docs/Errors.md @@ -79,7 +79,7 @@ The hook callback must be a function. #### FST_ERR_LOG_INVALID_DESTINATION -Logger acceptes either a `'stream'` or a `'file'` as the destination. +Logger accepts either a `'stream'` or a `'file'` as the destination. ### FST_ERR_REP_ALREADY_SENT diff --git a/docs/Server.md b/docs/Server.md index b081440fe37..81ad02d0e04 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -104,7 +104,7 @@ are not present on the object, they will be added accordingly: for incoming requests. The default function generates sequential identifiers. * `level`: the minimum logging level. If not set, it will be set to `'info'`. * `serializers`: a hash of serialization functions. By default, serializers - are added for `req` (incoming request objects), `res` (outgoing repsonse + are added for `req` (incoming request objects), `res` (outgoing response objets), and `err` (standard `Error` objects). When a log method receives an object with any of these properties then the respective serializer will be used for that property. For example: diff --git a/docs/Serverless.md b/docs/Serverless.md index 4e3098c72d3..fc8b9f52543 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -93,7 +93,7 @@ An example deployable with [claudia.js](https://claudiajs.com/tutorials/serverle ## Google Cloud Run -Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. It's primary purpose is to provide an infrastucture-abstracted environment to run arbitrary containers. As a result, Fastify can be deployed to Google Cloud Run with little-to-no code changes from the way you would write your Fastify app normally. +Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. It's primary purpose is to provide an infrastructure-abstracted environment to run arbitrary containers. As a result, Fastify can be deployed to Google Cloud Run with little-to-no code changes from the way you would write your Fastify app normally. *Follow the steps below to deploy to Google Cloud Run if you are already familiar with gcloud or just follow their [quickstart](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)*. diff --git a/docs/TypeScript.md b/docs/TypeScript.md index e176a029647..5fdd457e41b 100644 --- a/docs/TypeScript.md +++ b/docs/TypeScript.md @@ -7,7 +7,7 @@ Fastify is shipped with a typings file, but you may need to install `@types/node ## Types support We do care about the TypeScript community, and one of our core team members is currently reworking all types. We do our best to have the typings updated with the latest version of the API, but *it can happen* that the typings are not in sync.
-Luckly this is Open Source and you can contribute to fix them, we will be very happy to accept the fix and release it as soon as possible as a patch release. Checkout the [contributing](#contributing) rules! +Luckily this is Open Source and you can contribute to fix them, we will be very happy to accept the fix and release it as soon as possible as a patch release. Checkout the [contributing](#contributing) rules! Plugins may or may not include typings. See [Plugin Types](#plugin-types) for more information. @@ -106,7 +106,7 @@ const opts: fastify.RouteShorthandOptions = { } } -server.get('/ping/:bar', opts, (request, reply) => { +server.get('/ping/:bar', opts, (request, reply) => { console.log(request.query) // this is of type Query! console.log(request.params) // this is of type Params! console.log(request.body) // this is of type Body! @@ -204,7 +204,7 @@ When updating core types you should make a PR to this repository. Ensure you: ### Plugin Types -Plugins maintained by and orginized under the fastify organization on GitHub should ship with typings just like fastify itself does. +Plugins maintained by and organized under the fastify organization on GitHub should ship with typings just like fastify itself does. Some plugins already include typings but many do not. We are happy to accept contributions to those plugins without any typings, see [fastify-cors](https://github.com/fastify/fastify-cors) for an example of a plugin that comes with it's own typings. Typings for third-party-plugins may either be included with the plugin or hosted on DefinitelyTyped. Remember, if you author a plugin to either include typings or publish them on DefinitelyTyped! Information of how to install typings from DefinitelyTyped can be found [here](https://github.com/DefinitelyTyped/DefinitelyTyped#npm). From 639298b38ae2e16ee3c6e951614145a892964412 Mon Sep 17 00:00:00 2001 From: Zoron Date: Mon, 19 Aug 2019 21:01:44 +0800 Subject: [PATCH 070/144] docs(Hooks): (#1810) 1. move L7 to L33, for it is a description for Request/Reply hooks. 2. change the number of Request/Reply hooks from seven to eight. 3. add a description for different types of hooks in L7. --- docs/Hooks.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 410f9debb60..bddcf3bb977 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -4,7 +4,7 @@ Hooks are registered with the `fastify.addHook` method and allow you to listen to specific events in the application or request/response lifecycle. You have to register a hook before the event is triggered otherwise the event is lost. -By using the hooks you can interact directly inside the lifecycle of Fastify. There are seven different Hooks that you can use *(in order of execution)*: +By using the hooks you can interact directly inside the lifecycle of Fastify. There are Request/Reply hooks and application hooks: - [Request/Reply Hooks](#requestreply-hooks) - [onRequest](#onRequest) @@ -30,6 +30,8 @@ By using the hooks you can interact directly inside the lifecycle of Fastify. Th It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md).
Hooks are affected by Fastify's encapsulation, and can thus be applied to selected routes. See the [Scopes](#scope) section for more information. +There are eight different hooks that you can use in Request/Reply *(in order of execution)*: + ### onRequest ```js fastify.addHook('onRequest', (request, reply, done) => { From 0d6055d3e07b726c9bfbc779550adf78dab967f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20F=C3=A4cke?= Date: Wed, 21 Aug 2019 23:29:19 +0200 Subject: [PATCH 071/144] Update standard (#1816) * chore(package): update standard to version 14.0.0 * fix linting errors for standard@14 --- fastify.js | 6 ++--- lib/errors.js | 36 +++++++++++++------------- lib/route.js | 2 +- lib/schemas.js | 6 ++--- lib/wrapThenable.js | 2 +- package.json | 2 +- test/custom-parser.test.js | 2 +- test/custom-querystring-parser.test.js | 2 +- test/emit-warning.test.js | 6 ++--- test/helper.js | 14 +++++----- test/hooks-async.js | 4 +-- test/internals/errors.test.js | 2 +- test/internals/initialConfig.test.js | 4 +-- test/internals/plugin.test.js | 8 +++--- test/internals/reply.test.js | 4 +-- test/shared-schemas.test.js | 4 +-- test/throw.test.js | 4 +-- test/validation-error-handling.test.js | 6 ++--- test/versioned-routes.test.js | 2 +- 19 files changed, 58 insertions(+), 58 deletions(-) diff --git a/fastify.js b/fastify.js index 10229cfed81..04d69ba77d5 100644 --- a/fastify.js +++ b/fastify.js @@ -55,7 +55,7 @@ function build (options) { validateBodyLimitOption(options.bodyLimit) if (options.logger && options.logger.genReqId) { - process.emitWarning(`Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id`) + process.emitWarning("Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id") options.genReqId = options.logger.genReqId } @@ -332,11 +332,11 @@ function build (options) { // TODO: v3 instead of log a warning, throw an error if (name === 'onSend' || name === 'preSerialization') { if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) { - fastify.log.warn(`Async function has too many arguments. Async hooks should not use the 'next' argument.`, new Error().stack) + fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack) } } else { if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) { - fastify.log.warn(`Async function has too many arguments. Async hooks should not use the 'next' argument.`, new Error().stack) + fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack) } } diff --git a/lib/errors.js b/lib/errors.js index 21ce5805a81..d83eea65d10 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -7,42 +7,42 @@ const codes = {} * Basic */ -createError('FST_ERR_NOT_FOUND', `Not Found`, 404) +createError('FST_ERR_NOT_FOUND', 'Not Found', 404) /** * ContentTypeParser */ -createError('FST_ERR_CTP_ALREADY_PRESENT', `Content type parser '%s' already present.`) +createError('FST_ERR_CTP_ALREADY_PRESENT', "Content type parser '%s' already present.") createError('FST_ERR_CTP_INVALID_TYPE', 'The content type should be a string', 500, TypeError) createError('FST_ERR_CTP_EMPTY_TYPE', 'The content type cannot be an empty string', 500, TypeError) createError('FST_ERR_CTP_INVALID_HANDLER', 'The content type handler should be a function', 500, TypeError) -createError('FST_ERR_CTP_INVALID_PARSE_TYPE', `The body parser can only parse your data as 'string' or 'buffer', you asked '%s' which is not supported.`, 500, TypeError) +createError('FST_ERR_CTP_INVALID_PARSE_TYPE', "The body parser can only parse your data as 'string' or 'buffer', you asked '%s' which is not supported.", 500, TypeError) createError('FST_ERR_CTP_BODY_TOO_LARGE', 'Request body is too large', 413, RangeError) -createError('FST_ERR_CTP_INVALID_MEDIA_TYPE', `Unsupported Media Type: %s`, 415) +createError('FST_ERR_CTP_INVALID_MEDIA_TYPE', 'Unsupported Media Type: %s', 415) createError('FST_ERR_CTP_INVALID_CONTENT_LENGTH', 'Request body size did not match Content-Length', 400, RangeError) -createError('FST_ERR_CTP_EMPTY_JSON_BODY', `Body cannot be empty when content-type is set to 'application/json'`, 400) +createError('FST_ERR_CTP_EMPTY_JSON_BODY', "Body cannot be empty when content-type is set to 'application/json'", 400) /** * decorate */ -createError('FST_ERR_DEC_ALREADY_PRESENT', `The decorator '%s' has already been added!`) -createError('FST_ERR_DEC_MISSING_DEPENDENCY', `The decorator is missing dependency '%s'.`) +createError('FST_ERR_DEC_ALREADY_PRESENT', "The decorator '%s' has already been added!") +createError('FST_ERR_DEC_MISSING_DEPENDENCY', "The decorator is missing dependency '%s'.") /** * hooks */ -createError('FST_ERR_HOOK_INVALID_TYPE', `The hook name must be a string`, 500, TypeError) -createError('FST_ERR_HOOK_INVALID_HANDLER', `The hook callback must be a function`, 500, TypeError) +createError('FST_ERR_HOOK_INVALID_TYPE', 'The hook name must be a string', 500, TypeError) +createError('FST_ERR_HOOK_INVALID_HANDLER', 'The hook callback must be a function', 500, TypeError) /** * logger */ -createError('FST_ERR_LOG_INVALID_DESTINATION', `Cannot specify both logger.stream and logger.file options`) +createError('FST_ERR_LOG_INVALID_DESTINATION', 'Cannot specify both logger.stream and logger.file options') /** * reply */ -createError('FST_ERR_REP_INVALID_PAYLOAD_TYPE', `Attempted to send payload of invalid type '%s'. Expected a string or Buffer.`, 500, TypeError) +createError('FST_ERR_REP_INVALID_PAYLOAD_TYPE', "Attempted to send payload of invalid type '%s'. Expected a string or Buffer.", 500, TypeError) createError('FST_ERR_REP_ALREADY_SENT', 'Reply was already sent.') createError('FST_ERR_REP_SENT_VALUE', 'The only possible value for reply.sent is true.') createError('FST_ERR_SEND_INSIDE_ONERR', 'You cannot use `send` inside the `onError` hook') @@ -50,25 +50,25 @@ createError('FST_ERR_SEND_INSIDE_ONERR', 'You cannot use `send` inside the `onEr /** * schemas */ -createError('FST_ERR_SCH_MISSING_ID', `Missing schema $id property`) -createError('FST_ERR_SCH_ALREADY_PRESENT', `Schema with id '%s' already declared!`) -createError('FST_ERR_SCH_NOT_PRESENT', `Schema with id '%s' does not exist!`) -createError('FST_ERR_SCH_DUPLICATE', `Schema with '%s' already present!`) +createError('FST_ERR_SCH_MISSING_ID', 'Missing schema $id property') +createError('FST_ERR_SCH_ALREADY_PRESENT', "Schema with id '%s' already declared!") +createError('FST_ERR_SCH_NOT_PRESENT', "Schema with id '%s' does not exist!") +createError('FST_ERR_SCH_DUPLICATE', "Schema with '%s' already present!") /** * wrapThenable */ -createError('FST_ERR_PROMISE_NOT_FULLFILLED', `Promise may not be fulfilled with 'undefined' when statusCode is not 204`) +createError('FST_ERR_PROMISE_NOT_FULLFILLED', "Promise may not be fulfilled with 'undefined' when statusCode is not 204") /** * http2 */ -createError('FST_ERR_HTTP2_INVALID_VERSION', `HTTP2 is available only from node >= 8.8.1`) +createError('FST_ERR_HTTP2_INVALID_VERSION', 'HTTP2 is available only from node >= 8.8.1') /** * initialConfig */ -createError('FST_ERR_INIT_OPTS_INVALID', `Invalid initialization options: '%s'`) +createError('FST_ERR_INIT_OPTS_INVALID', "Invalid initialization options: '%s'") function createError (code, message, statusCode = 500, Base = Error) { if (!code) throw new Error('Fastify error code must not be empty') diff --git a/lib/route.js b/lib/route.js index 838053707ca..6b95ff5eb59 100644 --- a/lib/route.js +++ b/lib/route.js @@ -287,7 +287,7 @@ function buildRouting (options) { req.id = req.headers[requestIdHeader] || genReqId(req) req.originalUrl = req.url - var hostname = req.headers['host'] + var hostname = req.headers.host var ip = req.connection.remoteAddress var ips diff --git a/lib/schemas.js b/lib/schemas.js index e88ee1d66d0..d64b2d9cbda 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -23,7 +23,7 @@ Schemas.prototype.add = function (inputSchema) { ? inputSchema.valueOf() : inputSchema ) - const id = schema['$id'] + const id = schema.$id if (id === undefined) { throw new FST_ERR_SCH_MISSING_ID() } @@ -142,8 +142,8 @@ Schemas.prototype.getJsonSchemas = function (options) { const store = this.getSchemas() const schemasArray = Object.keys(store).map(schemaKey => { // if the shared-schema "replace-way" has been used, the $id field has been removed - if (store[schemaKey]['$id'] === undefined) { - store[schemaKey]['$id'] = schemaKey + if (store[schemaKey].$id === undefined) { + store[schemaKey].$id = schemaKey } return store[schemaKey] }) diff --git a/lib/wrapThenable.js b/lib/wrapThenable.js index 42cbb82db83..0cd05f19ea9 100644 --- a/lib/wrapThenable.js +++ b/lib/wrapThenable.js @@ -31,7 +31,7 @@ function wrapThenable (thenable, reply) { reply.send(err) } } else if (reply[kReplySent] === false) { - reply.log.error({ err: new FST_ERR_PROMISE_NOT_FULLFILLED() }, `Promise may not be fulfilled with 'undefined' when statusCode is not 204`) + reply.log.error({ err: new FST_ERR_PROMISE_NOT_FULLFILLED() }, "Promise may not be fulfilled with 'undefined' when statusCode is not 204") } }, function (err) { if (reply[kReplySentOverwritten] === true) { diff --git a/package.json b/package.json index 95c5c00bf47..db96e707475 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "simple-get": "^3.0.3", "snazzy": "^8.0.0", "split2": "^3.1.0", - "standard": "^13.0.1", + "standard": "^14.0.0", "tap": "^12.5.2", "tap-mocha-reporter": "^3.0.7", "then-sleep": "^1.0.1", diff --git a/test/custom-parser.test.js b/test/custom-parser.test.js index e56aa935918..60df454cebe 100644 --- a/test/custom-parser.test.js +++ b/test/custom-parser.test.js @@ -1060,7 +1060,7 @@ test('Wrong parseAs parameter', t => { fastify.addContentTypeParser('application/json', { parseAs: 'fireworks' }, () => {}) t.fail('should throw') } catch (err) { - t.is(err.message, `FST_ERR_CTP_INVALID_PARSE_TYPE: The body parser can only parse your data as 'string' or 'buffer', you asked 'fireworks' which is not supported.`) + t.is(err.message, "FST_ERR_CTP_INVALID_PARSE_TYPE: The body parser can only parse your data as 'string' or 'buffer', you asked 'fireworks' which is not supported.") } }) diff --git a/test/custom-querystring-parser.test.js b/test/custom-querystring-parser.test.js index a10e1822c89..9cf21ed5b5c 100644 --- a/test/custom-querystring-parser.test.js +++ b/test/custom-querystring-parser.test.js @@ -131,7 +131,7 @@ test('Custom querystring parser should be a function', t => { } catch (err) { t.strictEqual( err.message, - `querystringParser option should be a function, instead got 'number'` + "querystringParser option should be a function, instead got 'number'" ) } }) diff --git a/test/emit-warning.test.js b/test/emit-warning.test.js index 352ccdacfba..000dff7c86e 100644 --- a/test/emit-warning.test.js +++ b/test/emit-warning.test.js @@ -7,7 +7,7 @@ test('should emit warning using genReqId prop in logger options', t => { t.plan(1) process.once('warning', warning => { - t.strictEqual(warning.message, `Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id`) + t.strictEqual(warning.message, "Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id") }) Fastify({ logger: { genReqId: 'test' } }) @@ -17,7 +17,7 @@ test('should emit warning if basePath prop is used', t => { t.plan(1) process.once('warning', warning => { - t.strictEqual(warning.message, `basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix`) + t.strictEqual(warning.message, 'basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix') }) const fastify = Fastify({ basePath: '/test' }) @@ -28,7 +28,7 @@ test('should emit warning if preHandler is used', t => { t.plan(1) process.once('warning', warning => { - t.strictEqual(warning.message, `The route option \`beforeHandler\` has been deprecated, use \`preHandler\` instead`) + t.strictEqual(warning.message, 'The route option `beforeHandler` has been deprecated, use `preHandler` instead') }) const fastify = Fastify() diff --git a/test/helper.js b/test/helper.js index 23e1c927edf..cc5e8b9b797 100644 --- a/test/helper.js +++ b/test/helper.js @@ -221,7 +221,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { }) if (loMethod === 'options') { - test(`OPTIONS returns 415 - should return 415 if Content-Type is not json or plain text`, t => { + test('OPTIONS returns 415 - should return 415 if Content-Type is not json or plain text', t => { t.plan(2) sget({ method: upMethod, @@ -328,7 +328,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) @@ -344,7 +344,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) @@ -361,7 +361,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) @@ -378,7 +378,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) @@ -395,7 +395,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) @@ -412,7 +412,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: `FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'`, + message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", statusCode: 400 }) }) diff --git a/test/hooks-async.js b/test/hooks-async.js index 61aade9f9b7..df7201d845c 100644 --- a/test/hooks-async.js +++ b/test/hooks-async.js @@ -520,7 +520,7 @@ function asyncHookTest (t) { stream.on('data', line => { t.strictEqual(line.level, 40) - t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) + t.true(line.msg.startsWith("Async function has too many arguments. Async hooks should not use the 'next' argument.")) t.true(/test(\\|\/)hooks-async\.js/.test(line.msg)) }) @@ -536,7 +536,7 @@ function asyncHookTest (t) { stream.on('data', line => { t.strictEqual(line.level, 40) - t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) + t.true(line.msg.startsWith("Async function has too many arguments. Async hooks should not use the 'next' argument.")) t.true(/test(\\|\/)hooks-async\.js/.test(line.msg)) }) diff --git a/test/internals/errors.test.js b/test/internals/errors.test.js index 57654585795..b749b6f4abd 100644 --- a/test/internals/errors.test.js +++ b/test/internals/errors.test.js @@ -78,7 +78,7 @@ test('Should throw when error code has no message', t => { try { createError('code') } catch (err) { - t.equal(err.message, `Fastify error message must not be empty`) + t.equal(err.message, 'Fastify error message must not be empty') } }) diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index c0d648f1341..674ff4764df 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -55,7 +55,7 @@ test('Fastify.initialConfig should expose all options', t => { } }, deriveVersion: (req, ctx) => { - return req.headers['accept'] + return req.headers.accept } } @@ -153,7 +153,7 @@ test('Return an error if options do not match the validation schema', t => { } catch (error) { t.type(error, Error) t.equal(error.name, 'FastifyError [FST_ERR_INIT_OPTS_INVALID]') - t.equal(error.message, `FST_ERR_INIT_OPTS_INVALID: Invalid initialization options: '["should be boolean"]'`) + t.equal(error.message, 'FST_ERR_INIT_OPTS_INVALID: Invalid initialization options: \'["should be boolean"]\'') t.equal(error.code, 'FST_ERR_INIT_OPTS_INVALID') t.ok(error.stack) t.pass() diff --git a/test/internals/plugin.test.js b/test/internals/plugin.test.js index 02a7a9441e7..1c7012f9401 100644 --- a/test/internals/plugin.test.js +++ b/test/internals/plugin.test.js @@ -7,7 +7,7 @@ const pluginUtilsPublic = require('../../lib/pluginUtils.js') const pluginUtils = require('../../lib/pluginUtils')[Symbol.for('internals')] const symbols = require('../../lib/symbols.js') -test(`shouldSkipOverride should check the 'skip-override' symbol`, t => { +test("shouldSkipOverride should check the 'skip-override' symbol", t => { t.plan(2) yes[Symbol.for('skip-override')] = true @@ -19,7 +19,7 @@ test(`shouldSkipOverride should check the 'skip-override' symbol`, t => { function no () {} }) -test(`getMeta should return the object stored with the 'plugin-meta' symbol`, t => { +test("getMeta should return the object stored with the 'plugin-meta' symbol", t => { t.plan(1) const meta = { hello: 'world' } @@ -76,7 +76,7 @@ test('checkDecorators should check if the given decorator is present in the inst pluginUtils.checkDecorators.call(context, fn) t.fail('should throw') } catch (err) { - t.is(err.message, `The decorator 'plugin' is not present in Request`) + t.is(err.message, "The decorator 'plugin' is not present in Request") } function fn () {} @@ -117,7 +117,7 @@ test('checkDependencies should check if the given dependency is present in the i pluginUtils.checkDependencies.call(context, fn) t.fail('should throw') } catch (err) { - t.is(err.message, `The dependency 'plugin' of plugin 'test-plugin' is not registered`) + t.is(err.message, "The dependency 'plugin' of plugin 'test-plugin' is not registered") } function fn () {} diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index f4cc9726c8c..e95295c92cf 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -262,7 +262,7 @@ test('within an instance', t => { http.get(url, (response) => { t.strictEqual(response.headers['x-onsend'], 'yes') t.strictEqual(response.headers['content-length'], '0') - t.strictEqual(response.headers['location'], '/') + t.strictEqual(response.headers.location, '/') }) }) @@ -419,7 +419,7 @@ test('stream using reply.res.writeHead should return customize headers', t => { url: 'http://localhost:' + fastify.server.address().port }, (err, response, body) => { t.error(err) - t.strictEqual(response.headers['location'], '/') + t.strictEqual(response.headers.location, '/') t.strictEqual(response.headers['Content-Type'], undefined) t.deepEqual(body, buf) }) diff --git a/test/shared-schemas.test.js b/test/shared-schemas.test.js index 9a0f07740ac..ac06a60d631 100644 --- a/test/shared-schemas.test.js +++ b/test/shared-schemas.test.js @@ -701,7 +701,7 @@ test('The schema resolver should clean the $id key before passing it to the comp url: '/', method: 'GET', schema: { - description: `get`, + description: 'get', body: 'second#', response: { 200: 'second#' @@ -716,7 +716,7 @@ test('The schema resolver should clean the $id key before passing it to the comp url: '/', method: 'PATCH', schema: { - description: `patch`, + description: 'patch', body: 'first#', response: { 200: 'first#' diff --git a/test/throw.test.js b/test/throw.test.js index fc0c683265c..9736be7dfd3 100644 --- a/test/throw.test.js +++ b/test/throw.test.js @@ -131,7 +131,7 @@ test('Should throw on duplicate request decorator', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_ALREADY_PRESENT') - t.is(e.message, `FST_ERR_DEC_ALREADY_PRESENT: The decorator 'foo' has already been added!`) + t.is(e.message, "FST_ERR_DEC_ALREADY_PRESENT: The decorator 'foo' has already been added!") } }) @@ -146,7 +146,7 @@ test('Should throw if request decorator dependencies are not met', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') - t.is(e.message, `FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency 'world'.`) + t.is(e.message, "FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency 'world'.") } }) diff --git a/test/validation-error-handling.test.js b/test/validation-error-handling.test.js index 6b26e772e4b..d7d2f59748d 100644 --- a/test/validation-error-handling.test.js +++ b/test/validation-error-handling.test.js @@ -278,7 +278,7 @@ test('should return a defined output message parsing AJV errors', t => { url: '/' }, (err, res) => { t.error(err) - t.strictEqual(res.payload, `{"statusCode":400,"error":"Bad Request","message":"body should have required property 'name', body should have required property 'work'"}`) + t.strictEqual(res.payload, '{"statusCode":400,"error":"Bad Request","message":"body should have required property \'name\', body should have required property \'work\'"}') }) }) @@ -306,7 +306,7 @@ test('should return a defined output message parsing JOI errors', t => { url: '/' }, (err, res) => { t.error(err) - t.strictEqual(res.payload, `{"statusCode":400,"error":"Bad Request","message":"child \\"name\\" fails because [\\"name\\" is required]"}`) + t.strictEqual(res.payload, '{"statusCode":400,"error":"Bad Request","message":"child \\"name\\" fails because [\\"name\\" is required]"}') }) }) @@ -337,6 +337,6 @@ test('should return a defined output message parsing JOI error details', t => { url: '/' }, (err, res) => { t.error(err) - t.strictEqual(res.payload, `{"statusCode":400,"error":"Bad Request","message":"body \\"name\\" is required"}`) + t.strictEqual(res.payload, '{"statusCode":400,"error":"Bad Request","message":"body \\"name\\" is required"}') }) }) diff --git a/test/versioned-routes.test.js b/test/versioned-routes.test.js index e7d40480d6f..6a3f56283fa 100644 --- a/test/versioned-routes.test.js +++ b/test/versioned-routes.test.js @@ -417,7 +417,7 @@ test('Should register a versioned route with custome versioning strategy', t => } }, deriveVersion: (req, ctx) => { - return req.headers['accept'] + return req.headers.accept } } From d48c6a21f21d72b7683808aa88b3fdcd2e0478db Mon Sep 17 00:00:00 2001 From: Marco Ferraioli Date: Fri, 23 Aug 2019 14:14:51 +0200 Subject: [PATCH 072/144] add plugin for autogenerate crud route in Ecosystem (#1813) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 5a10786de10..f2509224314 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -51,6 +51,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-405`](https://github.com/Eomm/fastify-405) Fastify plugin that adds 405 HTTP status to your routes - [`fastify-amqp`](https://github.com/RafaelGSS/fastify-amqp) Fastify AMQP connection plugin, to use with RabbitMQ or another connector. Just a wrapper to [`amqplib`](https://github.com/squaremo/amqp.node). - [`fastify-angular-universal`](https://github.com/exequiel09/fastify-angular-universal) Angular server-side rendering support using [`@angular/platform-server`](https://github.com/angular/angular/tree/master/packages/platform-server) for Fastify +- [`fastify-autocrud`](https://github.com/paranoiasystem/fastify-autocrud) Plugin for autogenerate CRUD routes as fast as possible. - [`fastify-babel`](https://github.com/cfware/fastify-babel) Fastify plugin for development servers which require babel transformations of JavaScript sources. - [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your routes to the console, so you definitely know which endpoints are available. - [`fastify-bookshelf`](https://github.com/butlerx/fastify-bookshelfjs) Fastify plugin to add [bookshelf.js](http://bookshelfjs.org/) orm support. From 05c0329cce02d3124e868d13c4072b9c6c9981ad Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2019 11:40:38 +0200 Subject: [PATCH 073/144] chore(package): update @typescript-eslint/parser to version 2.0.0 (#1799) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db96e707475..cec433fc1bd 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "devDependencies": { "@types/node": "^11.13.18", "@typescript-eslint/eslint-plugin": "^1.13.0", - "@typescript-eslint/parser": "^1.13.0", + "@typescript-eslint/parser": "^2.0.0", "JSONStream": "^1.3.5", "ajv-pack": "^0.3.1", "autocannon": "^3.2.0", From 76c8879a07aa221f98aa9df89cfdac7ad844c521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20F=C3=A4cke?= Date: Wed, 28 Aug 2019 15:05:59 +0200 Subject: [PATCH 074/144] Improve default 404 route (#1826) --- lib/fourOhFour.js | 20 ++++++++++---------- test/404s.test.js | 2 +- test/logger.test.js | 6 +++++- test/route-prefix.test.js | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/fourOhFour.js b/lib/fourOhFour.js index 430663f6501..83f78563d84 100644 --- a/lib/fourOhFour.js +++ b/lib/fourOhFour.js @@ -34,14 +34,7 @@ function fourOhFour (options) { // 404 router, used for handling encapsulated 404 handlers const router = FindMyWay({ defaultRoute: fourOhFourFallBack }) - const fof = { - router, - setNotFoundHandler: setNotFoundHandler, - setContext: setContext, - arrange404: arrange404 - } - - return fof + return { router, setNotFoundHandler, setContext, arrange404 } function arrange404 (instance) { // Change the pointer of the fastify instance to itself, so register + prefix can add new 404 handler @@ -49,8 +42,15 @@ function fourOhFour (options) { instance[kCanSetNotFoundHandler] = true } - function basic404 (req, reply) { - reply.code(404).send(new Error('Not Found')) + function basic404 (request, reply) { + const { url, method } = request.raw + const message = `Route ${method}:${url} not found` + request.log.info(message) + reply.code(404).send({ + message, + error: 'Not Found', + statusCode: 404 + }) } function setContext (instance, context) { diff --git a/test/404s.test.js b/test/404s.test.js index 3a304e92ef7..fb96b8cc24f 100644 --- a/test/404s.test.js +++ b/test/404s.test.js @@ -979,7 +979,7 @@ test('log debug for 404', t => { const INFO_LEVEL = 30 t.strictEqual(JSON.parse(logStream.logs[0]).msg, 'incoming request') - t.strictEqual(JSON.parse(logStream.logs[1]).msg, 'Not Found') + t.strictEqual(JSON.parse(logStream.logs[1]).msg, 'Route GET:/not-found not found') t.strictEqual(JSON.parse(logStream.logs[1]).level, INFO_LEVEL) t.strictEqual(JSON.parse(logStream.logs[2]).msg, 'request completed') t.strictEqual(logStream.logs.length, 3) diff --git a/test/logger.test.js b/test/logger.test.js index b37ed63dd2f..1253894ebec 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -783,7 +783,11 @@ test('Should set a custom log level for a specific route', t => { test('The default 404 handler logs the incoming request', t => { t.plan(5) - const expectedMessages = ['incoming request', 'Not Found', 'request completed'] + const expectedMessages = [ + 'incoming request', + 'Route GET:/not-found not found', + 'request completed' + ] const splitStream = split(JSON.parse) splitStream.on('data', (line) => { diff --git a/test/route-prefix.test.js b/test/route-prefix.test.js index faa16392528..fb7d0c54a68 100644 --- a/test/route-prefix.test.js +++ b/test/route-prefix.test.js @@ -484,7 +484,7 @@ test('returns 404 status code with /prefix/ and / route - prefixTrailingSlash: " t.error(err) t.same(JSON.parse(res.payload), { error: 'Not Found', - message: 'Not Found', + message: 'Route GET:/prefix// not found', statusCode: 404 }) }) From 5f3ae9737f2c91d1b675a972b3c573b8b6ae7293 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Thu, 29 Aug 2019 18:51:50 +0200 Subject: [PATCH 075/144] Schema build error (#1808) * add route when build schema fails * remove file * adding tests * bubble up FastifyError * prefix test * removed file --- docs/Errors.md | 5 +++++ lib/errors.js | 1 + lib/route.js | 9 ++++++++- test/throw.test.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/Errors.md b/docs/Errors.md index 7cd61479591..e0038307203 100644 --- a/docs/Errors.md +++ b/docs/Errors.md @@ -111,6 +111,11 @@ A schema with the same `$id` already exists. No schema with the provided `$id` exists. + +#### FST_ERR_SCH_BUILD + +The JSON schema provided to one route is not valid. + #### FST_ERR_PROMISE_NOT_FULLFILLED diff --git a/lib/errors.js b/lib/errors.js index d83eea65d10..8f0274f0527 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -54,6 +54,7 @@ createError('FST_ERR_SCH_MISSING_ID', 'Missing schema $id property') createError('FST_ERR_SCH_ALREADY_PRESENT', "Schema with id '%s' already declared!") createError('FST_ERR_SCH_NOT_PRESENT', "Schema with id '%s' does not exist!") createError('FST_ERR_SCH_DUPLICATE', "Schema with '%s' already present!") +createError('FST_ERR_SCH_BUILD', 'Failed building the schema for %s: %s, due error %s') /** * wrapThenable diff --git a/lib/route.js b/lib/route.js index 6b95ff5eb59..f1bd5128ae5 100644 --- a/lib/route.js +++ b/lib/route.js @@ -11,6 +11,12 @@ const buildSchema = validation.build const { buildSchemaCompiler } = validation const { beforeHandlerWarning } = require('./warnings') +const { + codes: { + FST_ERR_SCH_BUILD + } +} = require('./errors') + const { kRoutePrefix, kLogLevel, @@ -214,7 +220,8 @@ function buildRouting (options) { buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas]) } catch (error) { - done(error) + // bubble up the FastifyError instance + done(error.code ? error : new FST_ERR_SCH_BUILD(opts.method, url, error.message)) return } } diff --git a/test/throw.test.js b/test/throw.test.js index 9736be7dfd3..0d7e01f3136 100644 --- a/test/throw.test.js +++ b/test/throw.test.js @@ -25,6 +25,52 @@ test('Fastify should throw on multiple assignment to the same route', t => { }) }) +test('Fastify should throw for an invalid schema, printing the error route - headers', t => { + t.plan(2) + + const badSchema = { + type: 'object', + properties: { + bad: { + type: 'bad-type' + } + } + } + + const fastify = Fastify() + fastify.get('/', { schema: { headers: badSchema } }, () => {}) + fastify.get('/not-loaded', { schema: { headers: badSchema } }, () => {}) + + fastify.ready(err => { + t.is(err.code, 'FST_ERR_SCH_BUILD') + t.isLike(err.message, /Failed building the schema for GET: \//) + }) +}) + +test('Fastify should throw for an invalid schema, printing the error route - body', t => { + t.plan(2) + + const badSchema = { + type: 'object', + properties: { + bad: { + type: 'bad-type' + } + } + } + + const fastify = Fastify() + fastify.register((instance, opts, next) => { + instance.post('/form', { schema: { body: badSchema } }, () => {}) + next() + }, { prefix: 'hello' }) + + fastify.ready(err => { + t.is(err.code, 'FST_ERR_SCH_BUILD') + t.isLike(err.message, /Failed building the schema for POST: \/hello\/form/) + }) +}) + test('Should throw on unsupported method', t => { t.plan(1) const fastify = Fastify() From 936b5090c39467cbebf4b212f62917e81e61e086 Mon Sep 17 00:00:00 2001 From: Jeka Kiselyov Date: Thu, 29 Aug 2019 19:52:08 +0300 Subject: [PATCH 076/144] Update Ecosystem.md (#1827) Plugin to expose REST API for Mongoose MongoDB connection. --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index f2509224314..818c285a80f 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -88,6 +88,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-markdown`](https://github.com/freezestudio/fastify-markdown) Plugin to markdown support. - [`fastify-metrics`](https://gitlab.com/m03geek/fastify-metrics) Plugin for exporting [Prometheus](https://prometheus.io) metrics. - [`fastify-mongo-memory`](https://github.com/chapuletta/fastify-mongo-memory) Fastify MongoDB in Memory Plugin for testing support. +- [`fastify-mongoose-api`](https://github.com/jeka-kiselyov/fastify-mongoose-api) Fastify plugin to create REST API methods based on Mongoose MongoDB models. - [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose) Fastify Mongoose plugin that connects to a MongoDB via the Mongoose plugin with support for Models. - [`fastify-multer`](https://github.com/fox1t/multer) Multer is a plugin for handling multipart/form-data, which is primarily used for uploading files. - [`fastify-nats`](https://github.com/mahmed8003/fastify-nats) Plugin to share [NATS](http://nats.io) client across Fastify. From 804256e74f5e7b8bc5b4c0b13738dcf343579b12 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 30 Aug 2019 11:59:59 +0200 Subject: [PATCH 077/144] fix: premature close test (#1833) There was an incorrect test count on the premature close tests where it never actually tested whether 'close' was emitted. `'close'` is actually not emitted and to fix these test we need to increment the test count and instead listen for `'aborted'`. --- test/stream.test.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/stream.test.js b/test/stream.test.js index 17365e116b1..1574e5abbd4 100644 --- a/test/stream.test.js +++ b/test/stream.test.js @@ -140,7 +140,7 @@ test('onSend hook stream', t => { }) test('Destroying streams prematurely', t => { - t.plan(5) + t.plan(6) let fastify = null const logStream = split(JSON.parse) @@ -190,7 +190,9 @@ test('Destroying streams prematurely', t => { response.on('readable', function () { response.destroy() }) - response.on('close', function () { + + // Node bug? Node never emits 'close' here. + response.on('aborted', function () { t.pass('Response closed') }) }) @@ -198,7 +200,7 @@ test('Destroying streams prematurely', t => { }) test('Destroying streams prematurely should call close method', t => { - t.plan(6) + t.plan(7) let fastify = null const logStream = split(JSON.parse) @@ -249,7 +251,8 @@ test('Destroying streams prematurely should call close method', t => { response.on('readable', function () { response.destroy() }) - response.on('close', function () { + // Node bug? Node never emits 'close' here. + response.on('aborted', function () { t.pass('Response closed') }) }) @@ -257,7 +260,7 @@ test('Destroying streams prematurely should call close method', t => { }) test('Destroying streams prematurely should call abort method', t => { - t.plan(6) + t.plan(7) let fastify = null const logStream = split(JSON.parse) @@ -309,7 +312,8 @@ test('Destroying streams prematurely should call abort method', t => { response.on('readable', function () { response.destroy() }) - response.on('close', function () { + // Node bug? Node never emits 'close' here. + response.on('aborted', function () { t.pass('Response closed') }) }) From db66ea32b126f1fb9d34c2acb5d5c894bddb0525 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 30 Aug 2019 21:34:52 +0200 Subject: [PATCH 078/144] Added Zeit Now docs (#1824) --- docs/Serverless.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 +++++------ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/docs/Serverless.md b/docs/Serverless.md index fc8b9f52543..8f96ea633e3 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -6,6 +6,7 @@ Run serverless applications and REST APIs using your existing Fastify applicatio - [AWS Lambda](#aws-lambda) - [Google Cloud Run](#google-cloud-run) +- [Zeit Now](#zeit-now) ### Attention Readers: > Fastify is not designed to run on serverless environments. @@ -190,3 +191,74 @@ gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed ``` Your app will be accessible from the URL GCP provides. + +## Zeit Now + +[now](https://zeit.co/home) provides zero configuration deployment for +Node.js applications. In order to use now, it is as simple as +configuring your `now.json` file like the following: + +```json +{ + "version": 2, + "builds": [ + { + "src": "api/serverless.js", + "use": "@now/node", + "config": { + "helpers": false + } + } + ], + "routes": [ + { "src": "/.*", "dest": "/api/serverless.js"} + ] +} +``` + +Then, write a `api/serverless.js` like so: + +```js +'use strict' + +const build = require('./index') + +const app = build() + +module.exports = async function (req, res) { + await app.ready() + app.server.emit('request', req, res) +} +``` + +And a `api/index.js` file: + +```js +'use strict' + +const fastify = require('fastify') + +function build () { + const app = fastify({ + logger: true + }) + + app.get('/', async (req, res) => { + const { name = 'World' } = req.query + req.log.info({ name }, 'hello world!') + return `Hello ${name}!` + }) + + return app +} + +module.exports = build +``` + +Note that you'll need to use Node 10 by setting it in `package.json`: + +```js + "engines": { + "node": "10.x" + }, +``` diff --git a/package.json b/package.json index cec433fc1bd..b9d7ba830bd 100644 --- a/package.json +++ b/package.json @@ -88,16 +88,16 @@ "node": ">=6" }, "devDependencies": { - "@types/node": "^11.13.18", + "@types/node": "^11.13.19", "@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/parser": "^2.0.0", "JSONStream": "^1.3.5", "ajv-pack": "^0.3.1", "autocannon": "^3.2.0", "branch-comparer": "^0.4.0", - "concurrently": "^4.1.1", + "concurrently": "^4.1.2", "cors": "^2.8.5", - "coveralls": "^3.0.5", + "coveralls": "^3.0.6", "dns-prefetch-control": "^0.2.0", "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", @@ -113,9 +113,9 @@ "ienoopen": "^1.0.0", "joi": "^12.0.0", "license-checker": "^25.0.1", - "lolex": "^4.0.1", + "lolex": "^4.2.0", "pre-commit": "^1.2.2", - "proxyquire": "^2.1.1", + "proxyquire": "^2.1.3", "pump": "^3.0.0", "semver": "^6.3.0", "send": "^0.17.0", @@ -133,13 +133,13 @@ "dependencies": { "abstract-logging": "^1.0.0", "ajv": "^6.10.2", - "avvio": "^6.1.1", - "fast-json-stringify": "^1.15.0", + "avvio": "^6.2.2", + "fast-json-stringify": "^1.15.4", "find-my-way": "^2.0.0", "flatstr": "^1.0.12", "light-my-request": "^3.4.1", "middie": "^4.0.1", - "pino": "^5.13.1", + "pino": "^5.13.2", "proxy-addr": "^2.0.4", "readable-stream": "^3.1.1", "rfdc": "^1.1.2", From c56a9c10c9eae1ca7611e4b076d6ae859707e9b1 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Fri, 30 Aug 2019 23:48:01 +0200 Subject: [PATCH 079/144] greenkeeper ignore @typescript-eslint/eslint-plugin (#1835) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b9d7ba830bd..555d0c6e11c 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,8 @@ "joi", "@types/node", "tap", - "tap-mocha-reporter" + "tap-mocha-reporter", + "@typescript-eslint/eslint-plugin" ] }, "standard": { From 00d72e5efb7b22d00cae6adf81b456dfa73a61d0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 30 Aug 2019 23:52:55 +0200 Subject: [PATCH 080/144] Bumped v2.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 555d0c6e11c..4be153ee9a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.7.1", + "version": "2.8.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 4eeb0a7cc8ccc02233e388f834c889d4e04c2dcf Mon Sep 17 00:00:00 2001 From: Ryan Albon Date: Sun, 1 Sep 2019 18:38:40 +0200 Subject: [PATCH 081/144] Improving Getting Started documentation (#1837) --- docs/Getting-Started.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 7f6d843fd49..ffe10cbcf43 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -41,7 +41,7 @@ fastify.listen(3000, function (err, address) { ``` Do you prefer to use `async/await`? Fastify supports it out-of-the-box.
-*(we also suggest using [make-promises-safe](https://github.com/mcollina/make-promises-safe) to avoid file descriptor and memory leaks)* +*(We also suggest using [make-promises-safe](https://github.com/mcollina/make-promises-safe) to avoid file descriptor and memory leaks.)* ```js const fastify = require('fastify')() @@ -61,8 +61,8 @@ start() ``` Awesome, that was easy.
-Unfortunately, writing a complex application requires significantly more code than this example. A classic problem when you are building a new application is how handle multiple files, asynchronous bootstrapping and the architecture of your code.
-Fastify offers an easy platform that helps solve all of problems, and more. +Unfortunately, writing a complex application requires significantly more code than this example. A classic problem when building a new application is how to handle multiple files, asynchronous bootstrapping and the architecture of your code.
+Fastify offers an easy platform that helps solve all these problems, and more. > ## Note > The above examples, and subsequent examples in this document, default to listening *only* on the localhost `127.0.0.1` interface. To listen on all available IPv4 interfaces the example should be modified to listen on `0.0.0.0` like so: @@ -115,7 +115,7 @@ module.exports = routes ``` In this example we used the `register` API. This API is the core of the Fastify framework, and is the only way to register routes, plugins and so on. -At the beginning of this guide we noted that Fastify provides a foundation that assists with the asynchronous bootstrapping of your application. Why this is important? +At the beginning of this guide we noted that Fastify provides a foundation that assists with the asynchronous bootstrapping of your application. Why is this important? Consider the scenario where a database connection is needed to handle data storage. Obviously the database connection needs to be available prior to the server accepting connections. How do we address this problem?
A typical solution is to use a complex callback, or promises, system that will mix the framework API with other libraries and the application code.
Fastify handles this internally, with minimum effort! @@ -123,6 +123,12 @@ Fastify handles this internally, with minimum effort! Let's rewrite the above example with a database connection.
*(we will use a simple example, for a robust solution consider using [`fastify-mongo`](https://github.com/fastify/fastify-mongodb) or another in the Fastify [ecosystem](https://github.com/fastify/fastify/blob/master/docs/Ecosystem.md))* +First, install `fastify-plugin`: + +``` +npm install --save fastify-plugin +``` + **server.js** ```js const fastify = require('fastify')({ @@ -186,11 +192,11 @@ module.exports = routes Wow, that was fast!
Let's recap what we have done here since we've introduced some new concepts.
As you can see, we used `register` both for the database connector and the routes registration. -This is one of the best features of Fastify, it will load your plugins in the same order you declare them, and it will load the next plugin only once the current one has been loaded. In this way we can register the database connector in the first plugin and use it in the second *(read [here](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#handle-the-scope) to understand how to handle the scope of a plugin)*. -Plugin loading starts when you call `fastify.listen()`, `fastify.inject()` or `fastify.ready()` +This is one of the best features of Fastify! It will load your plugins in the same order you declare them, and it will load the next plugin only once the current one has been loaded. In this way we can register the database connector in the first plugin and use it in the second *(read [here](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#handle-the-scope) to understand how to handle the scope of a plugin)*. +Plugin loading starts when you call `fastify.listen()`, `fastify.inject()` or `fastify.ready()`. -We have used the `decorate` API. Let's take a moment to understand what it is and how it works. A scenario is to use the same code/library in different parts of an application. A solution is to require the code/library that it is needed. This works, but is annoying because of duplicated code repeated and, if needed, long refactors.
-To solve this Fastify offers the `decorate` API, which adds custom objects to the Fastify namespace, so that they can be used everywhere. +We also used the `decorate` API. A common scenario is to use the same code/library in different parts of an application. One solution is to require the code/library that is needed. This works, but is annoying because of duplicated code repeated and, if needed, long refactors.
+To solve this Fastify offers the `decorate` API, which adds custom objects to the Fastify namespace so that they can be used everywhere. To dig deeper into how Fastify plugins work, how to develop new plugins, and for details on how to use the whole Fastify API to deal with the complexity of asynchronously bootstrapping an application, read [the hitchhiker's guide to plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins-Guide.md). @@ -205,7 +211,7 @@ To guarantee a consistent and predictable behavior of your application, we highl └── your services ``` In this way you will always have access to all of the properties declared in the current scope.
-As discussed previously, Fastify offers a solid encapsulation model, to help you build your application as single and independent services. If you want to register a plugin only for a subset of routes, you have just to replicate the above structure. +As discussed previously, Fastify offers a solid encapsulation model to help you build your application as single and independent services. If you want to register a plugin only for a subset of routes, you just have to replicate the above structure. ``` └── plugins (from the Fastify ecosystem) └── your plugins (your custom plugins) @@ -275,12 +281,12 @@ fastify.get('/', opts, async (request, reply) => { return { hello: 'world' } }) ``` -Simply by specifying a schema as shown, a speed up your of serialization by 2x or even 3x can be achieved. This also helps protect against leaking of sensitive data, since Fastify will serialize only the data present in the response schema. +Simply by specifying a schema as shown above, serialization can be sped up by a factor of 2 or even 3! This also helps protect against leaking of sensitive data, since Fastify will serialize only the data present in the response schema. Read [Validation and Serialization](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) to learn more. ### Extend your server -Fastify is built to be extremely extensible and very minimal, We believe that a bare minimum framework is all that is necessary to make great applications possible.
+Fastify is built to be extremely extensible and very minimal. We believe that a bare minimum framework is all that is necessary to make great applications possible.
In other words, Fastify is not a "batteries included" framework, and relies on an amazing [ecosystem](https://github.com/fastify/fastify/blob/master/docs/Ecosystem.md)! @@ -290,8 +296,7 @@ Read the [testing](https://github.com/fastify/fastify/blob/master/docs/Testing.m ### Run your server from CLI -Fastify also has CLI integration thanks to -[fastify-cli](https://github.com/fastify/fastify-cli). +Fastify also has CLI integration thanks to [fastify-cli](https://github.com/fastify/fastify-cli). First, install `fastify-cli`: From 8891ae8701633503e6732e84de4f7e2d3f4b8451 Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Wed, 4 Sep 2019 17:24:16 +0200 Subject: [PATCH 082/144] feat - Implement pluginName (#1836) * imlement pluginName * remove newline, improve test headline * add docs, build plugin name chain * improve docs * fix order * add test * Update docs/Server.md Co-Authored-By: Manuel Spigolon * improve tests --- docs/Server.md | 12 ++++ fastify.js | 17 ++++- lib/pluginUtils.js | 32 ++++++++++ lib/symbols.js | 3 +- test/plugin.test.js | 148 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 2 deletions(-) diff --git a/docs/Server.md b/docs/Server.md index 81ad02d0e04..7f169aa61f7 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -581,6 +581,18 @@ fastify.register(function (instance, opts, done) { }, { prefix: '/v1' }) ``` + +#### pluginName +Name of the current plugin. There are three ways to define a name (in order). + +1. If you use [fastify-plugin](https://github.com/fastify/fastify-plugin) the metadata `name` is used. +2. If you `module.exports` a plugin the filename is used. +3. If you use a regular [function declaration](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Defining_functions) the function name is used. + +*Fallback*: The first two lines of your plugin will represent the plugin name. Newlines are replaced by ` -- `. This will help to indentify the root cause when you deal with many plugins. + +Important: If you have to deal with nested plugins the name differs with the usage of the [fastify-plugin](https://github.com/fastify/fastify-plugin) because no new scope is created and therefore we have no place to attach contextual data. In that case the plugin name will represent the boot order of all involved plugins in the format of `plugin-A -> plugin-B`. + #### log The logger instance, check [here](https://github.com/fastify/fastify/blob/master/docs/Logging.md). diff --git a/fastify.js b/fastify.js index 04d69ba77d5..ace176f7a15 100644 --- a/fastify.js +++ b/fastify.js @@ -21,7 +21,8 @@ const { kFourOhFour, kState, kOptions, - kGlobalHooks + kGlobalHooks, + kPluginNameChain } = require('./lib/symbols.js') const { createServer } = require('./lib/server') @@ -130,6 +131,7 @@ function build (options) { onRegister: [] }, [pluginUtils.registeredPlugins]: [], + [kPluginNameChain]: [], // routes shorthand methods delete: function _delete (url, opts, handler) { return router.prepareRoute.call(this, 'DELETE', url, opts, handler) @@ -224,6 +226,15 @@ function build (options) { } }) + Object.defineProperty(fastify, 'pluginName', { + get: function () { + if (this[kPluginNameChain].length > 1) { + return this[kPluginNameChain].join(' -> ') + } + return this[kPluginNameChain][0] + } + }) + // Install and configure Avvio // Avvio will update the following Fastify methods: // - register @@ -424,7 +435,10 @@ function build (options) { // Everything that need to be encapsulated must be handled in this function. function override (old, fn, opts) { const shouldSkipOverride = pluginUtils.registerPlugin.call(old, fn) + if (shouldSkipOverride) { + // after every plugin registration we will enter a new name + old[kPluginNameChain].push(pluginUtils.getDisplayName(fn)) return old } @@ -441,6 +455,7 @@ function override (old, fn, opts) { instance[kSchemas] = buildSchemas(old[kSchemas]) instance.getSchemas = instance[kSchemas].getSchemas.bind(instance[kSchemas]) instance[pluginUtils.registeredPlugins] = Object.create(instance[pluginUtils.registeredPlugins]) + instance[kPluginNameChain] = [pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)] if (opts.prefix) { instance[kFourOhFour].arrange404(instance) diff --git a/lib/pluginUtils.js b/lib/pluginUtils.js index 1f154979354..319aec02598 100644 --- a/lib/pluginUtils.js +++ b/lib/pluginUtils.js @@ -11,6 +11,35 @@ function getMeta (fn) { return fn[Symbol.for('plugin-meta')] } +function getPluginName (func) { + // let's see if this is a file, and in that case use that + // this is common for plugins + const cache = require.cache + const keys = Object.keys(cache) + + for (var i = 0; i < keys.length; i++) { + if (cache[keys[i]].exports === func) { + return keys[i] + } + } + + // if not maybe it's a named function, so use that + if (func.name) { + return func.name + } + + return null +} + +function getFuncPreview (func) { + // takes the first two lines of the function if nothing else works + return func.toString().split('\n').slice(0, 2).map(s => s.trim()).join(' -- ') +} + +function getDisplayName (fn) { + return fn[Symbol.for('fastify.display-name')] +} + function shouldSkipOverride (fn) { return !!fn[Symbol.for('skip-override')] } @@ -71,7 +100,10 @@ function registerPlugin (fn) { } module.exports = { + getPluginName, + getFuncPreview, registeredPlugins, + getDisplayName, registerPlugin } diff --git a/lib/symbols.js b/lib/symbols.js index 97a9cc56b89..649c976d871 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -30,7 +30,8 @@ const keys = { kState: Symbol('fastify.state'), kOptions: Symbol('fastify.options'), kGlobalHooks: Symbol('fastify.globalHooks'), - kDisableRequestLogging: Symbol('fastify.disableRequestLogging') + kDisableRequestLogging: Symbol('fastify.disableRequestLogging'), + kPluginNameChain: Symbol('fastify.pluginNameChain') } module.exports = keys diff --git a/test/plugin.test.js b/test/plugin.test.js index 8d4f2d17149..3bc9bac27c9 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -312,6 +312,154 @@ test('check dependencies - should throw', t => { }) }) +test('set the plugin name based on the plugin displayName symbol', t => { + t.plan(5) + const fastify = Fastify() + + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A') + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB') + next() + }, { name: 'plugin-AB' })) + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB -> plugin-AC') + next() + }, { name: 'plugin-AC' })) + next() + }, { name: 'plugin-A' })) + + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB -> plugin-AC -> plugin-B') + next() + }, { name: 'plugin-B' })) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + +test('plugin name will change when using no encapsulation', t => { + t.plan(5) + const fastify = Fastify() + + fastify.register(fp((fastify, opts, next) => { + // store it in a different variable will hold the correct name + const pluginName = fastify.pluginName + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB') + next() + }, { name: 'plugin-AB' })) + fastify.register(fp((fastify, opts, next) => { + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB -> plugin-AC') + next() + }, { name: 'plugin-AC' })) + setImmediate(() => { + // normally we would expect the name plugin-A + // but we operate on the same instance in each plugin + t.strictEqual(fastify.pluginName, 'plugin-A -> plugin-AB -> plugin-AC') + t.strictEqual(pluginName, 'plugin-A') + }) + next() + }, { name: 'plugin-A' })) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + +test('plugin name is undefined when accessing in no plugin context', t => { + t.plan(2) + const fastify = Fastify() + + t.strictEqual(fastify.pluginName, undefined) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + +test('set the plugin name based on the plugin function name', t => { + t.plan(5) + const fastify = Fastify() + + fastify.register(function myPluginA (fastify, opts, next) { + t.strictEqual(fastify.pluginName, 'myPluginA') + fastify.register(function myPluginAB (fastify, opts, next) { + t.strictEqual(fastify.pluginName, 'myPluginAB') + next() + }) + setImmediate(() => { + // exact name due to encapsulation + t.strictEqual(fastify.pluginName, 'myPluginA') + }) + next() + }) + + fastify.register(function myPluginB (fastify, opts, next) { + t.strictEqual(fastify.pluginName, 'myPluginB') + next() + }) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + +test('approximate a plugin name when no meta data is available', t => { + t.plan(7) + const fastify = Fastify() + + fastify.register((fastify, opts, next) => { + // A + t.is(fastify.pluginName.startsWith('(fastify, opts, next)'), true) + t.is(fastify.pluginName.includes('// A'), true) + fastify.register((fastify, opts, next) => { + // B + t.is(fastify.pluginName.startsWith('(fastify, opts, next)'), true) + t.is(fastify.pluginName.includes('// B'), true) + next() + }) + setImmediate(() => { + t.is(fastify.pluginName.startsWith('(fastify, opts, next)'), true) + t.is(fastify.pluginName.includes('// A'), true) + }) + next() + }) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + +test('approximate a plugin name also when fastify-plugin has no meta data', t => { + t.plan(4) + const fastify = Fastify() + + fastify.register(fp((fastify, opts, next) => { + t.is(fastify.pluginName, 'plugin.test') + fastify.register(fp(function B (fastify, opts, next) { + // function has name + t.is(fastify.pluginName, 'plugin.test -> B') + next() + })) + setImmediate(() => { + t.is(fastify.pluginName, 'plugin.test -> B') + }) + next() + })) + + fastify.listen(0, err => { + t.error(err) + fastify.close() + }) +}) + test('plugin incapsulation', t => { t.plan(10) const fastify = Fastify() From 45d2d292b23157a0103e8a621b94af1d5b21e5a2 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Mon, 9 Sep 2019 17:11:22 -0300 Subject: [PATCH 083/144] feature: add onResponse hook in route declaration (#1838) --- docs/Hooks.md | 11 ++- fastify.d.ts | 3 + lib/route.js | 7 +- test/hooks.test.js | 5 +- test/route-hooks.test.js | 179 ++++++++++++++++++++------------------- 5 files changed, 114 insertions(+), 91 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index bddcf3bb977..e1ed437da7e 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -329,7 +329,7 @@ Note: using an arrow function will break the binding of this to the Fastify inst ## Route level hooks -You can declare one or more custom [onRequest](#onRequest), [preParsing](#preParsing), [preValidation](#preValidation), [preHandler](#preHandler) and [preSerialization](#preSerialization) hook(s) that will be **unique** for the route. +You can declare one or more custom [onRequest](#onRequest), [onReponse](#onResponse), [preParsing](#preParsing), [preValidation](#preValidation), [preHandler](#preHandler) and [preSerialization](#preSerialization) hook(s) that will be **unique** for the route. If you do so, those hooks always be executed as last hook in their category.
This can be useful if you need to run the authentication, and the [preParsing](#preParsing) or [preValidation](#preValidation) hooks are exactly what you need for doing that. Multiple route-level hooks can also be specified as an array. @@ -342,6 +342,11 @@ fastify.addHook('onRequest', (request, reply, done) => { done() }) +fastify.addHook('onResponse', (request, reply, done) => { + // your code + done() +}) + fastify.addHook('preParsing', (request, reply, done) => { // your code done() @@ -370,6 +375,10 @@ fastify.route({ // this hook will always be executed after the shared `onRequest` hooks done() }, + onResponse: function (request, reply, done) { + // this hook will always be executed after the shared `onResponse` hooks + done() + }, preParsing: function (request, reply, done) { // this hook will always be executed after the shared `preParsing` hooks done() diff --git a/fastify.d.ts b/fastify.d.ts index dd8ffa0c5a1..0c846016842 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -245,6 +245,9 @@ declare namespace fastify { onRequest?: | FastifyMiddleware | Array> + onResponse?: + | FastifyMiddleware + | Array> preParsing?: | FastifyMiddleware | Array> diff --git a/lib/route.js b/lib/route.js index f1bd5128ae5..127d9222eaf 100644 --- a/lib/route.js +++ b/lib/route.js @@ -6,6 +6,7 @@ const Context = require('./context') const { buildMiddie, onRunMiddlewares } = require('./middleware') const { hookRunner, hookIterator } = require('./hooks') const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS'] +const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization', 'onResponse'] const validation = require('./validation') const buildSchema = validation.build const { buildSchemaCompiler } = validation @@ -226,9 +227,7 @@ function buildRouting (options) { } } - const hooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization'] - - for (const hook of hooks) { + for (const hook of supportedHooks) { if (opts[hook]) { if (Array.isArray(opts[hook])) { opts[hook] = opts[hook].map(fn => fn.bind(this)) @@ -257,7 +256,7 @@ function buildRouting (options) { context.onError = onError.length ? onError : null context.onResponse = onResponse.length ? onResponse : null - for (const hook of hooks) { + for (const hook of supportedHooks) { const toSet = this[kHooks][hook].concat(opts[hook] || []) context[hook] = toSet.length ? toSet : null } diff --git a/test/hooks.test.js b/test/hooks.test.js index 7405ac42d25..7c25abd0e0b 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -14,7 +14,7 @@ const symbols = require('../lib/symbols.js') const payload = { hello: 'world' } test('hooks', t => { - t.plan(37) + t.plan(38) const fastify = Fastify() try { @@ -99,6 +99,9 @@ test('hooks', t => { t.is(reply.test, 'the reply has come') reply.code(200).send(payload) }, + onResponse: function (req, reply, done) { + t.ok('onResponse inside hook') + }, response: { 200: { type: 'object' diff --git a/test/route-hooks.test.js b/test/route-hooks.test.js index 0afbf401988..ee50251d536 100644 --- a/test/route-hooks.test.js +++ b/test/route-hooks.test.js @@ -3,7 +3,7 @@ const test = require('tap').test const Fastify = require('../') -function testHook (hook) { +function testExecutionHook (hook) { test(`${hook}`, t => { t.plan(3) const fastify = Fastify() @@ -29,22 +29,24 @@ function testHook (hook) { }) test(`${hook} option should be called after ${hook} hook`, t => { - t.plan(2) + t.plan(3) const fastify = Fastify() - let check = '' + const checker = Object.defineProperty({ calledTimes: 0 }, 'check', { + get: function () { return ++this.calledTimes } + }) fastify.addHook(hook, (req, reply, next) => { - check = 'a' + t.equal(checker.check, 1) next() }) fastify.post('/', { [hook]: (req, reply, done) => { - check += 'b' + t.equal(checker.check, 2) done() } }, (req, reply) => { - reply.send({ check }) + reply.send({}) }) fastify.inject({ @@ -53,11 +55,86 @@ function testHook (hook) { payload: { hello: 'world' } }, (err, res) => { t.error(err) - var payload = JSON.parse(res.payload) - t.deepEqual(payload, { check: 'ab' }) }) }) + test(`${hook} option could accept an array of functions`, t => { + t.plan(3) + const fastify = Fastify() + const checker = Object.defineProperty({ calledTimes: 0 }, 'check', { + get: function () { return ++this.calledTimes } + }) + + fastify.post('/', { + [hook]: [ + (req, reply, done) => { + t.equal(checker.check, 1) + done() + }, + (req, reply, done) => { + t.equal(checker.check, 2) + done() + } + ] + }, (req, reply) => { + reply.send({}) + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { hello: 'world' } + }, (err, res) => { + t.error(err) + }) + }) + + test(`${hook} option does not interfere with ${hook} hook`, t => { + t.plan(7) + const fastify = Fastify() + const checker = Object.defineProperty({ calledTimes: 0 }, 'check', { + get: function () { return ++this.calledTimes } + }) + + fastify.addHook(hook, (req, reply, next) => { + t.equal(checker.check, 1) + next() + }) + + fastify.post('/', { + [hook]: (req, reply, done) => { + t.equal(checker.check, 2) + done() + } + }, handler) + + fastify.post('/no', handler) + + function handler (req, reply) { + reply.send({}) + } + + fastify.inject({ + method: 'post', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(checker.calledTimes, 2) + + checker.calledTimes = 0 + + fastify.inject({ + method: 'post', + url: '/no' + }, (err, res) => { + t.error(err) + t.equal(checker.calledTimes, 1) + }) + }) + }) +} + +function testBeforeHandlerHook (hook) { test(`${hook} option should be unique per route`, t => { t.plan(4) const fastify = Fastify() @@ -153,79 +230,6 @@ function testHook (hook) { }) }) - test(`${hook} option could accept an array of functions`, t => { - t.plan(2) - const fastify = Fastify() - - fastify.post('/', { - [hook]: [ - (req, reply, done) => { - req.aa = 'a' - done() - }, - (req, reply, done) => { - req.aa += 'b' - done() - } - ] - }, (req, reply) => { - reply.send({ aa: req.aa }) - }) - - fastify.inject({ - method: 'POST', - url: '/', - payload: { hello: 'world' } - }, (err, res) => { - t.error(err) - var payload = JSON.parse(res.payload) - t.deepEqual(payload, { aa: 'ab' }) - }) - }) - - test(`${hook} option does not interfere with ${hook} hook`, t => { - t.plan(4) - const fastify = Fastify() - - fastify.addHook(hook, (req, reply, next) => { - req.check = 'a' - next() - }) - - fastify.post('/', { - [hook]: (req, reply, done) => { - req.check += 'b' - done() - } - }, handler) - - fastify.post('/no', handler) - - function handler (req, reply) { - reply.send({ check: req.check }) - } - - fastify.inject({ - method: 'post', - url: '/', - payload: { hello: 'world' } - }, (err, res) => { - t.error(err) - var payload = JSON.parse(res.payload) - t.deepEqual(payload, { check: 'ab' }) - }) - - fastify.inject({ - method: 'post', - url: '/no', - payload: { hello: 'world' } - }, (err, res) => { - t.error(err) - var payload = JSON.parse(res.payload) - t.deepEqual(payload, { check: 'a' }) - }) - }) - test(`${hook} option should keep the context`, t => { t.plan(3) const fastify = Fastify() @@ -281,11 +285,16 @@ function testHook (hook) { }) } +testExecutionHook('preHandler') +testExecutionHook('onRequest') +testExecutionHook('onResponse') +testExecutionHook('preValidation') +testExecutionHook('preParsing') // hooks that comes before the handler -testHook('preHandler') -testHook('onRequest') -testHook('preValidation') -testHook('preParsing') +testBeforeHandlerHook('preHandler') +testBeforeHandlerHook('onRequest') +testBeforeHandlerHook('preValidation') +testBeforeHandlerHook('preParsing') test('preHandler backwards compatibility with beforeHandler option (should emit a warning)', t => { t.plan(4) From 9a78d7e60f74c4e4b955839a8746518936561150 Mon Sep 17 00:00:00 2001 From: Michael Chris Lopez Date: Tue, 10 Sep 2019 13:57:51 +0800 Subject: [PATCH 084/144] Fix eslint "no-misused-promises" error in hooks (#1843) * fix eslint "no-misused-promises" error in hooks * test hook async function typescript --- fastify.d.ts | 4 ++-- test/types/index.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 0c846016842..e32ae493740 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -81,7 +81,7 @@ declare namespace fastify { req: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void, - ) => void + ) => void | Promise type FastifyMiddlewareWithPayload< HttpServer = http.Server, @@ -97,7 +97,7 @@ declare namespace fastify { reply: FastifyReply, payload: any, done: (err?: Error, value?: any) => void, - ) => void + ) => void | Promise type RequestHandler< HttpRequest = http.IncomingMessage, diff --git a/test/types/index.ts b/test/types/index.ts index e645b30934f..ee7bcc68eaa 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -117,6 +117,18 @@ server.addHook('preHandler', function (req, reply, next) { } }) +server.addHook('preHandler', async function (req, reply) { + this.log.debug('`this` is not `any`') + if (req.body.error) { + throw new Error('testing if middleware errors can be passed') + } else { + // `stream` can be accessed correctly because `server` is an http2 server. + console.log('req stream', req.req.stream) + console.log('res stream', reply.res.stream) + reply.code(200).send('ok') + } +}) + server.addHook('onRequest', function (req, reply, next) { this.log.debug('`this` is not `any`') console.log(`${req.raw.method} ${req.raw.url}`) From 6de4b89c26e8bbfb87d85a395df32898d6094fe9 Mon Sep 17 00:00:00 2001 From: Chia Wei Ong Date: Tue, 10 Sep 2019 18:36:19 +0200 Subject: [PATCH 085/144] Update Validation-and-Serialization.md (#1846) --- docs/Validation-and-Serialization.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 9d8704fea27..4429fc68b28 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -280,6 +280,7 @@ fastify.setSchemaCompiler(function (schema) { // Alternatively, you can set the schema compiler using the setter property: fastify.schemaCompiler = function (schema) { return ajv.compile(schema) }) ``` +_**Note:** If you use a custom instance of any validator (even Ajv), you have to add schemas to the validator instead of fastify, since fastify's default validator is no longer used, and fastify's `addSchema` method has no idea what validator you are using._ But maybe you want to change the validation library. Perhaps you like `Joi`. In this case, you can use it to validate the url parameters, body, and query string! From 8aa212822aa0354bdbebffa160b422714a7ce756 Mon Sep 17 00:00:00 2001 From: Zoron Date: Thu, 12 Sep 2019 17:05:52 +0800 Subject: [PATCH 086/144] docs(Reply): fix Errors (#1848) add 'code' to error struct --- docs/Reply.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Reply.md b/docs/Reply.md index c3856f266e7..2349e161415 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -225,11 +225,12 @@ If you pass to *send* an object that is an instance of *Error*, Fastify will aut ```js { error: String // the http error message + code: String // the Fastify error code message: String // the user error message statusCode: Number // the http status code } ``` -You can add some custom property to the Error object, such as `statusCode` and `headers`, that will be used to enhance the http response.
+You can add some custom property to the Error object, such as `headers`, that will be used to enhance the http response.
*Note: If you are passing an error to `send` and the statusCode is less than 400, Fastify will automatically set it at 500.* Tip: you can simplify errors by using the [`http-errors`](https://npm.im/http-errors) module or [`fastify-sensible`](https://github.com/fastify/fastify-sensible) plugin to generate errors: From 995aeba00bf6fbe1099a968d3b46011d373881dc Mon Sep 17 00:00:00 2001 From: lw <28566392+LW2904@users.noreply.github.com> Date: Thu, 12 Sep 2019 18:28:59 +0200 Subject: [PATCH 087/144] First batch of documentation fixes (#1850) * docs(Benchmarking): Improve grammar * docs(ContentTypeParser): Improve grammar * docs(Decorators): Improve grammar * docs(Errors): Improve grammar * docs(FluentSchema): Improve grammar * docs(GettingStarted): Improve grammar * docs(Hooks): Improve grammar * docs(HTTP2): Improve grammar * docs(Logging): Improve grammar * docs(Middleware): Improve grammar * docs(PluginsGuide): Improve grammar * Update middleware docs link * Always put Content-Type in backticks * docs(Hooks): Fix typo --- README.md | 2 +- docs/Benchmarking.md | 4 +- docs/ContentTypeParser.md | 22 +++---- docs/Decorators.md | 28 ++++----- docs/Errors.md | 16 +++--- docs/Fluent-Schema.md | 21 +++---- docs/Getting-Started.md | 45 +++++++-------- docs/HTTP2.md | 2 +- docs/Hooks.md | 118 +++++++++++++++++++------------------- docs/Logging.md | 20 +++---- docs/Middleware.md | 59 +++++++++++++++++++ docs/Middlewares.md | 59 ------------------- docs/Plugins-Guide.md | 104 ++++++++++++++++----------------- 13 files changed, 248 insertions(+), 252 deletions(-) create mode 100644 docs/Middleware.md delete mode 100644 docs/Middlewares.md diff --git a/README.md b/README.md index 1381304e73e..d6fb7a1c11c 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ matters to you. * Server * Routes * Logging -* Middlewares +* Middleware * Hooks * Decorators * Validation and Serialization diff --git a/docs/Benchmarking.md b/docs/Benchmarking.md index ae67a438c0f..c7643fa4298 100644 --- a/docs/Benchmarking.md +++ b/docs/Benchmarking.md @@ -1,13 +1,13 @@

Fastify

## Benchmarking -Benchmarking is important if you want to measure how a change can impact your application performance. We provide a simple way to benchmark your application from the point of view of a user and contributor. The setup allows you to automate benchmarks in different branches on different Node.js versions. +Benchmarking is important if you want to measure how a change can impact the performance of your application. We provide a simple way to benchmark your application from the point of view of a user and contributor. The setup allows you to automate benchmarks in different branches and on different Node.js versions. The modules we'll use: - [Autocannon](https://github.com/mcollina/autocannon): A HTTP/1.1 benchmarking tool written in node. - [Branch-comparer](https://github.com/StarpTech/branch-comparer): Checkout multiple git branches, execute scripts and log the results. - [Concurrently](https://github.com/kimmobrunfeldt/concurrently): Run commands concurrently. -- [Npx](https://github.com/zkat/npx) NPM package runner - We using it to run scripts against different Node.js Versions and execute local binaries. Shipped with npm@5.2.0. +- [Npx](https://github.com/zkat/npx) NPM package runner - We are using it to run scripts against different Node.js Versions and to execute local binaries. Shipped with npm@5.2.0. ## Simple diff --git a/docs/ContentTypeParser.md b/docs/ContentTypeParser.md index 8787e923c19..96885dae159 100644 --- a/docs/ContentTypeParser.md +++ b/docs/ContentTypeParser.md @@ -1,11 +1,11 @@

Fastify

-## Content Type Parser +## `Content-Type` Parser Natively, Fastify only supports `'application/json'` and `'text/plain'` content types. The default charset is `utf-8`. If you need to support different content types, you can use the `addContentTypeParser` API. *The default JSON and/or plain text parser can be changed.* -As with the other APIs, `addContentTypeParser` is encapsulated in the scope in which it is declared. This means that if you declare it in the root scope it will be available everywhere, while if you declare it inside a register it will be available only in that scope and its children. +As with the other APIs, `addContentTypeParser` is encapsulated in the scope in which it is declared. This means that if you declare it in the root scope it will be available everywhere, while if you declare it inside a plugin it will be available only in that scope and its children. -Fastify adds automatically the parsed request payload to the [Fastify request](https://github.com/fastify/fastify/blob/master/docs/Request.md) object, you can reach it with `request.body`. +Fastify automatically adds the parsed request payload to the [Fastify request](https://github.com/fastify/fastify/blob/master/docs/Request.md) object which you can access with `request.body`. ### Usage ```js @@ -14,13 +14,15 @@ fastify.addContentTypeParser('application/jsoff', function (req, done) { done(err, body) }) }) -// handle multiple content types as the same + +// Handle multiple content types with the same function fastify.addContentTypeParser(['text/xml', 'application/xml'], function (req, done) { xmlParser(req, function (err, body) { done(err, body) }) }) -// async also supported in Node versions >= 8.0.0 + +// Async is also supported in Node versions >= 8.0.0 fastify.addContentTypeParser('application/jsoff', async function (req) { var res = await new Promise((resolve, reject) => resolve(req)) return res @@ -32,13 +34,13 @@ You can also use the `hasContentTypeParser` API to find if a specific content ty ```js if (!fastify.hasContentTypeParser('application/jsoff')){ fastify.addContentTypeParser('application/jsoff', function (req, done) { - //code to parse request body /payload for given content type + // Code to parse request body/payload for the given content type }) } ``` #### Body Parser -You can parse the body of the request in two ways. The first one is shown above: you add a custom content type parser and handle the request stream. In the second one you should pass a `parseAs` option to the `addContentTypeParser` API, where you declare how you want to get the body, it could be `'string'` or `'buffer'`. If you use the `parseAs` option Fastify will internally handle the stream and perform some checks, such as the [maximum size](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-body-limit) of the body and the content length. If the limit is exceeded the custom parser will not be invoked. +You can parse the body of a request in two ways. The first one is shown above: you add a custom content type parser and handle the request stream. In the second one, you should pass a `parseAs` option to the `addContentTypeParser` API, where you declare how you want to get the body. It could be of type `'string'` or `'buffer'`. If you use the `parseAs` option, Fastify will internally handle the stream and perform some checks, such as the [maximum size](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-body-limit) of the body and the content length. If the limit is exceeded the custom parser will not be invoked. ```js fastify.addContentTypeParser('application/json', { parseAs: 'string' }, function (req, body, done) { try { @@ -58,8 +60,8 @@ See [`example/parser.js`](https://github.com/fastify/fastify/blob/master/example + `parseAs` (string): Either `'string'` or `'buffer'` to designate how the incoming data should be collected. Default: `'buffer'`. + `bodyLimit` (number): The maximum payload size, in bytes, that the custom parser will accept. Defaults to the global body limit passed to the [`Fastify factory function`](https://github.com/fastify/fastify/blob/master/docs/Server.md#bodylimit). -#### Catch All -There are some cases where you need to catch all requests regardless of their content type. With Fastify, you just need to add the `'*'` content type. +#### Catch-All +There are some cases where you need to catch all requests regardless of their content type. With Fastify, you can just use the `'*'` content type. ```js fastify.addContentTypeParser('*', function (req, done) { var data = '' @@ -70,7 +72,7 @@ fastify.addContentTypeParser('*', function (req, done) { }) ``` -In this way, all of the requests that do not have a corresponding content type parser will be handled by the specified function. +Using this, all requests that do not have a corresponding content type parser will be handled by the specified function. This is also useful for piping the request stream. You can define a content parser like: diff --git a/docs/Decorators.md b/docs/Decorators.md index de92d9e1a2b..21904784460 100644 --- a/docs/Decorators.md +++ b/docs/Decorators.md @@ -2,9 +2,9 @@ ## Decorators -If you need to add functionality to the Fastify instance, the `decorate` API is what you need. +If you need to add functionality to the Fastify instance, the `decorate` API is what you want. -The API allows you to add new properties to the Fastify instance. A value is not restricted to a function and could also be an object or a string, for example. +The API allows you to add new properties to the Fastify instance. Possible values are not restricted by type and could be functions, objects or strings, for example. ### Usage @@ -13,11 +13,11 @@ The API allows you to add new properties to the Fastify instance. A value is not Just call the `decorate` API and pass the name of the new property and its value. ```js fastify.decorate('utility', () => { - // something very useful + // Something very useful }) ``` -As said above, you can also decorate the instance with non-function values: +As mentioned above, you can also decorate the instance with non-function values: ```js fastify.decorate('conf', { db: 'some.db', @@ -25,7 +25,7 @@ fastify.decorate('conf', { }) ``` -Once you decorate the instance, you can access the value by using the name you passed as a parameter: +Once the instance was decorated, you can access the new value by using the name you passed as a parameter: ```js fastify.utility() @@ -34,14 +34,14 @@ console.log(fastify.conf.db) **decorateReply** -As the name suggests, this API is needed if you want to add new methods to the `Reply` core object. Just call the `decorateReply` API and pass the name of the new property and its value: +As the name suggests, this API can be used to add new methods to the `Reply` core object. Just call the `decorateReply` API and pass the name of the new property and its value: ```js fastify.decorateReply('utility', function () { - // something very useful + // Something very useful }) ``` -Note: using an arrow function will break the binding of `this` to the Fastify `reply` instance. +Note: using an arrow function will break the binding of `this` to the Fastify `Reply` instance. **decorateRequest** @@ -52,12 +52,12 @@ fastify.decorateRequest('utility', function () { }) ``` -Note: using an arrow function will break the binding of `this` to the Fastify `request` instance. +Note: using an arrow function will break the binding of `this` to the Fastify `Request` instance. -#### Decorators and encapsulation +#### Decorators and Encapsulation -If you define a decorator (using decorate, decorateRequest or decorateReply) with the same name more than once in the same **encapsulated** plugin, fastify will throw an exception. +If you define a decorator (using `decorate`, `decorateRequest` or `decorateReply`) with the same name more than once in the same **encapsulated** plugin, Fastify will throw an exception. As an example, the following will throw: @@ -65,7 +65,7 @@ As an example, the following will throw: const server = require('fastify')() server.decorateReply('view', function (template, args) { - // Amazing view rendering engine. + // Amazing view rendering engine }) server.get('/', (req, reply) => { @@ -75,7 +75,7 @@ server.get('/', (req, reply) => { // Somewhere else in our codebase, we define another // view decorator. This throws. server.decorateReply('view', function (template, args) { - // another rendering engine + // Another rendering engine }) server.listen(3000) @@ -96,7 +96,7 @@ server.register(async function (server, opts) { // plugin. This will not throw as outside of this encapsulated // plugin view is the old one, while inside it is the new one. server.decorateReply('view', function (template, args) { - // another rendering engine + // Another rendering engine }) server.get('/', (req, reply) => { diff --git a/docs/Errors.md b/docs/Errors.md index e0038307203..a95ea67bd0e 100644 --- a/docs/Errors.md +++ b/docs/Errors.md @@ -6,12 +6,12 @@ ### Error Handling -Uncaught errors are likely to cause memory leaks, file descriptor leaks and other major production issues. [Domains](https://nodejs.org/en/docs/guides/domain-postmortem/) were introduced to try fixing this issue, but they did not. Given the fact that it is not possible to process all uncaught errors in a sensible way, the best way to deal with them at the moment is to [crash](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly). In case of promises, make sure to [handle](https://nodejs.org/dist/latest-v8.x/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections) errors [correctly](https://github.com/mcollina/make-promises-safe). +Uncaught errors are likely to cause memory leaks, file descriptor leaks and other major production issues. [Domains](https://nodejs.org/en/docs/guides/domain-postmortem/) were introduced to try fixing this issue, but they did not. Given the fact that it is not possible to process all uncaught errors sensibly, the best way to deal with them at the moment is to [crash](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly). In case of promises, make sure to [handle](https://nodejs.org/dist/latest-v8.x/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections) errors [correctly](https://github.com/mcollina/make-promises-safe). Fastify follows an all-or-nothing approach and aims to be lean and optimal as much as possible. Thus, the developer is responsible for making sure that the errors are handled properly. Most of the errors are usually a result of unexpected input data, so we recommend specifying a [JSON.schema validation](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) for your input data. -Note that Fastify doesn't catch uncaught errors within callback based routes for you, so any uncaught errors will result in a crash. -If routes are declared as `async` though - the error will safely be caught by the promise and routed to the default error handler of Fastify for a generic `Internal Server Error` response. For customizing this behavior, you should use [setErrorHandler](https://github.com/fastify/fastify/blob/master/docs/Server.md#seterrorhandler). +Note that Fastify doesn't catch uncaught errors within callback-based routes for you, so any uncaught errors will result in a crash. +If routes are declared as `async` though - the error will safely be caught by the promise and routed to the default error handler of Fastify for a generic `Internal Server Error` response. For customizing this behaviour, you should use [setErrorHandler](https://github.com/fastify/fastify/blob/master/docs/Server.md#seterrorhandler). ### Fastify Error Codes @@ -24,7 +24,7 @@ The parser for this content type was already registered. #### FST_ERR_CTP_INVALID_TYPE -The content type should be a string. +The `Content-Type` should be a string. #### FST_ERR_CTP_EMPTY_TYPE @@ -44,12 +44,12 @@ The provided parse type is not supported. Accepted values are `string` or `buffe #### FST_ERR_CTP_BODY_TOO_LARGE -Request body is larger than the provided limit. +The request body is larger than the provided limit. #### FST_ERR_CTP_INVALID_MEDIA_TYPE -Received media type is not supported (i.e. there is no suitable content-type parser for it). +The received media type is not supported (i.e. there is no suitable `Content-Type` parser for it). #### FST_ERR_CTP_INVALID_CONTENT_LENGTH @@ -79,7 +79,7 @@ The hook callback must be a function. #### FST_ERR_LOG_INVALID_DESTINATION -Logger accepts either a `'stream'` or a `'file'` as the destination. +The logger accepts either a `'stream'` or a `'file'` as the destination. ### FST_ERR_REP_ALREADY_SENT @@ -119,4 +119,4 @@ The JSON schema provided to one route is not valid. #### FST_ERR_PROMISE_NOT_FULLFILLED -Promise may not be fulfilled with 'undefined' when statusCode is not 204. +A promise may not be fulfilled with 'undefined' when statusCode is not 204. diff --git a/docs/Fluent-Schema.md b/docs/Fluent-Schema.md index d05985ac173..415cd5202a0 100644 --- a/docs/Fluent-Schema.md +++ b/docs/Fluent-Schema.md @@ -2,19 +2,16 @@ ## Fluent Schema -The [Validation and Serialization](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) -has explained all the parameter accepted by Fastify to set a JSON Schema Validation, to validates -the input, and a JSON Schema Serialization to optimize the output. +The [Validation and Serialization](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) documentation outlines all parameters accepted by Fastify to set up JSON Schema Validation in order to validate the input, and JSON Schema Serialization in order to optimize the output. -To set up the JSON schemas of our Fastify application, we can use [`fluent-schema`][fluent-schema-repo] -to simplify this task and reuse constants values. +[`fluent-schema`](fluent-schema-repo) can be used to simplify this task while allowing the reuse of constants. ### Basic settings ```js const S = require('fluent-schema') -// You can have an object like this, or query a db to get the values +// You can have an object like this, or query a DB to get the values const MY_KEY = { KEY1: 'ONE', KEY2: 'TWO' @@ -41,7 +38,7 @@ const paramsJsonSchema = S.object() const headersJsonSchema = S.object() .prop('x-foo', S.string().required()) -// note that there is no need to call `.valueOf()`! +// Note that there is no need to call `.valueOf()`! const schema = { body: bodyJsonSchema, querystring: queryStringJsonSchema, // (or) query: queryStringJsonSchema @@ -56,11 +53,11 @@ fastify.post('/the/url', { schema }, handler) With `fluent-schema` you can manipulate your schemas in an easier and programmatic way and then reuse them thanks to the `addSchema()` method. You can refer to the schema in two different manners that are detailed -in [Validation-and-Serialization.md](./Validation-and-Serialization.md#adding-a-shared-schema) document. +in the [Validation-and-Serialization.md](./Validation-and-Serialization.md#adding-a-shared-schema) documentation. -Here some example usage: +Here are some usage examples: -**`$ref-way`**: refer to external schema. +**`$ref-way`**: refer to an external schema. ```js const addressSchema = S.object() @@ -74,7 +71,7 @@ const addressSchema = S.object() const commonSchemas = S.object() .id('https://fastify/demo') .definition('addressSchema', addressSchema) - .definition('otherSchema', otherSchema) // you can add any schemas you need + .definition('otherSchema', otherSchema) // You can add any schemas you need fastify.addSchema(commonSchemas) @@ -117,6 +114,6 @@ const schema = { body: bodyJsonSchema } fastify.post('/the/url', { schema }, handler) ``` -NB: you can mix up the usage `$ref-way` and the `replace-way` with `fastify.addSchema`. +NB: you can mix up the `$ref-way` and the `replace-way` when using `fastify.addSchema`. [fluent-schema-repo]: https://github.com/fastify/fluent-schema diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index ffe10cbcf43..59e706d0c54 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -2,7 +2,7 @@ ## Getting Started Hello! Thank you for checking out Fastify!
-This document aims to be a gentle introduction to the framework and its features. It is an elementary introduction with examples and links to other parts of the documentation.
+This document aims to be a gentle introduction to the framework and its features. It is an elementary preface with examples and links to other parts of the documentation.
Let's start! @@ -61,8 +61,8 @@ start() ``` Awesome, that was easy.
-Unfortunately, writing a complex application requires significantly more code than this example. A classic problem when building a new application is how to handle multiple files, asynchronous bootstrapping and the architecture of your code.
-Fastify offers an easy platform that helps solve all these problems, and more. +Unfortunately, writing a complex application requires significantly more code than this example. A classic problem when you are building a new application is how to handle multiple files, asynchronous bootstrapping and the architecture of your code.
+Fastify offers an easy platform that helps to solve all of the problems outlined above, and more! > ## Note > The above examples, and subsequent examples in this document, default to listening *only* on the localhost `127.0.0.1` interface. To listen on all available IPv4 interfaces the example should be modified to listen on `0.0.0.0` like so: @@ -83,9 +83,9 @@ Fastify offers an easy platform that helps solve all these problems, and more. ### Your first plugin -As with JavaScript everything is an object, with Fastify everything is a plugin.
+As with JavaScript, where everything is an object, with Fastify everything is a plugin.
Before digging into it, let's see how it works!
-Let's declare our basic server, but instead of declaring the route inside the entry point, we'll declare it in an external file (checkout the [route declaration](https://github.com/fastify/fastify/blob/master/docs/Routes.md) docs). +Let's declare our basic server, but instead of declaring the route inside the entry point, we'll declare it in an external file (check out the [route declaration](https://github.com/fastify/fastify/blob/master/docs/Routes.md) docs). ```js const fastify = require('fastify')({ logger: true @@ -113,11 +113,11 @@ async function routes (fastify, options) { module.exports = routes ``` -In this example we used the `register` API. This API is the core of the Fastify framework, and is the only way to register routes, plugins and so on. +In this example, we used the `register` API, which is the core of the Fastify framework. It is the only way to add routes, plugins, et cetera. -At the beginning of this guide we noted that Fastify provides a foundation that assists with the asynchronous bootstrapping of your application. Why is this important? -Consider the scenario where a database connection is needed to handle data storage. Obviously the database connection needs to be available prior to the server accepting connections. How do we address this problem?
-A typical solution is to use a complex callback, or promises, system that will mix the framework API with other libraries and the application code.
+At the beginning of this guide, we noted that Fastify provides a foundation that assists with asynchronous bootstrapping of your application. Why is this important? +Consider the scenario where a database connection is needed to handle data storage. Obviously, the database connection needs to be available before the server is accepting connections. How do we address this problem?
+A typical solution is to use a complex callback, or promises - a system that will mix the framework API with other libraries and the application code.
Fastify handles this internally, with minimum effort! Let's rewrite the above example with a database connection.
@@ -191,18 +191,17 @@ module.exports = routes Wow, that was fast!
Let's recap what we have done here since we've introduced some new concepts.
-As you can see, we used `register` both for the database connector and the routes registration. -This is one of the best features of Fastify! It will load your plugins in the same order you declare them, and it will load the next plugin only once the current one has been loaded. In this way we can register the database connector in the first plugin and use it in the second *(read [here](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#handle-the-scope) to understand how to handle the scope of a plugin)*. -Plugin loading starts when you call `fastify.listen()`, `fastify.inject()` or `fastify.ready()`. +As you can see, we used `register` both for the database connector and the registration of the routes. +This is one of the best features of Fastify, it will load your plugins in the same order you declare them, and it will load the next plugin only once the current one has been loaded. In this way, we can register the database connector in the first plugin and use it in the second *(read [here](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#handle-the-scope) to understand how to handle the scope of a plugin)*. +Plugin loading starts when you call `fastify.listen()`, `fastify.inject()` or `fastify.ready()` -We also used the `decorate` API. A common scenario is to use the same code/library in different parts of an application. One solution is to require the code/library that is needed. This works, but is annoying because of duplicated code repeated and, if needed, long refactors.
-To solve this Fastify offers the `decorate` API, which adds custom objects to the Fastify namespace so that they can be used everywhere. +We have also used the `decorate` API to add custom objects to the Fastify namespace, making them available for use everywhere. Use of this API is encouraged to faciliate easy code reuse and to decrease code or logic duplication. To dig deeper into how Fastify plugins work, how to develop new plugins, and for details on how to use the whole Fastify API to deal with the complexity of asynchronously bootstrapping an application, read [the hitchhiker's guide to plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins-Guide.md). ### Loading order of your plugins -To guarantee a consistent and predictable behavior of your application, we highly recommend to always load your code as shown below: +To guarantee a consistent and predictable behaviour of your application, we highly recommend to always load your code as shown below: ``` └── plugins (from the Fastify ecosystem) └── your plugins (your custom plugins) @@ -210,8 +209,8 @@ To guarantee a consistent and predictable behavior of your application, we highl └── hooks and middlewares └── your services ``` -In this way you will always have access to all of the properties declared in the current scope.
-As discussed previously, Fastify offers a solid encapsulation model to help you build your application as single and independent services. If you want to register a plugin only for a subset of routes, you just have to replicate the above structure. +In this way, you will always have access to all of the properties declared in the current scope.
+As discussed previously, Fastify offers a solid encapsulation model, to help you build your application as single and independent services. If you want to register a plugin only for a subset of routes, you just have to replicate the above structure. ``` └── plugins (from the Fastify ecosystem) └── your plugins (your custom plugins) @@ -236,7 +235,7 @@ As discussed previously, Fastify offers a solid encapsulation model to help you ### Validate your data -Data validation is extremely important and is a core concept of the framework.
+Data validation is extremely important and a core concept of the framework.
To validate incoming requests, Fastify uses [JSON Schema](http://json-schema.org/). Let's look at an example demonstrating validation for routes: ```js @@ -261,8 +260,8 @@ Read [Validation and Serialization](https://github.com/fastify/fastify/blob/mast ### Serialize your data -Fastify has first class support for JSON. It is extremely optimized to parse a JSON body and to serialize JSON output.
-To speed up JSON serialization (yes, it is slow!) use the `response` key of the schema option like so: +Fastify has first class support for JSON. It is extremely optimized to parse JSON bodies and to serialize JSON output.
+To speed up JSON serialization (yes, it is slow!) use the `response` key of the schema option as shown in the following example: ```js const opts = { schema: { @@ -281,17 +280,17 @@ fastify.get('/', opts, async (request, reply) => { return { hello: 'world' } }) ``` -Simply by specifying a schema as shown above, serialization can be sped up by a factor of 2 or even 3! This also helps protect against leaking of sensitive data, since Fastify will serialize only the data present in the response schema. +Simply by specifying a schema as shown, you can speed up serialization by a factor of 2-3. This also helps to protect against leakage of potentially sensitive data, since Fastify will serialize only the data present in the response schema. Read [Validation and Serialization](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md) to learn more. ### Extend your server -Fastify is built to be extremely extensible and very minimal. We believe that a bare minimum framework is all that is necessary to make great applications possible.
+Fastify is built to be extremely extensible and minimal, we believe that a bare bones framework is all that is necessary to make great applications possible.
In other words, Fastify is not a "batteries included" framework, and relies on an amazing [ecosystem](https://github.com/fastify/fastify/blob/master/docs/Ecosystem.md)! ### Test your server -Fastify does not offer a testing framework, but we do recommend a way to write your tests that uses the features and the architecture of Fastify.
+Fastify does not offer a testing framework, but we do recommend a way to write your tests that uses the features and architecture of Fastify.
Read the [testing](https://github.com/fastify/fastify/blob/master/docs/Testing.md) documentation to learn more! diff --git a/docs/HTTP2.md b/docs/HTTP2.md index a02507df1b9..57b23700172 100644 --- a/docs/HTTP2.md +++ b/docs/HTTP2.md @@ -35,7 +35,7 @@ fastify.get('/', function (request, reply) { fastify.listen(3000) ``` -ALPN negotiation allows to support both HTTPS and HTTP/2 over the same socket. +ALPN negotiation allows support for both HTTPS and HTTP/2 over the same socket. Node core `req` and `res` objects can be either [HTTP/1](https://nodejs.org/api/http.html) or [HTTP/2](https://nodejs.org/api/http2.html). _Fastify_ supports this out of the box: diff --git a/docs/Hooks.md b/docs/Hooks.md index e1ed437da7e..43eebf1ac59 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -2,9 +2,9 @@ ## Hooks -Hooks are registered with the `fastify.addHook` method and allow you to listen to specific events in the application or request/response lifecycle. You have to register a hook before the event is triggered otherwise the event is lost. +Hooks are registered with the `fastify.addHook` method and allow you to listen to specific events in the application or request/response lifecycle. You have to register a hook before the event is triggered, otherwise the event is lost. -By using the hooks you can interact directly inside the lifecycle of Fastify. There are Request/Reply hooks and application hooks: +By using hooks you can interact directly with the lifecycle of Fastify. There are Request/Reply hooks and application hooks: - [Request/Reply Hooks](#requestreply-hooks) - [onRequest](#onRequest) @@ -20,7 +20,7 @@ By using the hooks you can interact directly inside the lifecycle of Fastify. Th - [onRoute](#onroute) - [onRegister](#onregister) -**Notice:** the `done` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `done` callback in this situation unexpected behavior may occur, e.g. duplicate invocation of handlers. +**Notice:** the `done` callback is not available when using `async`/`await` or returning a `Promise`. If you do invoke a `done` callback in this situation unexpected behaviour may occur, e.g. duplicate invocation of handlers. ## Request/Reply Hooks @@ -35,18 +35,18 @@ There are eight different hooks that you can use in Request/Reply *(in order of ### onRequest ```js fastify.addHook('onRequest', (request, reply, done) => { - // some code + // Some code done() }) ``` Or `async/await`: ```js fastify.addHook('onRequest', async (request, reply) => { - // some code + // Some code await asyncMethod() - // error occurred + // Error occurred if (err) { - throw new Error('some errors occurred.') + throw new Error('Some errors occurred.') } return }) @@ -57,18 +57,18 @@ fastify.addHook('onRequest', async (request, reply) => { ### preParsing ```js fastify.addHook('preParsing', (request, reply, done) => { - // some code + // Some code done() }) ``` Or `async/await`: ```js fastify.addHook('preParsing', async (request, reply) => { - // some code + // Some code await asyncMethod() - // error occurred + // Error occurred if (err) { - throw new Error('some errors occurred.') + throw new Error('Some errors occurred.') } return }) @@ -76,18 +76,18 @@ fastify.addHook('preParsing', async (request, reply) => { ### preValidation ```js fastify.addHook('preValidation', (request, reply, done) => { - // some code + // Some code done() }) ``` Or `async/await`: ```js fastify.addHook('preValidation', async (request, reply) => { - // some code + // Some code await asyncMethod() - // error occurred + // Error occurred if (err) { - throw new Error('some errors occurred.') + throw new Error('Some errors occurred.') } return }) @@ -104,11 +104,11 @@ fastify.addHook('preHandler', (request, reply, done) => { Or `async/await`: ```js fastify.addHook('preHandler', async (request, reply) => { - // some code + // Some code await asyncMethod() - // error occurred + // Error occurred if (err) { - throw new Error('some errors occurred.') + throw new Error('Some errors occurred.') } return }) @@ -119,37 +119,37 @@ If you are using the `preSerialization` hook, you can change (or replace) the pa ```js fastify.addHook('preSerialization', (request, reply, payload, done) => { - var err = null; - var newPayload = { wrapped: payload } + const err = null; + const newPayload = { wrapped: payload } done(err, newPayload) }) ``` Or `async/await` ```js fastify.addHook('preSerialization', async (request, reply, payload) => { - return {wrapped: payload } + return { wrapped: payload } }) ``` -Note: the hook is NOT called if the payload is a `string`, a `Buffer`, a `stream`, or `null`. +Note: the hook is NOT called if the payload is a `string`, a `Buffer`, a `stream` or `null`. ### onError ```js fastify.addHook('onError', (request, reply, error, done) => { - // some code + // Some code done() }) ``` Or `async/await`: ```js fastify.addHook('onError', async (request, reply, error) => { - // useful for custom error logging - // you should not use this hook to update the error + // Useful for custom error logging + // You should not use this hook to update the error }) ``` This hook is useful if you need to do some custom error logging or add some specific header in case of error.
It is not intended for changing the error, and calling `reply.send` will throw an exception.
-This hook will be executed only after the `customErrorHandler` has been executed, and only if the `customErrorHandler` sends back an error to the user *(Note that the default `customErrorHandler` always send back the error to the user)*.
+This hook will be executed only after the `customErrorHandler` has been executed, and only if the `customErrorHandler` sends an error back to the user *(Note that the default `customErrorHandler` always sends the error back to the user)*.
**Notice:** unlike the other hooks, pass an error to the `done` function is not supported. ### onSend @@ -157,15 +157,15 @@ If you are using the `onSend` hook, you can change the payload. For example: ```js fastify.addHook('onSend', (request, reply, payload, done) => { - var err = null; - var newPayload = payload.replace('some-text', 'some-new-text') + const err = null; + const newPayload = payload.replace('some-text', 'some-new-text') done(err, newPayload) }) ``` Or `async/await`: ```js fastify.addHook('onSend', async (request, reply, payload) => { - var newPayload = payload.replace('some-text', 'some-new-text') + const newPayload = payload.replace('some-text', 'some-new-text') return newPayload }) ``` @@ -189,31 +189,31 @@ Note: If you change the payload, you may only change it to a `string`, a `Buffer ```js fastify.addHook('onResponse', (request, reply, done) => { - // some code + // Some code done() }) ``` Or `async/await`: ```js fastify.addHook('onResponse', async (request, reply) => { - // some code + // Some code await asyncMethod() - // error occurred + // Error occurred if (err) { - throw new Error('some errors occurred.') + throw new Error('Some errors occurred.') } return }) ``` -The `onResponse` hook is executed when a response has been sent, so you will not be able to send more data to the client, however you can use this hook to send some data to an external service or elaborate some statistics. +The `onResponse` hook is executed when a response has been sent, so you will not be able to send more data to the client. It can however be useful for sending data to external services, for example to gather statistics. ### Manage Errors from a hook If you get an error during the execution of your hook, just pass it to `done()` and Fastify will automatically close the request and send the appropriate error code to the user. ```js fastify.addHook('onRequest', (request, reply, done) => { - done(new Error('some error')) + done(new Error('Some error')) }) ``` @@ -221,19 +221,18 @@ If you want to pass a custom error code to the user, just use `reply.code()`: ```js fastify.addHook('preHandler', (request, reply, done) => { reply.code(400) - done(new Error('some error')) + done(new Error('Some error')) }) ``` *The error will be handled by [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md#errors).* - ### Respond to a request from a hook -If needed, you can respond to a request before you reach the route handler. An example could be an authentication hook. If you are using `onRequest` or `preHandler` use `reply.send`; if you are using a middleware, `res.end`. +If needed, you can respond to a request before you reach the route handler, for example when implementing an authentication hook. If you are using `onRequest` or `preHandler` use `reply.send`; if you are using a middleware, use `res.end`. ```js fastify.addHook('onRequest', (request, reply, done) => { - reply.send('early response') + reply.send('Early response') }) // Works with async functions too @@ -253,7 +252,7 @@ fastify.addHook('onRequest', (request, reply, done) => { ## Application Hooks -You are able to hook into the application-lifecycle as well. It's important to note that these hooks aren't fully encapsulated. The `this` inside the hooks are encapsulated but the handlers can respond to an event outside the encapsulation boundaries. +You can hook into the application-lifecycle as well. It's important to note that these hooks aren't fully encapsulated. The `this` inside the hooks are encapsulated but the handlers can respond to an event outside the encapsulation boundaries. - [onClose](#onclose) - [onRoute](#onroute) @@ -262,11 +261,11 @@ You are able to hook into the application-lifecycle as well. It's important to n ### onClose -Triggered when `fastify.close()` is invoked to stop the server. It is useful when [plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins.md) need a "shutdown" event, such as a connection to a database.
+Triggered when `fastify.close()` is invoked to stop the server. It is useful when [plugins](https://github.com/fastify/fastify/blob/master/docs/Plugins.md) need a "shutdown" event, for example to close an open connection to a database.
The first argument is the Fastify instance, the second one the `done` callback. ```js fastify.addHook('onClose', (instance, done) => { - // some code + // Some code done() }) ``` @@ -275,7 +274,7 @@ fastify.addHook('onClose', (instance, done) => { Triggered when a new route is registered. Listeners are passed a `routeOptions` object as the sole parameter. The interface is synchronous, and, as such, the listeners do not get passed a callback. ```js fastify.addHook('onRoute', (routeOptions) => { - // some code + //Some code routeOptions.method routeOptions.schema routeOptions.url @@ -286,7 +285,7 @@ fastify.addHook('onRoute', (routeOptions) => { ``` ### onRegister -Triggered when a new plugin function is registered, and a new encapsulation context is created, the hook will be executed **before** the plugin code.
+Triggered when a new plugin is registered and a new encapsulation context is created. The hook will be executed **before** the registered code.
This hook can be useful if you are developing a plugin that needs to know when a plugin context is formed, and you want to operate in that specific context.
**Note:** This hook will not be called if a plugin is wrapped inside [`fastify-plugin`](https://github.com/fastify/fastify-plugin). ```js @@ -307,7 +306,7 @@ fastify.register(async (instance, opts) => { }) fastify.addHook('onRegister', (instance) => { - // create a new array from the old one + // Create a new array from the old one // but without keeping the reference // allowing the user to have encapsulated // instances of the `data` property @@ -328,17 +327,16 @@ fastify.addHook('onRequest', function (request, reply, done) { Note: using an arrow function will break the binding of this to the Fastify instance. + ## Route level hooks You can declare one or more custom [onRequest](#onRequest), [onReponse](#onResponse), [preParsing](#preParsing), [preValidation](#preValidation), [preHandler](#preHandler) and [preSerialization](#preSerialization) hook(s) that will be **unique** for the route. -If you do so, those hooks always be executed as last hook in their category.
-This can be useful if you need to run the authentication, and the [preParsing](#preParsing) or [preValidation](#preValidation) hooks are exactly what you need for doing that. +If you do so, those hooks are always executed as the last hook in their category.
+This can be useful if you need to implement authentication, where the [preParsing](#preParsing) or [preValidation](#preValidation) hooks are exactly what you need. Multiple route-level hooks can also be specified as an array. -Let's make an example: - ```js fastify.addHook('onRequest', (request, reply, done) => { - // your code + // Your code done() }) @@ -348,22 +346,22 @@ fastify.addHook('onResponse', (request, reply, done) => { }) fastify.addHook('preParsing', (request, reply, done) => { - // your code + // Your code done() }) fastify.addHook('preValidation', (request, reply, done) => { - // your code + // Your code done() }) fastify.addHook('preHandler', (request, reply, done) => { - // your code + // Your code done() }) fastify.addHook('preSerialization', (request, reply, payload, done) => { - // your code + // Your code done() }) @@ -372,7 +370,7 @@ fastify.route({ url: '/', schema: { ... }, onRequest: function (request, reply, done) { - // this hook will always be executed after the shared `onRequest` hooks + // This hook will always be executed after the shared `onRequest` hooks done() }, onResponse: function (request, reply, done) { @@ -380,25 +378,25 @@ fastify.route({ done() }, preParsing: function (request, reply, done) { - // this hook will always be executed after the shared `preParsing` hooks + // This hook will always be executed after the shared `preParsing` hooks done() }, preValidation: function (request, reply, done) { - // this hook will always be executed after the shared `preValidation` hooks + // This hook will always be executed after the shared `preValidation` hooks done() }, preHandler: function (request, reply, done) { - // this hook will always be executed after the shared `preHandler` hooks + // This hook will always be executed after the shared `preHandler` hooks done() }, // // Example with an array. All hooks support this syntax. // // preHandler: [function (request, reply, done) { - // // this hook will always be executed after the shared `preHandler` hooks + // // This hook will always be executed after the shared `preHandler` hooks // done() // }], preSerialization: (request, reply, payload, done) => { - // manipulate the payload + // Manipulate the payload done(null, payload) }, handler: function (request, reply) { diff --git a/docs/Logging.md b/docs/Logging.md index 3bc68f95bf2..63ca38516f3 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -4,12 +4,12 @@ Logging is disabled by default, and you can enable it by passing `{ logger: true }` or `{ logger: { level: 'info' } }` when you create -the fastify instance. Note that if the logger is disabled, it is impossible to +a fastify instance. Note that if the logger is disabled, it is impossible to enable it at runtime. We use [abstract-logging](https://www.npmjs.com/package/abstract-logging) for this purpose. -Since Fastify is really focused on performances, it uses [pino](https://github.com/pinojs/pino) as its logger, with the default log level, when enabled, set to `'info'`. +Since Fastify is focused on performance, it uses [pino](https://github.com/pinojs/pino) as its logger, with the default log level, when enabled, set to `'info'`. Enabling the logger is extremely easy: @@ -24,14 +24,14 @@ fastify.get('/', options, function (request, reply) { }) ``` -If you want to pass some options to the logger, just pass the logger option to Fastify. -You can find all the options in the [Pino documentation](https://github.com/pinojs/pino/blob/master/docs/api.md#pinooptions-stream). If you want to specify a file destination, use: +If you want to pass some options to the logger, just pass them to Fastify. +You can find all available options in the [Pino documentation](https://github.com/pinojs/pino/blob/master/docs/api.md#pinooptions-stream). If you want to specify a file destination, use: ```js const fastify = require('fastify')({ logger: { level: 'info', - file: '/path/to/file' // will use pino.destination() + file: '/path/to/file' // Will use pino.destination() } }) @@ -41,7 +41,7 @@ fastify.get('/', options, function (request, reply) { }) ``` -If you want to pass a custom stream to the Pino instance, just add the stream field to the logger object. +If you want to pass a custom stream to the Pino instance, just add a stream field to the logger object. ```js const split = require('split2') @@ -57,9 +57,9 @@ const fastify = require('fastify')({ -By default fastify adds an id to every request for easier tracking. If the "request-id" header is present its value is used, otherwise a new incremental id is generated. See Fastify Factory [`requestIdHeader`](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-request-id-header) and Fastify Factory [`genReqId`](https://github.com/fastify/fastify/blob/master/docs/Server.md#gen-request-id) for customization options. +By default, fastify adds an id to every request for easier tracking. If the "request-id" header is present its value is used, otherwise a new incremental id is generated. See Fastify Factory [`requestIdHeader`](https://github.com/fastify/fastify/blob/master/docs/Server.md#factory-request-id-header) and Fastify Factory [`genReqId`](https://github.com/fastify/fastify/blob/master/docs/Server.md#gen-request-id) for customization options. -The default logger is configured with a set of standard serializers that serialize objects with `req`, `res`, and `err` properties. This behavior can be customized by specifying custom serializers. +The default logger is configured with a set of standard serializers that serialize objects with `req`, `res`, and `err` properties. This behaviour can be customized by specifying custom serializers. ```js const fastify = require('fastify')({ logger: { @@ -79,7 +79,7 @@ const fastify = require('fastify')({ prettyPrint: true, serializers: { res(res) { - // the default + // The default return { statusCode: res.statusCode } @@ -103,7 +103,7 @@ const fastify = require('fastify')({ ``` **Note**: The body not can serialize inside `req` method, because the request is serialized when we create the child logger. At that time, the body is not parsed yet. -See a approach to log `req.body` +See an approach to log `req.body` ```js app.addHook('preHandler', function (req, reply, done) { diff --git a/docs/Middleware.md b/docs/Middleware.md new file mode 100644 index 00000000000..24ba8073f79 --- /dev/null +++ b/docs/Middleware.md @@ -0,0 +1,59 @@ +

Fastify

+ +## Middleware + +Fastify provides an asynchronous [middleware engine](https://github.com/fastify/middie) out-of-the-box, which is compatible with [Express](https://expressjs.com/) and [Restify](http://restify.com/) middleware. + +*For help with understanding when middleware is executed, take a look at the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md) page.* + +Fastify middleware don't support the full syntax `middleware(err, req, res, next)`, because error handling is done inside Fastify. +Furthermore, methods added by Express and Restify to the enhanced versions of `req` and `res` are not supported in Fastify middlewares. + +Also, if you are using middleware that bundles different, smaller middleware, such as [*helmet*](https://helmetjs.github.io/), we recommend using the single modules for better performance. + +```js +fastify.use(require('cors')()) +fastify.use(require('dns-prefetch-control')()) +fastify.use(require('frameguard')()) +fastify.use(require('hide-powered-by')()) +fastify.use(require('hsts')()) +fastify.use(require('ienoopen')()) +fastify.use(require('x-xss-protection')()) +``` + +or, in the specific case of *helmet*, you can use the [*fastify-helmet*](https://github.com/fastify/fastify-helmet) [plugin](Plugins.md), which is an optimized helmet integration for fastify: + +```js +const fastify = require('fastify')() +const helmet = require('fastify-helmet') + +fastify.register(helmet) +``` + +Remember that middleware can be encapsulated, this means that you can decide where your middleware should run by using `register` as explained in the [plugins guide](https://github.com/fastify/fastify/blob/master/docs/Plugins-Guide.md). + +Fastify middleware also do not expose the `send` method or other methods specific to the Fastify [Reply]('./Reply.md' "Reply") instance. This is because Fastify wraps the incoming `req` and `res` Node instances using the [Request](./Request.md "Request") and [Reply](./Reply.md "Reply") objects internally, but this is done after the middleware phase. If you need to create middleware, you have to use the Node `req` and `res` instances. Otherwise, you can use the `preHandler` hook which already has the [Request](./Request.md "Request") and [Reply](./Reply.md "Reply") Fastify instances. For more information, see [Hooks](./Hooks.md "Hooks"). + + +#### Restrict middleware execution to a certain path(s) +If you need to run a middleware only under certain path(s), just pass the path as first parameter to `use` and you are done! + +*Note that this does not support routes with parameters, (eg: `/user/:id/comments`) and wildcards are not supported in multiple paths.* + +```js +const path = require('path') +const serveStatic = require('serve-static') + +// Single path +fastify.use('/css', serveStatic(path.join(__dirname, '/assets'))) + +// Wildcard path +fastify.use('/css/*', serveStatic(path.join(__dirname, '/assets'))) + +// Multiple paths +fastify.use(['/css', '/js'], serveStatic(path.join(__dirname, '/assets'))) +``` + + +#### Express middleware compatibility +Express modifies the prototype of the node core Request and Response objects heavily so Fastify cannot guarantee full middleware compatibility. Express specific functionality such as `res.sendFile()`, `res.send()` or `express.Router()` instances will not work with Fastify. For example, [cors](https://github.com/expressjs/cors) is compatible while [passport](https://github.com/jaredhanson/passport) is not. diff --git a/docs/Middlewares.md b/docs/Middlewares.md deleted file mode 100644 index dfe3d2069ec..00000000000 --- a/docs/Middlewares.md +++ /dev/null @@ -1,59 +0,0 @@ -

Fastify

- -## Middlewares - -Fastify out of the box provides an asynchronous [middleware engine](https://github.com/fastify/middie) compatible with [Express](https://expressjs.com/) and [Restify](http://restify.com/) middlewares. - -*If you need a visual feedback to understand when the middlewares are executed take a look to the [lifecycle](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md) page.* - -Fastify middlewares don't support the full syntax `middleware(err, req, res, next)`, because error handling is done inside Fastify. -Furthermore methods added by Express and Restify to the enhanced versions of `req` and `res` are not supported in Fastify middlewares. - -Also, if you are using a middleware that bundles different, smaller middlewares, such as [*helmet*](https://helmetjs.github.io/), we recommend to use the single modules to get better performances. - -```js -fastify.use(require('cors')()) -fastify.use(require('dns-prefetch-control')()) -fastify.use(require('frameguard')()) -fastify.use(require('hide-powered-by')()) -fastify.use(require('hsts')()) -fastify.use(require('ienoopen')()) -fastify.use(require('x-xss-protection')()) -``` - -or, in the specific case of *helmet*, you can use the [*fastify-helmet*](https://github.com/fastify/fastify-helmet) [plugin](Plugins.md), which is an optimized helmet integration for fastify: - -```js -const fastify = require('fastify')() -const helmet = require('fastify-helmet') - -fastify.register(helmet) -``` - -Remember that middlewares can be encapsulated, this means that you can decide where your middlewares should run by using `register` as explained in the [plugins guide](https://github.com/fastify/fastify/blob/master/docs/Plugins-Guide.md). - -Fastify middlewares also do not expose the `send` method or other methods specific to the Fastify [Reply]('./Reply.md' "Reply") instance. This is because Fastify wraps the incoming `req` and `res` Node instances using the [Request](./Request.md "Request") and [Reply](./Reply.md "Reply") objects internally, but this is done after the middlewares phase. If you need to create a middleware you have to use the Node `req` and `res` instances. Otherwise, you can use the `preHandler` hook that has the [Request](./Request.md "Request") and [Reply](./Reply.md "Reply") Fastify instances. For more information, see [Hooks](./Hooks.md "Hooks"). - - -#### Restrict middleware execution to a certain path(s) -If you need to run a middleware only under certain path(s), just pass the path as first parameter to `use` and you are done! - -*Note that this does not support routes with parameters, (eg: `/user/:id/comments`) and wildcard is not supported in multiple paths.* - -```js -const path = require('path') -const serveStatic = require('serve-static') - -// Single path -fastify.use('/css', serveStatic(path.join(__dirname, '/assets'))) - -// Wildcard path -fastify.use('/css/*', serveStatic(path.join(__dirname, '/assets'))) - -// Multiple paths -fastify.use(['/css', '/js'], serveStatic(path.join(__dirname, '/assets'))) -``` - - -#### Express middleware compatibility -Express modifies the prototype of the node core Request and Response objects heavily so Fastify cannot guarantee full middleware compatibility. Express specific functionality such as `res.sendFile()`, `res.send()` or `express.Router()` instances will not work with Fastify. For example, [cors](https://github.com/expressjs/cors) is compatible while [passport](https://github.com/jaredhanson/passport) is not. diff --git a/docs/Plugins-Guide.md b/docs/Plugins-Guide.md index 1223325346d..1f3ebc8b065 100644 --- a/docs/Plugins-Guide.md +++ b/docs/Plugins-Guide.md @@ -16,30 +16,29 @@ Fastify was built from the beginning to be an extremely modular system. We built ## Register -As in JavaScript everything is an object, in Fastify everything is a plugin.
-Your routes, your utilities and so on are all plugins. To add a new plugin, whatever its functionality is, in Fastify you have a nice and unique api to use: [`register`](https://github.com/fastify/fastify/blob/master/docs/Plugins.md). +As with JavaScript, where everything is an object, in Fastify everything is a plugin.
+Your routes, your utilities and so on are all plugins. To add a new plugin, whatever its functionality may be, in Fastify you have a nice and unique API: [`register`](https://github.com/fastify/fastify/blob/master/docs/Plugins.md). ```js fastify.register( require('./my-plugin'), { options } ) ``` -`register` creates a new Fastify context, this means that if you do any change to the Fastify instance, those changes will not be reflected in the context's ancestors. In other words, encapsulation! - +`register` creates a new Fastify context, which means that if you perform any changes on the Fastify instance, those changes will not be reflected in the context's ancestors. In other words, encapsulation! *Why is encapsulation important?*
-Well, let's say you are creating a new disruptive startup, what do you do? You create an api server with all your stuff, everything in the same place, a monolith!
-Ok, you are growing very fast and you want to change your architecture and try microservices. Usually this implies a huge amount of work, because of cross dependencies and the lack of separation of concerns.
-Fastify helps you a lot in this direction, because thanks to the encapsulation model it will completely avoid cross dependencies, and will help you structure your code in cohesive blocks. +Well, let's say you are creating a new disruptive startup, what do you do? You create an API server with all your stuff, everything in the same place, a monolith!
+Ok, you are growing very fast and you want to change your architecture and try microservices. Usually, this implies a huge amount of work, because of cross dependencies and a lack of separation of concerns in the codebase.
+Fastify helps you in that regard. Thanks to the encapsulation model it will completely avoid cross dependencies, and will help you structure your code into cohesive blocks. *Let's return to how to correctly use `register`.*
As you probably know, the required plugins must expose a single function with the following signature ```js module.exports = function (fastify, options, done) {} ``` -Where `fastify` is (pretty obvious) the encapsulated Fastify instance, `options` is the options object and `done` is the function you **must** call when your plugin is ready. +Where `fastify` is the encapsulated Fastify instance, `options` is the options object and `done` is the function you **must** call when your plugin is ready. -Fastify's plugin model is fully reentrant and graph-based, it handles without any kind of problem asynchronous code and it guarantees the load order of the plugins, even the close order! *How?* Glad you asked, checkout [`avvio`](https://github.com/mcollina/avvio)! Fastify starts loading the plugin __after__ `.listen()`, `.inject()` or `.ready()` are called. +Fastify's plugin model is fully reentrant and graph-based, it handles asynchronous code without any problems and it enforces both the load and close order of plugins. *How?* Glad you asked, check out [`avvio`](https://github.com/mcollina/avvio)! Fastify starts loading the plugin __after__ `.listen()`, `.inject()` or `.ready()` are called. Inside a plugin you can do whatever you want, register routes, utilities (we'll see this in a moment) and do nested registers, just remember to call `done` when everything is set up! ```js @@ -52,11 +51,11 @@ module.exports = function (fastify, options, done) { } ``` -Well, now you know how to use the `register` api and how it works, but how do we add new functionality to Fastify and even better, share them with other developers? +Well, now you know how to use the `register` API and how it works, but how do we add new functionality to Fastify and even better, share them with other developers? ## Decorators -Okay, let's say that you wrote an utility that is so good that you decided to make it available along all your code. How would you do it? Probably something like the following: +Okay, let's say that you wrote a utility that is so good that you decided to make it available along with all your code. How would you do it? Probably something like the following: ```js // your-awesome-utility.js module.exports = function (a, b) { @@ -65,40 +64,42 @@ module.exports = function (a, b) { ``` ```js const util = require('./your-awesome-utility') -console.log(util('that is ', ' awesome')) +console.log(util('that is ', 'awesome')) ``` -And now you will import your utility in every file you need it. (And don't forget that you will probably also need it in your test). +And now you will import your utility in every file you need it in. (And don't forget that you will probably also need it in your tests). -Fastify offers you a way nicer and elegant way to do this, *decorators*. -Create a decorator is extremely easy, just use the [`decorate`](https://github.com/fastify/fastify/blob/master/docs/Decorators.md) api: +Fastify offers you a more elegant and comfortable way to do this, *decorators*. +Creating a decorator is extremely easy, just use the [`decorate`](https://github.com/fastify/fastify/blob/master/docs/Decorators.md) API: ```js fastify.decorate('util', (a, b) => a + b) ``` -Now you can access your utility just by doing `fastify.util` whenever you need it, even inside your test.
-And here's starts the magic; do you remember that few lines above we talked about encapsulation? Well, using `register` and `decorate` in conjunction enable exactly that, let me show you an example to clarify this: +Now you can access your utility just by calling `fastify.util` whenever you need it - even inside your test.
+And here starts the magic; do you remember how just now we were talking about encapsulation? Well, using `register` and `decorate` in conjunction enable exactly that, let me show you an example to clarify this: ```js fastify.register((instance, opts, done) => { instance.decorate('util', (a, b) => a + b) - console.log(instance.util('that is ', ' awesome')) + console.log(instance.util('that is ', 'awesome')) done() }) fastify.register((instance, opts, done) => { - console.log(instance.util('that is ', ' awesome')) // this will throw an error + console.log(instance.util('that is ', 'awesome')) // This will throw an error done() }) ``` Inside the second register call `instance.util` will throw an error, because `util` exists only inside the first register context.
-Let's step back for a moment and get deeper on this: when using the `register` api you will create a new context every time and this avoids situations like the one mentioned few line above. But pay attention, the encapsulation works only for the ancestors and the brothers, but not for the sons. +Let's step back for a moment and dig deeper into this: every time you use the `register` API, a new context is created which avoids the negative situations mentioned above. + +Do note that encapsulation applies to the ancestors and siblings, but not the children. ```js fastify.register((instance, opts, done) => { instance.decorate('util', (a, b) => a + b) - console.log(instance.util('that is ', ' awesome')) + console.log(instance.util('that is ', 'awesome')) fastify.register((instance, opts, done) => { - console.log(instance.util('that is ', ' awesome')) // this will not throw an error + console.log(instance.util('that is ', 'awesome')) // This will not throw an error done() }) @@ -106,14 +107,14 @@ fastify.register((instance, opts, done) => { }) fastify.register((instance, opts, done) => { - console.log(instance.util('that is ', ' awesome')) // this will throw an error + console.log(instance.util('that is ', 'awesome')) // This will throw an error done() }) ``` -*Take home message: if you need that an utility is available in every part of your application, pay attention that is declared at the root scope of your application. Otherwise you can use `fastify-plugin` utility as described [here](#distribution).* +*Take home message: if you need an utility which is available in every part of your application, take care that it's declared in the root scope of your application. If that's not an option you can use the `fastify-plugin` utility as described [here](#distribution).* -`decorate` is not the unique api that you can use to extend the server functionalities, you can also use `decorateRequest` and `decorateReply`. +`decorate` is not the only API that you can use to extend the server functionality, you can also use `decorateRequest` and `decorateReply`. *`decorateRequest` and `decorateReply`? Why do we need them if we already have `decorate`?*
Good question, we added them to make Fastify more developer-friendly. Let's see an example: @@ -128,10 +129,10 @@ fastify.get('/html', (request, reply) => { .send(fastify.html({ hello: 'world' })) }) ``` -It works, but it can be way better! +It works, but it could be way better! ```js fastify.decorateReply('html', function (payload) { - this.type('text/html') // this is the 'Reply' object + this.type('text/html') // This is the 'Reply' object this.send(generateHtml(payload)) }) @@ -161,7 +162,7 @@ fastify.decorateRequest('setHeader', function (header) { this.isHappy = this.headers[header] }) -fastify.decorateRequest('isHappy', false) // this will be added to the Request object prototype, yay speed! +fastify.decorateRequest('isHappy', false) // This will be added to the Request object prototype, yay speed! fastify.addHook('preHandler', (request, reply, done) => { request.setHeader('happy') @@ -173,7 +174,7 @@ fastify.get('/happiness', (request, reply) => { }) ``` -We've seen how to extend server functionality and how handle the encapsulation system, but what if you need to add a function that must be executed every time that the server "[emits](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md)" an event? +We've seen how to extend server functionality and how to handle the encapsulation system, but what if you need to add a function that must be executed every time when the server "[emits](https://github.com/fastify/fastify/blob/master/docs/Lifecycle.md)" an event? ## Hooks @@ -191,9 +192,9 @@ fastify.get('/plugin2', (request, reply) => { reply.send(request) }) ``` -I think we all agree that this is terrible. Code repeat, awful readability and it cannot scale. +I think we all agree that this is terrible. Repeated code, awful readability and it cannot scale. -So what can you do to avoid this annoying issue? Yes, you are right, use an [hook](https://github.com/fastify/fastify/blob/master/docs/Hooks.md)!
+So what can you do to avoid this annoying issue? Yes, you are right, use a [hook](https://github.com/fastify/fastify/blob/master/docs/Hooks.md)!
```js fastify.decorate('util', (request, key, value) => { request[key] = value }) @@ -210,8 +211,8 @@ fastify.get('/plugin2', (request, reply) => { reply.send(request) }) ``` -Now for every request you will run your utility, it is obvious that you can register as many hooks as you need.
-It can happen that you want a hook that must be executed just for a subset of routes, how can you do that? Yep, encapsulation! +Now for every request, you will run your utility. Obviously you can register as many hooks as you need.
+Sometimes you want a hook that should be executed for just a subset of routes, how can you do that? Yep, encapsulation! ```js fastify.register((instance, opts, done) => { @@ -235,13 +236,13 @@ fastify.get('/plugin2', (request, reply) => { ``` Now your hook will run just for the first route! -As you probably noticed at this time, `request` and `reply` are not the standard Nodejs *request* and *response* objects, but Fastify's objects.
+As you probably noticed by now, `request` and `reply` are not the standard Nodejs *request* and *response* objects, but Fastify's objects.
- -## Middlewares -Fastify [supports](https://github.com/fastify/fastify/blob/master/docs/Middlewares.md) out of the box Express/Restify/Connect middlewares, this means that you can just drop-in your old code and it will work! *(faster, by the way)*
-Let's say that you are arriving from Express, and you already have some Middleware that does exactly what you need, and you don't want to redo all the work. -How we can do that? Checkout our middlewares engine, [middie](https://github.com/fastify/middie). + +## Middleware +Fastify [supports](https://github.com/fastify/fastify/blob/master/docs/Middleware.md) Express/Restify/Connect middleware out-of-the-box, which means that you can just drop-in your old code and it will work! *(faster, by the way)*
+Let's say that you are arriving from Express, and you already have some Middleware which does exactly what you need, and you don't want to redo all the work. +How we can do that? Check out our middleware engine, [middie](https://github.com/fastify/middie). ```js const yourMiddleware = require('your-middleware') fastify.use(yourMiddleware) @@ -249,13 +250,12 @@ fastify.use(yourMiddleware) ## How to handle encapsulation and distribution -Perfect, now you know (almost) all the tools that you can use to extend Fastify. But probably there is something you noted when trying out your code.
-How can you distribute your code? +Perfect, now you know (almost) all of the tools that you can use to extend Fastify. But chances are that you came across one big issue: how is distribution handled? -The preferred way to distribute a utility is to wrap all your code inside a `register`, in this way your plugin can support an asynchronous bootstrap *(since `decorate` is a synchronous api)*, in the case of a database connection for example. +The preferred way to distribute a utility is to wrap all your code inside a `register`, in this way your plugin can support asynchronous bootstrapping *(since `decorate` is a synchronous API)*, in the case of a database connection for example. -*Wait, what? Didn't you tell me that `register` creates an encapsulation and that what I create inside there will not be available outside?*
-Yes, I said that. But what I didn't tell you, is that you can tell to Fastify to avoid this behavior, with the [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module. +*Wait, what? Didn't you tell me that `register` creates an encapsulation and that the stuff I create inside will not be available outside?*
+Yes, I said that. But what I didn't tell you, is that you can tell to Fastify to avoid this behaviour, with the [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module. ```js const fp = require('fastify-plugin') const dbClient = require('db-client') @@ -269,7 +269,7 @@ function dbPlugin (fastify, opts, done) { module.exports = fp(dbPlugin) ``` -You can also tell to `fastify-plugin` to check the installed version of Fastify, in case of you need a specific api. +You can also tell `fastify-plugin` to check the installed version of Fastify, in case you need a specific API. As we mentioned earlier, Fastify starts loading its plugins __after__ `.listen()`, `.inject()` or `.ready()` are called and as such, __after__ they have been declared. This means that, even though the plugin may inject variables to the external fastify instance via [`decorate`](https://github.com/fastify/fastify/blob/master/docs/Decorators.md), the decorated variables will not be accessible before calling `.listen()`, `.inject()` or `.ready()`. @@ -295,14 +295,14 @@ In the above example, the `parent` variable of the function passed in as the sec ## Handle errors -It can happen that one of your plugins could fail during the startup. Maybe you expect it and you have a custom logic that will be triggered in that case. How can you do this? -The `after` api is what you need. `after` simply registers a callback that will be executed just after a register, and it can take up to three parameters.
-The callback changes based on the parameters your are giving: +It can happen that one of your plugins fails during startup. Maybe you expect it and you have a custom logic that will be triggered in that case. How can you implement this? +The `after` API is what you need. `after` simply registers a callback that will be executed just after a register, and it can take up to three parameters.
+The callback changes based on the parameters you are giving: 1. If no parameter is given to the callback and there is an error, that error will be passed to the next error handler. 1. If one parameter is given to the callback, that parameter will be the error object. 1. If two parameters are given to the callback, the first will be the error object, the second will be the done callback. -1. If three parameters are given to the callback, the first will be the error object, the second will be the top level context unless you have specified both server and override, in that case the context will be what the override returns, and the third the done callback. +1. If three parameters are given to the callback, the first will be the error object, the second will be the top-level context unless you have specified both server and override, in that case, the context will be what the override returns, and the third the done callback. Let's see how to use it: ```js @@ -317,14 +317,14 @@ fastify ## Let's start! Awesome, now you know everything you need to know about Fastify and its plugin system to start building your first plugin, and please if you do, tell us! We will add it to the [*ecosystem*](https://github.com/fastify/fastify#ecosystem) section of our documentation! -If you want to see some real world example, checkout: +If you want to see some real-world example, check out: - [`point-of-view`](https://github.com/fastify/point-of-view) Templates rendering (*ejs, pug, handlebars, marko*) plugin support for Fastify. - [`fastify-mongodb`](https://github.com/fastify/fastify-mongodb) -Fastify MongoDB connection plugin, with this you can share the same MongoDb connection pool in every part of your server. +Fastify MongoDB connection plugin, with this you can share the same MongoDB connection pool in every part of your server. - [`fastify-multipart`](https://github.com/fastify/fastify-multipart) Multipart support for Fastify - [`fastify-helmet`](https://github.com/fastify/fastify-helmet) Important security headers for Fastify -*Do you feel it's missing something here? Let us know! :)* +*Do you feel like something is missing here? Let us know! :)* From 13a228331af956573bff071df305f0c5d2d1791e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 13 Sep 2019 17:15:32 +0900 Subject: [PATCH 088/144] add FastifyReply#removeHeader method to fastify.d.ts (#1849) * add FastifyReply#removeHeader method to fastify.d.ts * add test * Update test/types/index.ts Co-Authored-By: Manuel Spigolon --- fastify.d.ts | 1 + test/types/index.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/fastify.d.ts b/fastify.d.ts index e32ae493740..4e31f63839d 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -165,6 +165,7 @@ declare namespace fastify { headers(headers: { [key: string]: any }): FastifyReply getHeader(name: string): string | undefined hasHeader(name: string): boolean + removeHeader(name: string): FastifyReply callNotFound(): void getResponseTime(): number type(contentType: string): FastifyReply diff --git a/test/types/index.ts b/test/types/index.ts index ee7bcc68eaa..f6921bd0405 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -665,3 +665,7 @@ const testReplyDecoration: TestReplyDecoration = function () { console.log('can access request from reply decorator', this.request.id) } server4.decorateReply('test-request-accessible-from-reply', testReplyDecoration) + +server4.get('/', (req, reply) => { + reply.removeHeader('x-foo').removeHeader('x-bar').send({}) +}) From 80ddb4ea2dddef828b5c366516b522f19b48e4f0 Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Tue, 17 Sep 2019 23:38:16 -0700 Subject: [PATCH 089/144] randomize file name in test (#1856) To avoid side effects from other processes, avoid using a predictable file name for the socket in the OS tmp directory. --- test/listen.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/listen.test.js b/test/listen.test.js index e8d01c98863..89925ae9b45 100644 --- a/test/listen.test.js +++ b/test/listen.test.js @@ -190,7 +190,7 @@ if (os.platform() !== 'win32') { const fastify = Fastify() t.tearDown(fastify.close.bind(fastify)) - const sockFile = path.join(os.tmpdir(), 'server.sock') + const sockFile = path.join(os.tmpdir(), `${(Math.random().toString(16) + '0000000').substr(2, 8)}-server.sock`) try { fs.unlinkSync(sockFile) } catch (e) { } From d50e173c6ca46a43c2b357de759cdf793ed6a1ee Mon Sep 17 00:00:00 2001 From: Nathan Woltman Date: Thu, 19 Sep 2019 00:04:41 -0400 Subject: [PATCH 090/144] Move Nathan Woltman to Past Collaborators (#1857) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6fb7a1c11c..9f724a35173 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,6 @@ Team members are listed in alphabetical order. * [__Cemre Mengu__](https://github.com/cemremengu), , * [__Manuel Spigolon__](https://github.com/eomm), , * [__James Sumners__](https://github.com/jsumners), , -* [__Nathan Woltman__](https://github.com/nwoltman), , ### Fastify Plugins team * [__Matteo Collina__](https://github.com/mcollina), , @@ -210,6 +209,7 @@ Great contributors on a specific area in the Fastify ecosystem will be invited t **Past Collaborators** * [__Çağatay Çalı__](https://github.com/cagataycali), , * [__Trivikram Kamat__](https://github.com/trivikr), , +* [__Nathan Woltman__](https://github.com/nwoltman), , ## Acknowledgements From 03d2408a2d910df2a4625c590fe15ee1ae0e277a Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Fri, 20 Sep 2019 15:31:53 +0200 Subject: [PATCH 091/144] add github workflow for CI and website, (#1853) add github workflow for CI and website,remove travis --- .github/workflows/ci.yml | 48 +++++++++++++++++++++ .github/workflows/package-manager-ci.yml | 55 ++++++++++++++++++++++++ .github/workflows/website.yml | 16 +++++++ .travis.yml | 20 --------- README.md | 7 ++- docs/LTS.md | 13 +++--- 6 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/package-manager-ci.yml create mode 100644 .github/workflows/website.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..58859d80fc7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + node-version: [6.x, 8.x, 10.x, 12.x] + os: [ubuntu-16.04, windows-2016, macOS-10.14] + + steps: + - uses: actions/checkout@v1 + + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install + run: | + npm install + + - name: Check licenses + run: | + npm run license-checker + + - name: Run tests + run: | + npm run test:report + + - name: Generate coverage + run: | + npm run coveralls -- -c + + - name: Coveralls Parallel + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel: true + + - name: Coveralls Finished + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml new file mode 100644 index 00000000000..d5c729c77ec --- /dev/null +++ b/.github/workflows/package-manager-ci.yml @@ -0,0 +1,55 @@ +name: package-manager-ci + +on: + push: + branches: + - master + +jobs: + pnpm: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # Maintenance and active LTS + node-version: [8.x, 10.x] + os: [ubuntu-16.04] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} in ${{ matrix.os }} + + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install with pnpm + uses: znck/pnpm@v0.1.0-alpha.1 + + - name: Run tests + run: | + npm run test + + yarn: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # Maintenance and active LTS + node-version: [8.x 10.x] + os: [ubuntu-16.04] + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js ${{ matrix.node-version }} in ${{ matrix.os }} + + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install with yarn + uses: Borales/actions-yarn@1.1.0 + + - name: Run tests + run: | + npm run test diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 00000000000..16cad6af310 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,16 @@ +name: website + +on: + push: + branches: + - master + paths: + - 'docs/*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Build website + run: | + curl -X POST --header 'Content-Type: application/json' "https://circleci.com/api/v1.1/project/github/fastify/website?circle-token=${{ secrets.circleci_token }}" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 307b233348c..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: node_js - -node_js: - - "12" - - "11" - - "10" - - "8" - - "6" - -script: - - npm run lint && npm run coveralls -- -c && npm run typescript - - npm run license-checker - -after_script: - - ./tools/website.sh - -notifications: - email: - on_success: never - on_failure: always diff --git a/README.md b/README.md index 9f724a35173..41e30b6af7c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@
-[![Build Status](https://travis-ci.org/fastify/fastify.svg?branch=master)](https://travis-ci.org/fastify/fastify) -[![Build Status](https://dev.azure.com/fastify/fastify/_apis/build/status/fastify.fastify)](https://dev.azure.com/fastify/fastify/_build/latest?definitionId=1) [![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify/badge.svg)](https://snyk.io/test/github/fastify/fastify) +![](https://github.com/fastify/fastify/workflows/ci/badge.svg) +![](https://github.com/fastify/fastify/workflows/package-manager-ci/badge.svg) +![](https://github.com/fastify/fastify/workflows/website/badge.svg) +[![Build Status](https://dev.azure.com/fastify/fastify/_apis/build/status/fastify.fastify)](https://dev.azure.com/fastify/fastify/_build/latest?definitionId=1) +[![Known Vulnerabilities](https://snyk.io/test/github/fastify/fastify/badge.svg)](https://snyk.io/test/github/fastify/fastify) [![Coverage Status](https://coveralls.io/repos/github/fastify/fastify/badge.svg?branch=master)](https://coveralls.io/github/fastify/fastify?branch=master) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) diff --git a/docs/LTS.md b/docs/LTS.md index ba40a30b379..37337de8056 100644 --- a/docs/LTS.md +++ b/docs/LTS.md @@ -40,10 +40,9 @@ A "month" is to be a period of 30 consecutive days. ### CI tested operating systems -| CI | OS | Version | Package Manager | Node.js | -| :-------------- | :------ | :------------- | :-------------- | :------------- | -| Travis | Linux | Ubuntu 14.04 | npm | 6,8,9,10 | -| Azure pipelines | Linux | Ubuntu 16.04 | yarn | ~~6¹~~,8,10,11 | -| Azure pipelines | Windows | vs2017-win2016 | npm | 6,8,10,11 | - -_¹ yarn supports only node >= 8_ +| CI | OS | Version | Package Manager | Node.js | +|----------------|---------|------------------------|---------------------------|-----------| +| Github Actions | Linux | Ubuntu 16.04 | npm | 6,8,10,12 | +| Github Actions | Linux | Ubuntu 16.04 | yarn,pnpm | 8,10 | +| Github Actions | Windows | Windows Server 2016 R2 | npm | 6,8,10,12 | +| Github Actions | MacOS | macOS X Mojave 10.14 | npm | 6,8,10,12 | From bd954ad3bd24d6cc61008cef84ee4e0f1ebfa6d7 Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Fri, 20 Sep 2019 17:42:57 +0200 Subject: [PATCH 092/144] possible fixes (#1861) * possible fixes * generate lcov file for coveralls * run tests only one time * dont generate html report --- .github/workflows/ci.yml | 6 +----- .github/workflows/package-manager-ci.yml | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58859d80fc7..81205fea1e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,11 +29,7 @@ jobs: - name: Run tests run: | - npm run test:report - - - name: Generate coverage - run: | - npm run coveralls -- -c + npm run test:ci - name: Coveralls Parallel uses: coverallsapp/github-action@master diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml index d5c729c77ec..28736629688 100644 --- a/.github/workflows/package-manager-ci.yml +++ b/.github/workflows/package-manager-ci.yml @@ -41,9 +41,9 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} in ${{ matrix.os }} - - uses: actions/setup-node@v1 + - name: Use Node.js + uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} diff --git a/package.json b/package.json index 4be153ee9a3..a615f5c79a8 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test:report": "npm run lint && npm run unit:report && npm run typescript", "test": "npm run lint && npm run unit && npm run typescript", "coverage": "npm run unit -- --cov --coverage-report=html", - "coveralls": "npm run unit -- --cov", + "test:ci": "npm run lint && npm run unit -- --cov --coverage-report=lcovonly && npm run typescript", "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"", "benchmark": "npx concurrently -k -s first \"node ./examples/simple.js\" \"npx autocannon -c 100 -d 5 -p 10 localhost:3000/\"", - "license-checker": "license-checker --production --onlyAllow='MIT;ISC;BSD-3-Clause;BSD-2-Clause'" + "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause\"" }, "repository": { "type": "git", From 7aafd27787442f3a533a6dcca0f55a39eee79f61 Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Fri, 20 Sep 2019 18:23:28 +0200 Subject: [PATCH 093/144] fix (#1862) --- .github/workflows/package-manager-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml index 28736629688..d1542962801 100644 --- a/.github/workflows/package-manager-ci.yml +++ b/.github/workflows/package-manager-ci.yml @@ -17,14 +17,16 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} in ${{ matrix.os }} + - name: Use Node.js uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install with pnpm uses: znck/pnpm@v0.1.0-alpha.1 + with: + args: install - name: Run tests run: | From e2e9236d960edaafa2037bc0e86433fa2de0d44b Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Sat, 21 Sep 2019 00:17:52 +0200 Subject: [PATCH 094/144] Fix/pkg manager ci (#1863) run script with package manager, install eslint as dev dep to avoid typescript-eslint error --- .github/workflows/package-manager-ci.yml | 6 +++--- package.json | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml index d1542962801..c86f9d00369 100644 --- a/.github/workflows/package-manager-ci.yml +++ b/.github/workflows/package-manager-ci.yml @@ -30,7 +30,7 @@ jobs: - name: Run tests run: | - npm run test + pnpm run test yarn: runs-on: ${{ matrix.os }} @@ -38,7 +38,7 @@ jobs: strategy: matrix: # Maintenance and active LTS - node-version: [8.x 10.x] + node-version: [8.x, 10.x] os: [ubuntu-16.04] steps: @@ -54,4 +54,4 @@ jobs: - name: Run tests run: | - npm run test + yarn run test diff --git a/package.json b/package.json index a615f5c79a8..760c47c641c 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,8 @@ }, "devDependencies": { "@types/node": "^11.13.19", - "@typescript-eslint/eslint-plugin": "^1.13.0", - "@typescript-eslint/parser": "^2.0.0", + "@typescript-eslint/eslint-plugin": "^2.3.0", + "@typescript-eslint/parser": "^2.3.0", "JSONStream": "^1.3.5", "ajv-pack": "^0.3.1", "autocannon": "^3.2.0", @@ -99,6 +99,7 @@ "cors": "^2.8.5", "coveralls": "^3.0.6", "dns-prefetch-control": "^0.2.0", + "eslint": "^6.4.0", "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", @@ -127,7 +128,7 @@ "tap": "^12.5.2", "tap-mocha-reporter": "^3.0.7", "then-sleep": "^1.0.1", - "typescript": "^3.5.3", + "typescript": "^3.6.3", "x-xss-protection": "^1.1.0" }, "dependencies": { From af42fe73d0e860993fd206e242e2e6fd4d035803 Mon Sep 17 00:00:00 2001 From: Dustin Deus Date: Sat, 21 Sep 2019 11:46:29 +0200 Subject: [PATCH 095/144] install yarn and pnpm in the host env (#1865) * install yarn and pnpm in the host env * use pnpm to run test * fix typo --- .github/workflows/package-manager-ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/package-manager-ci.yml b/.github/workflows/package-manager-ci.yml index c86f9d00369..f33cb41838b 100644 --- a/.github/workflows/package-manager-ci.yml +++ b/.github/workflows/package-manager-ci.yml @@ -24,9 +24,9 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install with pnpm - uses: znck/pnpm@v0.1.0-alpha.1 - with: - args: install + run: | + curl -L https://unpkg.com/@pnpm/self-installer | node + pnpm install - name: Run tests run: | @@ -50,7 +50,9 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install with yarn - uses: Borales/actions-yarn@1.1.0 + run: | + curl -o- -L https://yarnpkg.com/install.sh | bash + yarn install - name: Run tests run: | From 5a61f3e0b163bfa51df846e29cce6519c94ef5ee Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Mon, 23 Sep 2019 22:01:11 +0200 Subject: [PATCH 096/144] Rules to contributing to plugins (#1842) --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3b52323254..18e99094742 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,17 @@ Code for Fastify's **v1.x** is in [branch 1.x](https://github.com/fastify/fastif Declaring formal releases remains the prerogative of the lead maintainers. Do not bump version numbers in pull requests. +## Plugins + +The contributors to the Fastify's plugins must attend the same rules of the Fastify repository with few adjustements: + +1. A release can be published by any member. +1. The plugin version must follow the [semver](http://semver.org/) specification. +1. The Node.js compatibility must match with the Fastify's master branch. +1. The new release must have the changelog information stored in the GitHub release. + For this scope we suggest to adopt a tool like [`releasify`](https://github.com/fastify/releasify) to archive this. +1. PR opened by bots (like Greenkeeper) can be merged if the CI is green and the Node.js versions supported are the same of the plugin. + ## Changes to this arrangement This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. From 0e39b83c3bde8a9cb6531ad2dbe960be75199e1d Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 25 Sep 2019 14:01:49 +0200 Subject: [PATCH 097/144] Make reply a thenable (#1869) * Make reply a thenable * Docs and more tests * docs and test * reply.send() returns reply * Apply suggestions from code review Co-Authored-By: James Sumners * Clearer examples * Fix Node 6 --- docs/Reply.md | 21 +++++++++++- docs/Routes.md | 27 +++++++++++++-- lib/reply.js | 38 +++++++++++++++++---- test/async-await.js | 66 ++++++++++++++++++++++++++++++++++++ test/internals/reply.test.js | 63 +++++++++++++++++++++++++++++++--- test/promises.test.js | 55 ++++++++++++++++++++++++++++++ 6 files changed, 257 insertions(+), 13 deletions(-) diff --git a/docs/Reply.md b/docs/Reply.md index 2349e161415..d6a7a3fab91 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -22,7 +22,8 @@ - [Errors](#errors) - [Type of the final payload](#type-of-the-final-payload) - [Async-Await and Promises](#async-await-and-promises) - + - [.then](#then) + ### Introduction The second parameter of the handler function is `Reply`. @@ -309,3 +310,21 @@ fastify.get('/teapot', async function (request, reply) => { ``` If you want to know more please review [Routes#async-await](https://github.com/fastify/fastify/blob/master/docs/Routes.md#async-await). + + +### .then(fullfilled, rejected) + +As the name suggests, a `Reply` object can be awaited upon, i.e. `await reply` will wait until the reply is sent. +The `await` syntax calls the `reply.then()`. + +`reply.then(fullfilled, rejected)` accepts two parameters: + +- `fullfilled` will be called when a response has been fully sent, +- `rejected` will be called if the underlying stream had an error, e.g. +the socket has been destroyed. + +For more details, see: + +- https://github.com/fastify/fastify/issues/1864 for the discussion about this feature +- https://promisesaplus.com/ for the definition of thenables +- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then for the signature diff --git a/docs/Routes.md b/docs/Routes.md index 590438d2734..fff6edce903 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -168,7 +168,6 @@ fastify.get('/', options, async function (request, reply) { return processed }) ``` -**Warning:** You can't return `undefined`. For more details read [promise-resolution](#promise-resolution). As you can see we are not calling `reply.send` to send back the data to the user. You just need to return the body and you are done! @@ -180,8 +179,32 @@ fastify.get('/', options, async function (request, reply) { reply.send(processed) }) ``` + +If the route is wrapping a callback-based API that will call +`reply.send()` outside of the promise chain, it is possible to `await reply`: + +```js +fastify.get('/', options, async function (request, reply) { + setImmediate(() => { + reply.send({ hello: 'world' }) + }) + await reply +}) +``` + +Returning reply also works: + +```js +fastify.get('/', options, async function (request, reply) { + setImmediate(() => { + reply.send({ hello: 'world' }) + }) + return reply +}) +``` + **Warning:** -* If you use `return` and `reply.send` at the same time, the first one that happens takes precedence, the second value will be discarded, a *warn* log will also be emitted because you tried to send a response twice. +* When using both `return value` and `reply.send(value)` at the same time, the first one that happens takes precedence, the second value will be discarded, and a *warn* log will also be emitted because you tried to send a response twice. * You can't return `undefined`. For more details read [promise-resolution](#promise-resolution). diff --git a/lib/reply.js b/lib/reply.js index c4a8c6ad5d0..6e354e385a1 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -93,17 +93,17 @@ Reply.prototype.send = function (payload) { if (this[kReplySent]) { this.log.warn({ err: new FST_ERR_REP_ALREADY_SENT() }, 'Reply already sent') - return + return this } if (payload instanceof Error || this[kReplyIsError] === true) { onErrorHook(this, payload, onSendHook) - return + return this } if (payload === undefined) { onSendHook(this, payload) - return + return this } var contentType = getHeader(this, 'content-type') @@ -115,13 +115,13 @@ Reply.prototype.send = function (payload) { this[kReplyHeaders]['content-type'] = CONTENT_TYPE.OCTET } onSendHook(this, payload) - return + return this } if (hasContentType === false && typeof payload === 'string') { this[kReplyHeaders]['content-type'] = CONTENT_TYPE.PLAIN onSendHook(this, payload) - return + return this } } @@ -133,10 +133,12 @@ Reply.prototype.send = function (payload) { } preserializeHook(this, payload) - return + return this } onSendHook(this, payload) + + return this } Reply.prototype.getHeader = function (key) { @@ -237,6 +239,30 @@ Reply.prototype.getResponseTime = function () { return responseTime } +// Make reply a thenable, so it could be used with async/await. +// See +// - https://github.com/fastify/fastify/issues/1864 for the discussions +// - https://promisesaplus.com/ for the definition of thenable +// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then for the signature +Reply.prototype.then = function (fullfilled, rejected) { + if (this.sent) { + fullfilled() + return + } + + eos(this.res, function (err) { + // We must not treat ERR_STREAM_PREMATURE_CLOSE as + // an error because it is created by eos, not by the stream. + if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') { + if (rejected) { + rejected(err) + } + } else { + fullfilled() + } + }) +} + function preserializeHook (reply, payload) { if (reply.context.preSerialization !== null) { onSendHookRunner( diff --git a/test/async-await.js b/test/async-await.js index fa547be18a9..5276dea68f3 100644 --- a/test/async-await.js +++ b/test/async-await.js @@ -205,6 +205,72 @@ function asyncTest (t) { }) }) + test('await reply if we will be calling reply.send in the future', t => { + const lines = ['incoming request', 'request completed'] + t.plan(lines.length + 2) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + t.is(line.msg, lines.shift()) + }) + + const server = Fastify({ + logger: { + stream: splitStream + } + }) + const payload = { hello: 'world' } + + server.get('/', async function awaitMyFunc (req, reply) { + setImmediate(function () { + reply.send(payload) + }) + + await reply + }) + + server.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) + }) + + test('await reply if we will be calling reply.send in the future (error case)', t => { + const lines = ['incoming request', 'kaboom', 'request completed'] + t.plan(lines.length + 2) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + t.is(line.msg, lines.shift()) + }) + + const server = Fastify({ + logger: { + stream: splitStream + } + }) + + server.get('/', async function awaitMyFunc (req, reply) { + setImmediate(function () { + reply.send(new Error('kaboom')) + }) + + await reply + }) + + server.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 500) + }) + }) + test('support reply decorators with await', t => { t.plan(2) diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index e95295c92cf..9e851296962 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -6,6 +6,7 @@ const sget = require('simple-get').concat const http = require('http') const NotFound = require('http-errors').NotFound const Reply = require('../../lib/reply') +const { Writable } = require('readable-stream') const { kReplyErrorHandlerCalled, kReplyHeaders, @@ -36,14 +37,32 @@ test('Once called, Reply should return an object with methods', t => { test('reply.send throw with circular JSON', t => { t.plan(1) - const request = {} - const response = { setHeader: () => {} } - const reply = new Reply(request, response, null) + const response = { + setHeader: () => {}, + hasHeader: () => false, + getHeader: () => undefined, + writeHead: () => {}, + end: () => {} + } + const reply = new Reply(response, { onSend: [] }, null) t.throws(() => { var obj = {} obj.obj = obj reply.send(JSON.stringify(obj)) - }) + }, 'Converting circular structure to JSON') +}) + +test('reply.send returns itself', t => { + t.plan(1) + const response = { + setHeader: () => {}, + hasHeader: () => false, + getHeader: () => undefined, + writeHead: () => {}, + end: () => {} + } + const reply = new Reply(response, { onSend: [] }, null) + t.equal(reply.send('hello'), reply) }) test('reply.serializer should set a custom serializer', t => { @@ -1252,3 +1271,39 @@ test('reply should not call the custom serializer for errors and not found', t = t.strictEqual(res.statusCode, 404) }) }) + +test('reply.then', t => { + t.plan(2) + + function context () {} + function request () {} + + t.test('without an error', t => { + t.plan(1) + + const response = new Writable() + const reply = new Reply(response, context, request) + + reply.then(function () { + t.pass('fullfilled called') + }) + + response.destroy() + }) + + t.test('with an error', t => { + t.plan(1) + + const response = new Writable() + const reply = new Reply(response, context, request) + const _err = new Error('kaboom') + + reply.then(function () { + t.fail('fullfilled called') + }, function (err) { + t.equal(err, _err) + }) + + response.destroy(_err) + }) +}) diff --git a/test/promises.test.js b/test/promises.test.js index 27ad3b611cd..29c21c509a9 100644 --- a/test/promises.test.js +++ b/test/promises.test.js @@ -42,6 +42,24 @@ fastify.get('/double', function (req, reply) { return Promise.resolve({ hello: '42' }) }) +fastify.get('/thenable', opts, function (req, reply) { + setImmediate(function () { + reply.send({ hello: 'world' }) + }) + return reply +}) + +fastify.get('/thenable-error', opts, function (req, reply) { + setImmediate(function () { + reply.send(new Error('kaboom')) + }) + return reply +}) + +fastify.get('/return-reply', opts, function (req, reply) { + return reply.send({ hello: 'world' }) +}) + fastify.listen(0, err => { t.error(err) fastify.server.unref() @@ -82,4 +100,41 @@ fastify.listen(0, err => { t.deepEqual(JSON.parse(body), { hello: '42' }) }) }) + + test('thenable', t => { + t.plan(4) + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/thenable' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-length'], '' + body.length) + t.deepEqual(JSON.parse(body), { hello: 'world' }) + }) + }) + + test('thenable (error)', t => { + t.plan(2) + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/thenable-error' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 500) + }) + }) + + test('return-reply', t => { + t.plan(4) + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/return-reply' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-length'], '' + body.length) + t.deepEqual(JSON.parse(body), { hello: 'world' }) + }) + }) }) From c14aa0764f582767c8cc51c27bbeff849b86c39d Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Wed, 25 Sep 2019 12:31:00 -0300 Subject: [PATCH 098/144] docs(Plugins): removed prefix from list with fastify-plugin (#1819) * docs(Plugins): removed prefix from list with fastify-plugin * docs(Plugins): add notice for logLevel and prefix with fastify-plugin * tests: add test to ignore prefix * fix: removed async to support node6 --- docs/Plugins.md | 2 ++ test/plugin.test.js | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/Plugins.md b/docs/Plugins.md index f331ee9c3b2..7e26ec54d69 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -18,6 +18,8 @@ The optional `options` parameter for `fastify.register` supports a predefined se + [`logLevel`](https://github.com/fastify/fastify/blob/master/docs/Routes.md#custom-log-level) + [`prefix`](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#route-prefixing-options) +**Note: Those options will be ignored when used with fastify-plugin** + It is possible that Fastify will directly support other options in the future. Thus, to avoid collisions, a plugin should consider namespacing its options. For example, a plugin `foo` might be registered like so: ```js diff --git a/test/plugin.test.js b/test/plugin.test.js index 3bc9bac27c9..263dc1d542c 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -18,6 +18,29 @@ test('require a plugin', t => { }) }) +test('plugin metadata - ignore prefix', t => { + t.plan(2) + const fastify = Fastify() + + plugin[Symbol.for('skip-override')] = true + fastify.register(plugin, { prefix: 'foo' }) + + fastify.inject({ + method: 'GET', + url: '/' + }, function (err, res) { + t.error(err) + t.equals(res.payload, 'hello') + }) + + function plugin (instance, opts, next) { + instance.get('/', function (request, reply) { + reply.send('hello') + }) + next() + } +}) + test('fastify.register with fastify-plugin should not incapsulate his code', t => { t.plan(10) const fastify = Fastify() From 72451e901227048707fff157df6abeab6d5e23a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20F=C3=A4cke?= Date: Wed, 25 Sep 2019 17:31:44 +0200 Subject: [PATCH 099/144] Add SerayaEryn to team list (#1867) * Add SerayaEryn to team list * Add SerayaEryn to package.json --- README.md | 1 + package.json | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 41e30b6af7c..a70f48d512e 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ Team members are listed in alphabetical order. * [__Matteo Collina__](https://github.com/mcollina), , * [__Tomas Della Vedova__](https://github.com/delvedor), , * [__Dustin Deus__](https://github.com/StarpTech), , +* [__Denis Fäcke__](https://github.com/SerayaEryn), , * [__Luciano Mammino__](https://github.com/lmammino), , * [__Cemre Mengu__](https://github.com/cemremengu), , * [__Manuel Spigolon__](https://github.com/eomm), , diff --git a/package.json b/package.json index 760c47c641c..3400fc9d422 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,10 @@ { "name": "James Sumners", "url": "https://james.sumners.info" + }, + { + "name": "Denis Fäcke", + "url": "https://github.com/SerayaEryn" } ], "license": "MIT", From 5c00909721c16ceafae46da9378017a401ed96be Mon Sep 17 00:00:00 2001 From: Alik Send Date: Wed, 25 Sep 2019 18:36:26 +0300 Subject: [PATCH 100/144] Don't change resolved schema (#1872) * Don't change resolved schema Fixes #1871 * Restore version --- lib/schemas.js | 2 +- test/internals/schemas.test.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 test/internals/schemas.test.js diff --git a/lib/schemas.js b/lib/schemas.js index d64b2d9cbda..e08e1867add 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -39,7 +39,7 @@ Schemas.prototype.resolve = function (id) { if (this.store[id] === undefined) { throw new FST_ERR_SCH_NOT_PRESENT(id) } - return this.store[id] + return Object.assign({}, this.store[id]) } Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { diff --git a/test/internals/schemas.test.js b/test/internals/schemas.test.js new file mode 100644 index 00000000000..990294daaad --- /dev/null +++ b/test/internals/schemas.test.js @@ -0,0 +1,30 @@ +'use strict' + +const test = require('tap').test +const { Schemas } = require('../../lib/schemas') + +test('Should not change resolved schema', t => { + t.plan(4) + + const schemas = new Schemas() + schemas.add({ + $id: 'A', + field: 'value' + }) + const schema = { + a: 'A#' + } + const resolvedSchema = schemas.resolveRefs(schema) + + t.same(resolvedSchema.a, { + field: 'value' + }) + t.same(resolvedSchema.$id, undefined) + + schemas.getJsonSchemas() + + t.same(resolvedSchema.a, { + field: 'value' + }) + t.same(resolvedSchema.$id, undefined) +}) From ee060b03aa1171bb30a1dd7380fdc68da129965d Mon Sep 17 00:00:00 2001 From: Johannes Ewald Date: Wed, 25 Sep 2019 21:13:12 +0200 Subject: [PATCH 101/144] Fix Plugin type in fastify.d.ts (#1841) According to avvio (https://github.com/mcollina/avvio/blob/3a386683994bfb274355edbb0d7d892e0e066e9a/plugin.js#L89) the `register()` function expects plugins to be either synchronous functions, asynchronous functions with a `callback` argument or asynchronous functions that return promises. --- fastify.d.ts | 7 +++++-- test/types/index.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 4e31f63839d..935a583b3d7 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -21,7 +21,10 @@ declare function fastify(opts?: fastify.ServerOptionsAsSecureHttp2): fastify.Fas // eslint-disable-next-line no-redeclare declare namespace fastify { - type Plugin < HttpServer, HttpRequest, HttpResponse, T > = (instance: FastifyInstance< HttpServer, HttpRequest, HttpResponse >, opts: T, callback: (err?: FastifyError) => void) => void + type Plugin = + PluginInstance extends () => Promise ? + ((instance: FastifyInstance< HttpServer, HttpRequest, HttpResponse >, options: Options) => Promise) : + (instance: FastifyInstance, options: Options, callback: (err?: FastifyError) => void) => void; type Middleware < HttpServer, HttpRequest, HttpResponse > = (this: FastifyInstance, req: HttpRequest, res: HttpResponse, callback: (err?: FastifyError) => void) => void @@ -541,7 +544,7 @@ declare namespace fastify { /** * Registers a plugin */ - register>(plugin: Plugin, opts?: T): FastifyInstance + register, PluginInstance extends Function>(plugin: Plugin, options?: Options): FastifyInstance /** * `Register a callback that will be executed just after a register. diff --git a/test/types/index.ts b/test/types/index.ts index f6921bd0405..0a9a35d0fd5 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -102,6 +102,20 @@ server.use('/', (req, res, next) => { console.log(`${req.method} ${req.url}`) }) +// Third party plugin +// Also check if async functions are allowed to be passed to .register() +// https://github.com/fastify/fastify/pull/1841 +// All function parameters should be inferrable and should not produce 'any' +const thirdPartyPlugin: fastify.Plugin = (instance, options, callback) => {} +const thirdPartyPluginAsync: fastify.Plugin = async (instance, options) => {} + +server.register(thirdPartyPlugin) +server.register(thirdPartyPluginAsync) + +// Custom plugin +server.register((instance, options, callback) => {}) +server.register(async (instance, options) => {}) + /** * Test various hooks and different signatures */ From 27ff97cad56f01d5b5bcb71c0b919a04ae0b3a6a Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:47:02 +0200 Subject: [PATCH 102/144] fix(package): update tiny-lru to version 7.0.0 (#1874) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3400fc9d422..0e303619ce8 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "readable-stream": "^3.1.1", "rfdc": "^1.1.2", "secure-json-parse": "^1.0.0", - "tiny-lru": "^6.0.1" + "tiny-lru": "^7.0.0" }, "greenkeeper": { "ignore": [ From 4cf5c338edcdece7272f9935da91e6b1080eb190 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Fri, 27 Sep 2019 10:55:30 +0200 Subject: [PATCH 103/144] feature: schema resolver (#1858) * quick and dirty schema resolver * add test for body * add not staged edits * add encapsulation test * add mandatory schema compiler for resolver * add docs * Update docs/Validation-and-Serialization.md Co-Authored-By: James Sumners --- docs/Server.md | 5 + docs/Validation-and-Serialization.md | 62 ++++++ fastify.js | 10 + lib/errors.js | 1 + lib/handleRequest.js | 3 +- lib/route.js | 11 +- lib/schemas.js | 22 ++- lib/symbols.js | 1 + lib/validation.js | 4 +- test/schemas.test.js | 271 +++++++++++++++++++++++++++ 10 files changed, 378 insertions(+), 12 deletions(-) create mode 100644 test/schemas.test.js diff --git a/docs/Server.md b/docs/Server.md index 7f169aa61f7..0f042eac774 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -622,6 +622,11 @@ fastify.setReplySerializer(function (payload, statusCode){ #### setSchemaCompiler Set the schema compiler for all routes [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler). + +#### setSchemaResolver +Set the schema `$ref` resolver for all routes [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-resolver). + + #### schemaCompiler This property can be used to set the schema compiler, it is a shortcut for the `setSchemaCompiler` method, and get the schema compiler back for all routes. diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 4429fc68b28..1eeff93bd72 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -301,6 +301,68 @@ In that case the function returned by `schemaCompiler` returns an object like: * `error`: filled with an instance of `Error` or a string that describes the validation error * `value`: the coerced value that passed the validation + +#### Schema Resolver + +The `schemaResolver` is a function that works together with the `schemaCompiler`: you can't use it +with the default schema compiler. This feature is useful when you use complex schemas with `$ref` keyword +in your routes and a custom validator. + +This is needed because all the schemas you add to your custom compiler are unknown to Fastify but it +need to resolve the `$ref` paths. + +```js +const fastify = require('fastify')() +const Ajv = require('ajv') +const ajv = new Ajv() + +ajv.addSchema({ + $id: 'urn:schema:foo', + definitions: { + foo: { type: 'string' } + }, + type: 'object', + properties: { + foo: { $ref: '#/definitions/foo' } + } +}) +ajv.addSchema({ + $id: 'urn:schema:response', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:foo#/definitions/foo' } + } +}) +ajv.addSchema({ + $id: 'urn:schema:request', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:foo#/definitions/foo' } + } +}) + +fastify.setSchemaCompiler(schema => ajv.compile(schema)) +fastify.setSchemaResolver((ref) => { + return ajv.getSchema(ref).schema +}) + +fastify.route({ + method: 'POST', + url: '/', + schema: { + body: ajv.getSchema('urn:schema:request').schema, + response: { + '2xx': ajv.getSchema('urn:schema:response').schema + } + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } +}) +``` + ### Serialization Usually you will send your data to the clients via JSON, and Fastify has a powerful tool to help you, [fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify), which is used if you have provided an output schema in the route options. We encourage you to use an output schema, as it will increase your throughput by 100-400% depending on your payload and will prevent accidental disclosure of sensitive information. diff --git a/fastify.js b/fastify.js index ace176f7a15..20a14fbab77 100644 --- a/fastify.js +++ b/fastify.js @@ -13,6 +13,7 @@ const { kHooks, kSchemas, kSchemaCompiler, + kSchemaResolver, kReplySerializerDefault, kContentTypeParser, kReply, @@ -120,6 +121,7 @@ function build (options) { [kHooks]: new Hooks(), [kSchemas]: schemas, [kSchemaCompiler]: null, + [kSchemaResolver]: null, [kReplySerializerDefault]: null, [kContentTypeParser]: new ContentTypeParser(bodyLimit, (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning)), [kReply]: Reply.buildReply(Reply), @@ -171,6 +173,7 @@ function build (options) { addSchema: addSchema, getSchemas: schemas.getSchemas.bind(schemas), setSchemaCompiler: setSchemaCompiler, + setSchemaResolver: setSchemaResolver, setReplySerializer: setReplySerializer, // custom parsers addContentTypeParser: ContentTypeParser.helpers.addContentTypeParser, @@ -415,6 +418,13 @@ function build (options) { return this } + function setSchemaResolver (schemaRefResolver) { + throwIfAlreadyStarted('Cannot call "setSchemaResolver" when fastify instance is already started!') + + this[kSchemaResolver] = schemaRefResolver + return this + } + function setReplySerializer (replySerializer) { throwIfAlreadyStarted('Cannot call "setReplySerializer" when fastify instance is already started!') diff --git a/lib/errors.js b/lib/errors.js index 8f0274f0527..4f06481a1bf 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -55,6 +55,7 @@ createError('FST_ERR_SCH_ALREADY_PRESENT', "Schema with id '%s' already declared createError('FST_ERR_SCH_NOT_PRESENT', "Schema with id '%s' does not exist!") createError('FST_ERR_SCH_DUPLICATE', "Schema with '%s' already present!") createError('FST_ERR_SCH_BUILD', 'Failed building the schema for %s: %s, due error %s') +createError('FST_ERR_SCH_MISSING_COMPILER', 'You must provide a schemaCompiler to route %s %s to use the schemaResolver') /** * wrapThenable diff --git a/lib/handleRequest.js b/lib/handleRequest.js index d037bf94a25..d2d99f5f299 100644 --- a/lib/handleRequest.js +++ b/lib/handleRequest.js @@ -1,7 +1,6 @@ 'use strict' -const validation = require('./validation') -const validateSchema = validation.validate +const { validate: validateSchema } = require('./validation') const { hookRunner, hookIterator } = require('./hooks') const wrapThenable = require('./wrapThenable') diff --git a/lib/route.js b/lib/route.js index 127d9222eaf..558913a0328 100644 --- a/lib/route.js +++ b/lib/route.js @@ -14,7 +14,8 @@ const { beforeHandlerWarning } = require('./warnings') const { codes: { - FST_ERR_SCH_BUILD + FST_ERR_SCH_BUILD, + FST_ERR_SCH_MISSING_COMPILER } } = require('./errors') @@ -24,6 +25,7 @@ const { kHooks, kSchemas, kSchemaCompiler, + kSchemaResolver, kContentTypeParser, kReply, kReplySerializerDefault, @@ -213,13 +215,18 @@ function buildRouting (options) { // not called for every single route. Creating a new one for every route // is going to be very expensive. if (opts.schema) { + if (this[kSchemaCompiler] == null && this[kSchemaResolver]) { + done(new FST_ERR_SCH_MISSING_COMPILER(opts.method, url)) + return + } + try { if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) { const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true }) this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, schemaCache)) } - buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas]) + buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas], this[kSchemaResolver]) } catch (error) { // bubble up the FastifyError instance done(error.code ? error : new FST_ERR_SCH_BUILD(opts.method, url, error.message)) diff --git a/lib/schemas.js b/lib/schemas.js index e08e1867add..40080305284 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -18,7 +18,7 @@ function Schemas () { this.store = {} } -Schemas.prototype.add = function (inputSchema) { +Schemas.prototype.add = function (inputSchema, refResolver) { var schema = fastClone((inputSchema.isFluentSchema || inputSchema[kFluentSchema]) ? inputSchema.valueOf() : inputSchema @@ -32,7 +32,7 @@ Schemas.prototype.add = function (inputSchema) { throw new FST_ERR_SCH_ALREADY_PRESENT(id) } - this.store[id] = this.resolveRefs(schema, true) + this.store[id] = this.resolveRefs(schema, true, refResolver) } Schemas.prototype.resolve = function (id) { @@ -42,7 +42,7 @@ Schemas.prototype.resolve = function (id) { return Object.assign({}, this.store[id]) } -Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { +Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId, refResolver) { // alias query to querystring schema if (routeSchemas.query) { // check if our schema has both querystring and query @@ -65,7 +65,7 @@ Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { try { // this will work only for standard json schemas // other compilers such as Joi will fail - this.traverse(routeSchemas) + this.traverse(routeSchemas, refResolver) // when a plugin uses the 'skip-override' and call addSchema // the same JSON will be pass throug all the avvio tree. In this case @@ -99,15 +99,25 @@ Schemas.prototype.resolveRefs = function (routeSchemas, dontClearId) { return routeSchemas } -Schemas.prototype.traverse = function (schema) { +Schemas.prototype.traverse = function (schema, refResolver) { for (var key in schema) { // resolve the `sharedSchemaId#' only if is not a standard $ref JSON Pointer if (typeof schema[key] === 'string' && key !== '$schema' && key !== '$ref' && schema[key].slice(-1) === '#') { schema[key] = this.resolve(schema[key].slice(0, -1)) + } else if (key === '$ref' && refResolver) { + const refValue = schema[key] + + const refId = refValue.slice(0, refValue.indexOf('#')) + if (refId.length > 0 && !this.store[refId]) { + const resolvedSchema = refResolver(refId) + if (resolvedSchema) { + this.add(resolvedSchema, refResolver) + } + } } if (schema[key] !== null && typeof schema[key] === 'object') { - this.traverse(schema[key]) + this.traverse(schema[key], refResolver) } } } diff --git a/lib/symbols.js b/lib/symbols.js index 649c976d871..fb6777e9541 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -8,6 +8,7 @@ const keys = { kHooks: Symbol('fastify.hooks'), kSchemas: Symbol('fastify.schemas'), kSchemaCompiler: Symbol('fastify.schemaCompiler'), + kSchemaResolver: Symbol('fastify.schemaRefResolver'), kReplySerializerDefault: Symbol('fastify.replySerializerDefault'), kContentTypeParser: Symbol('fastify.contentTypeParser'), kReply: Symbol('fastify.Reply'), diff --git a/lib/validation.js b/lib/validation.js index fea6b863f1b..d2f982e3679 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -22,13 +22,13 @@ function getResponseSchema (responseSchemaDefinition, sharedSchemas) { }, {}) } -function build (context, compile, schemas) { +function build (context, compile, schemas, schemaResolver) { if (!context.schema) { return } generateFluentSchema(context.schema) - context.schema = schemas.resolveRefs(context.schema) + context.schema = schemas.resolveRefs(context.schema, false, schemaResolver) const headers = context.schema.headers diff --git a/test/schemas.test.js b/test/schemas.test.js new file mode 100644 index 00000000000..1d5f4c6b736 --- /dev/null +++ b/test/schemas.test.js @@ -0,0 +1,271 @@ +'use strict' + +const t = require('tap') +const test = t.test +const Fastify = require('..') + +const AJV = require('ajv') +const fastClone = require('rfdc')({ circles: false, proto: true }) + +const schemaUsed = { + $id: 'urn:schema:foo', + definitions: { + foo: { type: 'string' } + }, + type: 'object', + properties: { + foo: { $ref: '#/definitions/foo' } + } +} +const schemaParent = { + $id: 'urn:schema:response', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:foo#/definitions/foo' } + } +} + +const schemaRequest = { + $id: 'urn:schema:request', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:response#/properties/foo' } + } +} + +test('Should use the ref resolver - response', t => { + t.plan(2) + const fastify = Fastify() + const ajv = new AJV() + ajv.addSchema(fastClone(schemaParent)) + ajv.addSchema(fastClone(schemaUsed)) + + fastify.setSchemaCompiler(schema => ajv.compile(schema)) + fastify.setSchemaResolver((ref) => { + t.equals(ref, 'urn:schema:foo') + return ajv.getSchema(ref).schema + }) + + fastify.route({ + method: 'GET', + url: '/', + schema: { + response: { + '2xx': ajv.getSchema('urn:schema:response').schema + } + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + + fastify.ready(t.error) +}) + +test('Should use the ref resolver - body', t => { + t.plan(3) + const fastify = Fastify() + const ajv = new AJV() + ajv.addSchema(fastClone(schemaParent)) + ajv.addSchema(fastClone(schemaUsed)) + + fastify.setSchemaCompiler(schema => ajv.compile(schema)) + fastify.setSchemaResolver((ref) => { + t.equals(ref, 'urn:schema:foo') + return ajv.getSchema(ref).schema + }) + + fastify.route({ + method: 'POST', + url: '/', + schema: { + body: ajv.getSchema('urn:schema:response').schema + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { foo: 'bar' } + }, (err, res) => { + t.error(err) + t.deepEquals(JSON.parse(res.payload), { foo: 'bar' }) + }) +}) + +test('Encapsulation', t => { + t.plan(14) + const fastify = Fastify() + const ajv = new AJV() + ajv.addSchema(fastClone(schemaParent)) + ajv.addSchema(fastClone(schemaUsed)) + + fastify.register((instance, opts, next) => { + instance.setSchemaCompiler(schema => ajv.compile(schema)) + instance.setSchemaResolver((ref) => { + return ajv.getSchema(ref).schema + }) + instance.route({ + method: 'POST', + url: '/', + schema: { + body: ajv.getSchema('urn:schema:response').schema + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + + instance.register((instance, opts, next) => { + instance.route({ + method: 'POST', + url: '/two', + schema: { + body: ajv.getSchema('urn:schema:response').schema + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + next() + }) + next() + }) + + fastify.register((instance, opts, next) => { + instance.route({ + method: 'POST', + url: '/clean', + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + next() + }) + + fastify.ready(err => { + t.error(err) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { foo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 200) + t.deepEquals(JSON.parse(res.payload), { foo: 'bar' }) + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { wrongFoo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 400) + }) + + fastify.inject({ + method: 'POST', + url: '/two', + payload: { foo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 200) + t.deepEquals(JSON.parse(res.payload), { foo: 'bar' }) + }) + + fastify.inject({ + method: 'POST', + url: '/two', + payload: { wrongFoo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 400) + }) + + fastify.inject({ + method: 'POST', + url: '/clean', + payload: { wrongFoo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 200) + t.deepEquals(JSON.parse(res.payload), { foo: 'bar' }) + }) + }) +}) + +test('Schema resolver without schema compiler', t => { + t.plan(2) + const fastify = Fastify() + + fastify.setSchemaResolver(() => { t.fail('the schema resolver will never be called') }) + fastify.route({ + method: 'POST', + url: '/', + schema: {}, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + + fastify.ready(err => { + t.is(err.code, 'FST_ERR_SCH_MISSING_COMPILER') + t.isLike(err.message, /You must provide a schemaCompiler to route POST \/ to use the schemaResolver/) + }) +}) + +test('Triple $ref deep', t => { + t.plan(6) + + const fastify = Fastify() + const ajv = new AJV() + ajv.addSchema(fastClone(schemaParent)) + ajv.addSchema(fastClone(schemaUsed)) + ajv.addSchema(fastClone(schemaRequest)) + + fastify.setSchemaCompiler(schema => ajv.compile(schema)) + fastify.setSchemaResolver((ref) => { + return ajv.getSchema(ref).schema + }) + + fastify.route({ + method: 'POST', + url: '/', + schema: { + body: ajv.getSchema('urn:schema:request').schema, + response: { + '2xx': ajv.getSchema('urn:schema:response').schema + } + }, + handler (req, reply) { + reply.send({ foo: 'bar' }) + } + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { foo: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 200) + t.deepEquals(JSON.parse(res.payload), { foo: 'bar' }) + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { fool: 'bar' } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 400) + t.deepEquals(JSON.parse(res.payload).message, "body should have required property 'foo'") + }) +}) From 718002da4a48a90ff5fa26229011dfcc31f46b57 Mon Sep 17 00:00:00 2001 From: Boris D'Amato Date: Fri, 27 Sep 2019 19:51:51 +0100 Subject: [PATCH 104/144] Fix warning for onError hook (#1876) * Fix warning for onError hook The onError hook has 3 parameters (request, reply, error) and it's currently triggering a warning saying that it has too many arguments. This change adds `onError` the same condition branch as `onSend` and `preSerialization`. --- fastify.js | 2 +- test/hooks-async.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fastify.js b/fastify.js index 20a14fbab77..e7d04c950ea 100644 --- a/fastify.js +++ b/fastify.js @@ -344,7 +344,7 @@ function build (options) { throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!') // TODO: v3 instead of log a warning, throw an error - if (name === 'onSend' || name === 'preSerialization') { + if (name === 'onSend' || name === 'preSerialization' || name === 'onError') { if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) { fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack) } diff --git a/test/hooks-async.js b/test/hooks-async.js index df7201d845c..0ce00e79e1f 100644 --- a/test/hooks-async.js +++ b/test/hooks-async.js @@ -528,7 +528,7 @@ function asyncHookTest (t) { }) t.test('4 arguments', t => { - t.plan(6) + t.plan(9) const stream = split(JSON.parse) const fastify = Fastify({ logger: { stream } @@ -542,6 +542,7 @@ function asyncHookTest (t) { fastify.addHook('onSend', async (req, reply, payload, next) => {}) fastify.addHook('preSerialization', async (req, reply, payload, next) => {}) + fastify.addHook('onError', async (req, reply, error, next) => {}) }) t.end() From f84971f462dac80f7064e617f43f0a0fac4162c4 Mon Sep 17 00:00:00 2001 From: Zoron Date: Sat, 28 Sep 2019 15:01:23 +0800 Subject: [PATCH 105/144] Update Server.md (#1878) fix link: Middlewares.md -> Middleware.md --- docs/Server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Server.md b/docs/Server.md index 0f042eac774..c213acf452b 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -547,7 +547,7 @@ A plugin can be a set of routes, a server decorator or whatever, check [here](ht #### use -Function to add middlewares to Fastify, check [here](https://github.com/fastify/fastify/blob/master/docs/Middlewares.md). +Function to add middlewares to Fastify, check [here](https://github.com/fastify/fastify/blob/master/docs/Middleware.md). #### addHook From 89dd7493c06774600635b010ebac6a622bfd42f2 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 30 Sep 2019 15:48:30 +0200 Subject: [PATCH 106/144] Bumped v2.9.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e303619ce8..583d58ffd6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.8.0", + "version": "2.9.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From f199f9ff8723d7c04b93c6db3a941f675365d75b Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Sat, 5 Oct 2019 18:04:11 +0200 Subject: [PATCH 107/144] fix schema resolver for plain id (#1882) * fix schema resolver for plain id * update fast-json-stringify * add test on response schema * fix ajv compile error * add typings --- fastify.d.ts | 6 +++++ lib/schemas.js | 3 ++- package.json | 2 +- test/schemas.test.js | 53 ++++++++++++++++++++++++++++++++++++++++++++ test/types/index.ts | 7 ++++++ 5 files changed, 69 insertions(+), 2 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 935a583b3d7..10e2d405e38 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -116,6 +116,7 @@ declare namespace fastify { ) => void | Promise type SchemaCompiler = (schema: Object) => Function + type SchemaResolver = (ref: string) => Object type BodyParser = | ((req: HttpRequest, rawBody: RawBody, done: (err: Error | null, body?: any) => void) => void) @@ -680,6 +681,11 @@ declare namespace fastify { */ setSchemaCompiler(schemaCompiler: SchemaCompiler): FastifyInstance + /** + * Set the schema resolver to find the `$ref` schema object + */ + setSchemaResolver(schemaResolver: SchemaResolver): FastifyInstance + /** * Create a shared schema */ diff --git a/lib/schemas.js b/lib/schemas.js index 40080305284..e27062e9348 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -107,7 +107,8 @@ Schemas.prototype.traverse = function (schema, refResolver) { } else if (key === '$ref' && refResolver) { const refValue = schema[key] - const refId = refValue.slice(0, refValue.indexOf('#')) + const framePos = refValue.indexOf('#') + const refId = framePos >= 0 ? refValue.slice(0, framePos) : refValue if (refId.length > 0 && !this.store[refId]) { const resolvedSchema = refResolver(refId) if (resolvedSchema) { diff --git a/package.json b/package.json index 583d58ffd6d..f1a8c55c1cd 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "abstract-logging": "^1.0.0", "ajv": "^6.10.2", "avvio": "^6.2.2", - "fast-json-stringify": "^1.15.4", + "fast-json-stringify": "^1.15.5", "find-my-way": "^2.0.0", "flatstr": "^1.0.12", "light-my-request": "^3.4.1", diff --git a/test/schemas.test.js b/test/schemas.test.js index 1d5f4c6b736..69607dee7ba 100644 --- a/test/schemas.test.js +++ b/test/schemas.test.js @@ -269,3 +269,56 @@ test('Triple $ref deep', t => { t.deepEquals(JSON.parse(res.payload).message, "body should have required property 'foo'") }) }) + +test('$ref with a simple $id', t => { + t.plan(4) + const fastify = Fastify() + const ajv = new AJV() + ajv.addSchema(fastClone(schemaUsed)) + ajv.addSchema({ + $id: 'urn:schema:response', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:foo' } + } + }) + ajv.addSchema({ + $id: 'urn:schema:request', + type: 'object', + required: ['foo'], + properties: { + foo: { $ref: 'urn:schema:foo' } + } + }) + + fastify.setSchemaCompiler(schema => ajv.compile(schema)) + fastify.setSchemaResolver((ref) => { + t.equals(ref, 'urn:schema:foo') + return ajv.getSchema(ref).schema + }) + + fastify.route({ + method: 'POST', + url: '/', + schema: { + body: ajv.getSchema('urn:schema:request').schema, + response: { + '2xx': ajv.getSchema('urn:schema:response').schema + } + }, + handler (req, reply) { + reply.send({ foo: { foo: 'bar', bar: 'foo' } }) + } + }) + + fastify.inject({ + method: 'POST', + url: '/', + payload: { foo: { foo: 'bar' } } + }, (err, res) => { + t.error(err) + t.equals(res.statusCode, 200) + t.deepEquals(JSON.parse(res.payload), { foo: { foo: 'bar' } }) + }) +}) diff --git a/test/types/index.ts b/test/types/index.ts index 0a9a35d0fd5..67423ccb15d 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -558,6 +558,13 @@ server.setSchemaCompiler(function (schema: object) { return () => true }) +server.setSchemaResolver(function (ref: string) { + return { + $id: ref, + type: 'string' + } +}) + server.addSchema({}) server.addContentTypeParser('*', (req, done) => { From 309cc53e58728efdf278c1e462c10e1d12637737 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2019 07:38:26 +0200 Subject: [PATCH 108/144] chore(package): update concurrently to version 5.0.0 (#1886) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1a8c55c1cd..ec81678ff51 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "ajv-pack": "^0.3.1", "autocannon": "^3.2.0", "branch-comparer": "^0.4.0", - "concurrently": "^4.1.2", + "concurrently": "^5.0.0", "cors": "^2.8.5", "coveralls": "^3.0.6", "dns-prefetch-control": "^0.2.0", From 0aef7f323a79835ae6529a4f72c90744a731c74e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 7 Oct 2019 09:33:16 +0200 Subject: [PATCH 109/144] greenkeeper do not update lolex (#1887) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ec81678ff51..2a261ada23f 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,8 @@ "@types/node", "tap", "tap-mocha-reporter", - "@typescript-eslint/eslint-plugin" + "@typescript-eslint/eslint-plugin", + "lolex" ] }, "standard": { From de23870ceedb068289bc5998ce48edaaa98c2246 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Wed, 9 Oct 2019 17:13:30 +0200 Subject: [PATCH 110/144] add .headers to reply docs (#1890) --- docs/Reply.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/Reply.md b/docs/Reply.md index d6a7a3fab91..dd597e471b2 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -5,6 +5,7 @@ - [Introduction](#introduction) - [.code(statusCode)](#codestatuscode) - [.header(key, value)](#headerkey-value) + - [.headers(object)](#headersobject) - [.getHeader(key)](#getheaderkey) - [.removeHeader(key)](#removeheaderkey) - [.hasHeader(key)](#hasheaderkey) @@ -33,6 +34,7 @@ and properties: - `.code(statusCode)` - Sets the status code. - `.status(statusCode)` - An alias for `.code(statusCode)`. - `.header(name, value)` - Sets a response header. +- `.headers(object)` - Sets all the keys of the object as a response headers. - `.getHeader(name)` - Retrieve value of already set header. - `.removeHeader(key)` - Remove the value of a previously set header. - `.hasHeader(name)` - Determine if a header has been set. @@ -76,6 +78,16 @@ to `''`. For more information, see [`http.ServerResponse#setHeader`](https://nodejs.org/dist/latest/docs/api/http.html#http_response_setheader_name_value). + +### .headers(object) +Sets all the keys of the object as response headers. [`.header`](#headerkey-value) will be called under the hood. +```js +reply.headers({ + 'x-foo': 'foo', + 'x-bar': 'bar' +}) +``` + ### .getHeader(key) Retrieves the value of a previously set header. From a2e12110a22d58603400c2f012a7cb1dfce8c932 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Thu, 10 Oct 2019 23:14:08 +0200 Subject: [PATCH 111/144] feature: reply statusCode (#1892) --- docs/Reply.md | 11 +++++++++++ lib/reply.js | 9 +++++++++ test/internals/reply.test.js | 17 +++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/docs/Reply.md b/docs/Reply.md index dd597e471b2..d68c12b9e70 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -4,6 +4,7 @@ - [Reply](#reply) - [Introduction](#introduction) - [.code(statusCode)](#codestatuscode) + - [.statusCode](#statusCode) - [.header(key, value)](#headerkey-value) - [.headers(object)](#headersobject) - [.getHeader(key)](#getheaderkey) @@ -33,6 +34,7 @@ and properties: - `.code(statusCode)` - Sets the status code. - `.status(statusCode)` - An alias for `.code(statusCode)`. +- `.statusCode` - Read and set the HTTP status code. - `.header(name, value)` - Sets a response header. - `.headers(object)` - Sets all the keys of the object as a response headers. - `.getHeader(name)` - Retrieve value of already set header. @@ -71,6 +73,15 @@ fastify.get('/', {config: {foo: 'bar'}}, function (request, reply) { ### .code(statusCode) If not set via `reply.code`, the resulting `statusCode` will be `200`. + +### .statusCode +This property reads and sets the HTTP status code. It is an alias for `reply.code()` when used as a setter. +```js +if (reply.statusCode >= 299) { + reply.statusCode = 500 +} +``` + ### .header(key, value) Sets a response header. If the value is omitted or undefined it is coerced diff --git a/lib/reply.js b/lib/reply.js index 6e354e385a1..32ba0cabb45 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -86,6 +86,15 @@ Object.defineProperty(Reply.prototype, 'sent', { } }) +Object.defineProperty(Reply.prototype, 'statusCode', { + get () { + return this.res.statusCode + }, + set (value) { + this.code(value) + } +}) + Reply.prototype.send = function (payload) { if (this[kReplyIsRunningOnErrorHook] === true) { throw new FST_ERR_SEND_INSIDE_ONERR() diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index 9e851296962..fe05dc2f02c 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -924,6 +924,23 @@ test('.status() is an alias for .code()', t => { }) }) +test('.statusCode is getter and setter', t => { + t.plan(4) + const fastify = require('../..')() + + fastify.get('/', function (req, reply) { + t.ok(reply.statusCode, 200, 'default status value') + reply.statusCode = 418 + t.ok(reply.statusCode, 418) + reply.send() + }) + + fastify.inject('/', (err, res) => { + t.error(err) + t.is(res.statusCode, 418) + }) +}) + test('reply.header setting multiple cookies as multiple Set-Cookie headers', t => { t.plan(7) From 2e03a0709f578de067987473200d91f8d224974d Mon Sep 17 00:00:00 2001 From: delvedor Date: Fri, 11 Oct 2019 09:15:31 +0200 Subject: [PATCH 112/144] Bumped v2.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a261ada23f..12df7f3e120 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify", - "version": "2.9.0", + "version": "2.10.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", "typings": "fastify.d.ts", From 56a9467b027eb0f6c39edd9810429459880cd274 Mon Sep 17 00:00:00 2001 From: Tommaso Allevi Date: Sat, 12 Oct 2019 19:25:15 +0200 Subject: [PATCH 113/144] Test: After can access to decorations registered into plugin (#1891) * test: after can access to decorations registered into plugin * Fix identation * Fix identation #2 --- test/decorator.test.js | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/decorator.test.js b/test/decorator.test.js index d0bb4ad891e..ada71fc2206 100644 --- a/test/decorator.test.js +++ b/test/decorator.test.js @@ -660,3 +660,51 @@ test('a decorator should addSchema to all the encapsulated tree', t => { fastify.ready(t.error) }) + +test('after can access to a decorated instance and previous plugin decoration', t => { + t.plan(11) + const TEST_VALUE = {} + const OTHER_TEST_VALUE = {} + const NEW_TEST_VALUE = {} + + const fastify = Fastify() + + fastify.register(fp(function (instance, options, next) { + instance.decorate('test', TEST_VALUE) + + next() + })).after(function (err, instance, done) { + t.error(err) + t.equal(instance.test, TEST_VALUE) + + instance.decorate('test2', OTHER_TEST_VALUE) + done() + }) + + fastify.register(fp(function (instance, options, next) { + t.equal(instance.test, TEST_VALUE) + t.equal(instance.test2, OTHER_TEST_VALUE) + + instance.decorate('test3', NEW_TEST_VALUE) + + next() + })).after(function (err, instance, done) { + t.error(err) + t.equal(instance.test, TEST_VALUE) + t.equal(instance.test2, OTHER_TEST_VALUE) + t.equal(instance.test3, NEW_TEST_VALUE) + + done() + }) + + fastify.get('/', function (req, res) { + t.equal(this.test, TEST_VALUE) + t.equal(this.test2, OTHER_TEST_VALUE) + res.send({}) + }) + + fastify.inject('/') + .then(response => { + t.equal(response.statusCode, 200) + }) +}) From 63da5f9024f123056a17effb1cd30066b3365d17 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2019 22:24:55 +0100 Subject: [PATCH 114/144] chore(package): update h2url to version 0.2.0 (#1902) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12df7f3e120..bc873fb01ea 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "fluent-schema": "^0.7.4", "form-data": "^2.5.0", "frameguard": "^3.0.0", - "h2url": "^0.1.2", + "h2url": "^0.2.0", "helmet": "^3.20.0", "hide-powered-by": "^1.0.0", "hsts": "^2.1.0", From 3991088b4b22fb04263d197eb271973390f28e6a Mon Sep 17 00:00:00 2001 From: Tom Andrews Date: Sat, 19 Oct 2019 20:29:44 +0100 Subject: [PATCH 115/144] Add type definition for genReqId option (#1899) --- fastify.d.ts | 3 ++- test/types/index.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fastify.d.ts b/fastify.d.ts index 10e2d405e38..7559c571882 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -205,7 +205,8 @@ declare namespace fastify { deriveVersion(req: Object, ctx?: Context) : String, }, modifyCoreObjects?: boolean, - return503OnClosing?: boolean + return503OnClosing?: boolean, + genReqId?: () => number | string } interface ServerOptionsAsSecure extends ServerOptions { https: http2.SecureServerOptions diff --git a/test/types/index.ts b/test/types/index.ts index 67423ccb15d..4cdefdb86fa 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -64,7 +64,13 @@ const cors = require('cors') maxParamLength: 200, querystringParser: (str: string) => ({ str: str, strArray: [str] }), modifyCoreObjects: true, - return503OnClosing: true + return503OnClosing: true, + genReqId: () => { + if (Math.random() > 0.5) { + return Math.random().toString() + } + return Math.random() + } }) // custom types From 7f8c3f2ba7c7f01cd24397976e545dd57e924d47 Mon Sep 17 00:00:00 2001 From: Michael Chris Lopez Date: Sun, 20 Oct 2019 03:30:36 +0800 Subject: [PATCH 116/144] add missing fluent schema link (#1903) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a70f48d512e..a64cbb450df 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ matters to you. * Hooks * Decorators * Validation and Serialization +* Fluent Schema * Lifecycle * Reply * Request From 2d322a3d67addfffeba9cd4b9494c1b057e29f47 Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Mon, 21 Oct 2019 11:47:25 +0200 Subject: [PATCH 117/144] Updates fastify-multer repository (#1904) --- docs/Ecosystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 818c285a80f..b204855de98 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -90,7 +90,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-mongo-memory`](https://github.com/chapuletta/fastify-mongo-memory) Fastify MongoDB in Memory Plugin for testing support. - [`fastify-mongoose-api`](https://github.com/jeka-kiselyov/fastify-mongoose-api) Fastify plugin to create REST API methods based on Mongoose MongoDB models. - [`fastify-mongoose-driver`](https://github.com/alex-ppg/fastify-mongoose) Fastify Mongoose plugin that connects to a MongoDB via the Mongoose plugin with support for Models. -- [`fastify-multer`](https://github.com/fox1t/multer) Multer is a plugin for handling multipart/form-data, which is primarily used for uploading files. +- [`fastify-multer`](https://github.com/fox1t/fastify-multer) Multer is a plugin for handling multipart/form-data, which is primarily used for uploading files. - [`fastify-nats`](https://github.com/mahmed8003/fastify-nats) Plugin to share [NATS](http://nats.io) client across Fastify. - [`fastify-no-additional-properties`](https://github.com/greguz/fastify-no-additional-properties) Add `additionalProperties: false` by default to your JSON Schemas. - [`fastify-no-icon`](https://github.com/jsumners/fastify-no-icon) Plugin to eliminate thrown errors for `/favicon.ico` requests. From 097a0227ba8f5559448265628da9c2bf646cf443 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 24 Oct 2019 09:46:39 +0200 Subject: [PATCH 118/144] Add Node v13 to CI (#1912) --- .github/workflows/ci.yml | 2 +- build/azure-pipelines-npm-template.yml | 2 ++ build/azure-pipelines-yarn-template.yml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81205fea1e4..b6a3c535ebb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [6.x, 8.x, 10.x, 12.x] + node-version: [6.x, 8.x, 10.x, 12.x, 13.x] os: [ubuntu-16.04, windows-2016, macOS-10.14] steps: diff --git a/build/azure-pipelines-npm-template.yml b/build/azure-pipelines-npm-template.yml index 366ecacfc20..e4cd2b559db 100644 --- a/build/azure-pipelines-npm-template.yml +++ b/build/azure-pipelines-npm-template.yml @@ -5,6 +5,8 @@ jobs: strategy: matrix: + node_13_x: + node_version: 13.x node_12_x: node_version: 12.x node_11_x: diff --git a/build/azure-pipelines-yarn-template.yml b/build/azure-pipelines-yarn-template.yml index c4ba38021ed..4c6ec3184e6 100644 --- a/build/azure-pipelines-yarn-template.yml +++ b/build/azure-pipelines-yarn-template.yml @@ -5,6 +5,8 @@ jobs: strategy: matrix: + node_13_x: + node_version: 13.x node_12_x: node_version: 12.x node_11_x: From 8fc4d9dd2774d77e75d44b8b5980d1eb4dd14975 Mon Sep 17 00:00:00 2001 From: Thomas Vogel Date: Thu, 24 Oct 2019 10:43:12 +0200 Subject: [PATCH 119/144] adds fastify-https-redirect to ecosystem (#1911) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index b204855de98..136aea541b9 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -75,6 +75,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-healthcheck`](https://github.com/smartiniOnGitHub/fastify-healthcheck) Fastify plugin to serve an health check route and a probe script. - [`fastify-hemera`](https://github.com/hemerajs/fastify-hemera) Fastify Hemera plugin, for writing reliable & fault-tolerant microservices with [nats.io](https://nats.io/). - [`fastify-http2https`](https://github.com/lolo32/fastify-http2https) Redirect HTTP requests to HTTPS, both using the same port number, or different response on HTTP and HTTPS. +- [`fastify-https-redirect`](https://github.com/tomsvogel/fastify-https-redirect) Fastify plugin for auto redirect from http to https - [`fastify-http-client`](https://github.com/kenuyx/fastify-http-client) Plugin to send HTTP(s) requests. Built upon [urllib](https://github.com/node-modules/urllib). - [`fastify-influxdb`](https://github.com/alex-ppg/fastify-influxdb) Fastify InfluxDB plugin connecting to an InfluxDB instance via the Influx default package. - [`fastify-jwt-authz`](https://github.com/Ethan-Arrowood/fastify-jwt-authz) JWT user scope verifier. From 38560688ca81b7115d700c315a88872fe6e0ce60 Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Thu, 24 Oct 2019 15:29:48 +0200 Subject: [PATCH 120/144] Added support for onConstructorPoisoning (#1910) * Added support for onConstructorPoisoning * Updated dependencies * Updated docs * Updated test * Updated type definitions --- build/build-validation.js | 8 ++ docs/Server.md | 15 ++- fastify.d.ts | 1 + fastify.js | 6 +- lib/configValidator.js | 151 ++++++++++++++++++--------- lib/contentTypeParser.js | 8 +- package.json | 2 +- test/internals/initialConfig.test.js | 2 + test/proto-poisoning.test.js | 76 ++++++++++++++ 9 files changed, 210 insertions(+), 59 deletions(-) diff --git a/build/build-validation.js b/build/build-validation.js index ff1a2df17f6..852c6ce74f0 100644 --- a/build/build-validation.js +++ b/build/build-validation.js @@ -15,9 +15,12 @@ const ajv = new Ajv({ const defaultInitOptions = { bodyLimit: 1024 * 1024, // 1 MiB caseSensitive: true, + disableRequestLogging: false, ignoreTrailingSlash: false, maxParamLength: 100, onProtoPoisoning: 'error', + // TODO v3: default should be 'error' + onConstructorPoisoning: 'ignore', pluginTimeout: 10000, requestIdHeader: 'request-id', requestIdLogLabel: 'reqId' @@ -62,8 +65,13 @@ const schema = { then: { setDefaultValue: true } }, ignoreTrailingSlash: { type: 'boolean', default: defaultInitOptions.ignoreTrailingSlash }, + disableRequestLogging: { + type: 'boolean', + default: false + }, maxParamLength: { type: 'integer', default: defaultInitOptions.maxParamLength }, onProtoPoisoning: { type: 'string', default: defaultInitOptions.onProtoPoisoning }, + onConstructorPoisoning: { type: 'string', default: defaultInitOptions.onConstructorPoisoning }, pluginTimeout: { type: 'integer', default: defaultInitOptions.pluginTimeout }, requestIdHeader: { type: 'string', default: defaultInitOptions.requestIdHeader }, requestIdLogLabel: { type: 'string', default: defaultInitOptions.requestIdLogLabel } diff --git a/docs/Server.md b/docs/Server.md index c213acf452b..a10a176bcad 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -75,7 +75,7 @@ Defines the maximum payload, in bytes, the server is allowed to accept. Defines what action the framework must take when parsing a JSON object with `__proto__`. This functionality is provided by -[bourne](https://github.com/hapijs/bourne). +[secure-json-parse](https://github.com/fastify/secure-json-parse). See https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061 for more details about prototype poisoning attacks. @@ -83,6 +83,19 @@ Possible values are `'error'`, `'remove'` and `'ignore'`. + Default: `'error'` + +### `onConstructorPoisoning` + +Defines what action the framework must take when parsing a JSON object +with `constructor`. This functionality is provided by +[secure-json-parse](https://github.com/fastify/secure-json-parse). +See https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061 +for more details about prototype poisoning attacks. + +Possible values are `'error'`, `'remove'` and `'ignore'`. + ++ Default: `'ignore'` + ### `logger` diff --git a/fastify.d.ts b/fastify.d.ts index 7559c571882..b5d35faceee 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -191,6 +191,7 @@ declare namespace fastify { pluginTimeout?: number, disableRequestLogging?: boolean, onProtoPoisoning?: 'error' | 'remove' | 'ignore', + onConstructorPoisoning?: 'error' | 'remove' | 'ignore', logger?: any, trustProxy?: string | number | boolean | Array | TrustProxyFunction, maxParamLength?: number, diff --git a/fastify.js b/fastify.js index e7d04c950ea..03a9bf68754 100644 --- a/fastify.js +++ b/fastify.js @@ -123,7 +123,11 @@ function build (options) { [kSchemaCompiler]: null, [kSchemaResolver]: null, [kReplySerializerDefault]: null, - [kContentTypeParser]: new ContentTypeParser(bodyLimit, (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning)), + [kContentTypeParser]: new ContentTypeParser( + bodyLimit, + (options.onProtoPoisoning || defaultInitOptions.onProtoPoisoning), + (options.onConstructorPoisoning || defaultInitOptions.onConstructorPoisoning) + ), [kReply]: Reply.buildReply(Reply), [kRequest]: Request.buildRequest(Request), [kMiddlewares]: [], diff --git a/lib/configValidator.js b/lib/configValidator.js index 7315c614ba0..89f3cf2b8dc 100644 --- a/lib/configValidator.js +++ b/lib/configValidator.js @@ -14,10 +14,11 @@ var validate = (function() { if ((data && typeof data === "object" && !Array.isArray(data))) { if (data.bodyLimit === undefined) data.bodyLimit = 1048576; if (data.caseSensitive === undefined) data.caseSensitive = true; - if (data.disableRequestLogging === undefined) data.disableRequestLogging = false; if (data.ignoreTrailingSlash === undefined) data.ignoreTrailingSlash = false; + if (data.disableRequestLogging === undefined) data.disableRequestLogging = false; if (data.maxParamLength === undefined) data.maxParamLength = 100; if (data.onProtoPoisoning === undefined) data.onProtoPoisoning = "error"; + if (data.onConstructorPoisoning === undefined) data.onConstructorPoisoning = "ignore"; if (data.pluginTimeout === undefined) data.pluginTimeout = 10000; if (data.requestIdHeader === undefined) data.requestIdHeader = "request-id"; if (data.requestIdLogLabel === undefined) data.requestIdLogLabel = "reqId"; @@ -321,80 +322,81 @@ var validate = (function() { } var valid1 = errors === errs_1; if (valid1) { - var data1 = data.maxParamLength; + var data1 = data.disableRequestLogging; var errs_1 = errors; - if ((typeof data1 !== "number" || (data1 % 1) || data1 !== data1)) { + if (typeof data1 !== "boolean") { var dataType1 = typeof data1; var coerced1 = undefined; - if (dataType1 == 'boolean' || data1 === null || (dataType1 == 'string' && data1 && data1 == +data1 && !(data1 % 1))) coerced1 = +data1; + if (data1 === 'false' || data1 === 0 || data1 === null) coerced1 = false; + else if (data1 === 'true' || data1 === 1) coerced1 = true; if (coerced1 === undefined) { validate.errors = [{ keyword: 'type', - dataPath: (dataPath || '') + '.maxParamLength', - schemaPath: '#/properties/maxParamLength/type', + dataPath: (dataPath || '') + '.disableRequestLogging', + schemaPath: '#/properties/disableRequestLogging/type', params: { - type: 'integer' + type: 'boolean' }, - message: 'should be integer' + message: 'should be boolean' }]; return false; } else { data1 = coerced1; - data['maxParamLength'] = coerced1; + data['disableRequestLogging'] = coerced1; } } var valid1 = errors === errs_1; if (valid1) { - var data1 = data.onProtoPoisoning; + var data1 = data.maxParamLength; var errs_1 = errors; - if (typeof data1 !== "string") { + if ((typeof data1 !== "number" || (data1 % 1) || data1 !== data1)) { var dataType1 = typeof data1; var coerced1 = undefined; - if (dataType1 == 'number' || dataType1 == 'boolean') coerced1 = '' + data1; - else if (data1 === null) coerced1 = ''; + if (dataType1 == 'boolean' || data1 === null || (dataType1 == 'string' && data1 && data1 == +data1 && !(data1 % 1))) coerced1 = +data1; if (coerced1 === undefined) { validate.errors = [{ keyword: 'type', - dataPath: (dataPath || '') + '.onProtoPoisoning', - schemaPath: '#/properties/onProtoPoisoning/type', + dataPath: (dataPath || '') + '.maxParamLength', + schemaPath: '#/properties/maxParamLength/type', params: { - type: 'string' + type: 'integer' }, - message: 'should be string' + message: 'should be integer' }]; return false; } else { data1 = coerced1; - data['onProtoPoisoning'] = coerced1; + data['maxParamLength'] = coerced1; } } var valid1 = errors === errs_1; if (valid1) { - var data1 = data.pluginTimeout; + var data1 = data.onProtoPoisoning; var errs_1 = errors; - if ((typeof data1 !== "number" || (data1 % 1) || data1 !== data1)) { + if (typeof data1 !== "string") { var dataType1 = typeof data1; var coerced1 = undefined; - if (dataType1 == 'boolean' || data1 === null || (dataType1 == 'string' && data1 && data1 == +data1 && !(data1 % 1))) coerced1 = +data1; + if (dataType1 == 'number' || dataType1 == 'boolean') coerced1 = '' + data1; + else if (data1 === null) coerced1 = ''; if (coerced1 === undefined) { validate.errors = [{ keyword: 'type', - dataPath: (dataPath || '') + '.pluginTimeout', - schemaPath: '#/properties/pluginTimeout/type', + dataPath: (dataPath || '') + '.onProtoPoisoning', + schemaPath: '#/properties/onProtoPoisoning/type', params: { - type: 'integer' + type: 'string' }, - message: 'should be integer' + message: 'should be string' }]; return false; } else { data1 = coerced1; - data['pluginTimeout'] = coerced1; + data['onProtoPoisoning'] = coerced1; } } var valid1 = errors === errs_1; if (valid1) { - var data1 = data.requestIdHeader; + var data1 = data.onConstructorPoisoning; var errs_1 = errors; if (typeof data1 !== "string") { var dataType1 = typeof data1; @@ -404,8 +406,8 @@ var validate = (function() { if (coerced1 === undefined) { validate.errors = [{ keyword: 'type', - dataPath: (dataPath || '') + '.requestIdHeader', - schemaPath: '#/properties/requestIdHeader/type', + dataPath: (dataPath || '') + '.onConstructorPoisoning', + schemaPath: '#/properties/onConstructorPoisoning/type', params: { type: 'string' }, @@ -414,35 +416,86 @@ var validate = (function() { return false; } else { data1 = coerced1; - data['requestIdHeader'] = coerced1; + data['onConstructorPoisoning'] = coerced1; } } var valid1 = errors === errs_1; if (valid1) { - var data1 = data.requestIdLogLabel; + var data1 = data.pluginTimeout; var errs_1 = errors; - if (typeof data1 !== "string") { + if ((typeof data1 !== "number" || (data1 % 1) || data1 !== data1)) { var dataType1 = typeof data1; var coerced1 = undefined; - if (dataType1 == 'number' || dataType1 == 'boolean') coerced1 = '' + data1; - else if (data1 === null) coerced1 = ''; + if (dataType1 == 'boolean' || data1 === null || (dataType1 == 'string' && data1 && data1 == +data1 && !(data1 % 1))) coerced1 = +data1; if (coerced1 === undefined) { validate.errors = [{ keyword: 'type', - dataPath: (dataPath || '') + '.requestIdLogLabel', - schemaPath: '#/properties/requestIdLogLabel/type', + dataPath: (dataPath || '') + '.pluginTimeout', + schemaPath: '#/properties/pluginTimeout/type', params: { - type: 'string' + type: 'integer' }, - message: 'should be string' + message: 'should be integer' }]; return false; } else { data1 = coerced1; - data['requestIdLogLabel'] = coerced1; + data['pluginTimeout'] = coerced1; } } var valid1 = errors === errs_1; + if (valid1) { + var data1 = data.requestIdHeader; + var errs_1 = errors; + if (typeof data1 !== "string") { + var dataType1 = typeof data1; + var coerced1 = undefined; + if (dataType1 == 'number' || dataType1 == 'boolean') coerced1 = '' + data1; + else if (data1 === null) coerced1 = ''; + if (coerced1 === undefined) { + validate.errors = [{ + keyword: 'type', + dataPath: (dataPath || '') + '.requestIdHeader', + schemaPath: '#/properties/requestIdHeader/type', + params: { + type: 'string' + }, + message: 'should be string' + }]; + return false; + } else { + data1 = coerced1; + data['requestIdHeader'] = coerced1; + } + } + var valid1 = errors === errs_1; + if (valid1) { + var data1 = data.requestIdLogLabel; + var errs_1 = errors; + if (typeof data1 !== "string") { + var dataType1 = typeof data1; + var coerced1 = undefined; + if (dataType1 == 'number' || dataType1 == 'boolean') coerced1 = '' + data1; + else if (data1 === null) coerced1 = ''; + if (coerced1 === undefined) { + validate.errors = [{ + keyword: 'type', + dataPath: (dataPath || '') + '.requestIdLogLabel', + schemaPath: '#/properties/requestIdLogLabel/type', + params: { + type: 'string' + }, + message: 'should be string' + }]; + return false; + } else { + data1 = coerced1; + data['requestIdLogLabel'] = coerced1; + } + } + var valid1 = errors === errs_1; + } + } } } } @@ -507,11 +560,11 @@ validate.schema = { "setDefaultValue": true } }, - "disableRequestLogging": { + "ignoreTrailingSlash": { "type": "boolean", "default": false }, - "ignoreTrailingSlash": { + "disableRequestLogging": { "type": "boolean", "default": false }, @@ -523,6 +576,10 @@ validate.schema = { "type": "string", "default": "error" }, + "onConstructorPoisoning": { + "type": "string", + "default": "ignore" + }, "pluginTimeout": { "type": "integer", "default": 10000 @@ -545,14 +602,4 @@ function customRule0 (schemaParamValue, validatedParamValue, validationSchemaObj return true } -module.exports.defaultInitOptions = { - "bodyLimit":1048576, - "caseSensitive":true, - "disableRequestLogging": false, - "ignoreTrailingSlash":false, - "maxParamLength":100, - "onProtoPoisoning":"error", - "pluginTimeout":10000, - "requestIdHeader":"request-id", - "requestIdLogLabel":"reqId" -} +module.exports.defaultInitOptions = {"bodyLimit":1048576,"caseSensitive":true,"disableRequestLogging":false,"ignoreTrailingSlash":false,"maxParamLength":100,"onProtoPoisoning":"error","onConstructorPoisoning":"ignore","pluginTimeout":10000,"requestIdHeader":"request-id","requestIdLogLabel":"reqId"} diff --git a/lib/contentTypeParser.js b/lib/contentTypeParser.js index 4fab37f13d5..7ad4320fd69 100644 --- a/lib/contentTypeParser.js +++ b/lib/contentTypeParser.js @@ -22,8 +22,8 @@ const { } } = require('./errors') -function ContentTypeParser (bodyLimit, onProtoPoisoning) { - this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning) +function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning) { + this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning) this.customParsers = {} this.customParsers['application/json'] = new Parser(true, false, bodyLimit, this[kDefaultJsonParse]) this.customParsers['text/plain'] = new Parser(true, false, bodyLimit, defaultPlainTextParser) @@ -189,7 +189,7 @@ function rawBody (request, reply, options, parser, done) { } } -function getDefaultJsonParser (onProtoPoisoning) { +function getDefaultJsonParser (onProtoPoisoning, onConstructorPoisoning) { return defaultJsonParser function defaultJsonParser (req, body, done) { @@ -198,7 +198,7 @@ function getDefaultJsonParser (onProtoPoisoning) { } try { - var json = secureJson.parse(body, { protoAction: onProtoPoisoning }) + var json = secureJson.parse(body, { protoAction: onProtoPoisoning, constructorAction: onConstructorPoisoning }) } catch (err) { err.statusCode = 400 return done(err, undefined) diff --git a/package.json b/package.json index bc873fb01ea..5b465935444 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "proxy-addr": "^2.0.4", "readable-stream": "^3.1.1", "rfdc": "^1.1.2", - "secure-json-parse": "^1.0.0", + "secure-json-parse": "^2.0.0", "tiny-lru": "^7.0.0" }, "greenkeeper": { diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index 674ff4764df..8c9108d41a8 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -25,6 +25,7 @@ test('without options passed to Fastify, initialConfig should expose default val ignoreTrailingSlash: false, maxParamLength: 100, onProtoPoisoning: 'error', + onConstructorPoisoning: 'ignore', pluginTimeout: 10000, requestIdHeader: 'request-id', requestIdLogLabel: 'reqId' @@ -228,6 +229,7 @@ test('Should not have issues when passing stream options to Pino.js', t => { ignoreTrailingSlash: true, maxParamLength: 100, onProtoPoisoning: 'error', + onConstructorPoisoning: 'ignore', pluginTimeout: 10000, requestIdHeader: 'request-id', requestIdLogLabel: 'reqId' diff --git a/test/proto-poisoning.test.js b/test/proto-poisoning.test.js index 2498ee6969c..425972e21ca 100644 --- a/test/proto-poisoning.test.js +++ b/test/proto-poisoning.test.js @@ -81,3 +81,79 @@ test('proto-poisoning ignore', t => { }) }) }) + +test('constructor-poisoning ignore (default in v2)', t => { + t.plan(3) + + const fastify = Fastify() + t.tearDown(fastify.close.bind(fastify)) + + fastify.post('/', (request, reply) => { + reply.send('ok') + }) + + fastify.listen(0, function (err) { + t.error(err) + + sget({ + method: 'POST', + url: 'http://localhost:' + fastify.server.address().port, + headers: { 'Content-Type': 'application/json' }, + body: '{ "constructor": { "prototype": { "foo": "bar" } } }' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + }) + }) +}) + +test('constructor-poisoning error', t => { + t.plan(3) + + const fastify = Fastify({ onConstructorPoisoning: 'error' }) + t.tearDown(fastify.close.bind(fastify)) + + fastify.post('/', (request, reply) => { + t.fail('handler should not be called') + }) + + fastify.listen(0, function (err) { + t.error(err) + + sget({ + method: 'POST', + url: 'http://localhost:' + fastify.server.address().port, + headers: { 'Content-Type': 'application/json' }, + body: '{ "constructor": { "prototype": { "foo": "bar" } } }' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 400) + }) + }) +}) + +test('constructor-poisoning remove', t => { + t.plan(4) + + const fastify = Fastify({ onProtoPoisoning: 'remove' }) + t.tearDown(fastify.close.bind(fastify)) + + fastify.post('/', (request, reply) => { + t.equal(undefined, Object.assign({}, request.body).foo) + reply.send({ ok: true }) + }) + + fastify.listen(0, function (err) { + t.error(err) + + sget({ + method: 'POST', + url: 'http://localhost:' + fastify.server.address().port, + headers: { 'Content-Type': 'application/json' }, + body: '{ "constructor": { "prototype": { "foo": "bar" } } }' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + }) + }) +}) From 8dd654affc07648e990d08b9ebd3c5dd1c51b0c9 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Fri, 25 Oct 2019 11:14:34 -0300 Subject: [PATCH 121/144] Add log serializer (#1901) * feat: add logSerializer * tests(logSerializer): add test for logSerializer by route * test(logger): add tests for logSerializer * test(logger): fix lint errors * docs(Routes): adjust description of LogSerializer * fix(logSerializer): adjust prevent for 404 route * refactor(route): adjust childLogger with conditional * refactor(logSerializer): changed logSerializer to logSerializers * fix(router): refactor logSerializer to work in node 6 * tests(logSerializers): add more test to check override by route * tests(logSerializers): add test to check inherit from contexts * refactor: adjust prevent memory alocation unecessary --- docs/Hooks.md | 1 + docs/Plugins.md | 1 + docs/Routes.md | 59 +++++++++ fastify.d.ts | 2 + fastify.js | 6 + lib/context.js | 3 +- lib/route.js | 16 ++- lib/symbols.js | 1 + test/hooks.test.js | 11 +- test/logger.test.js | 309 ++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 405 insertions(+), 4 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 43eebf1ac59..68e0543dfbd 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -280,6 +280,7 @@ fastify.addHook('onRoute', (routeOptions) => { routeOptions.url routeOptions.bodyLimit routeOptions.logLevel + routeOptions.logSerializers routeOptions.prefix }) ``` diff --git a/docs/Plugins.md b/docs/Plugins.md index 7e26ec54d69..921e22e4011 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -16,6 +16,7 @@ fastify.register(plugin, [options]) The optional `options` parameter for `fastify.register` supports a predefined set of options that Fastify itself will use, except when the plugin has been wrapped with [fastify-plugin](https://github.com/fastify/fastify-plugin). This options object will also be passed to the plugin upon invocation, regardless of whether or not the plugin has been wrapped. The currently supported list of Fastify specific options is: + [`logLevel`](https://github.com/fastify/fastify/blob/master/docs/Routes.md#custom-log-level) ++ [`logSerializers`](https://github.com/fastify/fastify/blob/master/docs/Routes.md#custom-log-serializer) + [`prefix`](https://github.com/fastify/fastify/blob/master/docs/Plugins.md#route-prefixing-options) **Note: Those options will be ignored when used with fastify-plugin** diff --git a/docs/Routes.md b/docs/Routes.md index fff6edce903..cb850400865 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -31,6 +31,7 @@ They need to be in * `schemaCompiler(schema)`: the function that build the schema for the validations. See [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler) * `bodyLimit`: prevents the default JSON body parser from parsing request bodies larger than this number of bytes. Must be an integer. You may also set this option globally when first creating the Fastify instance with `fastify(options)`. Defaults to `1048576` (1 MiB). * `logLevel`: set log level for this route. See below. +* `logSerializers`: set serializers to log for this route. * `config`: object used to store custom configuration. * `version`: a [semver](http://semver.org/) compatible string that defined the version of the endpoint. [Example](https://github.com/fastify/fastify/blob/master/docs/Routes.md#version). * `prefixTrailingSlash`: string used to determine how to handle passing `/` as a route with a prefix. @@ -294,6 +295,64 @@ fastify.get('/', { logLevel: 'warn' }, (request, reply) => { ``` *Remember that the custom log level is applied only to the routes, and not to the global Fastify Logger, accessible with `fastify.log`* + +### Custom Log Serializer + +In some context, you may need to log a large object but it could be a waste of resources for some routes. In this case, you can define some [`serializer`](https://github.com/pinojs/pino/blob/master/docs/api.md#bindingsserializers-object) and attach them in the right context! + +```js +const fastify = require('fastify')({ logger: true }) + +fastify.register(require('./routes/user'), { + logSerializers: { + user: (value) => `My serializer one - ${value.name}` + } +}) +fastify.register(require('./routes/events'), { + logSerializers: { + user: (value) => `My serializer two - ${value.name} ${value.surname}` + } +}) + +fastify.listen(3000) +``` + +You can inherit serializers by context: + +```js +const fastify = Fastify({ + logger: { + level: 'info', + serializers: { + user (req) { + return { + method: req.method, + url: req.url, + headers: req.headers, + hostname: req.hostname, + remoteAddress: req.ip, + remotePort: req.connection.remotePort + } + } + } + } +}) + +fastify.register(context1, { + logSerializers: { + user: value => `My serializer father - ${value}` + } +}) + +async function context1 (fastify, opts) { + fastify.get('/', (req, reply) => { + req.log.info({ user: 'call father serializer', key: 'another key' }) // shows: { user: 'My serializer father - call father serializer', key: 'another key' } + reply.send({}) + }) +} + +fastify.listen(3000) +``` ### Config diff --git a/fastify.d.ts b/fastify.d.ts index b5d35faceee..9e7e59be559 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -271,6 +271,7 @@ declare namespace fastify { schemaCompiler?: SchemaCompiler bodyLimit?: number logLevel?: string + logSerializers?: Object config?: any prefixTrailingSlash?: 'slash' | 'no-slash' | 'both' } @@ -298,6 +299,7 @@ declare namespace fastify { interface RegisterOptions { [key: string]: any, prefix?: string, + logSerializers?: Object } /** diff --git a/fastify.js b/fastify.js index 03a9bf68754..560d8dd54aa 100644 --- a/fastify.js +++ b/fastify.js @@ -10,6 +10,7 @@ const { kBodyLimit, kRoutePrefix, kLogLevel, + kLogSerializers, kHooks, kSchemas, kSchemaCompiler, @@ -118,6 +119,7 @@ function build (options) { [kBodyLimit]: bodyLimit, [kRoutePrefix]: '', [kLogLevel]: '', + [kLogSerializers]: null, [kHooks]: new Hooks(), [kSchemas]: schemas, [kSchemaCompiler]: null, @@ -471,6 +473,10 @@ function override (old, fn, opts) { instance[pluginUtils.registeredPlugins] = Object.create(instance[pluginUtils.registeredPlugins]) instance[kPluginNameChain] = [pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)] + if (instance[kLogSerializers] || opts.logSerializers) { + instance[kLogSerializers] = Object.assign(Object.create(instance[kLogSerializers]), opts.logSerializers) + } + if (opts.prefix) { instance[kFourOhFour].arrange404(instance) } diff --git a/lib/context.js b/lib/context.js index 189779da7e5..6d2e199fb77 100644 --- a/lib/context.js +++ b/lib/context.js @@ -4,7 +4,7 @@ const { kFourOhFourContext, kReplySerializerDefault } = require('./symbols.js') // Objects that holds the context of every request // Every route holds an instance of this object. -function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, bodyLimit, logLevel, attachValidation, replySerializer) { +function Context (schema, handler, Reply, Request, contentTypeParser, config, errorHandler, bodyLimit, logLevel, logSerializers, attachValidation, replySerializer) { this.schema = schema this.handler = handler this.Reply = Reply @@ -20,6 +20,7 @@ function Context (schema, handler, Reply, Request, contentTypeParser, config, er this._middie = null this._parserOptions = { limit: bodyLimit || null } this.logLevel = logLevel + this.logSerializers = logSerializers this[kFourOhFourContext] = null this.attachValidation = attachValidation this[kReplySerializerDefault] = replySerializer diff --git a/lib/route.js b/lib/route.js index 558913a0328..355dfbe3d92 100644 --- a/lib/route.js +++ b/lib/route.js @@ -22,6 +22,7 @@ const { const { kRoutePrefix, kLogLevel, + kLogSerializers, kHooks, kSchemas, kSchemaCompiler, @@ -180,6 +181,10 @@ function buildRouting (options) { opts.prefix = prefix opts.logLevel = opts.logLevel || this[kLogLevel] + if (this[kLogSerializers] || opts.logSerializers) { + opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers) + } + if (opts.attachValidation == null) { opts.attachValidation = false } @@ -207,6 +212,7 @@ function buildRouting (options) { this._errorHandler, opts.bodyLimit, opts.logLevel, + opts.logSerializers, opts.attachValidation, this[kReplySerializerDefault] ) @@ -312,7 +318,15 @@ function buildRouting (options) { } } - var childLogger = logger.child({ [requestIdLogLabel]: req.id, level: context.logLevel }) + var loggerOpts = { + [requestIdLogLabel]: req.id, + level: context.logLevel + } + + if (context.logSerializers) { + loggerOpts.serializers = context.logSerializers + } + var childLogger = logger.child(loggerOpts) childLogger[kDisableRequestLogging] = disableRequestLogging // added hostname, ip, and ips back to the Node req object to maintain backward compatibility diff --git a/lib/symbols.js b/lib/symbols.js index fb6777e9541..2b27b7de0f3 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -5,6 +5,7 @@ const keys = { kBodyLimit: Symbol('fastify.bodyLimit'), kRoutePrefix: Symbol('fastify.routePrefix'), kLogLevel: Symbol('fastify.logLevel'), + kLogSerializers: Symbol('fastify.logSerializers'), kHooks: Symbol('fastify.hooks'), kSchemas: Symbol('fastify.schemas'), kSchemaCompiler: Symbol('fastify.schemaCompiler'), diff --git a/test/hooks.test.js b/test/hooks.test.js index 7c25abd0e0b..caea07908e3 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -488,7 +488,7 @@ test('onRoute hook should pass correct route with custom prefix', t => { }) test('onRoute hook should pass correct route with custom options', t => { - t.plan(5) + t.plan(6) const fastify = Fastify() fastify.register((instance, opts, next) => { instance.addHook('onRoute', function (route) { @@ -496,8 +496,15 @@ test('onRoute hook should pass correct route with custom options', t => { t.strictEqual(route.url, '/foo') t.strictEqual(route.logLevel, 'info') t.strictEqual(route.bodyLimit, 100) + t.type(route.logSerializers.test, 'function') }) - instance.get('/foo', { logLevel: 'info', bodyLimit: 100 }, function (req, reply) { + instance.get('/foo', { + logLevel: 'info', + bodyLimit: 100, + logSerializers: { + test: value => value + } + }, function (req, reply) { reply.send() }) next() diff --git a/test/logger.test.js b/test/logger.test.js index 1253894ebec..0590eb3677b 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -567,6 +567,40 @@ test('Should set a custom logLevel for a plugin', t => { }) }) +test('Should set a custom logSerializers for a plugin', t => { + t.plan(3) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, 'XHello') + } + }) + + const logger = pino({ level: 'error' }, splitStream) + + const fastify = Fastify({ + logger + }) + + fastify.register(function (instance, opts, next) { + instance.get('/plugin', (req, reply) => { + req.log.info({ test: 'Hello' }) // we should see this log + reply.send({ hello: 'world' }) + }) + next() + }, { logLevel: 'info', logSerializers: { test: value => 'X' + value } }) + + fastify.inject({ + method: 'GET', + url: '/plugin' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + test('Should set a custom logLevel for every plugin', t => { const lines = ['incoming request', 'request completed', 'info', 'debug'] t.plan(18) @@ -634,6 +668,281 @@ test('Should set a custom logLevel for every plugin', t => { }) }) +test('Should set a custom logSerializers for every plugin', t => { + const lines = ['Hello', 'XHello', 'ZHello'] + t.plan(9) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, lines.shift()) + } + }) + + const logger = pino({ level: 'info' }, splitStream) + const fastify = Fastify({ + logger + }) + + fastify.get('/', (req, reply) => { + req.log.warn({ test: 'Hello' }) + reply.send({ hello: 'world' }) + }) + + fastify.register(function (instance, opts, next) { + instance.get('/test1', (req, reply) => { + req.log.info({ test: 'Hello' }) + reply.send({ hello: 'world' }) + }) + next() + }, { logSerializers: { test: value => 'X' + value } }) + + fastify.register(function (instance, opts, next) { + instance.get('/test2', (req, reply) => { + req.log.info({ test: 'Hello' }) + reply.send({ hello: 'world' }) + }) + next() + }, { logSerializers: { test: value => 'Z' + value } }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test1' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/test2' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('Should override serializers from route', t => { + t.plan(3) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, 'ZHello') + } + }) + + const logger = pino({ level: 'info' }, splitStream) + const fastify = Fastify({ + logger + }) + + fastify.register(function (instance, opts, next) { + instance.get('/', { + logSerializers: { + test: value => 'Z' + value // should override + } + }, (req, reply) => { + req.log.info({ test: 'Hello' }) + reply.send({ hello: 'world' }) + }) + next() + }, { logSerializers: { test: value => 'X' + value } }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('Should override serializers from plugin', t => { + t.plan(3) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, 'ZHello') + } + }) + + const logger = pino({ level: 'info' }, splitStream) + const fastify = Fastify({ + logger + }) + + fastify.register(function (instance, opts, next) { + instance.register(context1, { + logSerializers: { + test: value => 'Z' + value // should override + } + }) + next() + }, { logSerializers: { test: value => 'X' + value } }) + + function context1 (instance, opts, next) { + instance.get('/', (req, reply) => { + req.log.info({ test: 'Hello' }) + reply.send({ hello: 'world' }) + }) + next() + } + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('Should use serializers from plugin and route', t => { + t.plan(4) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, 'XHello') + } + if (line.test2) { + t.is(line.test2, 'ZHello') + } + }) + + const logger = pino({ level: 'info' }, splitStream) + const fastify = Fastify({ + logger + }) + + fastify.register(context1, { + logSerializers: { test: value => 'X' + value } + }) + + function context1 (instance, opts, next) { + instance.get('/', { + logSerializers: { + test2: value => 'Z' + value + } + }, (req, reply) => { + req.log.info({ test: 'Hello', test2: 'Hello' }) // { test: 'XHello', test2: 'ZHello' } + reply.send({ hello: 'world' }) + }) + next() + } + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('Should use serializers from instance fastify and route', t => { + t.plan(4) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test) { + t.is(line.test, 'XHello') + } + if (line.test2) { + t.is(line.test2, 'ZHello') + } + }) + + const logger = pino({ + level: 'info', + serializers: { + test: value => 'X' + value, + test2: value => 'This should be override - ' + value + } + }, splitStream) + const fastify = Fastify({ + logger + }) + + fastify.get('/', { + logSerializers: { + test2: value => 'Z' + value + } + }, (req, reply) => { + req.log.info({ test: 'Hello', test2: 'Hello' }) // { test: 'XHello', test2: 'ZHello' } + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('Should use serializers inherit from contexts', t => { + t.plan(5) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + if (line.test && line.test2 && line.test3) { + t.is(line.test, 'XHello') + t.is(line.test2, 'YHello') + t.is(line.test3, 'ZHello') + } + }) + + const logger = pino({ + level: 'info', + serializers: { + test: value => 'X' + value + } + }, splitStream) + + const fastify = Fastify({ logger }) + fastify.register(context1, { logSerializers: { test2: value => 'Y' + value } }) + + function context1 (instance, opts, next) { + instance.get('/', { + logSerializers: { + test3: value => 'Z' + value + } + }, (req, reply) => { + req.log.info({ test: 'Hello', test2: 'Hello', test3: 'Hello' }) // { test: 'XHello', test2: 'YHello', test3: 'ZHello' } + reply.send({ hello: 'world' }) + }) + next() + } + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + test('Should increase the log level for a specific plugin', t => { t.plan(4) From 7c4c7d64ee84655cfa00d34e450404cf10145662 Mon Sep 17 00:00:00 2001 From: Vano Devium Date: Fri, 25 Oct 2019 17:15:25 +0300 Subject: [PATCH 122/144] Added fastify-qs plugin (#1906) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 136aea541b9..4ad9ffd4d00 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -101,6 +101,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-openapi-glue`](https://github.com/seriousme/fastify-openapi-glue) Glue for Open Api specifications in Fastify, autogenerates routes based on an Open Api Specification - [`fastify-oracle`](https://github.com/cemremengu/fastify-oracle) Attaches an [`oracledb`](https://github.com/oracle/node-oracledb) connection pool to a Fastify server instance. - [`fastify-orientdb`](https://github.com/mahmed8003/fastify-orientdb) Fastify OrientDB connection plugin, with which you can share the OrientDB connection across every part of your server. +- [`fastify-qs`](https://github.com/webdevium/fastify-qs) A plugin for Fastify that adds support for parsing URL query parameters with [qs](https://github.com/ljharb/qs). - [`fastify-rbac`](https://gitlab.com/m03geek/fastify-rbac) Fastify role-based access control plugin. - [`fastify-register-routes`](https://github.com/israeleriston/fastify-register-routes) Plugin to automatically load routes from a specified path and optionally limit loaded file names by a regular expression. - [`fastify-response-time`](https://github.com/lolo32/fastify-response-time) Add `X-Response-Time` header at each request for Fastify, in milliseconds. From 11d286e4cab57d466b58385ee7a091abcc5348cc Mon Sep 17 00:00:00 2001 From: Tomas Della Vedova Date: Sat, 26 Oct 2019 12:11:30 +0200 Subject: [PATCH 123/144] Handle invalid url components (#1888) * Fix #1884 * Updated test --- fastify.js | 10 ++++++++++ package.json | 2 +- test/404s.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/fastify.js b/fastify.js index 560d8dd54aa..4609b6d89d0 100644 --- a/fastify.js +++ b/fastify.js @@ -87,6 +87,7 @@ function build (options) { const router = buildRouting({ config: { defaultRoute: defaultRoute, + onBadUrl: onBadUrl, ignoreTrailingSlash: options.ignoreTrailingSlash || defaultInitOptions.ignoreTrailingSlash, maxParamLength: options.maxParamLength || defaultInitOptions.maxParamLength, caseSensitive: options.caseSensitive, @@ -410,6 +411,15 @@ function build (options) { fourOhFour.router.lookup(req, res) } + function onBadUrl (path, req, res) { + const body = `{"error":"Bad Request","message":"'${path}' is not a valid url component","statusCode":400}` + res.writeHead(400, { + 'Content-Type': 'application/json', + 'Content-Length': body.length + }) + res.end(body) + } + function setNotFoundHandler (opts, handler) { throwIfAlreadyStarted('Cannot call "setNotFoundHandler" when fastify instance is already started!') diff --git a/package.json b/package.json index 5b465935444..c64baf68e08 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "ajv": "^6.10.2", "avvio": "^6.2.2", "fast-json-stringify": "^1.15.5", - "find-my-way": "^2.0.0", + "find-my-way": "^2.2.0", "flatstr": "^1.0.12", "light-my-request": "^3.4.1", "middie": "^4.0.1", diff --git a/test/404s.test.js b/test/404s.test.js index fb96b8cc24f..7fa716c1653 100644 --- a/test/404s.test.js +++ b/test/404s.test.js @@ -1733,3 +1733,43 @@ test('Should fail to invoke callNotFound inside a 404 handler', t => { t.is(res.payload, '404 Not Found') }) }) + +test('400 in case of bad url (pre find-my-way v2.2.0 was a 404)', t => { + t.test('Dyamic route', t => { + t.plan(3) + const fastify = Fastify() + fastify.get('/hello/:id', () => t.fail('we should not be here')) + fastify.inject({ + url: '/hello/%world', + method: 'GET' + }, (err, response) => { + t.error(err) + t.strictEqual(response.statusCode, 400) + t.deepEqual(JSON.parse(response.payload), { + error: 'Bad Request', + message: "'%world' is not a valid url component", + statusCode: 400 + }) + }) + }) + + t.test('Wildcard', t => { + t.plan(3) + const fastify = Fastify() + fastify.get('*', () => t.fail('we should not be here')) + fastify.inject({ + url: '/hello/%world', + method: 'GET' + }, (err, response) => { + t.error(err) + t.strictEqual(response.statusCode, 400) + t.deepEqual(JSON.parse(response.payload), { + error: 'Bad Request', + message: "'/hello/%world' is not a valid url component", + statusCode: 400 + }) + }) + }) + + t.end() +}) From 16173896d42ce34f7af5d702587d9487762cf444 Mon Sep 17 00:00:00 2001 From: Kayla Ngan Date: Sat, 2 Nov 2019 06:26:55 -0400 Subject: [PATCH 124/144] Update Actions to use latest versions of all OS's (#1907) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6a3c535ebb..511a7b23137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: node-version: [6.x, 8.x, 10.x, 12.x, 13.x] - os: [ubuntu-16.04, windows-2016, macOS-10.14] + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v1 From d0e78d16d151db402332386127385fed8541a9cb Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Sat, 2 Nov 2019 22:07:16 +0100 Subject: [PATCH 125/144] less coverage messages (#1918) --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 511a7b23137..74326a15388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: ci on: [push, pull_request] jobs: - build: + test: runs-on: ${{ matrix.os }} strategy: @@ -37,8 +37,12 @@ jobs: github-token: ${{ secrets.github_token }} parallel: true + coverage: + needs: test + runs-on: ubuntu-latest + steps: - name: Coveralls Finished uses: coverallsapp/github-action@master with: - github-token: ${{ secrets.github_token }} + github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true From c6e6ca02f0444f7ffd3dbb6385d090fff58818b4 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2019 09:51:13 +0100 Subject: [PATCH 126/144] chore(package): update form-data to version 3.0.0 (#1923) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c64baf68e08..6c4c2c3a85e 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", "fluent-schema": "^0.7.4", - "form-data": "^2.5.0", + "form-data": "^3.0.0", "frameguard": "^3.0.0", "h2url": "^0.2.0", "helmet": "^3.20.0", From f01eb40934ca29c083a2702793022d58c0be041a Mon Sep 17 00:00:00 2001 From: Kevin Rambaud Date: Fri, 8 Nov 2019 12:51:39 -0500 Subject: [PATCH 127/144] Update documentation to fix anchors for Hooks (#1928) --- docs/Hooks.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/Hooks.md b/docs/Hooks.md index 68e0543dfbd..a8312f3db57 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -7,14 +7,14 @@ Hooks are registered with the `fastify.addHook` method and allow you to listen t By using hooks you can interact directly with the lifecycle of Fastify. There are Request/Reply hooks and application hooks: - [Request/Reply Hooks](#requestreply-hooks) - - [onRequest](#onRequest) - - [preParsing](#preParsing) - - [preValidation](#preValidation) - - [preHandler](#preHandler) - - [preSerialization](#preSerialization) - - [onError](#onError) - - [onSend](#onSend) - - [onResponse](#onResponse) + - [onRequest](#onrequest) + - [preParsing](#preparsing) + - [preValidation](#prevalidation) + - [preHandler](#prehandler) + - [preSerialization](#preserialization) + - [onError](#onerror) + - [onSend](#onsend) + - [onResponse](#onresponse) - [Application Hooks](#application-hooks) - [onClose](#onclose) - [onRoute](#onroute) From 6069d85d7e5d0657801249f04387476d0b7f41bb Mon Sep 17 00:00:00 2001 From: Nilesh Mali <44889195+nm-infy@users.noreply.github.com> Date: Sat, 9 Nov 2019 15:14:11 +0530 Subject: [PATCH 128/144] Added missing types for Server Options (#1922) * Added missing typescript types * Added http2 server factory test --- fastify.d.ts | 7 ++++++- test/types/index.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/fastify.d.ts b/fastify.d.ts index 9e7e59be559..3426eecec9b 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -184,6 +184,8 @@ declare namespace fastify { request: FastifyRequest } type TrustProxyFunction = (addr: string, index: number) => boolean + type ServerFactoryHandlerFunction = (request: http.IncomingMessage | http2.Http2ServerRequest, response: http.ServerResponse | http2.Http2ServerResponse) => void + type ServerFactoryFunction = (handler: ServerFactoryHandlerFunction, options: ServerOptions) => http.Server | http2.Http2Server interface ServerOptions { caseSensitive?: boolean, ignoreTrailingSlash?: boolean, @@ -207,7 +209,10 @@ declare namespace fastify { }, modifyCoreObjects?: boolean, return503OnClosing?: boolean, - genReqId?: () => number | string + genReqId?: () => number | string, + requestIdHeader?: string, + requestIdLogLabel?: string, + serverFactory?: ServerFactoryFunction } interface ServerOptionsAsSecure extends ServerOptions { https: http2.SecureServerOptions diff --git a/test/types/index.ts b/test/types/index.ts index 4cdefdb86fa..76391ac0775 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -70,6 +70,24 @@ const cors = require('cors') return Math.random().toString() } return Math.random() + }, + requestIdHeader: 'request-id', + requestIdLogLabel: 'reqId', + serverFactory: (handler, options) => { + const server = http.createServer((req, res) => { + handler(req, res) + }) + return server + } + }) + + // http2 server factory option + const otherHttp2Server = fastify({ + serverFactory: (handler, options) => { + const server = http2.createServer((req, res) => { + handler(req, res) + }) + return server } }) From 87596485b1c9afeafdf78fb7c72ee8a72eac2086 Mon Sep 17 00:00:00 2001 From: Marc Fisher Date: Tue, 12 Nov 2019 02:52:35 -0600 Subject: [PATCH 129/144] Fix/1932 Add version field to the route options (#1933) --- fastify.d.ts | 1 + test/types/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/fastify.d.ts b/fastify.d.ts index 3426eecec9b..02c61cebe92 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -278,6 +278,7 @@ declare namespace fastify { logLevel?: string logSerializers?: Object config?: any + version?: string prefixTrailingSlash?: 'slash' | 'no-slash' | 'both' } diff --git a/test/types/index.ts b/test/types/index.ts index 76391ac0775..555b682356d 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -257,6 +257,7 @@ const opts: fastify.RouteShorthandOptions () => {}, bodyLimit: 5000, logLevel: 'trace', + version: '1.0.0', config: { } } const optsWithHandler: fastify.RouteShorthandOptions = { From 6d200b61b1948566dc6b9d2d71dd0bdfa0f52c98 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Thu, 14 Nov 2019 08:21:30 -0600 Subject: [PATCH 130/144] Update abstract-logging to v2 (#1941) --- lib/logger.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/logger.js b/lib/logger.js index 1c27eecad6a..077e24c539f 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -6,7 +6,7 @@ * License: MIT (https://raw.githubusercontent.com/pinojs/pino-http/master/LICENSE) */ -const abstractLogging = require('abstract-logging') +const nullLogger = require('abstract-logging') const pino = require('pino') const { serializersSym } = pino.symbols const { isValidLogger } = require('./validation') @@ -82,7 +82,7 @@ function createLogger (options) { }) return { logger, hasLogger: true } } else if (!options.logger) { - const logger = Object.create(abstractLogging) + const logger = nullLogger logger.child = () => logger return { logger, hasLogger: false } } else { diff --git a/package.json b/package.json index 6c4c2c3a85e..f3b6568a864 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "x-xss-protection": "^1.1.0" }, "dependencies": { - "abstract-logging": "^1.0.0", + "abstract-logging": "^2.0.0", "ajv": "^6.10.2", "avvio": "^6.2.2", "fast-json-stringify": "^1.15.5", From 480dd934de1ed5b55ffca531a6e88539b20b5bd7 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Thu, 14 Nov 2019 16:27:18 +0100 Subject: [PATCH 131/144] Update Routes.md (#1942) --- docs/Routes.md | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/Routes.md b/docs/Routes.md index cb850400865..8848b281bf7 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -1,14 +1,34 @@

Fastify

## Routes -You have two ways to declare a route with Fastify, the shorthand method and the full declaration. Let's start with the second one: + +The routes methods will configure the endpoints of your application. +You have two ways to declare a route with Fastify, the shorthand method and the full declaration. + +- [Full Declaration](#full-declaration) +- [Route Options](#options) +- [Shorthand Declaration](#shorthand-declaration) +- [URL Parameters](#url-building) +- [Use `async`/`await`](#async-await) +- [Promise resolution](#promise-resolution) +- [Route Prefixing](#route-prefixing) +- Logs + - [Custom Log Level](#custom-log-level) + - [Custom Log Serializer](#custom-log-serializer) +- [Route handler configuration](#routes-config) +- [Route's Versioning](#version) + ### Full declaration + ```js fastify.route(options) ``` -* `method`: currently it supports `'DELETE'`, `'GET'`, `'HEAD'`, `'PATCH'`, `'POST'`, `'PUT'` and `'OPTIONS'`. It could also be an array of methods. + +### Routes option + +* `method`: currently it supports `'DELETE'`, `'GET'`, `'HEAD'`, `'PATCH'`, `'POST'`, `'PUT'` and `'OPTIONS'`. It could also be an array of methods. * `url`: the path of the url to match this route (alias: `path`). * `schema`: an object containing the schemas for the request and response. They need to be in @@ -23,10 +43,12 @@ They need to be in * `response`: filter and generate a schema for the response, setting a schema allows us to have 10-20% more throughput. * `attachValidation`: attach `validationError` to request, if there is a schema validation error, instead of sending the error to the error handler. -* `onRequest(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#route-hooks) as soon that a request is received, it could also be an array of functions. -* `preValidation(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#route-hooks) called after the shared `preValidation` hooks, useful if you need to perform authentication at route level for example, it could also be an array of functions. -* `preHandler(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#route-hooks) called just before the request handler, it could also be an array of functions. -* `preSerialization(request, reply, payload, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#route-hooks) called just before the serialization, it could also be an array of functions. +* `onRequest(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#onrequest) as soon that a request is received, it could also be an array of functions. +* `preParsing(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#preparsing) called before parsing the request, it could also be an array of functions. +* `preValidation(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#prevalidation) called after the shared `preValidation` hooks, useful if you need to perform authentication at route level for example, it could also be an array of functions. +* `preHandler(request, reply, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#prehandler) called just before the request handler, it could also be an array of functions. +* `preSerialization(request, reply, payload, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#preserialization) called just before the serialization, it could also be an array of functions. +* `onResponse(request, reply, payload, done)`: a [function](https://github.com/fastify/fastify/blob/master/docs/Hooks.md#onresponse) called when a response has been sent, so you will not be able to send more data to the client. It could also be an array of functions. * `handler(request, reply)`: the function that will handle this request. * `schemaCompiler(schema)`: the function that build the schema for the validations. See [here](https://github.com/fastify/fastify/blob/master/docs/Validation-and-Serialization.md#schema-compiler) * `bodyLimit`: prevents the default JSON body parser from parsing request bodies larger than this number of bytes. Must be an integer. You may also set this option globally when first creating the Fastify instance with `fastify(options)`. Defaults to `1048576` (1 MiB). @@ -238,6 +260,7 @@ fastify.register(require('./routes/v2/users'), { prefix: '/v2' }) fastify.listen(3000) ``` + ```js // routes/v1/users.js module.exports = function (fastify, opts, done) { @@ -245,6 +268,7 @@ module.exports = function (fastify, opts, done) { done() } ``` + ```js // routes/v2/users.js module.exports = function (fastify, opts, done) { @@ -287,6 +311,7 @@ fastify.register(require('./routes/events'), { logLevel: 'debug' }) fastify.listen(3000) ``` + Or you can directly pass it to a route: ```js fastify.get('/', { logLevel: 'warn' }, (request, reply) => { @@ -374,10 +399,12 @@ fastify.listen(3000) ### Version + #### Default If needed you can provide a version option, which will allow you to declare multiple versions of the same route. The versioning should follow the [semver](http://semver.org/) specification.
Fastify will automatically detect the `Accept-Version` header and route the request accordingly (advanced ranges and pre-releases currently are not supported).
*Be aware that using this feature will cause a degradation of the overall performances of the router.* + ```js fastify.route({ method: 'GET', @@ -398,7 +425,9 @@ fastify.inject({ // { hello: 'world' } }) ``` + If you declare multiple versions with the same major or minor, Fastify will always choose the highest compatible with the `Accept-Version` header value.
If the request will not have the `Accept-Version` header, a 404 error will be returned. + #### Custom It's possible to define a custom versioning logic. This can be done through the [`versioning`](https://github.com/fastify/fastify/blob/master/docs/Server.md#versioning) configuration, when creating a fastify server instance. From 4e10f69473602cc0befde1a9e9a075be17bd07d5 Mon Sep 17 00:00:00 2001 From: Shogun Date: Tue, 19 Nov 2019 15:14:39 +0100 Subject: [PATCH 132/144] Add fastify-errors-properties plugin to Ecosystem (#1944) * Add fastify-errors-properties plugin * Put plugin in the right order. --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 4ad9ffd4d00..89c5ba497d9 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -64,6 +64,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-decorators`](https://github.com/L2jLiga/fastify-decorators) Fastify plugin that provides the set of TypeScript decorators. - [`fastify-dynamodb`](https://github.com/matrus2/fastify-dynamodb) AWS DynamoDB plugin for Fastify. It exposes [AWS.DynamoDB.DocumentClient()](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html) object. - [`fastify-error-page`](https://github.com/hemerajs/fastify-error-page) Fastify plugin to print errors in structured HTML to the browser. +- [`fastify-errors-properties`](https://github.com/ShogunPanda/fastify-errors-properties) A error handling plugin for Fastify that enables additional properties in errors. - [`fastify-favicon`](https://github.com/smartiniOnGitHub/fastify-favicon) Fastify plugin to serve default favicon. - [`fastify-feature-flags`](https://gitlab.com/m03geek/fastify-feature-flags) Fastify feature flags plugin with multiple providers support (e.g. env, [config](http://lorenwest.github.io/node-config/), [unleash](https://unleash.github.io/)). - [`fastify-file-upload`](https://github.com/huangang/fastify-file-upload) Fastify plugin for uploading files. From b43b58744816b0ac3181250b1a74c84c297424dd Mon Sep 17 00:00:00 2001 From: Shogun Date: Wed, 20 Nov 2019 17:01:59 +0100 Subject: [PATCH 133/144] Add fastify-auth0-verify to Ecosystem. (#1947) --- docs/Ecosystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index 89c5ba497d9..e75b69423a3 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -51,6 +51,7 @@ Plugins maintained by the fastify team are listed under [Core](#core) while plug - [`fastify-405`](https://github.com/Eomm/fastify-405) Fastify plugin that adds 405 HTTP status to your routes - [`fastify-amqp`](https://github.com/RafaelGSS/fastify-amqp) Fastify AMQP connection plugin, to use with RabbitMQ or another connector. Just a wrapper to [`amqplib`](https://github.com/squaremo/amqp.node). - [`fastify-angular-universal`](https://github.com/exequiel09/fastify-angular-universal) Angular server-side rendering support using [`@angular/platform-server`](https://github.com/angular/angular/tree/master/packages/platform-server) for Fastify +- [`fastify-auth0-verify`](https://github.com/nearform/fastify-auth0-verify): Auth0 verification plugin for Fastify, internally uses [fastify-jwt](https://npm.im/fastify-jwt) and [jsonwebtoken](https://npm.im/jsonwebtoken). - [`fastify-autocrud`](https://github.com/paranoiasystem/fastify-autocrud) Plugin for autogenerate CRUD routes as fast as possible. - [`fastify-babel`](https://github.com/cfware/fastify-babel) Fastify plugin for development servers which require babel transformations of JavaScript sources. - [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your routes to the console, so you definitely know which endpoints are available. From e720c38c32fd528bf5b202dfdc9e18ba72ce8512 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2019 13:25:13 +0100 Subject: [PATCH 134/144] chore(package): update fluent-schema to version 0.8.0 (#1950) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3b6568a864..5af447e82fc 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.5.0", - "fluent-schema": "^0.7.4", + "fluent-schema": "^0.8.0", "form-data": "^3.0.0", "frameguard": "^3.0.0", "h2url": "^0.2.0", From ebed766f8df49dc37a3ba2864caeae478525e7cc Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Sat, 1 Jun 2019 17:01:11 -0400 Subject: [PATCH 135/144] remove travis from next branch (#1670) --- .npmignore | 1 - docs/LTS.md | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.npmignore b/.npmignore index 7f3bd63c002..68f824efd2b 100644 --- a/.npmignore +++ b/.npmignore @@ -5,7 +5,6 @@ .gitignore .github .nyc_output -.travis.yml coverage/ tools/ azure-pipelines-npm-template.yml diff --git a/docs/LTS.md b/docs/LTS.md index 37337de8056..2b4063db1f9 100644 --- a/docs/LTS.md +++ b/docs/LTS.md @@ -31,10 +31,10 @@ A "month" is to be a period of 30 consecutive days. ### Schedule -| Version | Release Date | End Of LTS Date | Node.js | -| :------ | :----------- | :-------------- | :-------------- | -| 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11 | -| 2.0.0 | 2019-02-25 | TBD | 6, 8, 10, 11 | +| Version | Release Date | End Of LTS Date | Node.js | +| :------ | :----------- | :-------------- | :------------------ | +| 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11, 12 | +| 2.0.0 | 2019-02-25 | TBD | 6, 8, 10, 11, 12 | From 4d50cf89e16eb1358353ed0dacb8be81bfdbbcf6 Mon Sep 17 00:00:00 2001 From: Sandro Martini Date: Wed, 3 Jul 2019 11:03:02 +0200 Subject: [PATCH 136/144] Drop nodejs 6 for next (Fastify 3) (#1713) * update requirements to Node.js 8 LTS or later; update docs add TODO for code updates/cleanup (check if do here or if in the other issue/PR) Update README.md Co-Authored-By: Matteo Collina Update docs/Benchmarking.md Co-Authored-By: Matteo Collina Update package.json Co-Authored-By: Matteo Collina revert version update getHeader to use hasHeader (available since Node.js 7.7.x) and remove fallback implementation try yarn builds for Node.js 8 aligned with requirements in 'package.json', to check if it works with version range align even npm azure pipelines to those of yarn (Node.js 8.x requirements) update code to Node.js 8 as minimum version (remove TODO etc) update code to Node.js 8 as minimum version (remove TODO etc) update code to Node.js 8 as minimum version (remove TODO etc) move getHeader implementation directly in Reply.prototype.getHeader drop Node.js 11 from doc and from CI on Azure (yarn and npm) (try to) simplify info on HTTP2 support CI on Azure, specify version range in a way more aligned to npm syntax * restore 'hooks-async.js' and set the '.test.js' extension to run it like others; remove its tests from 'hooks.test.js' * remove direct reference to main fastify source from https tests, not really needed --- README.md | 7 +- build/azure-pipelines-npm-template.yml | 6 +- build/azure-pipelines-yarn-template.yml | 4 +- docs/Benchmarking.md | 2 +- docs/HTTP2.md | 6 +- docs/LTS.md | 12 +- lib/reply.js | 43 +- package.json | 2 +- test/async-await.js | 623 ------------------ test/async-await.test.js | 558 +++++++++++++++- test/fluent-schema.js | 179 ----- test/fluent-schema.test.js | 126 +++- test/hooks-async.js | 552 ---------------- test/hooks-async.test.js | 469 +++++++++++++ test/hooks.test.js | 8 - test/http2/{closing.js => closing.test.js} | 0 test/http2/http2.test.js | 16 - ...module.js => missing-http2-module.test.js} | 0 test/http2/{plain.js => plain.test.js} | 0 ...llback.js => secure-with-fallback.test.js} | 0 test/http2/{secure.js => secure.test.js} | 0 ...-method.js => unknown-http-method.test.js} | 0 test/reply-error.test.js | 41 +- 23 files changed, 1183 insertions(+), 1471 deletions(-) delete mode 100644 test/async-await.js delete mode 100644 test/fluent-schema.js delete mode 100644 test/hooks-async.js create mode 100644 test/hooks-async.test.js rename test/http2/{closing.js => closing.test.js} (100%) delete mode 100644 test/http2/http2.test.js rename test/http2/{missing-http2-module.js => missing-http2-module.test.js} (100%) rename test/http2/{plain.js => plain.test.js} (100%) rename test/http2/{secure-with-fallback.js => secure-with-fallback.test.js} (100%) rename test/http2/{secure.js => secure.test.js} (100%) rename test/http2/{unknown-http-method.js => unknown-http-method.test.js} (100%) diff --git a/README.md b/README.md index a64cbb450df..b0b55090b6e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ How can you efficiently handle the resources of your server, knowing that you ar Enter Fastify. Fastify is a web framework highly focused on providing the best developer experience with the least overhead and a powerful plugin architecture. It is inspired by Hapi and Express and as far as we know, it is one of the fastest web frameworks in town. +### Requirements + +Node.js 8 LTS (8.16.0) or later; recommended 10 LTS (10.16.0) or later. + ### Install Install with npm: @@ -105,9 +109,10 @@ fastify generate For more information, see the [Fastify CLI documentation](https://github.com/fastify/fastify-cli). -### Fastify v1.x +### Fastify v1.x and v2.x Code for Fastify's **v1.x** is in [Branch 1.x](https://github.com/fastify/fastify/tree/1.x), so all Fastify 1.x related changes should be based on **`branch 1.x`**. +In a similar way, all Fastify 2.x related changes should be based on **`branch 2.x`**. > ## Note > `.listen` binds to the local host, `localhost`, interface by default (`127.0.0.1` or `::1`, depending on the operating system configuration). If you are running Fastify in a container (Docker, [GCP](https://cloud.google.com/), etc.), you may need to bind to `0.0.0.0`. Be careful when deciding to listen on all interfaces; it comes with inherent [security risks](https://web.archive.org/web/20170711105010/https://snyk.io/blog/mongodb-hack-and-secure-defaults/). diff --git a/build/azure-pipelines-npm-template.yml b/build/azure-pipelines-npm-template.yml index e4cd2b559db..f313e431ce4 100644 --- a/build/azure-pipelines-npm-template.yml +++ b/build/azure-pipelines-npm-template.yml @@ -9,14 +9,10 @@ jobs: node_version: 13.x node_12_x: node_version: 12.x - node_11_x: - node_version: 11.x node_10_x: node_version: 10.x node_8_x: - node_version: 8.x - node_6_x: - node_version: 6.x + node_version: "^8.16.0" maxParallel: 5 steps: diff --git a/build/azure-pipelines-yarn-template.yml b/build/azure-pipelines-yarn-template.yml index 4c6ec3184e6..52f8c6751e1 100644 --- a/build/azure-pipelines-yarn-template.yml +++ b/build/azure-pipelines-yarn-template.yml @@ -9,12 +9,10 @@ jobs: node_version: 13.x node_12_x: node_version: 12.x - node_11_x: - node_version: 11.x node_10_x: node_version: 10.x node_8_x: - node_version: 8.x + node_version: "^8.16.0" maxParallel: 5 steps: diff --git a/docs/Benchmarking.md b/docs/Benchmarking.md index c7643fa4298..cf3726708e7 100644 --- a/docs/Benchmarking.md +++ b/docs/Benchmarking.md @@ -18,7 +18,7 @@ npm run benchmark ### Run the test against different Node.js versions ✨ ```sh -npx -p node@6 -- npm run benchmark +npx -p node@10 -- npm run benchmark ``` ## Advanced diff --git a/docs/HTTP2.md b/docs/HTTP2.md index 57b23700172..e0ce0602060 100644 --- a/docs/HTTP2.md +++ b/docs/HTTP2.md @@ -2,9 +2,9 @@ ## HTTP2 -_Fastify_ offers **experimental support** for HTTP2 starting from Node -8.8.0, which includes HTTP2 without a flag. _Fastify_ supports HTTP2 -both over HTTPS or over plaintext. Note that HTTP2 is available only for node versions >= `8.8.1`. +_Fastify_ offers **experimental support** for HTTP2 starting from +Node 8 LTS, which includes HTTP2 without a flag; HTTP2 is supported +both over HTTPS or over plaintext. Currently none of the HTTP2-specific APIs are available through _Fastify_, but Node's `req` and `res` can be access through our diff --git a/docs/LTS.md b/docs/LTS.md index 2b4063db1f9..df70e66fac4 100644 --- a/docs/LTS.md +++ b/docs/LTS.md @@ -35,14 +35,14 @@ A "month" is to be a period of 30 consecutive days. | :------ | :----------- | :-------------- | :------------------ | | 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11, 12 | | 2.0.0 | 2019-02-25 | TBD | 6, 8, 10, 11, 12 | +| 3.0.0 | TBD | TBD | 8, 10, 12 | ### CI tested operating systems -| CI | OS | Version | Package Manager | Node.js | -|----------------|---------|------------------------|---------------------------|-----------| -| Github Actions | Linux | Ubuntu 16.04 | npm | 6,8,10,12 | -| Github Actions | Linux | Ubuntu 16.04 | yarn,pnpm | 8,10 | -| Github Actions | Windows | Windows Server 2016 R2 | npm | 6,8,10,12 | -| Github Actions | MacOS | macOS X Mojave 10.14 | npm | 6,8,10,12 | +| CI | OS | Version | Package Manager | Node.js | +| :-------------- | :------ | :------------- | :-------------- | :------------ | +| Azure pipelines | Linux | Ubuntu 16.04 | npm, yarn | 8, 10, 12 | +| Azure pipelines | Windows | vs2017-win2016 | npm, yarn | 8, 10, 12 | +| Azure pipelines | Mac | macOS 10.14 | npm, yarn | 8, 10, 12 | diff --git a/lib/reply.js b/lib/reply.js index 32ba0cabb45..7ad03e9e850 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -50,8 +50,6 @@ const { } } = require('./errors') -var getHeader - function Reply (res, context, request, log) { this.res = res this.context = context @@ -115,7 +113,7 @@ Reply.prototype.send = function (payload) { return this } - var contentType = getHeader(this, 'content-type') + var contentType = this.getHeader('content-type') var hasContentType = contentType !== undefined if (payload !== null) { @@ -151,7 +149,13 @@ Reply.prototype.send = function (payload) { } Reply.prototype.getHeader = function (key) { - return getHeader(this, key) + key = key.toLowerCase() + var res = this.res + var value = this[kReplyHeaders][key] + if (value === undefined && res.hasHeader(key)) { + value = res.getHeader(key) + } + return value } Reply.prototype.hasHeader = function (key) { @@ -567,37 +571,6 @@ function notFound (reply) { function noop () {} -function getHeaderProper (reply, key) { - key = key.toLowerCase() - var res = reply.res - var value = reply[kReplyHeaders][key] - if (value === undefined && res.hasHeader(key)) { - value = res.getHeader(key) - } - return value -} - -function getHeaderFallback (reply, key) { - key = key.toLowerCase() - var res = reply.res - var value = reply[kReplyHeaders][key] - if (value === undefined) { - value = res.getHeader(key) - } - return value -} - -// ponyfill for hasHeader. It has been introduced into Node 7.7, -// so it's ok to use it in 8+ -{ - const v = process.version.match(/v(\d+)/)[1] - if (Number(v) > 7) { - getHeader = getHeaderProper - } else { - getHeader = getHeaderFallback - } -} - module.exports = Reply module.exports.buildReply = buildReply module.exports.setupResponseListeners = setupResponseListeners diff --git a/package.json b/package.json index 5af447e82fc..b23a8f19095 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ }, "homepage": "https://www.fastify.io/", "engines": { - "node": ">=6" + "node": ">=8.16.0" }, "devDependencies": { "@types/node": "^11.13.19", diff --git a/test/async-await.js b/test/async-await.js deleted file mode 100644 index 5276dea68f3..00000000000 --- a/test/async-await.js +++ /dev/null @@ -1,623 +0,0 @@ -'use strict' - -const sget = require('simple-get').concat -const Fastify = require('..') -const split = require('split2') -const pino = require('pino') -const statusCodes = require('http').STATUS_CODES -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) - -const opts = { - schema: { - response: { - '2xx': { - type: 'object', - properties: { - hello: { - type: 'string' - } - } - } - } - } -} - -function asyncTest (t) { - const test = t.test - - test('async await', t => { - t.plan(11) - const fastify = Fastify() - try { - fastify.get('/', opts, async function awaitMyFunc (req, reply) { - await sleep(200) - return { hello: 'world' } - }) - t.pass() - } catch (e) { - t.fail() - } - - try { - fastify.get('/no-await', opts, async function (req, reply) { - return { hello: 'world' } - }) - t.pass() - } catch (e) { - t.fail() - } - - fastify.listen(0, err => { - t.error(err) - fastify.server.unref() - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port - }, (err, response, body) => { - t.error(err) - t.strictEqual(response.statusCode, 200) - t.strictEqual(response.headers['content-length'], '' + body.length) - t.deepEqual(JSON.parse(body), { hello: 'world' }) - }) - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/no-await' - }, (err, response, body) => { - t.error(err) - t.strictEqual(response.statusCode, 200) - t.strictEqual(response.headers['content-length'], '' + body.length) - t.deepEqual(JSON.parse(body), { hello: 'world' }) - }) - }) - }) - - test('ignore the result of the promise if reply.send is called beforehand (undefined)', t => { - t.plan(4) - - const server = Fastify() - const payload = { hello: 'world' } - - server.get('/', async function awaitMyFunc (req, reply) { - reply.send(payload) - }) - - t.tearDown(server.close.bind(server)) - - server.listen(0, (err) => { - t.error(err) - sget({ - method: 'GET', - url: 'http://localhost:' + server.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.deepEqual(payload, JSON.parse(body)) - t.strictEqual(res.statusCode, 200) - }) - }) - }) - - test('ignore the result of the promise if reply.send is called beforehand (object)', t => { - t.plan(4) - - const server = Fastify() - const payload = { hello: 'world2' } - - server.get('/', async function awaitMyFunc (req, reply) { - reply.send(payload) - return { hello: 'world' } - }) - - t.tearDown(server.close.bind(server)) - - server.listen(0, (err) => { - t.error(err) - sget({ - method: 'GET', - url: 'http://localhost:' + server.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.deepEqual(payload, JSON.parse(body)) - t.strictEqual(res.statusCode, 200) - }) - }) - }) - - test('server logs an error if reply.send is called and a value is returned via async/await', t => { - const lines = ['incoming request', 'request completed', 'Reply already sent'] - t.plan(lines.length + 2) - - const splitStream = split(JSON.parse) - splitStream.on('data', (line) => { - t.is(line.msg, lines.shift()) - }) - - const logger = pino(splitStream) - - const fastify = Fastify({ - logger - }) - - fastify.get('/', async (req, reply) => { - reply.send({ hello: 'world' }) - return { hello: 'world2' } - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - const payload = JSON.parse(res.payload) - t.deepEqual(payload, { hello: 'world' }) - }) - }) - - test('ignore the result of the promise if reply.send is called beforehand (undefined)', t => { - t.plan(4) - - const server = Fastify() - const payload = { hello: 'world' } - - server.get('/', async function awaitMyFunc (req, reply) { - reply.send(payload) - }) - - t.tearDown(server.close.bind(server)) - - server.listen(0, (err) => { - t.error(err) - sget({ - method: 'GET', - url: 'http://localhost:' + server.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.deepEqual(payload, JSON.parse(body)) - t.strictEqual(res.statusCode, 200) - }) - }) - }) - - test('ignore the result of the promise if reply.send is called beforehand (object)', t => { - t.plan(4) - - const server = Fastify() - const payload = { hello: 'world2' } - - server.get('/', async function awaitMyFunc (req, reply) { - reply.send(payload) - return { hello: 'world' } - }) - - t.tearDown(server.close.bind(server)) - - server.listen(0, (err) => { - t.error(err) - sget({ - method: 'GET', - url: 'http://localhost:' + server.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.deepEqual(payload, JSON.parse(body)) - t.strictEqual(res.statusCode, 200) - }) - }) - }) - - test('await reply if we will be calling reply.send in the future', t => { - const lines = ['incoming request', 'request completed'] - t.plan(lines.length + 2) - - const splitStream = split(JSON.parse) - splitStream.on('data', (line) => { - t.is(line.msg, lines.shift()) - }) - - const server = Fastify({ - logger: { - stream: splitStream - } - }) - const payload = { hello: 'world' } - - server.get('/', async function awaitMyFunc (req, reply) { - setImmediate(function () { - reply.send(payload) - }) - - await reply - }) - - server.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - const payload = JSON.parse(res.payload) - t.deepEqual(payload, { hello: 'world' }) - }) - }) - - test('await reply if we will be calling reply.send in the future (error case)', t => { - const lines = ['incoming request', 'kaboom', 'request completed'] - t.plan(lines.length + 2) - - const splitStream = split(JSON.parse) - splitStream.on('data', (line) => { - t.is(line.msg, lines.shift()) - }) - - const server = Fastify({ - logger: { - stream: splitStream - } - }) - - server.get('/', async function awaitMyFunc (req, reply) { - setImmediate(function () { - reply.send(new Error('kaboom')) - }) - - await reply - }) - - server.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 500) - }) - }) - - test('support reply decorators with await', t => { - t.plan(2) - - const fastify = Fastify() - - fastify.decorateReply('wow', function () { - setImmediate(() => { - this.send({ hello: 'world' }) - }) - }) - - fastify.get('/', async (req, reply) => { - await sleep(1) - reply.wow() - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - const payload = JSON.parse(res.payload) - t.deepEqual(payload, { hello: 'world' }) - }) - }) - - test('support 204', t => { - t.plan(2) - - const fastify = Fastify() - - fastify.get('/', async (req, reply) => { - reply.code(204) - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 204) - }) - }) - - test('inject async await', async t => { - t.plan(1) - - const fastify = Fastify() - - fastify.get('/', (req, reply) => { - reply.send({ hello: 'world' }) - }) - - try { - const res = await fastify.inject({ method: 'GET', url: '/' }) - t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) - } catch (err) { - t.fail(err) - } - }) - - test('inject async await - when the server is up', async t => { - t.plan(2) - - const fastify = Fastify() - - fastify.get('/', (req, reply) => { - reply.send({ hello: 'world' }) - }) - - try { - const res = await fastify.inject({ method: 'GET', url: '/' }) - t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) - } catch (err) { - t.fail(err) - } - - await sleep(200) - - try { - const res2 = await fastify.inject({ method: 'GET', url: '/' }) - t.deepEqual({ hello: 'world' }, JSON.parse(res2.payload)) - } catch (err) { - t.fail(err) - } - }) - - test('async await plugin', async t => { - t.plan(1) - - const fastify = Fastify() - - fastify.register(async (fastify, opts) => { - fastify.get('/', (req, reply) => { - reply.send({ hello: 'world' }) - }) - - await sleep(200) - }) - - try { - const res = await fastify.inject({ method: 'GET', url: '/' }) - t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) - } catch (err) { - t.fail(err) - } - }) - - test('does not call reply.send() twice if 204 reponse is already sent', t => { - t.plan(2) - - const fastify = Fastify() - - fastify.get('/', async (req, reply) => { - reply.code(204).send() - reply.send = () => { - throw new Error('reply.send() was called twice') - } - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.equal(res.statusCode, 204) - }) - }) - - test('error is logged because promise was fulfilled with undefined', t => { - t.plan(3) - - var fastify = null - var stream = split(JSON.parse) - try { - fastify = Fastify({ - logger: { - stream: stream, - level: 'error' - } - }) - } catch (e) { - t.fail() - } - - t.tearDown(fastify.close.bind(fastify)) - - fastify.get('/', async (req, reply) => { - reply.code(200) - }) - - stream.once('data', line => { - t.strictEqual(line.msg, 'Promise may not be fulfilled with \'undefined\' when statusCode is not 204') - }) - - fastify.listen(0, (err) => { - t.error(err) - fastify.server.unref() - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/', - timeout: 500 - }, (err, res, body) => { - t.is(err.message, 'Request timed out') - }) - }) - }) - - test('error is not logged because promise was fulfilled with undefined but statusCode 204 was set', t => { - t.plan(3) - - var fastify = null - var stream = split(JSON.parse) - try { - fastify = Fastify({ - logger: { - stream: stream, - level: 'error' - } - }) - } catch (e) { - t.fail() - } - - t.tearDown(fastify.close.bind(fastify)) - - fastify.get('/', async (req, reply) => { - reply.code(204) - }) - - stream.once('data', line => { - t.fail('should not log an error') - }) - - fastify.listen(0, (err) => { - t.error(err) - fastify.server.unref() - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.strictEqual(res.statusCode, 204) - }) - }) - }) - - test('error is not logged because promise was fulfilled with undefined but response was sent before promise resolution', t => { - t.plan(4) - - var fastify = null - var stream = split(JSON.parse) - var payload = { hello: 'world' } - try { - fastify = Fastify({ - logger: { - stream: stream, - level: 'error' - } - }) - } catch (e) { - t.fail() - } - - t.tearDown(fastify.close.bind(fastify)) - - fastify.get('/', async (req, reply) => { - reply.send(payload) - }) - - stream.once('data', line => { - t.fail('should not log an error') - }) - - fastify.listen(0, (err) => { - t.error(err) - fastify.server.unref() - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port + '/' - }, (err, res, body) => { - t.error(err) - t.strictEqual(res.statusCode, 200) - t.deepEqual( - payload, - JSON.parse(body) - ) - }) - }) - }) - - test('Thrown Error instance sets HTTP status code', t => { - t.plan(3) - - const fastify = Fastify() - - const err = new Error('winter is coming') - err.statusCode = 418 - - fastify.get('/', async (req, reply) => { - throw err - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (error, res) => { - t.error(error) - t.strictEqual(res.statusCode, 418) - t.deepEqual( - { - error: statusCodes['418'], - message: err.message, - statusCode: 418 - }, - JSON.parse(res.payload) - ) - }) - }) - - test('customErrorHandler support', t => { - t.plan(4) - - const fastify = Fastify() - - fastify.get('/', async (req, reply) => { - const error = new Error('ouch') - error.statusCode = 400 - throw error - }) - - fastify.setErrorHandler(async err => { - t.is(err.message, 'ouch') - const error = new Error('kaboom') - error.statusCode = 401 - throw error - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.strictEqual(res.statusCode, 401) - t.deepEqual( - { - error: statusCodes['401'], - message: 'kaboom', - statusCode: 401 - }, - JSON.parse(res.payload) - ) - }) - }) - - test('customErrorHandler support without throwing', t => { - t.plan(4) - - const fastify = Fastify() - - fastify.get('/', async (req, reply) => { - const error = new Error('ouch') - error.statusCode = 400 - throw error - }) - - fastify.setErrorHandler(async (err, req, reply) => { - t.is(err.message, 'ouch') - reply.code(401).send('kaboom') - reply.send = t.fail.bind(t, 'should not be called') - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.strictEqual(res.statusCode, 401) - t.deepEqual( - 'kaboom', - res.payload - ) - }) - }) -} - -module.exports = asyncTest diff --git a/test/async-await.test.js b/test/async-await.test.js index 07329fe8a5f..babc97c5251 100644 --- a/test/async-await.test.js +++ b/test/async-await.test.js @@ -1,11 +1,553 @@ 'use strict' -const semver = require('semver') -const tap = require('tap') - -if (semver.gt(process.versions.node, '8.0.0')) { - require('./async-await')(tap) -} else { - tap.pass('Skip because Node version < 8') - tap.end() +const t = require('tap') +const test = t.test +const sget = require('simple-get').concat +const Fastify = require('..') +const split = require('split2') +const pino = require('pino') +const statusCodes = require('http').STATUS_CODES +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +const opts = { + schema: { + response: { + '2xx': { + type: 'object', + properties: { + hello: { + type: 'string' + } + } + } + } + } } + +test('async await', t => { + t.plan(11) + const fastify = Fastify() + try { + fastify.get('/', opts, async function awaitMyFunc (req, reply) { + await sleep(200) + return { hello: 'world' } + }) + t.pass() + } catch (e) { + t.fail() + } + + try { + fastify.get('/no-await', opts, async function (req, reply) { + return { hello: 'world' } + }) + t.pass() + } catch (e) { + t.fail() + } + + fastify.listen(0, err => { + t.error(err) + fastify.server.unref() + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-length'], '' + body.length) + t.deepEqual(JSON.parse(body), { hello: 'world' }) + }) + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/no-await' + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-length'], '' + body.length) + t.deepEqual(JSON.parse(body), { hello: 'world' }) + }) + }) +}) + +test('ignore the result of the promise if reply.send is called beforehand (undefined)', t => { + t.plan(4) + + const server = Fastify() + const payload = { hello: 'world' } + + server.get('/', async function awaitMyFunc (req, reply) { + reply.send(payload) + }) + + t.tearDown(server.close.bind(server)) + + server.listen(0, (err) => { + t.error(err) + sget({ + method: 'GET', + url: 'http://localhost:' + server.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.deepEqual(payload, JSON.parse(body)) + t.strictEqual(res.statusCode, 200) + }) + }) +}) + +test('ignore the result of the promise if reply.send is called beforehand (object)', t => { + t.plan(4) + + const server = Fastify() + const payload = { hello: 'world2' } + + server.get('/', async function awaitMyFunc (req, reply) { + reply.send(payload) + return { hello: 'world' } + }) + + t.tearDown(server.close.bind(server)) + + server.listen(0, (err) => { + t.error(err) + sget({ + method: 'GET', + url: 'http://localhost:' + server.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.deepEqual(payload, JSON.parse(body)) + t.strictEqual(res.statusCode, 200) + }) + }) +}) + +test('server logs an error if reply.send is called and a value is returned via async/await', t => { + const lines = ['incoming request', 'request completed', 'Reply already sent'] + t.plan(lines.length + 2) + + const splitStream = split(JSON.parse) + splitStream.on('data', (line) => { + t.is(line.msg, lines.shift()) + }) + + const logger = pino(splitStream) + + const fastify = Fastify({ + logger + }) + + fastify.get('/', async (req, reply) => { + reply.send({ hello: 'world' }) + return { hello: 'world2' } + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('ignore the result of the promise if reply.send is called beforehand (undefined)', t => { + t.plan(4) + + const server = Fastify() + const payload = { hello: 'world' } + + server.get('/', async function awaitMyFunc (req, reply) { + reply.send(payload) + }) + + t.tearDown(server.close.bind(server)) + + server.listen(0, (err) => { + t.error(err) + sget({ + method: 'GET', + url: 'http://localhost:' + server.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.deepEqual(payload, JSON.parse(body)) + t.strictEqual(res.statusCode, 200) + }) + }) +}) + +test('ignore the result of the promise if reply.send is called beforehand (object)', t => { + t.plan(4) + + const server = Fastify() + const payload = { hello: 'world2' } + + server.get('/', async function awaitMyFunc (req, reply) { + reply.send(payload) + return { hello: 'world' } + }) + + t.tearDown(server.close.bind(server)) + + server.listen(0, (err) => { + t.error(err) + sget({ + method: 'GET', + url: 'http://localhost:' + server.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.deepEqual(payload, JSON.parse(body)) + t.strictEqual(res.statusCode, 200) + }) + }) +}) + +test('support reply decorators with await', t => { + t.plan(2) + + const fastify = Fastify() + + fastify.decorateReply('wow', function () { + setImmediate(() => { + this.send({ hello: 'world' }) + }) + }) + + fastify.get('/', async (req, reply) => { + await sleep(1) + reply.wow() + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.deepEqual(payload, { hello: 'world' }) + }) +}) + +test('support 204', t => { + t.plan(2) + + const fastify = Fastify() + + fastify.get('/', async (req, reply) => { + reply.code(204) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 204) + }) +}) + +test('inject async await', async t => { + t.plan(1) + + const fastify = Fastify() + + fastify.get('/', (req, reply) => { + reply.send({ hello: 'world' }) + }) + + try { + const res = await fastify.inject({ method: 'GET', url: '/' }) + t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) + } catch (err) { + t.fail(err) + } +}) + +test('inject async await - when the server is up', async t => { + t.plan(2) + + const fastify = Fastify() + + fastify.get('/', (req, reply) => { + reply.send({ hello: 'world' }) + }) + + try { + const res = await fastify.inject({ method: 'GET', url: '/' }) + t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) + } catch (err) { + t.fail(err) + } + + await sleep(200) + + try { + const res2 = await fastify.inject({ method: 'GET', url: '/' }) + t.deepEqual({ hello: 'world' }, JSON.parse(res2.payload)) + } catch (err) { + t.fail(err) + } +}) + +test('async await plugin', async t => { + t.plan(1) + + const fastify = Fastify() + + fastify.register(async (fastify, opts) => { + fastify.get('/', (req, reply) => { + reply.send({ hello: 'world' }) + }) + + await sleep(200) + }) + + try { + const res = await fastify.inject({ method: 'GET', url: '/' }) + t.deepEqual({ hello: 'world' }, JSON.parse(res.payload)) + } catch (err) { + t.fail(err) + } +}) + +test('does not call reply.send() twice if 204 reponse is already sent', t => { + t.plan(2) + + const fastify = Fastify() + + fastify.get('/', async (req, reply) => { + reply.code(204).send() + reply.send = () => { + throw new Error('reply.send() was called twice') + } + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 204) + }) +}) + +test('error is logged because promise was fulfilled with undefined', t => { + t.plan(3) + + var fastify = null + var stream = split(JSON.parse) + try { + fastify = Fastify({ + logger: { + stream: stream, + level: 'error' + } + }) + } catch (e) { + t.fail() + } + + t.tearDown(fastify.close.bind(fastify)) + + fastify.get('/', async (req, reply) => { + reply.code(200) + }) + + stream.once('data', line => { + t.strictEqual(line.msg, 'Promise may not be fulfilled with \'undefined\' when statusCode is not 204') + }) + + fastify.listen(0, (err) => { + t.error(err) + fastify.server.unref() + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/', + timeout: 500 + }, (err, res, body) => { + t.is(err.message, 'Request timed out') + }) + }) +}) + +test('error is not logged because promise was fulfilled with undefined but statusCode 204 was set', t => { + t.plan(3) + + var fastify = null + var stream = split(JSON.parse) + try { + fastify = Fastify({ + logger: { + stream: stream, + level: 'error' + } + }) + } catch (e) { + t.fail() + } + + t.tearDown(fastify.close.bind(fastify)) + + fastify.get('/', async (req, reply) => { + reply.code(204) + }) + + stream.once('data', line => { + t.fail('should not log an error') + }) + + fastify.listen(0, (err) => { + t.error(err) + fastify.server.unref() + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.strictEqual(res.statusCode, 204) + }) + }) +}) + +test('error is not logged because promise was fulfilled with undefined but response was sent before promise resolution', t => { + t.plan(4) + + var fastify = null + var stream = split(JSON.parse) + var payload = { 'hello': 'world' } + try { + fastify = Fastify({ + logger: { + stream: stream, + level: 'error' + } + }) + } catch (e) { + t.fail() + } + + t.tearDown(fastify.close.bind(fastify)) + + fastify.get('/', async (req, reply) => { + reply.send(payload) + }) + + stream.once('data', line => { + t.fail('should not log an error') + }) + + fastify.listen(0, (err) => { + t.error(err) + fastify.server.unref() + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + '/' + }, (err, res, body) => { + t.error(err) + t.strictEqual(res.statusCode, 200) + t.deepEqual( + payload, + JSON.parse(body) + ) + }) + }) +}) + +test('Thrown Error instance sets HTTP status code', t => { + t.plan(3) + + const fastify = Fastify() + + const err = new Error('winter is coming') + err.statusCode = 418 + + fastify.get('/', async (req, reply) => { + throw err + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (error, res) => { + t.error(error) + t.strictEqual(res.statusCode, 418) + t.deepEqual( + { + error: statusCodes['418'], + message: err.message, + statusCode: 418 + }, + JSON.parse(res.payload) + ) + }) +}) + +test('customErrorHandler support', t => { + t.plan(4) + + const fastify = Fastify() + + fastify.get('/', async (req, reply) => { + const error = new Error('ouch') + error.statusCode = 400 + throw error + }) + + fastify.setErrorHandler(async err => { + t.is(err.message, 'ouch') + const error = new Error('kaboom') + error.statusCode = 401 + throw error + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.statusCode, 401) + t.deepEqual( + { + error: statusCodes['401'], + message: 'kaboom', + statusCode: 401 + }, + JSON.parse(res.payload) + ) + }) +}) + +test('customErrorHandler support without throwing', t => { + t.plan(4) + + const fastify = Fastify() + + fastify.get('/', async (req, reply) => { + const error = new Error('ouch') + error.statusCode = 400 + throw error + }) + + fastify.setErrorHandler(async (err, req, reply) => { + t.is(err.message, 'ouch') + reply.code(401).send('kaboom') + reply.send = t.fail.bind(t, 'should not be called') + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.statusCode, 401) + t.deepEqual( + 'kaboom', + res.payload + ) + }) +}) diff --git a/test/fluent-schema.js b/test/fluent-schema.js deleted file mode 100644 index 87542a329b7..00000000000 --- a/test/fluent-schema.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict' - -const Fastify = require('..') -const S = require('fluent-schema') - -function fluentSchemaTest (t) { - const test = t.test - - test('fluent-schema generate a valid JSON Schema in "$ref-way"', t => { - t.plan(1) - - const fastify = new Fastify() - - const addressSchema = S.object() - .id('#address') - .prop('line1').required() - .prop('line2') - .prop('country').required() - .prop('city').required() - .prop('zipcode').required() - .valueOf() - - const commonSchemas = S.object() - .id('https://fastify/demo') - .definition('addressSchema', addressSchema) - .valueOf() - - fastify.addSchema(commonSchemas) - - const bodyJsonSchema = S.object() - .prop('residence', S.ref('https://fastify/demo#address')).required() - .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() - .valueOf() - - const schema = { body: bodyJsonSchema } - fastify.post('/the/url', { schema }, () => { }) - - fastify.ready(t.error) - }) - - test('fluent-schema generate a valid JSON Schema in "replace-way"', t => { - t.plan(1) - - const fastify = new Fastify() - - const sharedAddressSchema = { - $id: 'sharedAddress', - type: 'object', - required: ['line1', 'country', 'city', 'zipcode'], - properties: { - line1: { type: 'string' }, - line2: { type: 'string' }, - country: { type: 'string' }, - city: { type: 'string' }, - zipcode: { type: 'string' } - } - } - - fastify.addSchema(sharedAddressSchema) - - const bodyJsonSchema = { - type: 'object', - properties: { - vacation: 'sharedAddress#' - } - } - const schema = { body: bodyJsonSchema } - - fastify.post('/the/url', { schema }, () => { }) - - fastify.ready(t.error) - }) - - test('fluent-schema mix-up of "$ref-way" and "replace-way"', t => { - t.plan(1) - - const fastify = new Fastify() - - const addressSchema = S.object() - .id('#address') - .prop('line1').required() - .prop('line2') - .prop('country').required() - .prop('city').required() - .prop('zipcode').required() - .valueOf() - - const commonSchemas = S.object() - .id('https://fastify/demo') - .definition('addressSchema', addressSchema) - .valueOf() - - const sharedAddressSchema = { - $id: 'sharedAddress', - type: 'object', - required: ['line1', 'country', 'city', 'zipcode'], - properties: { - line1: { type: 'string' }, - line2: { type: 'string' }, - country: { type: 'string' }, - city: { type: 'string' }, - zipcode: { type: 'string' } - } - } - - fastify.addSchema(commonSchemas) - fastify.addSchema(sharedAddressSchema) - - const bodyJsonSchema = S.object() - .prop('residence', S.ref('https://fastify/demo#address')).required() - .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() - .valueOf() - - // add the key with the string value to use shared schema in "replace-way" - bodyJsonSchema.properties.vacation = 'sharedAddress#' - - const schema = { body: bodyJsonSchema } - - fastify.post('/the/url', { schema }, () => { }) - - fastify.ready(t.error) - }) - - test('Should call valueOf internally', t => { - t.plan(1) - - const fastify = new Fastify() - - const addressSchema = S.object() - .id('#address') - .prop('line1').required() - .prop('line2') - .prop('country').required() - .prop('city').required() - .prop('zipcode').required() - - const commonSchemas = S.object() - .id('https://fastify/demo') - .definition('addressSchema', addressSchema) - - fastify.addSchema(commonSchemas) - - fastify.route({ - method: 'POST', - url: '/query', - handler: () => {}, - schema: { - query: S.object().prop('hello', S.string()).required(), - body: S.object().prop('hello', S.string()).required(), - params: S.object().prop('hello', S.string()).required(), - headers: S.object().prop('hello', S.string()).required(), - response: { - 200: S.object().prop('hello', S.string()).required(), - 201: S.object().prop('hello', S.string()).required() - } - } - }) - - fastify.route({ - method: 'POST', - url: '/querystring', - handler: () => {}, - schema: { - querystring: S.object().prop('hello', S.string()).required(), - body: S.object().prop('hello', S.string()).required(), - params: S.object().prop('hello', S.string()).required(), - headers: S.object().prop('hello', S.string()).required(), - response: { - 200: S.object().prop('hello', S.string()).required(), - 201: S.object().prop('hello', S.string()).required() - } - } - }) - - fastify.ready(t.error) - }) -} - -module.exports = fluentSchemaTest diff --git a/test/fluent-schema.test.js b/test/fluent-schema.test.js index d1b965eef02..41112e9c32c 100644 --- a/test/fluent-schema.test.js +++ b/test/fluent-schema.test.js @@ -1,11 +1,121 @@ 'use strict' const t = require('tap') -const semver = require('semver') - -if (semver.gt(process.versions.node, '8.0.0')) { - require('./fluent-schema')(t) -} else { - t.pass('Skip because Node version < 8') - t.end() -} +const test = t.test +const Fastify = require('..') +const S = require('fluent-schema') + +test('fluent-schema generate a valid JSON Schema in "$ref-way"', t => { + t.plan(1) + + const fastify = new Fastify() + + const addressSchema = S.object() + .id('#address') + .prop('line1').required() + .prop('line2') + .prop('country').required() + .prop('city').required() + .prop('zipcode').required() + .valueOf() + + const commonSchemas = S.object() + .id('https://fastify/demo') + .definition('addressSchema', addressSchema) + .valueOf() + + fastify.addSchema(commonSchemas) + + const bodyJsonSchema = S.object() + .prop('residence', S.ref('https://fastify/demo#address')).required() + .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() + .valueOf() + + const schema = { body: bodyJsonSchema } + fastify.post('/the/url', { schema }, () => { }) + + fastify.ready(t.error) +}) + +test('fluent-schema generate a valid JSON Schema in "replace-way"', t => { + t.plan(1) + + const fastify = new Fastify() + + const sharedAddressSchema = { + $id: 'sharedAddress', + type: 'object', + required: ['line1', 'country', 'city', 'zipcode'], + properties: { + line1: { type: 'string' }, + line2: { type: 'string' }, + country: { type: 'string' }, + city: { type: 'string' }, + zipcode: { type: 'string' } + } + } + + fastify.addSchema(sharedAddressSchema) + + const bodyJsonSchema = { + type: 'object', + properties: { + vacation: 'sharedAddress#' + } + } + const schema = { body: bodyJsonSchema } + + fastify.post('/the/url', { schema }, () => { }) + + fastify.ready(t.error) +}) + +test('fluent-schema mix-up of "$ref-way" and "replace-way"', t => { + t.plan(1) + + const fastify = new Fastify() + + const addressSchema = S.object() + .id('#address') + .prop('line1').required() + .prop('line2') + .prop('country').required() + .prop('city').required() + .prop('zipcode').required() + .valueOf() + + const commonSchemas = S.object() + .id('https://fastify/demo') + .definition('addressSchema', addressSchema) + .valueOf() + + const sharedAddressSchema = { + $id: 'sharedAddress', + type: 'object', + required: ['line1', 'country', 'city', 'zipcode'], + properties: { + line1: { type: 'string' }, + line2: { type: 'string' }, + country: { type: 'string' }, + city: { type: 'string' }, + zipcode: { type: 'string' } + } + } + + fastify.addSchema(commonSchemas) + fastify.addSchema(sharedAddressSchema) + + const bodyJsonSchema = S.object() + .prop('residence', S.ref('https://fastify/demo#address')).required() + .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() + .valueOf() + + // add the key with the string value to use shared schema in "replace-way" + bodyJsonSchema.properties.vacation = 'sharedAddress#' + + const schema = { body: bodyJsonSchema } + + fastify.post('/the/url', { schema }, () => { }) + + fastify.ready(t.error) +}) diff --git a/test/hooks-async.js b/test/hooks-async.js deleted file mode 100644 index 0ce00e79e1f..00000000000 --- a/test/hooks-async.js +++ /dev/null @@ -1,552 +0,0 @@ -'use strict' - -const split = require('split2') -const sget = require('simple-get').concat -const Fastify = require('..') -const fs = require('fs') -const { promisify } = require('util') -const sleep = promisify(setTimeout) - -function asyncHookTest (t) { - const test = t.test - test('async hooks', t => { - t.plan(21) - - const fastify = Fastify() - fastify.addHook('onRequest', async function (request, reply) { - await sleep(1) - request.test = 'the request is coming' - reply.test = 'the reply has come' - if (request.raw.method === 'DELETE') { - throw new Error('some error') - } - }) - - fastify.addHook('preHandler', async function (request, reply) { - await sleep(1) - t.is(request.test, 'the request is coming') - t.is(reply.test, 'the reply has come') - if (request.raw.method === 'HEAD') { - throw new Error('some error') - } - }) - - fastify.addHook('onSend', async function (request, reply, payload) { - await sleep(1) - t.ok('onSend called') - }) - - fastify.addHook('onResponse', async function (request, reply) { - await sleep(1) - t.ok('onResponse called') - }) - - fastify.get('/', function (request, reply) { - t.is(request.test, 'the request is coming') - t.is(reply.test, 'the reply has come') - reply.code(200).send({ hello: 'world' }) - }) - - fastify.head('/', function (req, reply) { - reply.code(200).send({ hello: 'world' }) - }) - - fastify.delete('/', function (req, reply) { - reply.code(200).send({ hello: 'world' }) - }) - - fastify.listen(0, err => { - t.error(err) - fastify.server.unref() - - sget({ - method: 'GET', - url: 'http://localhost:' + fastify.server.address().port - }, (err, response, body) => { - t.error(err) - t.strictEqual(response.statusCode, 200) - t.strictEqual(response.headers['content-length'], '' + body.length) - t.deepEqual(JSON.parse(body), { hello: 'world' }) - }) - - sget({ - method: 'HEAD', - url: 'http://localhost:' + fastify.server.address().port - }, (err, response, body) => { - t.error(err) - t.strictEqual(response.statusCode, 500) - }) - - sget({ - method: 'DELETE', - url: 'http://localhost:' + fastify.server.address().port - }, (err, response, body) => { - t.error(err) - t.strictEqual(response.statusCode, 500) - }) - }) - }) - - test('modify payload', t => { - t.plan(10) - const fastify = Fastify() - const payload = { hello: 'world' } - const modifiedPayload = { hello: 'modified' } - const anotherPayload = '"winter is coming"' - - fastify.addHook('onSend', async function (request, reply, thePayload) { - t.ok('onSend called') - t.deepEqual(JSON.parse(thePayload), payload) - return thePayload.replace('world', 'modified') - }) - - fastify.addHook('onSend', async function (request, reply, thePayload) { - t.ok('onSend called') - t.deepEqual(JSON.parse(thePayload), modifiedPayload) - return anotherPayload - }) - - fastify.addHook('onSend', async function (request, reply, thePayload) { - t.ok('onSend called') - t.strictEqual(thePayload, anotherPayload) - }) - - fastify.get('/', (req, reply) => { - reply.send(payload) - }) - - fastify.inject({ - method: 'GET', - url: '/' - }, (err, res) => { - t.error(err) - t.strictEqual(res.payload, anotherPayload) - t.strictEqual(res.statusCode, 200) - t.strictEqual(res.headers['content-length'], '18') - }) - }) - - test('onRequest hooks should be able to block a request', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('onRequest', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('onRequest', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('preHandler', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.ok('called') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preHandler hooks should be able to block a request', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('preHandler', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('preHandler', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.equal(payload, 'hello') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preValidation hooks should be able to block a request', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('preValidation', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('preValidation', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.equal(payload, 'hello') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preValidation hooks should be able to block a request with async onSend', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('preValidation', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - await sleep(10) - t.equal(payload, 'hello') - }) - - fastify.addHook('preHandler', async (request, reply) => { - t.fail('we should not be here') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.post('/', { - schema: { - body: { - type: 'object', - properties: { - hello: { - type: 'string' - } - }, - required: ['hello'] - } - } - }, function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'POST' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preSerialization hooks should be able to modify the payload', t => { - t.plan(3) - const fastify = Fastify() - - fastify.addHook('preSerialization', async (req, reply, payload) => { - return { hello: 'another world' } - }) - - fastify.get('/', function (request, reply) { - reply.send({ hello: 'world' }) - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.deepEqual(JSON.parse(res.payload), { hello: 'another world' }) - }) - }) - - test('preSerialization hooks should handle errors', t => { - t.plan(3) - const fastify = Fastify() - - fastify.addHook('preSerialization', async (req, reply, payload) => { - throw new Error('kaboom') - }) - - fastify.get('/', function (request, reply) { - reply.send({ hello: 'world' }) - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 500) - t.deepEqual(JSON.parse(res.payload), { error: 'Internal Server Error', message: 'kaboom', statusCode: 500 }) - }) - }) - - test('onRequest hooks should be able to block a request (last hook)', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('onRequest', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('preHandler', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.ok('called') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preHandler hooks should be able to block a request (last hook)', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('preHandler', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.equal(payload, 'hello') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('preHandler hooks should be able to block a request (last hook) with a delay in onSend', t => { - t.plan(5) - const fastify = Fastify() - - fastify.addHook('preHandler', async (req, reply) => { - reply.send('hello') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - await sleep(10) - t.equal(payload, 'hello') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - t.is(res.payload, 'hello') - }) - }) - - test('onRequest respond with a stream', t => { - t.plan(4) - const fastify = Fastify() - - fastify.addHook('onRequest', async (req, reply) => { - return new Promise((resolve, reject) => { - const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') - // stream.pipe(res) - // res.once('finish', resolve) - reply.send(stream) - reply.res.once('finish', () => resolve()) - }) - }) - - fastify.addHook('onRequest', async (req, res) => { - t.fail('this should not be called') - }) - - fastify.addHook('preHandler', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.ok('called') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - }) - }) - - test('preHandler respond with a stream', t => { - t.plan(7) - const fastify = Fastify() - - fastify.addHook('onRequest', async (req, res) => { - t.ok('called') - }) - - // we are calling `reply.send` inside the `preHandler` hook with a stream, - // this triggers the `onSend` hook event if `preHanlder` has not yet finished - const order = [1, 2] - - fastify.addHook('preHandler', async (req, reply) => { - return new Promise((resolve, reject) => { - const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') - reply.send(stream) - reply.res.once('finish', () => { - t.is(order.shift(), 2) - resolve() - }) - }) - }) - - fastify.addHook('preHandler', async (req, reply) => { - t.fail('this should not be called') - }) - - fastify.addHook('onSend', async (req, reply, payload) => { - t.is(order.shift(), 1) - t.is(typeof payload.pipe, 'function') - }) - - fastify.addHook('onResponse', async (request, reply) => { - t.ok('called') - }) - - fastify.get('/', function (request, reply) { - t.fail('we should not be here') - }) - - fastify.inject({ - url: '/', - method: 'GET' - }, (err, res) => { - t.error(err) - t.is(res.statusCode, 200) - }) - }) - - test('Should log a warning if is an async function with `next`', t => { - t.test('3 arguments', t => { - t.plan(3) - const stream = split(JSON.parse) - const fastify = Fastify({ - logger: { stream } - }) - - stream.on('data', line => { - t.strictEqual(line.level, 40) - t.true(line.msg.startsWith("Async function has too many arguments. Async hooks should not use the 'next' argument.")) - t.true(/test(\\|\/)hooks-async\.js/.test(line.msg)) - }) - - fastify.addHook('onRequest', async (req, reply, next) => {}) - }) - - t.test('4 arguments', t => { - t.plan(9) - const stream = split(JSON.parse) - const fastify = Fastify({ - logger: { stream } - }) - - stream.on('data', line => { - t.strictEqual(line.level, 40) - t.true(line.msg.startsWith("Async function has too many arguments. Async hooks should not use the 'next' argument.")) - t.true(/test(\\|\/)hooks-async\.js/.test(line.msg)) - }) - - fastify.addHook('onSend', async (req, reply, payload, next) => {}) - fastify.addHook('preSerialization', async (req, reply, payload, next) => {}) - fastify.addHook('onError', async (req, reply, error, next) => {}) - }) - - t.end() - }) -} - -module.exports = asyncHookTest diff --git a/test/hooks-async.test.js b/test/hooks-async.test.js new file mode 100644 index 00000000000..294236eea9c --- /dev/null +++ b/test/hooks-async.test.js @@ -0,0 +1,469 @@ +'use strict' + +const t = require('tap') +const test = t.test +const split = require('split2') +const sget = require('simple-get').concat +const Fastify = require('../fastify') +const fs = require('fs') +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + +test('async hooks', t => { + t.plan(21) + + const fastify = Fastify() + fastify.addHook('onRequest', async function (request, reply) { + await sleep(1) + request.test = 'the request is coming' + reply.test = 'the reply has come' + if (request.raw.method === 'DELETE') { + throw new Error('some error') + } + }) + + fastify.addHook('preHandler', async function (request, reply) { + await sleep(1) + t.is(request.test, 'the request is coming') + t.is(reply.test, 'the reply has come') + if (request.raw.method === 'HEAD') { + throw new Error('some error') + } + }) + + fastify.addHook('onSend', async function (request, reply, payload) { + await sleep(1) + t.ok('onSend called') + }) + + fastify.addHook('onResponse', async function (request, reply) { + await sleep(1) + t.ok('onResponse called') + }) + + fastify.get('/', function (request, reply) { + t.is(request.test, 'the request is coming') + t.is(reply.test, 'the reply has come') + reply.code(200).send({ hello: 'world' }) + }) + + fastify.head('/', function (req, reply) { + reply.code(200).send({ hello: 'world' }) + }) + + fastify.delete('/', function (req, reply) { + reply.code(200).send({ hello: 'world' }) + }) + + fastify.listen(0, err => { + t.error(err) + fastify.server.unref() + + sget({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 200) + t.strictEqual(response.headers['content-length'], '' + body.length) + t.deepEqual(JSON.parse(body), { hello: 'world' }) + }) + + sget({ + method: 'HEAD', + url: 'http://localhost:' + fastify.server.address().port + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 500) + }) + + sget({ + method: 'DELETE', + url: 'http://localhost:' + fastify.server.address().port + }, (err, response, body) => { + t.error(err) + t.strictEqual(response.statusCode, 500) + }) + }) +}) + +test('modify payload', t => { + t.plan(10) + const fastify = Fastify() + const payload = { hello: 'world' } + const modifiedPayload = { hello: 'modified' } + const anotherPayload = '"winter is coming"' + + fastify.addHook('onSend', async function (request, reply, thePayload) { + t.ok('onSend called') + t.deepEqual(JSON.parse(thePayload), payload) + return thePayload.replace('world', 'modified') + }) + + fastify.addHook('onSend', async function (request, reply, thePayload) { + t.ok('onSend called') + t.deepEqual(JSON.parse(thePayload), modifiedPayload) + return anotherPayload + }) + + fastify.addHook('onSend', async function (request, reply, thePayload) { + t.ok('onSend called') + t.strictEqual(thePayload, anotherPayload) + }) + + fastify.get('/', (req, reply) => { + reply.send(payload) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.strictEqual(res.payload, anotherPayload) + t.strictEqual(res.statusCode, 200) + t.strictEqual(res.headers['content-length'], '18') + }) +}) + +test('onRequest hooks should be able to block a request', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('onRequest', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('onRequest', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('preHandler', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.ok('called') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) +}) + +test('preHandler hooks should be able to block a request', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('preHandler', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('preHandler', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.equal(payload, 'hello') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) +}) + +test('preValidation hooks should be able to block a request', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('preValidation', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('preValidation', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.equal(payload, 'hello') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) +}) + +test('preSerialization hooks should be able to modify the payload', t => { + t.plan(3) + const fastify = Fastify() + + fastify.addHook('preSerialization', async (req, reply, payload) => { + return { hello: 'another world' } + }) + + fastify.get('/', function (request, reply) { + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.deepEqual(JSON.parse(res.payload), { hello: 'another world' }) + }) +}) + +test('preSerialization hooks should handle errors', t => { + t.plan(3) + const fastify = Fastify() + + fastify.addHook('preSerialization', async (req, reply, payload) => { + throw new Error('kaboom') + }) + + fastify.get('/', function (request, reply) { + reply.send({ hello: 'world' }) + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 500) + t.deepEqual(JSON.parse(res.payload), { error: 'Internal Server Error', message: 'kaboom', statusCode: 500 }) + }) +}) + +test('onRequest hooks should be able to block a request (last hook)', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('onRequest', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('preHandler', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.ok('called') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) +}) + +test('preHandler hooks should be able to block a request (last hook)', t => { + t.plan(5) + const fastify = Fastify() + + fastify.addHook('preHandler', async (req, reply) => { + reply.send('hello') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.equal(payload, 'hello') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + t.is(res.payload, 'hello') + }) +}) + +test('onRequest respond with a stream', t => { + t.plan(4) + const fastify = Fastify() + + fastify.addHook('onRequest', async (req, reply) => { + return new Promise((resolve, reject) => { + const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') + // stream.pipe(res) + // res.once('finish', resolve) + reply.send(stream) + reply.res.once('finish', () => resolve()) + }) + }) + + fastify.addHook('onRequest', async (req, res) => { + t.fail('this should not be called') + }) + + fastify.addHook('preHandler', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.ok('called') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + }) +}) + +test('preHandler respond with a stream', t => { + t.plan(7) + const fastify = Fastify() + + fastify.addHook('onRequest', async (req, res) => { + t.ok('called') + }) + + // we are calling `reply.send` inside the `preHandler` hook with a stream, + // this triggers the `onSend` hook event if `preHanlder` has not yet finished + const order = [1, 2] + + fastify.addHook('preHandler', async (req, reply) => { + return new Promise((resolve, reject) => { + const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') + reply.send(stream) + reply.res.once('finish', () => { + t.is(order.shift(), 2) + resolve() + }) + }) + }) + + fastify.addHook('preHandler', async (req, reply) => { + t.fail('this should not be called') + }) + + fastify.addHook('onSend', async (req, reply, payload) => { + t.is(order.shift(), 1) + t.is(typeof payload.pipe, 'function') + }) + + fastify.addHook('onResponse', async (request, reply) => { + t.ok('called') + }) + + fastify.get('/', function (request, reply) { + t.fail('we should not be here') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.is(res.statusCode, 200) + }) +}) + +test('Should log a warning if is an async function with `next`', t => { + t.test('3 arguments', t => { + t.plan(3) + const stream = split(JSON.parse) + const fastify = Fastify({ + logger: { stream } + }) + + stream.on('data', line => { + t.strictEqual(line.level, 40) + t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) + t.true(/test(\\|\/)hooks-async\.test\.js/.test(line.msg)) + }) + + fastify.addHook('onRequest', async (req, reply, next) => {}) + }) + + t.test('4 arguments', t => { + t.plan(6) + const stream = split(JSON.parse) + const fastify = Fastify({ + logger: { stream } + }) + + stream.on('data', line => { + t.strictEqual(line.level, 40) + t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) + t.true(/test(\\|\/)hooks-async\.test\.js/.test(line.msg)) + }) + + fastify.addHook('onSend', async (req, reply, payload, next) => {}) + fastify.addHook('preSerialization', async (req, reply, payload, next) => {}) + }) + + t.end() +}) diff --git a/test/hooks.test.js b/test/hooks.test.js index caea07908e3..64e572e9bdb 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -2,7 +2,6 @@ const t = require('tap') const test = t.test -const semver = require('semver') const sget = require('simple-get').concat const stream = require('stream') const Fastify = require('..') @@ -2684,10 +2683,3 @@ test('onRegister hook should be called / 4', t => { t.error(err) }) }) - -if (semver.gt(process.versions.node, '8.0.0')) { - require('./hooks-async')(t) -} else { - t.pass('Skip because Node version < 8') - t.end() -} diff --git a/test/http2/closing.js b/test/http2/closing.test.js similarity index 100% rename from test/http2/closing.js rename to test/http2/closing.test.js diff --git a/test/http2/http2.test.js b/test/http2/http2.test.js deleted file mode 100644 index bf5879f7586..00000000000 --- a/test/http2/http2.test.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' - -const semver = require('semver') -const tap = require('tap') - -if (semver.gt(process.versions.node, '8.8.0')) { - require('./plain') - require('./secure') - require('./secure-with-fallback') - require('./unknown-http-method') - require('./missing-http2-module') - require('./closing') -} else { - tap.pass('Skip because Node version < 8.8') - tap.end() -} diff --git a/test/http2/missing-http2-module.js b/test/http2/missing-http2-module.test.js similarity index 100% rename from test/http2/missing-http2-module.js rename to test/http2/missing-http2-module.test.js diff --git a/test/http2/plain.js b/test/http2/plain.test.js similarity index 100% rename from test/http2/plain.js rename to test/http2/plain.test.js diff --git a/test/http2/secure-with-fallback.js b/test/http2/secure-with-fallback.test.js similarity index 100% rename from test/http2/secure-with-fallback.js rename to test/http2/secure-with-fallback.test.js diff --git a/test/http2/secure.js b/test/http2/secure.test.js similarity index 100% rename from test/http2/secure.js rename to test/http2/secure.test.js diff --git a/test/http2/unknown-http-method.js b/test/http2/unknown-http-method.test.js similarity index 100% rename from test/http2/unknown-http-method.js rename to test/http2/unknown-http-method.test.js diff --git a/test/reply-error.test.js b/test/reply-error.test.js index c240c8c747a..4ac5901cedb 100644 --- a/test/reply-error.test.js +++ b/test/reply-error.test.js @@ -3,7 +3,6 @@ const t = require('tap') const test = t.test const net = require('net') -const semver = require('semver') const Fastify = require('..') const statusCodes = require('http').STATUS_CODES @@ -101,34 +100,32 @@ test('onRequest hook error handling with external done', t => { }) }) -if (semver.gt(process.versions.node, '6.0.0')) { - test('Should reply 400 on client error', t => { - t.plan(2) +test('Should reply 400 on client error', t => { + t.plan(2) - const fastify = Fastify() - fastify.listen(0, err => { - t.error(err) + const fastify = Fastify() + fastify.listen(0, err => { + t.error(err) - const client = net.connect(fastify.server.address().port) - client.end('oooops!') + const client = net.connect(fastify.server.address().port) + client.end('oooops!') - var chunks = '' - client.on('data', chunk => { - chunks += chunk - }) + var chunks = '' + client.on('data', chunk => { + chunks += chunk + }) - client.once('end', () => { - const body = JSON.stringify({ - error: 'Bad Request', - message: 'Client Error', - statusCode: 400 - }) - t.equal(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`, chunks) - fastify.close() + client.once('end', () => { + const body = JSON.stringify({ + error: 'Bad Request', + message: 'Client Error', + statusCode: 400 }) + t.equal(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`, chunks) + fastify.close() }) }) -} +}) test('Error instance sets HTTP status code', t => { t.plan(3) From 935dd92f1ada5ab291a323ef3438f0cb6b678827 Mon Sep 17 00:00:00 2001 From: Sandro Martini Date: Tue, 9 Jul 2019 09:23:27 +0200 Subject: [PATCH 137/144] update packages to latest release (that now requires Node.js 8 or later) (#1732) explicit update all dependencies to latest; only Tap will be updated later update 'joi' to the new package name ('@hapi/joi') update tap and tap-mocha-reporter to latest; note that this currently breaks some tests fix failing tests (since Tap 14.x) on cloned options; explicit set Tap version to latest update test to not clone anymore object containing Buffer instances remove boom from greenkeeper ignore list --- docs/Validation-and-Serialization.md | 2 +- package.json | 45 +++++++++++--------------- test/input-validation.js | 2 +- test/internals/initialConfig.test.js | 4 +-- test/post.test.js | 2 +- test/validation-error-handling.test.js | 2 +- 6 files changed, 25 insertions(+), 32 deletions(-) diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 1eeff93bd72..ec052a335ce 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -285,7 +285,7 @@ _**Note:** If you use a custom instance of any validator (even Ajv), you have to But maybe you want to change the validation library. Perhaps you like `Joi`. In this case, you can use it to validate the url parameters, body, and query string! ```js -const Joi = require('joi') +const Joi = require('@hapi/joi') fastify.post('/the/url', { schema: { diff --git a/package.json b/package.json index b23a8f19095..9747d35f796 100644 --- a/package.json +++ b/package.json @@ -92,12 +92,12 @@ "node": ">=8.16.0" }, "devDependencies": { - "@types/node": "^11.13.19", + "@types/node": "^12.0.10", "@typescript-eslint/eslint-plugin": "^2.3.0", "@typescript-eslint/parser": "^2.3.0", "JSONStream": "^1.3.5", "ajv-pack": "^0.3.1", - "autocannon": "^3.2.0", + "autocannon": "^4.0.0", "branch-comparer": "^0.4.0", "concurrently": "^5.0.0", "cors": "^2.8.5", @@ -106,34 +106,34 @@ "eslint": "^6.4.0", "eslint-import-resolver-node": "^0.3.2", "fast-json-body": "^1.1.0", - "fastify-plugin": "^1.5.0", + "fastify-plugin": "^1.6.0", "fluent-schema": "^0.8.0", "form-data": "^3.0.0", - "frameguard": "^3.0.0", + "frameguard": "^3.1.0", "h2url": "^0.2.0", "helmet": "^3.20.0", - "hide-powered-by": "^1.0.0", - "hsts": "^2.1.0", + "hide-powered-by": "^1.1.0", + "hsts": "^2.2.0", "http-errors": "^1.7.1", - "ienoopen": "^1.0.0", - "joi": "^12.0.0", + "ienoopen": "^1.1.0", + "@hapi/joi": "^15.1.0", "license-checker": "^25.0.1", "lolex": "^4.2.0", "pre-commit": "^1.2.2", "proxyquire": "^2.1.3", "pump": "^3.0.0", "semver": "^6.3.0", - "send": "^0.17.0", - "serve-static": "^1.13.2", + "send": "^0.17.1", + "serve-static": "^1.14.1", "simple-get": "^3.0.3", "snazzy": "^8.0.0", - "split2": "^3.1.0", + "split2": "^3.1.1", "standard": "^14.0.0", - "tap": "^12.5.2", - "tap-mocha-reporter": "^3.0.7", + "tap": "^14.4.1", + "tap-mocha-reporter": "^4.0.1", "then-sleep": "^1.0.1", "typescript": "^3.6.3", - "x-xss-protection": "^1.1.0" + "x-xss-protection": "^1.2.0" }, "dependencies": { "abstract-logging": "^2.0.0", @@ -145,22 +145,15 @@ "light-my-request": "^3.4.1", "middie": "^4.0.1", "pino": "^5.13.2", - "proxy-addr": "^2.0.4", - "readable-stream": "^3.1.1", - "rfdc": "^1.1.2", + "proxy-addr": "^2.0.5", + "readable-stream": "^3.4.0", + "rfdc": "^1.1.4", "secure-json-parse": "^2.0.0", "tiny-lru": "^7.0.0" }, "greenkeeper": { "ignore": [ - "autocannon", - "boom", - "joi", - "@types/node", - "tap", - "tap-mocha-reporter", - "@typescript-eslint/eslint-plugin", - "lolex" + "@types/node" ] }, "standard": { @@ -168,4 +161,4 @@ "lib/configValidator.js" ] } -} +} \ No newline at end of file diff --git a/test/input-validation.js b/test/input-validation.js index 5a70a56d689..5b21880974d 100644 --- a/test/input-validation.js +++ b/test/input-validation.js @@ -2,7 +2,7 @@ const sget = require('simple-get').concat const Ajv = require('ajv') -const Joi = require('joi') +const Joi = require('@hapi/joi') module.exports.payloadMethod = function (method, t) { const test = t.test diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index 8c9108d41a8..890d080a778 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -186,8 +186,8 @@ test('Original options must not be altered (test deep cloning)', t => { const originalOptions = { https: { allowHTTP1: true, - key: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.key')), - cert: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.cert')) + key: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.key'), 'utf8').toString('base64'), + cert: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.cert'), 'utf8').toString('base64') } } diff --git a/test/post.test.js b/test/post.test.js index 144a3b5a82e..82733b4dc9d 100644 --- a/test/post.test.js +++ b/test/post.test.js @@ -1,7 +1,7 @@ 'use strict' const t = require('tap') -const Joi = require('joi') +const Joi = require('@hapi/joi') require('./helper').payloadMethod('post', t) require('./input-validation').payloadMethod('post', t) diff --git a/test/validation-error-handling.test.js b/test/validation-error-handling.test.js index d7d2f59748d..29f0b7e30a7 100644 --- a/test/validation-error-handling.test.js +++ b/test/validation-error-handling.test.js @@ -1,7 +1,7 @@ 'use strict' const t = require('tap') -const Joi = require('joi') +const Joi = require('@hapi/joi') const Fastify = require('..') const test = t.test From 93add0cc1111122b8b011de81c97ae1fdd7da0dd Mon Sep 17 00:00:00 2001 From: Sandro Martini Date: Tue, 16 Jul 2019 11:33:30 +0200 Subject: [PATCH 138/144] Remove old/deprecated code for Fastify v3 (#1750) * remove beforeHandler and related implementation and warnings if used (it's no more supported) * remove basePath and related warnings (no more supported) * remove genReqId and related warnings (no more supported) --- fastify.js | 7 ----- lib/fourOhFour.js | 7 +---- lib/warnings.js | 7 ----- test/emit-warning.test.js | 39 ---------------------------- test/internals/initialConfig.test.js | 4 +-- test/route-hooks.test.js | 32 ----------------------- 6 files changed, 3 insertions(+), 93 deletions(-) delete mode 100644 lib/warnings.js delete mode 100644 test/emit-warning.test.js diff --git a/fastify.js b/fastify.js index 4609b6d89d0..002b99335f8 100644 --- a/fastify.js +++ b/fastify.js @@ -229,13 +229,6 @@ function build (options) { } }) - Object.defineProperty(fastify, 'basePath', { - get: function () { - process.emitWarning('basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix') - return this[kRoutePrefix] - } - }) - Object.defineProperty(fastify, 'pluginName', { get: function () { if (this[kPluginNameChain].length > 1) { diff --git a/lib/fourOhFour.js b/lib/fourOhFour.js index 83f78563d84..9a04232b1dd 100644 --- a/lib/fourOhFour.js +++ b/lib/fourOhFour.js @@ -18,7 +18,6 @@ const { kMiddlewares, kHooks } = require('./symbols.js') -const { beforeHandlerWarning } = require('./warnings') const { buildMiddie } = require('./middleware') /** @@ -76,10 +75,6 @@ function fourOhFour (options) { } if (typeof opts === 'object') { - if (opts.preHandler == null && opts.beforeHandler != null) { - beforeHandlerWarning() - opts.preHandler = opts.beforeHandler - } if (opts.preHandler) { if (Array.isArray(opts.preHandler)) { opts.preHandler = opts.preHandler.map(hook => hook.bind(_fastify)) @@ -136,7 +131,7 @@ function fourOhFour (options) { const preParsing = this[kHooks].preParsing.concat(opts.preParsing || []) const preValidation = this[kHooks].preValidation.concat(opts.preValidation || []) const preSerialization = this[kHooks].preSerialization.concat(opts.preSerialization || []) - const preHandler = this[kHooks].preHandler.concat(opts.beforeHandler || opts.preHandler || []) + const preHandler = this[kHooks].preHandler.concat(opts.preHandler || []) const onSend = this[kHooks].onSend const onError = this[kHooks].onError const onResponse = this[kHooks].onResponse diff --git a/lib/warnings.js b/lib/warnings.js deleted file mode 100644 index c3cd5df890f..00000000000 --- a/lib/warnings.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -module.exports.beforeHandlerWarning = function beforeHandlerWarning () { - if (beforeHandlerWarning.called) return - beforeHandlerWarning.called = true - process.emitWarning('The route option `beforeHandler` has been deprecated, use `preHandler` instead') -} diff --git a/test/emit-warning.test.js b/test/emit-warning.test.js deleted file mode 100644 index 000dff7c86e..00000000000 --- a/test/emit-warning.test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -const { test } = require('tap') -const Fastify = require('..') - -test('should emit warning using genReqId prop in logger options', t => { - t.plan(1) - - process.once('warning', warning => { - t.strictEqual(warning.message, "Using 'genReqId' in logger options is deprecated. Use fastify options instead. See: https://www.fastify.io/docs/latest/Server/#gen-request-id") - }) - - Fastify({ logger: { genReqId: 'test' } }) -}) - -test('should emit warning if basePath prop is used', t => { - t.plan(1) - - process.once('warning', warning => { - t.strictEqual(warning.message, 'basePath is deprecated. Use prefix instead. See: https://www.fastify.io/docs/latest/Server/#prefix') - }) - - const fastify = Fastify({ basePath: '/test' }) - return fastify.basePath -}) - -test('should emit warning if preHandler is used', t => { - t.plan(1) - - process.once('warning', warning => { - t.strictEqual(warning.message, 'The route option `beforeHandler` has been deprecated, use `preHandler` instead') - }) - - const fastify = Fastify() - - fastify.setNotFoundHandler({ - beforeHandler: (req, reply, done) => done() - }, () => {}) -}) diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index 890d080a778..94ea3ee0a83 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -60,6 +60,7 @@ test('Fastify.initialConfig should expose all options', t => { } } + let reqId = 0 const options = { http2: true, https: { @@ -76,8 +77,7 @@ test('Fastify.initialConfig should expose all options', t => { pluginTimeout: 20000, querystringParser: str => str, genReqId: function (req) { - let i = 0 - return i++ + return reqId++ }, logger: pino({ level: 'info' }), versioning, diff --git a/test/route-hooks.test.js b/test/route-hooks.test.js index ee50251d536..619a2a0d4ec 100644 --- a/test/route-hooks.test.js +++ b/test/route-hooks.test.js @@ -296,38 +296,6 @@ testBeforeHandlerHook('onRequest') testBeforeHandlerHook('preValidation') testBeforeHandlerHook('preParsing') -test('preHandler backwards compatibility with beforeHandler option (should emit a warning)', t => { - t.plan(4) - const fastify = Fastify() - - process.on('warning', warn => { - t.strictEqual( - warn.message, - 'The route option `beforeHandler` has been deprecated, use `preHandler` instead' - ) - t.ok(warn.stack.indexOf(__filename) >= 0) - }) - - fastify.post('/', { - beforeHandler: (req, reply, done) => { - req.body.preHandler = true - done() - } - }, (req, reply) => { - reply.send(req.body) - }) - - fastify.inject({ - method: 'POST', - url: '/', - payload: { hello: 'world' } - }, (err, res) => { - t.error(err) - var payload = JSON.parse(res.payload) - t.deepEqual(payload, { preHandler: true, hello: 'world' }) - }) -}) - test('preValidation option should be called before preHandler hook', t => { t.plan(3) const fastify = Fastify() From 0b36a3d51d099df8b4410e47d6fd8b987389f99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8F=9C?= Date: Sat, 27 Jul 2019 18:10:15 +0800 Subject: [PATCH 139/144] chore: change async hooks warning to error for next version (#1766) * chore: change async hooks warning to error * test: change test case for 'change async hooks warning to error' --- fastify.js | 5 ++--- test/hooks-async.test.js | 47 +++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/fastify.js b/fastify.js index 002b99335f8..42805d76e5c 100644 --- a/fastify.js +++ b/fastify.js @@ -343,14 +343,13 @@ function build (options) { function addHook (name, fn) { throwIfAlreadyStarted('Cannot call "addHook" when fastify instance is already started!') - // TODO: v3 instead of log a warning, throw an error if (name === 'onSend' || name === 'preSerialization' || name === 'onError') { if (fn.constructor.name === 'AsyncFunction' && fn.length === 4) { - fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack) + throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.') } } else { if (fn.constructor.name === 'AsyncFunction' && fn.length === 3) { - fastify.log.warn("Async function has too many arguments. Async hooks should not use the 'next' argument.", new Error().stack) + throw new Error('Async function has too many arguments. Async hooks should not use the \'done\' argument.') } } diff --git a/test/hooks-async.test.js b/test/hooks-async.test.js index 294236eea9c..2416d56f625 100644 --- a/test/hooks-async.test.js +++ b/test/hooks-async.test.js @@ -2,7 +2,6 @@ const t = require('tap') const test = t.test -const split = require('split2') const sget = require('simple-get').concat const Fastify = require('../fastify') const fs = require('fs') @@ -431,38 +430,32 @@ test('preHandler respond with a stream', t => { }) }) -test('Should log a warning if is an async function with `next`', t => { +test('Should log a warning if is an async function with `done`', t => { t.test('3 arguments', t => { - t.plan(3) - const stream = split(JSON.parse) - const fastify = Fastify({ - logger: { stream } - }) - - stream.on('data', line => { - t.strictEqual(line.level, 40) - t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) - t.true(/test(\\|\/)hooks-async\.test\.js/.test(line.msg)) - }) + t.plan(1) + const fastify = Fastify() - fastify.addHook('onRequest', async (req, reply, next) => {}) + try { + fastify.addHook('onRequest', async (req, reply, done) => {}) + } catch (e) { + t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + } }) t.test('4 arguments', t => { - t.plan(6) - const stream = split(JSON.parse) - const fastify = Fastify({ - logger: { stream } - }) + t.plan(2) + const fastify = Fastify() - stream.on('data', line => { - t.strictEqual(line.level, 40) - t.true(line.msg.startsWith(`Async function has too many arguments. Async hooks should not use the 'next' argument.`)) - t.true(/test(\\|\/)hooks-async\.test\.js/.test(line.msg)) - }) - - fastify.addHook('onSend', async (req, reply, payload, next) => {}) - fastify.addHook('preSerialization', async (req, reply, payload, next) => {}) + try { + fastify.addHook('onSend', async (req, reply, payload, done) => {}) + } catch (e) { + t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + } + try { + fastify.addHook('preSerialization', async (req, reply, payload, done) => {}) + } catch (e) { + t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + } }) t.end() From 8d379d3b758c11c2878212a2b6f383019836f4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8F=9C?= Date: Tue, 30 Jul 2019 01:17:49 +0800 Subject: [PATCH 140/144] test: change hooks test cases from next to done (#1774) --- test/hooks.test.js | 722 ++++++++++++++++++++++----------------------- 1 file changed, 361 insertions(+), 361 deletions(-) diff --git a/test/hooks.test.js b/test/hooks.test.js index 64e572e9bdb..24aae971b71 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -17,13 +17,13 @@ test('hooks', t => { const fastify = Fastify() try { - fastify.addHook('preHandler', function (request, reply, next) { + fastify.addHook('preHandler', function (request, reply, done) { t.is(request.test, 'the request is coming') t.is(reply.test, 'the reply has come') if (request.raw.method === 'HEAD') { - next(new Error('some error')) + done(new Error('some error')) } else { - next() + done() } }) t.pass() @@ -32,11 +32,11 @@ test('hooks', t => { } try { - fastify.addHook('preParsing', function (request, reply, next) { + fastify.addHook('preParsing', function (request, reply, done) { request.preParsing = true t.is(request.test, 'the request is coming') t.is(reply.test, 'the reply has come') - next() + done() }) t.pass() } catch (e) { @@ -44,11 +44,11 @@ test('hooks', t => { } try { - fastify.addHook('preValidation', function (request, reply, next) { + fastify.addHook('preValidation', function (request, reply, done) { t.is(request.preParsing, true) t.is(request.test, 'the request is coming') t.is(reply.test, 'the reply has come') - next() + done() }) t.pass() } catch (e) { @@ -56,9 +56,9 @@ test('hooks', t => { } try { - fastify.addHook('preSerialization', function (request, reply, payload, next) { + fastify.addHook('preSerialization', function (request, reply, payload, done) { t.ok('preSerialization called') - next() + done() }) t.pass() } catch (e) { @@ -66,13 +66,13 @@ test('hooks', t => { } try { - fastify.addHook('onRequest', function (request, reply, next) { + fastify.addHook('onRequest', function (request, reply, done) { request.test = 'the request is coming' reply.test = 'the reply has come' if (request.raw.method === 'DELETE') { - next(new Error('some error')) + done(new Error('some error')) } else { - next() + done() } }) t.pass() @@ -80,14 +80,14 @@ test('hooks', t => { t.fail() } - fastify.addHook('onResponse', function (request, reply, next) { + fastify.addHook('onResponse', function (request, reply, done) { t.ok('onResponse called') - next() + done() }) - fastify.addHook('onSend', function (req, reply, thePayload, next) { + fastify.addHook('onSend', function (req, reply, thePayload, done) { t.ok('onSend called') - next() + done() }) fastify.route({ @@ -152,17 +152,17 @@ test('onRequest hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() - fastify.register((instance, opts, next) => { - instance.addHook('onRequest', (req, reply, next) => { + fastify.register((instance, opts, done) => { + instance.addHook('onRequest', (req, reply, done) => { t.strictEqual(req.raw.url, '/plugin') - next() + done() }) instance.get('/plugin', (request, reply) => { reply.send() }) - next() + done() }) fastify.get('/root', (request, reply) => { @@ -187,10 +187,10 @@ test('onRequest hook should support encapsulation / 2', t => { fastify.addHook('onRequest', () => {}) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRequest', () => {}) pluginInstance = instance - next() + done() }) fastify.ready(err => { @@ -205,11 +205,11 @@ test('onRequest hook should support encapsulation / 3', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('onRequest', function (req, reply, next) { + fastify.addHook('onRequest', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) req.first = true - next() + done() }) fastify.decorate('hello2', 'world') @@ -220,14 +220,14 @@ test('onRequest hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello3', 'world') - instance.addHook('onRequest', function (req, reply, next) { + instance.addHook('onRequest', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok(this.hello3) req.second = true - next() + done() }) instance.get('/second', (req, reply) => { @@ -236,7 +236,7 @@ test('onRequest hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -270,10 +270,10 @@ test('preHandler hook should support encapsulation / 5', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('preHandler', function (req, res, next) { + fastify.addHook('preHandler', function (req, res, done) { t.ok(this.hello) req.first = true - next() + done() }) fastify.get('/first', (req, reply) => { @@ -282,13 +282,13 @@ test('preHandler hook should support encapsulation / 5', t => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') - instance.addHook('preHandler', function (req, res, next) { + instance.addHook('preHandler', function (req, res, done) { t.ok(this.hello) t.ok(this.hello2) req.second = true - next() + done() }) instance.get('/second', (req, reply) => { @@ -297,7 +297,7 @@ test('preHandler hook should support encapsulation / 5', t => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -330,14 +330,14 @@ test('onRoute hook should be called / 1', t => { t.plan(2) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', () => { t.pass() }) instance.get('/', opts, function (req, reply) { reply.send() }) - next() + done() }) fastify.ready(err => { @@ -355,7 +355,7 @@ test('onRoute hook should be called / 2', t => { firstHandler++ }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.pass() secondHandler++ @@ -363,7 +363,7 @@ test('onRoute hook should be called / 2', t => { instance.get('/', opts, function (req, reply) { reply.send() }) - next() + done() }) .after(() => { t.strictEqual(firstHandler, 1) @@ -387,12 +387,12 @@ test('onRoute hook should be called / 3', t => { t.pass() }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.pass() }) instance.get('/a', handler) - next() + done() }) .after((err, done) => { t.error(err) @@ -410,7 +410,7 @@ test('onRoute hook should be called / 3', t => { test('onRoute should keep the context', t => { t.plan(4) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('test', true) instance.addHook('onRoute', onRoute) t.ok(instance.prototype === fastify.prototype) @@ -424,7 +424,7 @@ test('onRoute should keep the context', t => { reply.send() }) - next() + done() }) fastify.close((err) => { @@ -441,7 +441,7 @@ test('onRoute hook should pass correct route', t => { t.strictEqual(route.path, '/') }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.strictEqual(route.method, 'GET') t.strictEqual(route.url, '/') @@ -450,7 +450,7 @@ test('onRoute hook should pass correct route', t => { instance.get('/', opts, function (req, reply) { reply.send() }) - next() + done() }) fastify.ready(err => { @@ -468,7 +468,7 @@ test('onRoute hook should pass correct route with custom prefix', t => { t.strictEqual(route.prefix, '/v1') }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.strictEqual(route.method, 'GET') t.strictEqual(route.url, '/v1/foo') @@ -478,7 +478,7 @@ test('onRoute hook should pass correct route with custom prefix', t => { instance.get('/foo', opts, function (req, reply) { reply.send() }) - next() + done() }, { prefix: '/v1' }) fastify.ready(err => { @@ -489,7 +489,7 @@ test('onRoute hook should pass correct route with custom prefix', t => { test('onRoute hook should pass correct route with custom options', t => { t.plan(6) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.strictEqual(route.method, 'GET') t.strictEqual(route.url, '/foo') @@ -506,7 +506,7 @@ test('onRoute hook should pass correct route with custom options', t => { }, function (req, reply) { reply.send() }) - next() + done() }) fastify.ready(err => { @@ -517,7 +517,7 @@ test('onRoute hook should pass correct route with custom options', t => { test('onRoute hook should receive any route option', t => { t.plan(4) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.strictEqual(route.method, 'GET') t.strictEqual(route.url, '/foo') @@ -526,7 +526,7 @@ test('onRoute hook should receive any route option', t => { instance.get('/foo', { auth: 'basic' }, function (req, reply) { reply.send() }) - next() + done() }) fastify.ready(err => { @@ -537,7 +537,7 @@ test('onRoute hook should receive any route option', t => { test('onRoute hook should preserve system route configuration', t => { t.plan(4) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.strictEqual(route.method, 'GET') t.strictEqual(route.url, '/foo') @@ -546,7 +546,7 @@ test('onRoute hook should preserve system route configuration', t => { instance.get('/foo', { url: '/bar', method: 'POST' }, function (req, reply) { reply.send() }) - next() + done() }) fastify.ready(err => { @@ -560,12 +560,12 @@ test('onRoute hook should preserve handler function in options of shorthand rout const handler = (req, reply) => {} const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.strictEqual(route.handler, handler) }) instance.get('/foo', { handler }) - next() + done() }) fastify.ready(err => { @@ -613,8 +613,8 @@ test('onResponse hook should log request error', t => { t.equal(line.level, 50) }) - fastify.addHook('onResponse', (request, reply, next) => { - next(new Error('kaboom')) + fastify.addHook('onResponse', (request, reply, done) => { + done(new Error('kaboom')) }) fastify.get('/root', (request, reply) => { @@ -631,10 +631,10 @@ test('onResponse hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() - fastify.register((instance, opts, next) => { - instance.addHook('onResponse', (request, reply, next) => { + fastify.register((instance, opts, done) => { + instance.addHook('onResponse', (request, reply, done) => { t.strictEqual(reply.plugin, true) - next() + done() }) instance.get('/plugin', (request, reply) => { @@ -642,7 +642,7 @@ test('onResponse hook should support encapsulation / 1', t => { reply.send() }) - next() + done() }) fastify.get('/root', (request, reply) => { @@ -667,10 +667,10 @@ test('onResponse hook should support encapsulation / 2', t => { fastify.addHook('onResponse', () => {}) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onResponse', () => {}) pluginInstance = instance - next() + done() }) fastify.ready(err => { @@ -685,30 +685,30 @@ test('onResponse hook should support encapsulation / 3', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('onResponse', function (request, reply, next) { + fastify.addHook('onResponse', function (request, reply, done) { t.ok(this.hello) t.ok('onResponse called') - next() + done() }) fastify.get('/first', (req, reply) => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') - instance.addHook('onResponse', function (request, reply, next) { + instance.addHook('onResponse', function (request, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok('onResponse called') - next() + done() }) instance.get('/second', (req, reply) => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -744,10 +744,10 @@ test('onSend hook should support encapsulation / 1', t => { fastify.addHook('onSend', () => {}) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('onSend', () => {}) pluginInstance = instance - next() + done() }) fastify.ready(err => { @@ -762,30 +762,30 @@ test('onSend hook should support encapsulation / 2', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('onSend', function (request, reply, thePayload, next) { + fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok(this.hello) t.ok('onSend called') - next() + done() }) fastify.get('/first', (req, reply) => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') - instance.addHook('onSend', function (request, reply, thePayload, next) { + instance.addHook('onSend', function (request, reply, thePayload, done) { t.ok(this.hello) t.ok(this.hello2) t.ok('onSend called') - next() + done() }) instance.get('/second', (req, reply) => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -818,53 +818,53 @@ test('onSend hook is called after payload is serialized and headers are set', t t.plan(30) const fastify = Fastify() - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { const thePayload = { hello: 'world' } - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.deepEqual(JSON.parse(payload), thePayload) t.strictEqual(reply[symbols.kReplyHeaders]['content-type'], 'application/json; charset=utf-8') - next() + done() }) instance.get('/json', (request, reply) => { reply.send(thePayload) }) - next() + done() }) - fastify.register((instance, opts, next) => { - instance.addHook('onSend', function (request, reply, payload, next) { + fastify.register((instance, opts, done) => { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(payload, 'some text') t.strictEqual(reply[symbols.kReplyHeaders]['content-type'], 'text/plain; charset=utf-8') - next() + done() }) instance.get('/text', (request, reply) => { reply.send('some text') }) - next() + done() }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { const thePayload = Buffer.from('buffer payload') - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(payload, thePayload) t.strictEqual(reply[symbols.kReplyHeaders]['content-type'], 'application/octet-stream') - next() + done() }) instance.get('/buffer', (request, reply) => { reply.send(thePayload) }) - next() + done() }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { var chunk = 'stream payload' const thePayload = new stream.Readable({ read () { @@ -873,26 +873,26 @@ test('onSend hook is called after payload is serialized and headers are set', t } }) - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(payload, thePayload) t.strictEqual(reply[symbols.kReplyHeaders]['content-type'], 'application/octet-stream') - next() + done() }) instance.get('/stream', (request, reply) => { reply.send(thePayload) }) - next() + done() }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { const serializedPayload = 'serialized' - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(payload, serializedPayload) t.strictEqual(reply[symbols.kReplyHeaders]['content-type'], 'text/custom') - next() + done() }) instance.get('/custom-serializer', (request, reply) => { @@ -902,7 +902,7 @@ test('onSend hook is called after payload is serialized and headers are set', t .send('needs to be serialized') }) - next() + done() }) fastify.inject({ @@ -963,23 +963,23 @@ test('modify payload', t => { const modifiedPayload = { hello: 'modified' } const anotherPayload = '"winter is coming"' - fastify.addHook('onSend', function (request, reply, thePayload, next) { + fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.deepEqual(JSON.parse(thePayload), payload) thePayload = thePayload.replace('world', 'modified') - next(null, thePayload) + done(null, thePayload) }) - fastify.addHook('onSend', function (request, reply, thePayload, next) { + fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.deepEqual(JSON.parse(thePayload), modifiedPayload) - next(null, anotherPayload) + done(null, anotherPayload) }) - fastify.addHook('onSend', function (request, reply, thePayload, next) { + fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.strictEqual(thePayload, anotherPayload) - next() + done() }) fastify.get('/', (req, reply) => { @@ -1001,10 +1001,10 @@ test('clear payload', t => { t.plan(6) const fastify = Fastify() - fastify.addHook('onSend', function (request, reply, payload, next) { + fastify.addHook('onSend', function (request, reply, payload, done) { t.ok('onSend called') reply.code(304) - next(null, null) + done(null, null) }) fastify.get('/', (req, reply) => { @@ -1026,12 +1026,12 @@ test('clear payload', t => { test('onSend hook throws', t => { t.plan(7) const fastify = Fastify() - fastify.addHook('onSend', function (request, reply, payload, next) { + fastify.addHook('onSend', function (request, reply, payload, done) { if (request.raw.method === 'DELETE') { - next(new Error('some error')) + done(new Error('some error')) return } - next() + done() }) fastify.get('/', (req, reply) => { @@ -1071,14 +1071,14 @@ test('onSend hook should receive valid request and reply objects if onRequest ho fastify.decorateRequest('testDecorator', 'testDecoratorVal') fastify.decorateReply('testDecorator', 'testDecoratorVal') - fastify.addHook('onRequest', function (req, reply, next) { - next(new Error('onRequest hook failed')) + fastify.addHook('onRequest', function (req, reply, done) { + done(new Error('onRequest hook failed')) }) - fastify.addHook('onSend', function (request, reply, payload, next) { + fastify.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.testDecorator, 'testDecoratorVal') t.strictEqual(reply.testDecorator, 'testDecoratorVal') - next() + done() }) fastify.get('/', (req, reply) => { @@ -1101,14 +1101,14 @@ test('onSend hook should receive valid request and reply objects if middleware f fastify.decorateRequest('testDecorator', 'testDecoratorVal') fastify.decorateReply('testDecorator', 'testDecoratorVal') - fastify.use(function (req, res, next) { - next(new Error('middlware failed')) + fastify.use(function (req, res, done) { + done(new Error('middlware failed')) }) - fastify.addHook('onSend', function (request, reply, payload, next) { + fastify.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.testDecorator, 'testDecoratorVal') t.strictEqual(reply.testDecorator, 'testDecoratorVal') - next() + done() }) fastify.get('/', (req, reply) => { @@ -1135,10 +1135,10 @@ test('onSend hook should receive valid request and reply objects if a custom con done(new Error('content type parser failed')) }) - fastify.addHook('onSend', function (request, reply, payload, next) { + fastify.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.testDecorator, 'testDecoratorVal') t.strictEqual(reply.testDecorator, 'testDecoratorVal') - next() + done() }) fastify.get('/', (req, reply) => { @@ -1180,27 +1180,27 @@ test('onRequest hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1221,27 +1221,27 @@ test('preValidation hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('preValidation', (req, reply, next) => { + fastify.addHook('preValidation', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('preValidation', (req, reply, next) => { + fastify.addHook('preValidation', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1262,27 +1262,27 @@ test('preParsing hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('preParsing', (req, reply, next) => { + fastify.addHook('preParsing', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('preParsing', (req, reply, next) => { + fastify.addHook('preParsing', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1303,23 +1303,23 @@ test('preHandler hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.equal(payload, 'hello') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1340,23 +1340,23 @@ test('onRequest hooks should be able to block a request (last hook)', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1377,19 +1377,19 @@ test('preHandler hooks should be able to block a request (last hook)', t => { t.plan(5) const fastify = Fastify() - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { reply.send('hello') - next() + done() }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.equal(payload, 'hello') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1410,29 +1410,29 @@ test('onRequest respond with a stream', t => { t.plan(4) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') // stream.pipe(res) - // res.once('finish', next) + // res.once('finish', done) reply.send(stream) }) - fastify.addHook('onRequest', (req, res, next) => { + fastify.addHook('onRequest', (req, res, done) => { t.fail('this should not be called') }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1452,37 +1452,37 @@ test('preHandler respond with a stream', t => { t.plan(7) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { t.ok('called') - next() + done() }) // we are calling `reply.send` inside the `preHandler` hook with a stream, // this triggers the `onSend` hook event if `preHanlder` has not yet finished const order = [1, 2] - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') reply.send(stream) reply.res.once('finish', () => { t.is(order.shift(), 2) - next() + done() }) }) - fastify.addHook('preHandler', (req, reply, next) => { + fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) - fastify.addHook('onSend', (req, reply, payload, next) => { + fastify.addHook('onSend', (req, reply, payload, done) => { t.is(order.shift(), 1) t.is(typeof payload.pipe, 'function') - next() + done() }) - fastify.addHook('onResponse', (request, reply, next) => { + fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') - next() + done() }) fastify.get('/', function (request, reply) { @@ -1502,31 +1502,31 @@ test('Register an hook after a plugin inside a plugin', t => { t.plan(6) const fastify = Fastify() - fastify.register(fp(function (instance, opts, next) { - instance.addHook('preHandler', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) - next() + done() })) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('preHandler', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) - instance.addHook('preHandler', function (req, reply, next) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) - next() + done() })) fastify.inject({ @@ -1543,36 +1543,36 @@ test('Register an hook after a plugin inside a plugin (with preHandler option)', t.plan(7) const fastify = Fastify() - fastify.register(fp(function (instance, opts, next) { - instance.addHook('preHandler', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) instance.get('/', { - preHandler: (req, reply, next) => { + preHandler: (req, reply, done) => { t.ok('called') - next() + done() } }, function (request, reply) { reply.send({ hello: 'world' }) }) - next() + done() })) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('preHandler', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) - instance.addHook('preHandler', function (req, reply, next) { + instance.addHook('preHandler', function (req, reply, done) { t.ok('called') - next() + done() }) - next() + done() })) fastify.inject({ @@ -1589,36 +1589,36 @@ test('Register hooks inside a plugin after an encapsulated plugin', t => { t.plan(7) const fastify = Fastify() - fastify.register(function (instance, opts, next) { + fastify.register(function (instance, opts, done) { instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) - next() + done() }) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('onRequest', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('onRequest', function (req, reply, done) { t.ok('called') - next() + done() }) - instance.addHook('preHandler', function (request, reply, next) { + instance.addHook('preHandler', function (request, reply, done) { t.ok('called') - next() + done() }) - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.ok('called') - next() + done() }) - instance.addHook('onResponse', function (request, reply, next) { + instance.addHook('onResponse', function (request, reply, done) { t.ok('called') - next() + done() }) - next() + done() })) fastify.inject('/', (err, res) => { @@ -1632,11 +1632,11 @@ test('onRequest hooks should run in the order in which they are defined', t => { t.plan(9) const fastify = Fastify() - fastify.register(function (instance, opts, next) { - instance.addHook('onRequest', function (req, reply, next) { + fastify.register(function (instance, opts, done) { + instance.addHook('onRequest', function (req, reply, done) { t.strictEqual(req.previous, undefined) req.previous = 1 - next() + done() }) instance.get('/', function (request, reply) { @@ -1644,41 +1644,41 @@ test('onRequest hooks should run in the order in which they are defined', t => { reply.send({ hello: 'world' }) }) - instance.register(fp(function (i, opts, next) { - i.addHook('onRequest', function (req, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onRequest', function (req, reply, done) { t.strictEqual(req.previous, 1) req.previous = 2 - next() + done() }) - next() + done() })) - next() + done() }) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('onRequest', function (req, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('onRequest', function (req, reply, done) { t.strictEqual(req.previous, 2) req.previous = 3 - next() + done() }) - instance.register(fp(function (i, opts, next) { - i.addHook('onRequest', function (req, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onRequest', function (req, reply, done) { t.strictEqual(req.previous, 3) req.previous = 4 - next() + done() }) - next() + done() })) - instance.addHook('onRequest', function (req, reply, next) { + instance.addHook('onRequest', function (req, reply, done) { t.strictEqual(req.previous, 4) req.previous = 5 - next() + done() }) - next() + done() })) fastify.inject('/', (err, res) => { @@ -1692,11 +1692,11 @@ test('preHandler hooks should run in the order in which they are defined', t => t.plan(9) const fastify = Fastify() - fastify.register(function (instance, opts, next) { - instance.addHook('preHandler', function (request, reply, next) { + fastify.register(function (instance, opts, done) { + instance.addHook('preHandler', function (request, reply, done) { t.strictEqual(request.previous, undefined) request.previous = 1 - next() + done() }) instance.get('/', function (request, reply) { @@ -1704,41 +1704,41 @@ test('preHandler hooks should run in the order in which they are defined', t => reply.send({ hello: 'world' }) }) - instance.register(fp(function (i, opts, next) { - i.addHook('preHandler', function (request, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('preHandler', function (request, reply, done) { t.strictEqual(request.previous, 1) request.previous = 2 - next() + done() }) - next() + done() })) - next() + done() }) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('preHandler', function (request, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('preHandler', function (request, reply, done) { t.strictEqual(request.previous, 2) request.previous = 3 - next() + done() }) - instance.register(fp(function (i, opts, next) { - i.addHook('preHandler', function (request, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('preHandler', function (request, reply, done) { t.strictEqual(request.previous, 3) request.previous = 4 - next() + done() }) - next() + done() })) - instance.addHook('preHandler', function (request, reply, next) { + instance.addHook('preHandler', function (request, reply, done) { t.strictEqual(request.previous, 4) request.previous = 5 - next() + done() }) - next() + done() })) fastify.inject('/', (err, res) => { @@ -1752,51 +1752,51 @@ test('onSend hooks should run in the order in which they are defined', t => { t.plan(8) const fastify = Fastify() - fastify.register(function (instance, opts, next) { - instance.addHook('onSend', function (request, reply, payload, next) { + fastify.register(function (instance, opts, done) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.previous, undefined) request.previous = 1 - next() + done() }) instance.get('/', function (request, reply) { reply.send({}) }) - instance.register(fp(function (i, opts, next) { - i.addHook('onSend', function (request, reply, payload, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.previous, 1) request.previous = 2 - next() + done() }) - next() + done() })) - next() + done() }) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('onSend', function (request, reply, payload, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.previous, 2) request.previous = 3 - next() + done() }) - instance.register(fp(function (i, opts, next) { - i.addHook('onSend', function (request, reply, payload, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.previous, 3) request.previous = 4 - next() + done() }) - next() + done() })) - instance.addHook('onSend', function (request, reply, payload, next) { + instance.addHook('onSend', function (request, reply, payload, done) { t.strictEqual(request.previous, 4) - next(null, '5') + done(null, '5') }) - next() + done() })) fastify.inject('/', (err, res) => { @@ -1810,51 +1810,51 @@ test('onResponse hooks should run in the order in which they are defined', t => t.plan(8) const fastify = Fastify() - fastify.register(function (instance, opts, next) { - instance.addHook('onResponse', function (request, reply, next) { + fastify.register(function (instance, opts, done) { + instance.addHook('onResponse', function (request, reply, done) { t.strictEqual(reply.previous, undefined) reply.previous = 1 - next() + done() }) instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) - instance.register(fp(function (i, opts, next) { - i.addHook('onResponse', function (request, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onResponse', function (request, reply, done) { t.strictEqual(reply.previous, 1) reply.previous = 2 - next() + done() }) - next() + done() })) - next() + done() }) - fastify.register(fp(function (instance, opts, next) { - instance.addHook('onResponse', function (request, reply, next) { + fastify.register(fp(function (instance, opts, done) { + instance.addHook('onResponse', function (request, reply, done) { t.strictEqual(reply.previous, 2) reply.previous = 3 - next() + done() }) - instance.register(fp(function (i, opts, next) { - i.addHook('onResponse', function (request, reply, next) { + instance.register(fp(function (i, opts, done) { + i.addHook('onResponse', function (request, reply, done) { t.strictEqual(reply.previous, 3) reply.previous = 4 - next() + done() }) - next() + done() })) - instance.addHook('onResponse', function (request, reply, next) { + instance.addHook('onResponse', function (request, reply, done) { t.strictEqual(reply.previous, 4) - next() + done() }) - next() + done() })) fastify.inject('/', (err, res) => { @@ -1893,9 +1893,9 @@ test('If a response header has been set inside an hook it shoulod not be overwri t.plan(5) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { reply.header('X-Custom-Header', 'hello') - next() + done() }) fastify.get('/', (request, reply) => { @@ -1915,9 +1915,9 @@ test('If the content type has been set inside an hook it should not be changed', t.plan(5) const fastify = Fastify() - fastify.addHook('onRequest', (req, reply, next) => { + fastify.addHook('onRequest', (req, reply, done) => { reply.header('content-type', 'text/html') - next() + done() }) fastify.get('/', (request, reply) => { @@ -1937,7 +1937,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { t.plan(18) const fastify = Fastify() - fastify.addHook('onRequest', function (request, reply, next) { + fastify.addHook('onRequest', function (request, reply, done) { t.deepEqual(request.body, null) t.deepEqual(request.query, { key: 'value' }) t.deepEqual(request.params, { greeting: 'hello' }) @@ -1948,10 +1948,10 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) - next() + done() }) - fastify.addHook('preParsing', function (request, reply, next) { + fastify.addHook('preParsing', function (request, reply, done) { t.deepEqual(request.body, null) t.deepEqual(request.query, { key: 'value' }) t.deepEqual(request.params, { greeting: 'hello' }) @@ -1962,10 +1962,10 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) - next() + done() }) - fastify.addHook('preValidation', function (request, reply, next) { + fastify.addHook('preValidation', function (request, reply, done) { t.deepEqual(request.body, { hello: 'world' }) t.deepEqual(request.query, { key: 'value' }) t.deepEqual(request.params, { greeting: 'hello' }) @@ -1976,10 +1976,10 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) - next() + done() }) - fastify.addHook('onResponse', function (request, reply, next) { + fastify.addHook('onResponse', function (request, reply, done) { t.deepEqual(request.body, { hello: 'world' }) t.deepEqual(request.query, { key: 'value' }) t.deepEqual(request.params, { greeting: 'hello' }) @@ -1990,7 +1990,7 @@ test('request in onRequest, preParsing, preValidation and onResponse', t => { 'user-agent': 'lightMyRequest', 'x-custom': 'hello' }) - next() + done() }) fastify.post('/:greeting', function (req, reply) { @@ -2012,17 +2012,17 @@ test('preValidation hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() - fastify.register((instance, opts, next) => { - instance.addHook('preValidation', (req, reply, next) => { + fastify.register((instance, opts, done) => { + instance.addHook('preValidation', (req, reply, done) => { t.strictEqual(req.raw.url, '/plugin') - next() + done() }) instance.get('/plugin', (request, reply) => { reply.send() }) - next() + done() }) fastify.get('/root', (request, reply) => { @@ -2047,10 +2047,10 @@ test('preValidation hook should support encapsulation / 2', t => { fastify.addHook('preValidation', () => {}) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('preValidation', () => {}) pluginInstance = instance - next() + done() }) fastify.ready(err => { @@ -2065,11 +2065,11 @@ test('preValidation hook should support encapsulation / 3', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('preValidation', function (req, reply, next) { + fastify.addHook('preValidation', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) req.first = true - next() + done() }) fastify.decorate('hello2', 'world') @@ -2080,14 +2080,14 @@ test('preValidation hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello3', 'world') - instance.addHook('preValidation', function (req, reply, next) { + instance.addHook('preValidation', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok(this.hello3) req.second = true - next() + done() }) instance.get('/second', (req, reply) => { @@ -2096,7 +2096,7 @@ test('preValidation hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -2132,9 +2132,9 @@ test('onError hook', t => { const err = new Error('kaboom') - fastify.addHook('onError', (request, reply, error, next) => { + fastify.addHook('onError', (request, reply, error, done) => { t.match(error, err) - next() + done() }) fastify.get('/', (req, reply) => { @@ -2161,14 +2161,14 @@ test('reply.send should throw if called inside the onError hook', t => { const err = new Error('kaboom') - fastify.addHook('onError', (request, reply, error, next) => { + fastify.addHook('onError', (request, reply, error, done) => { try { reply.send() t.fail('Should throw') } catch (err) { t.is(err.code, 'FST_ERR_SEND_INSIDE_ONERR') } - next() + done() }) fastify.get('/', (req, reply) => { @@ -2200,9 +2200,9 @@ test('onError hook with setErrorHandler', t => { reply.send(err) }) - fastify.addHook('onError', (request, reply, error, next) => { + fastify.addHook('onError', (request, reply, error, done) => { t.match(error, err) - next() + done() }) fastify.get('/', (req, reply) => { @@ -2231,7 +2231,7 @@ test('onError hook with setErrorHandler', t => { reply.send({ hello: 'world' }) }) - fastify.addHook('onError', (request, reply, error, next) => { + fastify.addHook('onError', (request, reply, error, done) => { t.fail('Should not be called') }) @@ -2257,17 +2257,17 @@ test('preParsing hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() - fastify.register((instance, opts, next) => { - instance.addHook('preParsing', (req, reply, next) => { + fastify.register((instance, opts, done) => { + instance.addHook('preParsing', (req, reply, done) => { t.strictEqual(req.raw.url, '/plugin') - next() + done() }) instance.get('/plugin', (request, reply) => { reply.send() }) - next() + done() }) fastify.get('/root', (request, reply) => { @@ -2292,10 +2292,10 @@ test('preParsing hook should support encapsulation / 2', t => { fastify.addHook('preParsing', function a () {}) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.addHook('preParsing', function b () {}) pluginInstance = instance - next() + done() }) fastify.ready(err => { @@ -2310,11 +2310,11 @@ test('preParsing hook should support encapsulation / 3', t => { const fastify = Fastify() fastify.decorate('hello', 'world') - fastify.addHook('preParsing', function (req, reply, next) { + fastify.addHook('preParsing', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) req.first = true - next() + done() }) fastify.decorate('hello2', 'world') @@ -2325,14 +2325,14 @@ test('preParsing hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.decorate('hello3', 'world') - instance.addHook('preParsing', function (req, reply, next) { + instance.addHook('preParsing', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok(this.hello3) req.second = true - next() + done() }) instance.get('/second', (req, reply) => { @@ -2341,7 +2341,7 @@ test('preParsing hook should support encapsulation / 3', t => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -2374,11 +2374,11 @@ test('preSerialization hook should run before serialization and be able to modif t.plan(5) const fastify = Fastify() - fastify.addHook('preSerialization', function (req, reply, payload, next) { + fastify.addHook('preSerialization', function (req, reply, payload, done) { payload.hello += '1' payload.world = 'ok' - next(null, payload) + done(null, payload) }) fastify.route({ @@ -2425,8 +2425,8 @@ test('preSerialization hook should run before serialization and be able to modif test('preSerialization hook should be able to throw errors which are not validated against schema response', t => { const fastify = Fastify() - fastify.addHook('preSerialization', function (req, reply, payload, next) { - next(new Error('preSerialization aborted')) + fastify.addHook('preSerialization', function (req, reply, payload, done) { + done(new Error('preSerialization aborted')) }) fastify.route({ @@ -2472,13 +2472,13 @@ test('preSerialization hook which returned error should still run onError hooks' t.plan(4) const fastify = Fastify() - fastify.addHook('preSerialization', function (req, reply, payload, next) { - next(new Error('preSerialization aborted')) + fastify.addHook('preSerialization', function (req, reply, payload, done) { + done(new Error('preSerialization aborted')) }) - fastify.addHook('onError', function (req, reply, payload, next) { + fastify.addHook('onError', function (req, reply, payload, done) { t.pass() - next() + done() }) fastify.get('/first', (req, reply) => { @@ -2503,16 +2503,16 @@ test('preSerialization hooks should run in the order in which they are defined', t.plan(5) const fastify = Fastify() - fastify.addHook('preSerialization', function (req, reply, payload, next) { + fastify.addHook('preSerialization', function (req, reply, payload, done) { payload.hello += '2' - next(null, payload) + done(null, payload) }) - fastify.addHook('preSerialization', function (req, reply, payload, next) { + fastify.addHook('preSerialization', function (req, reply, payload, done) { payload.hello += '1' - next(null, payload) + done(null, payload) }) fastify.get('/first', (req, reply) => { @@ -2539,28 +2539,28 @@ test('preSerialization hooks should support encapsulation', t => { t.plan(9) const fastify = Fastify() - fastify.addHook('preSerialization', function (req, reply, payload, next) { + fastify.addHook('preSerialization', function (req, reply, payload, done) { payload.hello += '1' - next(null, payload) + done(null, payload) }) fastify.get('/first', (req, reply) => { reply.send({ hello: 'world' }) }) - fastify.register((instance, opts, next) => { - instance.addHook('preSerialization', function (req, reply, payload, next) { + fastify.register((instance, opts, done) => { + instance.addHook('preSerialization', function (req, reply, payload, done) { payload.hello += '2' - next(null, payload) + done(null, payload) }) instance.get('/second', (req, reply) => { reply.send({ hello: 'world' }) }) - next() + done() }) fastify.listen(0, err => { @@ -2593,8 +2593,8 @@ test('onRegister hook should be called / 1', t => { t.plan(3) const fastify = Fastify() - fastify.register((instance, opts, next) => { - next() + fastify.register((instance, opts, done) => { + done() }) fastify.addHook('onRegister', instance => { @@ -2611,15 +2611,15 @@ test('onRegister hook should be called / 2', t => { t.plan(5) const fastify = Fastify() - fastify.register((instance, opts, next) => { - instance.register((instance, opts, next) => { - next() + fastify.register((instance, opts, done) => { + instance.register((instance, opts, done) => { + done() }) - next() + done() }) - fastify.register((instance, opts, next) => { - next() + fastify.register((instance, opts, done) => { + done() }) fastify.addHook('onRegister', instance => { @@ -2638,20 +2638,20 @@ test('onRegister hook should be called / 3', t => { fastify.decorate('data', []) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { instance.data.push(1) - instance.register((instance, opts, next) => { + instance.register((instance, opts, done) => { instance.data.push(2) t.deepEqual(instance.data, [1, 2]) - next() + done() }) t.deepEqual(instance.data, [1]) - next() + done() }) - fastify.register((instance, opts, next) => { + fastify.register((instance, opts, done) => { t.deepEqual(instance.data, []) - next() + done() }) fastify.addHook('onRegister', instance => { @@ -2667,8 +2667,8 @@ test('onRegister hook should be called / 4', t => { t.plan(2) const fastify = Fastify() - function plugin (instance, opts, next) { - next() + function plugin (instance, opts, done) { + done() } plugin[Symbol.for('skip-override')] = true From 24cf33d9a37a7e4a04243fb1779e837390271032 Mon Sep 17 00:00:00 2001 From: James Sumners Date: Fri, 9 Aug 2019 18:51:25 -0400 Subject: [PATCH 141/144] Remove code from error name (#1786) Add FastifyError.toString to display code in `console.log(error)` --- lib/errors.js | 7 +++-- test/content-length.test.js | 8 +++--- test/custom-parser.test.js | 34 +++++++++++++++---------- test/decorator.test.js | 5 ++-- test/helper.js | 12 ++++----- test/http2/missing-http2-module.test.js | 5 ++-- test/internals/decorator.test.js | 4 +-- test/internals/errors.test.js | 29 ++++++++++----------- test/internals/handleRequest.test.js | 2 +- test/internals/hooks.test.js | 4 +-- test/internals/initialConfig.test.js | 4 +-- test/internals/logger.test.js | 2 +- test/internals/reply.test.js | 10 +++++--- test/plugin.test.js | 2 +- test/reply-error.test.js | 4 +-- test/shared-schemas.test.js | 8 +++--- test/throw.test.js | 4 +-- 17 files changed, 78 insertions(+), 66 deletions(-) diff --git a/lib/errors.js b/lib/errors.js index 4f06481a1bf..9dc6b123152 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -80,7 +80,7 @@ function createError (code, message, statusCode = 500, Base = Error) { function FastifyError (a, b, c) { Error.captureStackTrace(this, FastifyError) - this.name = `FastifyError [${code}]` + this.name = `FastifyError` this.code = code // more performant than spread (...) operator @@ -94,11 +94,14 @@ function createError (code, message, statusCode = 500, Base = Error) { this.message = message } - this.message = `${this.code}: ${this.message}` this.statusCode = statusCode || undefined } FastifyError.prototype[Symbol.toStringTag] = 'Error' + FastifyError.prototype.toString = function () { + return `${this.name} [${this.code}]: ${this.message}` + } + inherits(FastifyError, Base) codes[code] = FastifyError diff --git a/test/content-length.test.js b/test/content-length.test.js index 3b30e3768a5..eb48eb4e71f 100644 --- a/test/content-length.test.js +++ b/test/content-length.test.js @@ -28,7 +28,7 @@ test('default 413 with bodyLimit option', t => { t.deepEqual(JSON.parse(res.payload), { error: 'Payload Too Large', code: 'FST_ERR_CTP_BODY_TOO_LARGE', - message: 'FST_ERR_CTP_BODY_TOO_LARGE: Request body is too large', + message: 'Request body is too large', statusCode: 413 }) }) @@ -59,7 +59,7 @@ test('default 400 with wrong content-length', t => { t.deepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_INVALID_CONTENT_LENGTH', - message: 'FST_ERR_CTP_INVALID_CONTENT_LENGTH: Request body size did not match Content-Length', + message: 'Request body size did not match Content-Length', statusCode: 400 }) }) @@ -96,7 +96,7 @@ test('custom 413 with bodyLimit option', t => { t.deepEqual(JSON.parse(res.payload), { error: 'Payload Too Large', code: 'FST_ERR_CTP_BODY_TOO_LARGE', - message: 'FST_ERR_CTP_BODY_TOO_LARGE: Request body is too large', + message: 'Request body is too large', statusCode: 413 }) }) @@ -134,7 +134,7 @@ test('custom 400 with wrong content-length', t => { t.deepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_INVALID_CONTENT_LENGTH', - message: 'FST_ERR_CTP_INVALID_CONTENT_LENGTH: Request body size did not match Content-Length', + message: 'Request body size did not match Content-Length', statusCode: 400 }) }) diff --git a/test/custom-parser.test.js b/test/custom-parser.test.js index 60df454cebe..40c017d398e 100644 --- a/test/custom-parser.test.js +++ b/test/custom-parser.test.js @@ -324,38 +324,41 @@ test('contentTypeParser shouldn\'t support request with undefined "Content-Type" }) test('the content type should be a string', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() try { fastify.addContentTypeParser(null, () => {}) t.fail() } catch (err) { - t.is(err.message, 'FST_ERR_CTP_INVALID_TYPE: The content type should be a string') + t.is(err.code, 'FST_ERR_CTP_INVALID_TYPE') + t.is(err.message, 'The content type should be a string') } }) test('the content type cannot be an empty string', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() try { fastify.addContentTypeParser('', () => {}) t.fail() } catch (err) { - t.is(err.message, 'FST_ERR_CTP_EMPTY_TYPE: The content type cannot be an empty string') + t.is(err.code, 'FST_ERR_CTP_EMPTY_TYPE') + t.is(err.message, 'The content type cannot be an empty string') } }) test('the content type handler should be a function', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() try { fastify.addContentTypeParser('aaa', null) t.fail() } catch (err) { - t.is(err.message, 'FST_ERR_CTP_INVALID_HANDLER: The content type handler should be a function') + t.is(err.code, 'FST_ERR_CTP_INVALID_HANDLER') + t.is(err.message, 'The content type handler should be a function') } }) @@ -629,7 +632,7 @@ test('Can override the default json parser in a plugin', t => { }) test('Can\'t override the json parser multiple times', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() fastify.addContentTypeParser('application/json', function (req, done) { @@ -646,12 +649,13 @@ test('Can\'t override the json parser multiple times', t => { }) }) } catch (err) { - t.is(err.message, 'FST_ERR_CTP_ALREADY_PRESENT: Content type parser \'application/json\' already present.') + t.is(err.code, 'FST_ERR_CTP_ALREADY_PRESENT') + t.is(err.message, 'Content type parser \'application/json\' already present.') } }) test('Can\'t override the plain text parser multiple times', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() fastify.addContentTypeParser('text/plain', function (req, done) { @@ -668,7 +672,8 @@ test('Can\'t override the plain text parser multiple times', t => { }) }) } catch (err) { - t.is(err.message, 'FST_ERR_CTP_ALREADY_PRESENT: Content type parser \'text/plain\' already present.') + t.is(err.code, 'FST_ERR_CTP_ALREADY_PRESENT') + t.is(err.message, 'Content type parser \'text/plain\' already present.') } }) @@ -1053,14 +1058,15 @@ test('The charset should not interfere with the content type handling', t => { }) test('Wrong parseAs parameter', t => { - t.plan(1) + t.plan(2) const fastify = Fastify() try { fastify.addContentTypeParser('application/json', { parseAs: 'fireworks' }, () => {}) t.fail('should throw') } catch (err) { - t.is(err.message, "FST_ERR_CTP_INVALID_PARSE_TYPE: The body parser can only parse your data as 'string' or 'buffer', you asked 'fireworks' which is not supported.") + t.is(err.code, 'FST_ERR_CTP_INVALID_PARSE_TYPE') + t.is(err.message, 'The body parser can only parse your data as \'string\' or \'buffer\', you asked \'fireworks\' which is not supported.') } }) @@ -1098,7 +1104,7 @@ test('Should allow defining the bodyLimit per parser', t => { statusCode: 413, code: 'FST_ERR_CTP_BODY_TOO_LARGE', error: 'Payload Too Large', - message: 'FST_ERR_CTP_BODY_TOO_LARGE: Request body is too large' + message: 'Request body is too large' }) fastify.close() }) @@ -1137,7 +1143,7 @@ test('route bodyLimit should take precedence over a custom parser bodyLimit', t statusCode: 413, code: 'FST_ERR_CTP_BODY_TOO_LARGE', error: 'Payload Too Large', - message: 'FST_ERR_CTP_BODY_TOO_LARGE: Request body is too large' + message: 'Request body is too large' }) fastify.close() }) diff --git a/test/decorator.test.js b/test/decorator.test.js index ada71fc2206..2c56d69b00a 100644 --- a/test/decorator.test.js +++ b/test/decorator.test.js @@ -47,7 +47,7 @@ test('hasServerMethod should check if the given method already exist', t => { }) test('decorate should throw if a declared dependency is not present', t => { - t.plan(2) + t.plan(3) const fastify = Fastify() fastify.register((instance, opts, next) => { @@ -55,7 +55,8 @@ test('decorate should throw if a declared dependency is not present', t => { instance.decorate('test', () => {}, ['dependency']) t.fail() } catch (e) { - t.is(e.message, 'FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency \'dependency\'.') + t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') + t.is(e.message, 'The decorator is missing dependency \'dependency\'.') } next() }) diff --git a/test/helper.js b/test/helper.js index cc5e8b9b797..4765772d778 100644 --- a/test/helper.js +++ b/test/helper.js @@ -328,7 +328,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) @@ -344,7 +344,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) @@ -361,7 +361,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) @@ -378,7 +378,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) @@ -395,7 +395,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(res.payload), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) @@ -412,7 +412,7 @@ module.exports.payloadMethod = function (method, t, isSetErrorHandler = false) { t.strictDeepEqual(JSON.parse(body.toString()), { error: 'Bad Request', code: 'FST_ERR_CTP_EMPTY_JSON_BODY', - message: "FST_ERR_CTP_EMPTY_JSON_BODY: Body cannot be empty when content-type is set to 'application/json'", + message: 'Body cannot be empty when content-type is set to \'application/json\'', statusCode: 400 }) }) diff --git a/test/http2/missing-http2-module.test.js b/test/http2/missing-http2-module.test.js index 01e98e5983c..d2575ea4ce8 100644 --- a/test/http2/missing-http2-module.test.js +++ b/test/http2/missing-http2-module.test.js @@ -7,11 +7,12 @@ const server = proxyquire('../../lib/server', { http2: null }) const Fastify = proxyquire('../..', { './lib/server.js': server }) test('should throw when http2 module cannot be found', t => { - t.plan(1) + t.plan(2) try { Fastify({ http2: true }) t.fail('fastify did not throw expected error') } catch (err) { - t.equal(err.message, 'FST_ERR_HTTP2_INVALID_VERSION: HTTP2 is available only from node >= 8.8.1') + t.is(err.code, 'FST_ERR_HTTP2_INVALID_VERSION') + t.equal(err.message, 'HTTP2 is available only from node >= 8.8.1') } }) diff --git a/test/internals/decorator.test.js b/test/internals/decorator.test.js index a7d0a930b6f..20401f81eed 100644 --- a/test/internals/decorator.test.js +++ b/test/internals/decorator.test.js @@ -75,7 +75,7 @@ test('checkDependencies should throw if a dependency is not present', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') - t.is(e.message, 'FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency \'test\'.') + t.is(e.message, 'The decorator is missing dependency \'test\'.') } }) @@ -94,7 +94,7 @@ test('decorate should internally call checkDependencies', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') - t.is(e.message, 'FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency \'test\'.') + t.is(e.message, 'The decorator is missing dependency \'test\'.') } }) diff --git a/test/internals/errors.test.js b/test/internals/errors.test.js index b749b6f4abd..df4a63cd1c3 100644 --- a/test/internals/errors.test.js +++ b/test/internals/errors.test.js @@ -9,8 +9,8 @@ test('Create error with zero parameter', t => { const NewError = createError('CODE', 'Not available') const err = new NewError() t.type(err, Error) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: Not available') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'Not available') t.equal(err.code, 'CODE') t.equal(err.statusCode, 500) t.ok(err.stack) @@ -21,8 +21,8 @@ test('Create error with 1 parameter', t => { const NewError = createError('CODE', 'hey %s') const err = new NewError('alice') t.type(err, Error) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: hey alice') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'hey alice') t.equal(err.code, 'CODE') t.equal(err.statusCode, 500) t.ok(err.stack) @@ -33,8 +33,8 @@ test('Create error with 2 parameters', t => { const NewError = createError('CODE', 'hey %s, I like your %s') const err = new NewError('alice', 'attitude') t.type(err, Error) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: hey alice, I like your attitude') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'hey alice, I like your attitude') t.equal(err.code, 'CODE') t.equal(err.statusCode, 500) t.ok(err.stack) @@ -45,8 +45,8 @@ test('Create error with 3 parameters', t => { const NewError = createError('CODE', 'hey %s, I like your %s %s') const err = new NewError('alice', 'attitude', 'see you') t.type(err, Error) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: hey alice, I like your attitude see you') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'hey alice, I like your attitude see you') t.equal(err.code, 'CODE') t.equal(err.statusCode, 500) t.ok(err.stack) @@ -57,8 +57,8 @@ test('Create error with no statusCode property', t => { const NewError = createError('CODE', 'hey %s', 0) const err = new NewError('dude') t.type(err, Error) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: hey dude') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'hey dude') t.equal(err.code, 'CODE') t.notOk(err.statusCode) t.ok(err.stack) @@ -88,17 +88,16 @@ test('Create error with different base', t => { const err = new NewError('dude') t.type(err, Error) t.type(err, TypeError) - t.equal(err.name, 'FastifyError [CODE]') - t.equal(err.message, 'CODE: hey dude') + t.equal(err.name, 'FastifyError') + t.equal(err.message, 'hey dude') t.equal(err.code, 'CODE') t.equal(err.statusCode, 500) t.ok(err.stack) }) -test('Error has appropriate string tag', t => { +test('FastifyError.toString returns code', t => { t.plan(1) const NewError = createError('CODE', 'foo') const err = new NewError() - const str = Object.prototype.toString.call(err) - t.equal(str, '[object Error]') + t.equal(err.toString(), 'FastifyError [CODE]: foo') }) diff --git a/test/internals/handleRequest.test.js b/test/internals/handleRequest.test.js index 01e5a1b82a1..86cf10aa2bc 100644 --- a/test/internals/handleRequest.test.js +++ b/test/internals/handleRequest.test.js @@ -250,7 +250,7 @@ test('request should respond with an error if an unserialized payload is sent in t.strictDeepEqual(JSON.parse(res.payload), { error: 'Internal Server Error', code: 'FST_ERR_REP_INVALID_PAYLOAD_TYPE', - message: 'FST_ERR_REP_INVALID_PAYLOAD_TYPE: Attempted to send payload of invalid type \'object\'. Expected a string or Buffer.', + message: 'Attempted to send payload of invalid type \'object\'. Expected a string or Buffer.', statusCode: 500 }) }) diff --git a/test/internals/hooks.test.js b/test/internals/hooks.test.js index 2ee46f820ed..719ed7f2671 100644 --- a/test/internals/hooks.test.js +++ b/test/internals/hooks.test.js @@ -70,7 +70,7 @@ test('should throw on wrong parameters', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_HOOK_INVALID_TYPE') - t.is(e.message, 'FST_ERR_HOOK_INVALID_TYPE: The hook name must be a string') + t.is(e.message, 'The hook name must be a string') } try { @@ -78,6 +78,6 @@ test('should throw on wrong parameters', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_HOOK_INVALID_HANDLER') - t.is(e.message, 'FST_ERR_HOOK_INVALID_HANDLER: The hook callback must be a function') + t.is(e.message, 'The hook callback must be a function') } }) diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index 94ea3ee0a83..964da362a7f 100644 --- a/test/internals/initialConfig.test.js +++ b/test/internals/initialConfig.test.js @@ -153,8 +153,8 @@ test('Return an error if options do not match the validation schema', t => { t.fail() } catch (error) { t.type(error, Error) - t.equal(error.name, 'FastifyError [FST_ERR_INIT_OPTS_INVALID]') - t.equal(error.message, 'FST_ERR_INIT_OPTS_INVALID: Invalid initialization options: \'["should be boolean"]\'') + t.equal(error.name, 'FastifyError') + t.equal(error.message, 'Invalid initialization options: \'["should be boolean"]\'') t.equal(error.code, 'FST_ERR_INIT_OPTS_INVALID') t.ok(error.stack) t.pass() diff --git a/test/internals/logger.test.js b/test/internals/logger.test.js index 3b6574fafd3..a0569d38050 100644 --- a/test/internals/logger.test.js +++ b/test/internals/logger.test.js @@ -109,6 +109,6 @@ test('The logger should error if both stream and file destination are given', t }) } catch (err) { t.is(err.code, 'FST_ERR_LOG_INVALID_DESTINATION') - t.is(err.message, 'FST_ERR_LOG_INVALID_DESTINATION: Cannot specify both logger.stream and logger.file options') + t.is(err.message, 'Cannot specify both logger.stream and logger.file options') } }) diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index fe05dc2f02c..d4b5f3b80a1 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -977,14 +977,15 @@ test('reply.header setting multiple cookies as multiple Set-Cookie headers', t = }) test('should throw error when passing falsy value to reply.sent', t => { - t.plan(3) + t.plan(4) const fastify = require('../..')() fastify.get('/', function (req, reply) { try { reply.sent = false } catch (err) { - t.strictEqual(err.message, 'FST_ERR_REP_SENT_VALUE: The only possible value for reply.sent is true.') + t.is(err.code, 'FST_ERR_REP_SENT_VALUE') + t.strictEqual(err.message, 'The only possible value for reply.sent is true.') reply.send() } }) @@ -996,7 +997,7 @@ test('should throw error when passing falsy value to reply.sent', t => { }) test('should throw error when attempting to set reply.sent more than once', t => { - t.plan(3) + t.plan(4) const fastify = require('../..')() fastify.get('/', function (req, reply) { @@ -1004,7 +1005,8 @@ test('should throw error when attempting to set reply.sent more than once', t => try { reply.sent = true } catch (err) { - t.strictEqual(err.message, 'FST_ERR_REP_ALREADY_SENT: Reply was already sent.') + t.is(err.code, 'FST_ERR_REP_ALREADY_SENT') + t.strictEqual(err.message, 'Reply was already sent.') } reply.res.end() }) diff --git a/test/plugin.test.js b/test/plugin.test.js index 263dc1d542c..4ffcf39afee 100644 --- a/test/plugin.test.js +++ b/test/plugin.test.js @@ -294,7 +294,7 @@ test('check dependencies - should throw', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') - t.is(e.message, 'FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency \'test\'.') + t.is(e.message, 'The decorator is missing dependency \'test\'.') } n() })) diff --git a/test/reply-error.test.js b/test/reply-error.test.js index 4ac5901cedb..27083cebc87 100644 --- a/test/reply-error.test.js +++ b/test/reply-error.test.js @@ -357,7 +357,7 @@ test('\'*\' should throw an error due to serializer can not handle the payload t } catch (err) { t.type(err, TypeError) t.is(err.code, 'FST_ERR_REP_INVALID_PAYLOAD_TYPE') - t.is(err.message, "FST_ERR_REP_INVALID_PAYLOAD_TYPE: Attempted to send payload of invalid type 'object'. Expected a string or Buffer.") + t.is(err.message, "Attempted to send payload of invalid type 'object'. Expected a string or Buffer.") } }) @@ -382,7 +382,7 @@ test('should throw an error if the custom serializer does not serialize the payl } catch (err) { t.type(err, TypeError) t.is(err.code, 'FST_ERR_REP_INVALID_PAYLOAD_TYPE') - t.is(err.message, "FST_ERR_REP_INVALID_PAYLOAD_TYPE: Attempted to send payload of invalid type 'object'. Expected a string or Buffer.") + t.is(err.message, "Attempted to send payload of invalid type 'object'. Expected a string or Buffer.") } }) diff --git a/test/shared-schemas.test.js b/test/shared-schemas.test.js index ac06a60d631..e354b11e6a4 100644 --- a/test/shared-schemas.test.js +++ b/test/shared-schemas.test.js @@ -51,7 +51,7 @@ test('Should throw if the $id property is missing', t => { fastify.addSchema({ type: 'string' }) } catch (err) { t.is(err.code, 'FST_ERR_SCH_MISSING_ID') - t.is(err.message, 'FST_ERR_SCH_MISSING_ID: Missing schema $id property') + t.is(err.message, 'Missing schema $id property') } }) @@ -64,7 +64,7 @@ test('Cannot add multiple times the same id', t => { fastify.addSchema({ $id: 'id' }) } catch (err) { t.is(err.code, 'FST_ERR_SCH_ALREADY_PRESENT') - t.is(err.message, 'FST_ERR_SCH_ALREADY_PRESENT: Schema with id \'id\' already declared!') + t.is(err.message, 'Schema with id \'id\' already declared!') } }) @@ -85,7 +85,7 @@ test('Should throw of the schema does not exists', t => { fastify.ready(err => { t.is(err.code, 'FST_ERR_SCH_NOT_PRESENT') - t.is(err.message, 'FST_ERR_SCH_NOT_PRESENT: Schema with id \'test\' does not exist!') + t.is(err.message, 'Schema with id \'test\' does not exist!') }) }) @@ -249,7 +249,7 @@ test('Encapsulation should intervene', t => { fastify.ready(err => { t.is(err.code, 'FST_ERR_SCH_NOT_PRESENT') - t.is(err.message, 'FST_ERR_SCH_NOT_PRESENT: Schema with id \'encapsulation\' does not exist!') + t.is(err.message, 'Schema with id \'encapsulation\' does not exist!') }) }) diff --git a/test/throw.test.js b/test/throw.test.js index 0d7e01f3136..5b294c24fdc 100644 --- a/test/throw.test.js +++ b/test/throw.test.js @@ -177,7 +177,7 @@ test('Should throw on duplicate request decorator', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_ALREADY_PRESENT') - t.is(e.message, "FST_ERR_DEC_ALREADY_PRESENT: The decorator 'foo' has already been added!") + t.is(e.message, 'The decorator \'foo\' has already been added!') } }) @@ -192,7 +192,7 @@ test('Should throw if request decorator dependencies are not met', t => { t.fail() } catch (e) { t.is(e.code, 'FST_ERR_DEC_MISSING_DEPENDENCY') - t.is(e.message, "FST_ERR_DEC_MISSING_DEPENDENCY: The decorator is missing dependency 'world'.") + t.is(e.message, 'The decorator is missing dependency \'world\'.') } }) From 0f25e243b242477aa15ecbafa667aca95c2faa86 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Tue, 27 Aug 2019 05:38:03 -0400 Subject: [PATCH 142/144] TypeScript Refactor (#1569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * utilize conditional to control generic specifics * Constrain and default generics * change declaration name for testing. define overload factory func * simplify to a single function declaration again * completed route method type declarations * split up types * remove fastify2.d.ts file * Add light-my-request and move logger to own file * Add FastifyError type * Utilize DRY RawBases and RawDefault logic * Update logger types * Fix generics in logger * Update route do add shorthand with handler param * Type register and plugin. Modify export structure * Move instance to own file and fix imports * Suppor esm and default import syntax * modify exports and imports for instance and fix generics * add import tests * add addHook methods * Add content-type-parser types * Export all types. Ready for review 🚀 * fix route types (change to interfaces) * Initial implementation of tsd tests. Modify types to interfaces * delete old types * More testing and updates * added server factory and more tests * update jsdocs, update tsd * delete old type test * update npm scripts, remove typescript linting * fix npm script * udpate schema type and doc * try adding explicit azure-pipeline trigger * revert 6e83a45 * fix strict log types * Improve route generics * fix serverFactory test * Add abarre review * Updated content-type-parser tests * update logger types * improve types for schema, context, routes, and register * add context to other route based methods * add import syntax comment * add linting for typescript files * fix export * remove comment * combine Raw(Req|Reply) Base and Default * allow request headers to be mergable * fix hooks * add Symbol to decorate method * fix file paths for windows --- fastify.d.ts | 792 ++--------------------- fastify.js | 17 +- package.json | 30 +- test/imports.test.js | 18 + test/types/content-type-parser.test-d.ts | 53 ++ test/types/fastify.test-d.ts | 17 + test/types/hooks.test-d.ts | 43 ++ test/types/index.ts | 717 -------------------- test/types/logger.test-d.ts | 33 + test/types/plugin.test-d.ts | 15 + test/types/route.test-d.ts | 108 ++++ test/types/serverFactory.test-d.ts | 38 ++ test/types/tsconfig.json | 11 - types/.eslintrc.json | 30 + types/content-type-parser.d.ts | 63 ++ types/context.d.ts | 8 + types/error.d.ts | 23 + types/hooks.d.ts | 145 +++++ types/instance.d.ts | 107 +++ types/logger.d.ts | 49 ++ types/middleware.d.ts | 49 ++ types/plugin.d.ts | 25 + types/register.d.ts | 16 + types/reply.d.ts | 30 + types/request.d.ts | 23 + types/route.d.ts | 127 ++++ types/schema.d.ts | 17 + types/serverFactory.d.ts | 19 + types/tsconfig.json | 13 + types/utils.d.ts | 34 + 30 files changed, 1210 insertions(+), 1460 deletions(-) create mode 100644 test/imports.test.js create mode 100644 test/types/content-type-parser.test-d.ts create mode 100644 test/types/fastify.test-d.ts create mode 100644 test/types/hooks.test-d.ts delete mode 100644 test/types/index.ts create mode 100644 test/types/logger.test-d.ts create mode 100644 test/types/plugin.test-d.ts create mode 100644 test/types/route.test-d.ts create mode 100644 test/types/serverFactory.test-d.ts delete mode 100644 test/types/tsconfig.json create mode 100644 types/.eslintrc.json create mode 100644 types/content-type-parser.d.ts create mode 100644 types/context.d.ts create mode 100644 types/error.d.ts create mode 100644 types/hooks.d.ts create mode 100644 types/instance.d.ts create mode 100644 types/logger.d.ts create mode 100644 types/middleware.d.ts create mode 100644 types/plugin.d.ts create mode 100644 types/register.d.ts create mode 100644 types/reply.d.ts create mode 100644 types/request.d.ts create mode 100644 types/route.d.ts create mode 100644 types/schema.d.ts create mode 100644 types/serverFactory.d.ts create mode 100644 types/tsconfig.json create mode 100644 types/utils.d.ts diff --git a/fastify.d.ts b/fastify.d.ts index 02c61cebe92..5a7960421c0 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -1,729 +1,79 @@ -/* eslint no-unused-vars: 0 */ -/* eslint no-undef: 0 */ -/* eslint space-infix-ops: 0 */ - -/// - import * as http from 'http' import * as http2 from 'http2' import * as https from 'https' -declare function fastify< - HttpServer extends (http.Server | http2.Http2Server) = http.Server, - HttpRequest extends (http.IncomingMessage | http2.Http2ServerRequest) = http.IncomingMessage, - HttpResponse extends (http.ServerResponse | http2.Http2ServerResponse) = http.ServerResponse ->(opts?: fastify.ServerOptions): fastify.FastifyInstance; -declare function fastify(opts?: fastify.ServerOptionsAsHttp): fastify.FastifyInstance; -declare function fastify(opts?: fastify.ServerOptionsAsSecureHttp): fastify.FastifyInstance; -declare function fastify(opts?: fastify.ServerOptionsAsHttp2): fastify.FastifyInstance; -declare function fastify(opts?: fastify.ServerOptionsAsSecureHttp2): fastify.FastifyInstance; - -// eslint-disable-next-line no-redeclare -declare namespace fastify { - - type Plugin = - PluginInstance extends () => Promise ? - ((instance: FastifyInstance< HttpServer, HttpRequest, HttpResponse >, options: Options) => Promise) : - (instance: FastifyInstance, options: Options, callback: (err?: FastifyError) => void) => void; - - type Middleware < HttpServer, HttpRequest, HttpResponse > = (this: FastifyInstance, req: HttpRequest, res: HttpResponse, callback: (err?: FastifyError) => void) => void - - type DefaultQuery = { [k: string]: any } - type DefaultParams = { [k: string]: any } - type DefaultHeaders = { [k: string]: any } - type DefaultBody = any - - type HTTPMethod = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS' - - interface ValidationResult { - keyword: string; - dataPath: string; - schemaPath: string; - params: { - [type: string]: string; - }, - message: string; - } - - /** - * Fastify custom error - */ - interface FastifyError extends Error { - statusCode?: number; - /** - * Validation errors - */ - validation?: Array; - } - - interface Logger { - fatal(msg: string, ...args: any[]): void; - fatal(obj: {}, msg?: string, ...args: any[]): void; - error(msg: string, ...args: any[]): void; - error(obj: {}, msg?: string, ...args: any[]): void; - warn(msg: string, ...args: any[]): void; - warn(obj: {}, msg?: string, ...args: any[]): void; - info(msg: string, ...args: any[]): void; - info(obj: {}, msg?: string, ...args: any[]): void; - debug(msg: string, ...args: any[]): void; - debug(obj: {}, msg?: string, ...args: any[]): void; - trace(msg: string, ...args: any[]): void; - trace(obj: {}, msg?: string, ...args: any[]): void; - } - - type FastifyMiddleware< - HttpServer = http.Server, - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > = ( - this: FastifyInstance, - req: FastifyRequest, - reply: FastifyReply, - done: (err?: Error) => void, - ) => void | Promise - - type FastifyMiddlewareWithPayload< - HttpServer = http.Server, - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > = ( - this: FastifyInstance, - req: FastifyRequest, - reply: FastifyReply, - payload: any, - done: (err?: Error, value?: any) => void, - ) => void | Promise - - type RequestHandler< - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > = ( - this: FastifyInstance, - request: FastifyRequest, - reply: FastifyReply, - ) => void | Promise - - type SchemaCompiler = (schema: Object) => Function - type SchemaResolver = (ref: string) => Object - - type BodyParser = - | ((req: HttpRequest, rawBody: RawBody, done: (err: Error | null, body?: any) => void) => void) - | ((req: HttpRequest, rawBody: RawBody) => Promise) - - type ContentTypeParser = - | ((req: HttpRequest, done: (err: Error | null, body?: any) => void) => void) - | ((req: HttpRequest) => Promise) - - interface FastifyContext { - config: any - } - - /** - * fastify's wrapped version of node.js IncomingMessage - */ - interface FastifyRequest< - HttpRequest = http.IncomingMessage, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > { - query: Query - - params: Params - - headers: Headers - - body: Body - - id: any - - ip: string - ips: string[] - hostname: string - - raw: HttpRequest - req: HttpRequest - log: Logger - } - - /** - * Response object that is used to build and send a http response - */ - interface FastifyReply { - code(statusCode: number): FastifyReply - status(statusCode: number): FastifyReply - header(name: string, value: any): FastifyReply - headers(headers: { [key: string]: any }): FastifyReply - getHeader(name: string): string | undefined - hasHeader(name: string): boolean - removeHeader(name: string): FastifyReply - callNotFound(): void - getResponseTime(): number - type(contentType: string): FastifyReply - redirect(url: string): FastifyReply - redirect(statusCode: number, url: string): FastifyReply - serialize(payload: any): string - serializer(fn: Function): FastifyReply - send(payload?: any): FastifyReply - sent: boolean - res: HttpResponse - context: FastifyContext - request: FastifyRequest - } - type TrustProxyFunction = (addr: string, index: number) => boolean - type ServerFactoryHandlerFunction = (request: http.IncomingMessage | http2.Http2ServerRequest, response: http.ServerResponse | http2.Http2ServerResponse) => void - type ServerFactoryFunction = (handler: ServerFactoryHandlerFunction, options: ServerOptions) => http.Server | http2.Http2Server - interface ServerOptions { - caseSensitive?: boolean, - ignoreTrailingSlash?: boolean, - bodyLimit?: number, - pluginTimeout?: number, - disableRequestLogging?: boolean, - onProtoPoisoning?: 'error' | 'remove' | 'ignore', - onConstructorPoisoning?: 'error' | 'remove' | 'ignore', - logger?: any, - trustProxy?: string | number | boolean | Array | TrustProxyFunction, - maxParamLength?: number, - querystringParser?: (str: string) => { [key: string]: string | string[] }, - versioning? : { - storage() : { - get(version: String) : Function | null, - set(version: String, store: Function) : void, - del(version: String) : void, - empty() : void - }, - deriveVersion(req: Object, ctx?: Context) : String, +import { FastifyRequest } from './types/request' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './types/utils' +import { FastifyLoggerOptions } from './types/logger' +import { FastifyInstance } from './types/instance' +import { FastifyServerFactory } from './types/serverFactory' + +/** + * Fastify factor function for the standard fastify http, https, or http2 server instance. + * + * The default function utilizes http + * + * @param opts Fastify server options + * @returns Fastify server instance + */ +export default function fastify< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + Logger = FastifyLoggerOptions +>(opts?: FastifyServerOptions): FastifyInstance; + +/** + * Options for a fastify server instance. Utilizes conditional logic on the generic server parameter to enforce certain https and http2 + */ +export type FastifyServerOptions< + RawServer extends RawServerBase = RawServerDefault, + Logger = FastifyLoggerOptions +> = { + http2?: RawServer extends http2.Http2Server ? true : false, + https?: RawServer extends https.Server + ? https.ServerOptions + : RawServer extends http2.Http2SecureServer + ? http2.SecureServerOptions + : null, + ignoreTrailingSlash?: boolean, + bodyLimit?: number, + pluginTimeout?: number, + onProtoPoisoing?: 'error' | 'remove' | 'ignore', + logger?: boolean | Logger, + serverFactory?: FastifyServerFactory, + caseSensitive?: boolean, + requestIdHeader?: string, + genReqId?: (req: FastifyRequest>) => string, + trustProxy?: boolean | string | string[] | number | TrustProxyFunction, + querystringParser?: (str: string) => { [key: string]: string | string[] }, + versioning?: { + storage(): { + get(version: string): Function | null, + set(version: string, store: Function): void + del(version: string): void, + empty(): void }, - modifyCoreObjects?: boolean, - return503OnClosing?: boolean, - genReqId?: () => number | string, - requestIdHeader?: string, - requestIdLogLabel?: string, - serverFactory?: ServerFactoryFunction - } - interface ServerOptionsAsSecure extends ServerOptions { - https: http2.SecureServerOptions - } - interface ServerOptionsAsHttp extends ServerOptions { - http2?: false - } - interface ServerOptionsAsSecureHttp extends ServerOptionsAsHttp, ServerOptionsAsSecure {} - interface ServerOptionsAsHttp2 extends ServerOptions { - http2: true - } - interface ServerOptionsAsSecureHttp2 extends ServerOptionsAsHttp2, ServerOptionsAsSecure {} - - // TODO - define/import JSONSchema types - type JSONSchema = Object - - interface RouteSchema { - body?: JSONSchema - querystring?: JSONSchema - params?: JSONSchema - headers?: JSONSchema - response?: { - [code: number]: JSONSchema, - [code: string]: JSONSchema - } - } - - /** - * Optional configuration parameters for the route being created - */ - interface RouteShorthandOptions< - HttpServer = http.Server, - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > { - schema?: RouteSchema - attachValidation?: boolean - onRequest?: - | FastifyMiddleware - | Array> - onResponse?: - | FastifyMiddleware - | Array> - preParsing?: - | FastifyMiddleware - | Array> - preValidation?: - | FastifyMiddleware - | Array> - preHandler?: - | FastifyMiddleware - | Array> - preSerialization?: - FastifyMiddlewareWithPayload - | Array> - handler?: RequestHandler - schemaCompiler?: SchemaCompiler - bodyLimit?: number - logLevel?: string - logSerializers?: Object - config?: any - version?: string - prefixTrailingSlash?: 'slash' | 'no-slash' | 'both' - } - - /** - * Route configuration options such as "url" and "method" - */ - interface RouteOptions< - HttpServer = http.Server, - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse, - Query = DefaultQuery, - Params = DefaultParams, - Headers = DefaultHeaders, - Body = DefaultBody - > extends RouteShorthandOptions { - method: HTTPMethod | HTTPMethod[] - url: string - handler: RequestHandler - } - - /** - * Register options - */ - interface RegisterOptions { - [key: string]: any, - prefix?: string, - logSerializers?: Object - } - - /** - * Fake http inject options - */ - interface HTTPInjectOptions { - url: string, - method?: HTTPMethod, - authority?: string, - headers?: DefaultHeaders, - query?: DefaultQuery, - remoteAddress?: string, - payload?: string | object | Buffer | NodeJS.ReadableStream - simulate?: { - end?: boolean, - split?: boolean, - error?: boolean, - close?: boolean - }, - validate?: boolean - } - - /** - * Fake http inject response - */ - interface HTTPInjectResponse { - raw: { - req: NodeJS.ReadableStream, - res: http.ServerResponse - }, - headers: Record, - statusCode: number, - statusMessage: string, - payload: string, - rawPayload: Buffer, - trailers: object - } - - /** - * Server listen options - */ - interface ListenOptions { - port?: number; - host?: string; - backlog?: number; - path?: string; - exclusive?: boolean; - readableAll?: boolean; - writableAll?: boolean; - /** - * @default false - */ - ipv6Only?: boolean; - } - - /** - * Represents the fastify instance created by the factory function the module exports. - */ - interface FastifyInstance { - server: HttpServer - log: Logger - schemaCompiler: SchemaCompiler - - /** - * Adds a route to the server - */ - route( - opts: RouteOptions, - ): FastifyInstance - - /** - * Defines a GET route with the given mount path, options, and handler - */ - get( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a GET route with the given mount path and handler - */ - get( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a PUT route with the given mount path, options, and handler - */ - put( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a PUT route with the given mount path and handler - */ - put( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a PATCH route with the given mount path, options, and handler - */ - patch( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a PATCH route with the given mount path and handler - */ - patch( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a POST route with the given mount path, options, and handler - */ - post( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a POST route with the given mount path and handler - */ - post( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a HEAD route with the given mount path, options, and handler - */ - head( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a HEAD route with the given mount path and handler - */ - head( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a DELETE route with the given mount path, options, and handler - */ - delete( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a DELETE route with the given mount path and handler - */ - delete( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a OPTIONS route with the given mount path, options, and handler - */ - options( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a OPTIONS route with the given mount path and handler - */ - options( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Defines a route for all the supported methods with the given mount path, options, and handler - */ - all( - url: string, - opts: RouteShorthandOptions, - handler?: RequestHandler, - ): FastifyInstance - - /** - * Defines a route for all the supported methods with the given mount path and handler - */ - all( - url: string, - handler: RequestHandler, - ): FastifyInstance - - /** - * Starts the server on the given port after all the plugins are loaded, - * internally waits for the .ready() event. The callback is the same as the - * Node core. - */ - listen(callback: (err: Error, address: string) => void): void - listen(port: number, callback: (err: Error, address: string) => void): void - listen(port: number, address: string, callback: (err: Error, address: string) => void): void - listen(port: number, address: string, backlog: number, callback: (err: Error, address: string) => void): void - listen(options: ListenOptions, callback: (err: Error, address: string) => void): void - listen(sockFile: string, callback: (err: Error, address: string) => void): void - listen(port: number, address?: string, backlog?: number): Promise - listen(sockFile: string): Promise - listen(options: ListenOptions): Promise - - /** - * Registers a listener function that is invoked when all the plugins have - * been loaded. It receives an error parameter if something went wrong. - */ - ready(): Promise> - ready(readyListener: (err: Error) => void): void - ready(readyListener: (err: Error, done: Function) => void): void - ready(readyListener: (err: Error, context: FastifyInstance, done: Function) => void): void - - /** - * Call this function to close the server instance and run the "onClose" callback - */ - close(closeListener: () => void): void - close(): Promise - - /** - * Apply the given middleware to all incoming requests - */ - use(middleware: Middleware): void - - /** - * Apply the given middleware to routes matching the given path - */ - use(path: string, middleware: Middleware): void - - /** - * Registers a plugin - */ - register, PluginInstance extends Function>(plugin: Plugin, options?: Options): FastifyInstance - - /** - * `Register a callback that will be executed just after a register. - * It can take up to three parameters - */ - after(afterListener: (err: Error) => void): void - after(afterListener: (err: Error, done: Function) => void): void - after(afterListener: (err: Error, context: FastifyInstance, done: Function) => void): void - - /** - * Decorate this fastify instance with new properties. Throws an execption if - * you attempt to add the same decorator name twice - */ - decorate(name: string, decoration: any, dependencies?: Array): FastifyInstance - - /** - * Decorate reply objects with new properties. Throws an execption if - * you attempt to add the same decorator name twice - */ - decorateReply(name: string, decoration: any, dependencies?: Array): FastifyInstance - - /** - * Decorate request objects with new properties. Throws an execption if - * you attempt to add the same decorator name twice - */ - decorateRequest(name: string, decoration: any, dependencies?: Array): FastifyInstance - - /** - * Determines if the given named decorator is available - */ - hasDecorator(name: string): boolean - - /** - * Determines if the given named request decorator is available - */ - hasRequestDecorator(name: string): boolean - - /** - * Determines if the given named reply decorator is available - */ - hasReplyDecorator(name: string): boolean - - /** - * Add a hook that is triggered when a request is initially received - */ - addHook(name: 'onRequest', hook: FastifyMiddleware): FastifyInstance - - /** - * Add a hook that is triggered after the onRequest hook and middlewares, but before body parsing - */ - addHook(name: 'preParsing', hook: FastifyMiddleware): FastifyInstance - - /** - * Add a hook that is triggered after the onRequest, middlewares, and body parsing, but before the validation - */ - addHook(name: 'preValidation', hook: FastifyMiddleware): FastifyInstance - - /** - * Hook that is fired after a request is processed, but before the response is serialized - * hook - */ - addHook(name: 'preSerialization', hook: FastifyMiddlewareWithPayload): FastifyInstance - - /** - * Hook that is fired before a request is processed, but after the "preValidation" - * hook - */ - addHook(name: 'preHandler', hook: FastifyMiddleware): FastifyInstance - - /** - * Hook that is fired after a request is processed, but before the "onResponse" - * hook - */ - addHook(name: 'onSend', hook: (this: FastifyInstance, req: FastifyRequest, reply: FastifyReply, payload: any, done: (err?: Error, value?: any) => void) => void): FastifyInstance - - /** - * Hook that is fired if `reply.send` is invoked with an Error - */ - addHook(name: 'onError', hook: (this: FastifyInstance, req: FastifyRequest, reply: FastifyReply, error: FastifyError, done: () => void) => void): FastifyInstance - - /** - * Hook that is called when a response is about to be sent to a client - */ - addHook(name: 'onResponse', hook: FastifyMiddleware): FastifyInstance - - /** - * Adds a hook that is triggered when server.close is called. Useful for closing connections - * and performing cleanup tasks - */ - addHook(name: 'onClose', hook: (instance: FastifyInstance, done: () => void) => void): FastifyInstance - - /** - * Adds a hook that is triggered when a new route is registered. Listeners are passed a - * routeOptions object as the sole parameter. - * The interface is synchronous, and, as such, the listeners do not get passed a callback. - */ - addHook(name: 'onRoute', hook: (opts: RouteOptions & { path: string, prefix: string }) => void): FastifyInstance - - /** - * Adds a hook that is triggered when Fastify a new plugin is being registered. - * This hook can be useful if you are developing a plugin that needs to use the encapsulation functionality of Fastify. - * The interface is synchronous, and, as such, the listeners do not get passed a callback. - */ - addHook(name: 'onRegister', hook: (instance: FastifyInstance) => void): FastifyInstance - - /** - * Useful for testing http requests without running a sever - */ - inject(opts: HTTPInjectOptions | string, cb: (err: Error, res: HTTPInjectResponse) => void): void - - /** - * Useful for testing http requests without running a sever - */ - inject(opts: HTTPInjectOptions | string): Promise - - /** - * Set the 404 handler - */ - setNotFoundHandler(handler: (request: FastifyRequest, reply: FastifyReply) => void): void - - /** - * Set a function that will be called whenever an error happens - */ - setErrorHandler(handler: (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => void): void - - /** - * Set a function that will be called whenever an error happens - */ - setReplySerializer(handler: (payload: string | object | Buffer | NodeJS.ReadableStream, statusCode: number) => string): void - - /** - * Set the schema compiler for all routes. - */ - setSchemaCompiler(schemaCompiler: SchemaCompiler): FastifyInstance - - /** - * Set the schema resolver to find the `$ref` schema object - */ - setSchemaResolver(schemaResolver: SchemaResolver): FastifyInstance - - /** - * Create a shared schema - */ - addSchema(schema: object): FastifyInstance - - /** - * Get all shared schemas - */ - getSchemas(): {[shemaId: string]: Object} - - /** - * Add a content type parser - */ - addContentTypeParser(contentType: string | string[], opts: { bodyLimit?: number }, parser: ContentTypeParser): void - addContentTypeParser(contentType: string | string[], opts: { parseAs: 'string'; bodyLimit?: number }, parser: BodyParser): void - addContentTypeParser(contentType: string | string[], opts: { parseAs: 'buffer'; bodyLimit?: number }, parser: BodyParser): void - addContentTypeParser(contentType: string | string[], parser: ContentTypeParser): void - - /** - * Check if a parser for the specified content type exists - */ - hasContentTypeParser(contentType: string): boolean; - - /** - * Prints the representation of the internal radix tree used by the router - */ - printRoutes(): string + deriveVersion(req: Object, ctx?: Context): string // not a fan of using Object here. Also what is Context? Can either of these be better defined? } } -export = fastify; +type TrustProxyFunction = (address: string, hop: number) => boolean + +/* Export all additional types */ +export { FastifyRequest } from './types/request' +export { FastifyReply } from './types/reply' +export { FastifyPlugin, FastifyPluginOptions } from './types/plugin' +export { FastifyInstance } from './types/instance' +export { FastifyLoggerOptions, FastifyLogFn, LogLevels } from './types/logger' +export { FastifyMiddleware, FastifyMiddlewareWithPayload } from './types/middleware' +export { FastifyContext } from './types/context' +export { RouteHandlerMethod, RouteOptions, RouteShorthandMethod, RouteShorthandOptions, RouteShorthandOptionsWithHandler } from './types/route' +export { RegisterOptions } from './types/register' +export { FastifyBodyParser, FastifyContentTypeParser, AddContentTypeParser, hasContentTypeParser } from './types/content-type-parser' +export { FastifyError, ValidationResult } from './types/error' +export { FastifySchema, FastifySchemaCompiler } from './types/schema' +export { HTTPMethods, RawServerBase, RawRequestDefaultExpression, RawReplyDefaultExpression, RawServerDefault } from './types/utils' +export { onCloseHookHandler, onRouteHookHandler, onRequestHookHandler, onSendHookHandler, onErrorHookHandler, preHandlerHookHandler, preParsingHookHandler, preSerializationHookHandler, preValidationHookHandler, AddHook, addHookHandler } from './types/hooks' +export { FastifyServerFactory, FastifyServerFactoryHandler } from './types/serverFactory' +export { fastify } diff --git a/fastify.js b/fastify.js index 42805d76e5c..2aa72155de5 100644 --- a/fastify.js +++ b/fastify.js @@ -43,7 +43,7 @@ const build404 = require('./lib/fourOhFour') const getSecuredInitialConfig = require('./lib/initialConfigValidation') const { defaultInitOptions } = getSecuredInitialConfig -function build (options) { +function fastify (options) { // Options validations options = options || {} @@ -506,4 +506,17 @@ function buildRoutePrefix (instancePrefix, pluginPrefix) { return instancePrefix + pluginPrefix } -module.exports = build +/** + * These export configurations enable JS and TS developers + * to consumer fastify in whatever way best suits their needs. + * Some examples of supported import syntax includes: + * - `const fastify = require('fastify')` + * - `const { fastify } = require('fastify')` + * - `import * as Fastify from 'fastify'` + * - `import { fastify, TSC_definition } from 'fastify'` + * - `import fastify from 'fastify'` + * - `import fastify, { TSC_definition } from 'fastify'` + */ +fastify.fastify = fastify +fastify.default = fastify +module.exports = fastify diff --git a/package.json b/package.json index 9747d35f796..f507708c0e3 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,17 @@ "version": "2.10.0", "description": "Fast and low overhead web framework, for Node.js", "main": "fastify.js", - "typings": "fastify.d.ts", + "types": "fastify.d.ts", "scripts": { "lint": "npm run lint:standard && npm run lint:typescript", "lint:standard": "standard --verbose | snazzy", - "lint:typescript": "standard --parser @typescript-eslint/parser --plugin @typescript-eslint/eslint-plugin test/types/*.ts fastify.d.ts", - "unit": "tap --no-esm -J test/*.test.js test/*/*.test.js", - "unit:report": "tap --no-esm -J test/*.test.js test/*/*.test.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap", + "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts", + "unit": "tap -J test/*.test.js test/*/*.test.js", + "unit:report": "tap -J test/*.test.js test/*/*.test.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap", "unit:junit": "tap-mocha-reporter xunit < out.tap > test/junit-testresults.xml", - "typescript": "tsc --project ./test/types/tsconfig.json", - "test:report": "npm run lint && npm run unit:report && npm run typescript", - "test": "npm run lint && npm run unit && npm run typescript", + "test:typescript": "tsd", + "test:report": "npm run lint && npm run unit:report && npm run test:typescript", + "test": "npm run lint && npm run unit && npm run test:typescript", "coverage": "npm run unit -- --cov --coverage-report=html", "test:ci": "npm run lint && npm run unit -- --cov --coverage-report=lcovonly && npm run typescript", "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"", @@ -92,6 +92,7 @@ "node": ">=8.16.0" }, "devDependencies": { + "@hapi/joi": "^15.1.0", "@types/node": "^12.0.10", "@typescript-eslint/eslint-plugin": "^2.3.0", "@typescript-eslint/parser": "^2.3.0", @@ -104,7 +105,12 @@ "coveralls": "^3.0.6", "dns-prefetch-control": "^0.2.0", "eslint": "^6.4.0", + "eslint-config-standard": "^14.0.1", "eslint-import-resolver-node": "^0.3.2", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-node": "^9.1.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", "fast-json-body": "^1.1.0", "fastify-plugin": "^1.6.0", "fluent-schema": "^0.8.0", @@ -116,7 +122,6 @@ "hsts": "^2.2.0", "http-errors": "^1.7.1", "ienoopen": "^1.1.0", - "@hapi/joi": "^15.1.0", "license-checker": "^25.0.1", "lolex": "^4.2.0", "pre-commit": "^1.2.2", @@ -132,6 +137,7 @@ "tap": "^14.4.1", "tap-mocha-reporter": "^4.0.1", "then-sleep": "^1.0.1", + "tsd": "^0.7.3", "typescript": "^3.6.3", "x-xss-protection": "^1.2.0" }, @@ -158,7 +164,13 @@ }, "standard": { "ignore": [ - "lib/configValidator.js" + "lib/configValidator.js", + "fastify.d.ts", + "types/*", + "test/types/*" ] + }, + "tsd": { + "directory": "test/types" } } \ No newline at end of file diff --git a/test/imports.test.js b/test/imports.test.js new file mode 100644 index 00000000000..73a47cad163 --- /dev/null +++ b/test/imports.test.js @@ -0,0 +1,18 @@ +'use strict' + +const t = require('tap') +const test = t.test + +test('should import as default', t => { + t.plan(2) + const fastify = require('..') + t.ok(fastify) + t.is(typeof fastify, 'function') +}) + +test('should import as esm', t => { + t.plan(2) + const { fastify } = require('..') + t.ok(fastify) + t.is(typeof fastify, 'function') +}) diff --git a/test/types/content-type-parser.test-d.ts b/test/types/content-type-parser.test-d.ts new file mode 100644 index 00000000000..49956b1970e --- /dev/null +++ b/test/types/content-type-parser.test-d.ts @@ -0,0 +1,53 @@ +import fastify from '../../fastify' +import { expectType } from 'tsd' +import { IncomingMessage } from 'http' + +expectType(fastify().addContentTypeParser('contentType', function (req, done) { + expectType(req) + done(null) +})) + +// Body limit options + +expectType(fastify().addContentTypeParser('contentType', { bodyLimit: 99 }, function (req, done) { + expectType(req) + done(null) +})) + +// Array for contentType + +expectType(fastify().addContentTypeParser(['contentType'], function (req, done) { + expectType(req) + done(null) +})) + +// Body Parser - the generic after addContentTypeParser enforces the type of the `body` parameter as well as the value of the `parseAs` property + +expectType(fastify().addContentTypeParser('bodyContentType', { parseAs: 'string' }, function (req, body, done) { + expectType(req) + expectType(body) + done(null) +})) + +expectType(fastify().addContentTypeParser('bodyContentType', { parseAs: 'buffer' }, function (req, body, done) { + expectType(req) + expectType(body) + done(null) +})) + +expectType(fastify().addContentTypeParser('contentType', async function (req: IncomingMessage) { + expectType(req) + return null +})) + +expectType(fastify().addContentTypeParser('bodyContentType', { parseAs: 'string' }, async function (req: IncomingMessage, body: string) { + expectType(req) + expectType(body) + return null +})) + +expectType(fastify().addContentTypeParser('bodyContentType', { parseAs: 'buffer' }, async function (req: IncomingMessage, body: Buffer) { + expectType(req) + expectType(body) + return null +})) diff --git a/test/types/fastify.test-d.ts b/test/types/fastify.test-d.ts new file mode 100644 index 00000000000..a0dc00f2b48 --- /dev/null +++ b/test/types/fastify.test-d.ts @@ -0,0 +1,17 @@ +import fastify, { FastifyInstance } from '../../fastify' +import * as http from 'http' +import * as https from 'https' +import * as http2 from 'http2' +import { expectType, expectError } from 'tsd' + +// FastifyInstance +// http server +expectType>(fastify()) +expectType>(fastify()) +// https server +expectType>(fastify()) +// http2 server +expectType>(fastify()) +expectType>(fastify()) +expectError(fastify({ http2: false })) // http2 option must be true +expectError(fastify({ http2: false })) // http2 option must be true diff --git a/test/types/hooks.test-d.ts b/test/types/hooks.test-d.ts new file mode 100644 index 00000000000..f59b56445ec --- /dev/null +++ b/test/types/hooks.test-d.ts @@ -0,0 +1,43 @@ +import fastify from '../../fastify' + +// Not sure how else to test this other than just making sure these definitions are valid + +fastify().addHook('onRequest', (request, reply, done) => { + // some code + done() +}) + +fastify().addHook('preParsing', (request, reply, done) => { + // some code + done() +}) + +fastify().addHook('preValidation', (request, reply, done) => { + // some code + done() +}) + +fastify().addHook('preHandler', (request, reply, done) => { + // some code + done() +}) + +fastify().addHook('preSerialization', (request, reply, payload, done) => { + // some code + done() +}) + +fastify().addHook('onError', (request, reply, error, done) => { + // some code + done() +}) + +fastify().addHook('onSend', (request, reply, payload, done) => { + // some code + done() +}) + +fastify().addHook('onResponse', (request, reply, done) => { + // some code + done() +}) diff --git a/test/types/index.ts b/test/types/index.ts deleted file mode 100644 index 555b682356d..00000000000 --- a/test/types/index.ts +++ /dev/null @@ -1,717 +0,0 @@ -/* eslint no-unused-vars: 0 */ -/* eslint no-undef: 0 */ -// This file will be passed to the TypeScript CLI to verify our typings compile - -import * as fastify from '../../fastify' -import * as http from 'http' -import * as http2 from 'http2' -import { readFileSync } from 'fs' - -// were importing cors using require, which causes it to be an `any`. This is done because `cors` exports -// itself as an express.RequestHandler which is not compatible with the fastify TypeScript types -const cors = require('cors') - -{ - // http - const h1Server = fastify() - // https - const h1SecureServer = fastify({ - https: { - cert: readFileSync('path/to/cert.pem'), - key: readFileSync('path/to/key.pem') - } - }) - // http2 - const h2Server = fastify({ http2: true }) - // secure http2 - const h2SecureServer = fastify({ - http2: true, - https: { - cert: readFileSync('path/to/cert.pem'), - key: readFileSync('path/to/key.pem') - } - }) - const h2AllowH1SecureServer = fastify({ - http2: true, - https: { - allowHTTP1: true, - cert: readFileSync('path/to/cert.pem'), - key: readFileSync('path/to/key.pem') - } - }) - // logger true - const logAllServer = fastify({ logger: true }) - logAllServer.addHook('onRequest', (req, reply, next) => { - console.log('can access req', req.headers) - next() - }) - - logAllServer.addHook('preParsing', (req, reply, next) => { - next() - }) - - logAllServer.addHook('preValidation', (req, reply, next) => { - next() - }) - - logAllServer.addHook('preSerialization', async (req, reply, payload, next) => payload) - - // other simple options - const otherServer = fastify({ - caseSensitive: false, - ignoreTrailingSlash: true, - bodyLimit: 1000, - maxParamLength: 200, - querystringParser: (str: string) => ({ str: str, strArray: [str] }), - modifyCoreObjects: true, - return503OnClosing: true, - genReqId: () => { - if (Math.random() > 0.5) { - return Math.random().toString() - } - return Math.random() - }, - requestIdHeader: 'request-id', - requestIdLogLabel: 'reqId', - serverFactory: (handler, options) => { - const server = http.createServer((req, res) => { - handler(req, res) - }) - return server - } - }) - - // http2 server factory option - const otherHttp2Server = fastify({ - serverFactory: (handler, options) => { - const server = http2.createServer((req, res) => { - handler(req, res) - }) - return server - } - }) - - // custom types - interface CustomIncomingMessage extends http.IncomingMessage { - getDeviceType: () => string; - } - const customServer: fastify.FastifyInstance = fastify() - customServer.use((req, res, next) => { - console.log('can access props from CustomIncomingMessage', req.getDeviceType()) - }) - - interface CustomHttp2IncomingMessage extends http2.Http2ServerRequest { - getDeviceType: () => string; - } - - const customHttp2Server: fastify.FastifyInstance = fastify() - customHttp2Server.use((req, res, next) => { - console.log('can access props from CustomIncomingMessage', req.getDeviceType()) - }) -} - -const server = fastify({ - https: { - cert: readFileSync('path/to/cert.pem'), - key: readFileSync('path/to/key.pem') - }, - http2: true -}) - -// Third party middleware -server.use(cors()) - -// Custom middleware -server.use('/', (req, res, next) => { - console.log(`${req.method} ${req.url}`) -}) - -// Third party plugin -// Also check if async functions are allowed to be passed to .register() -// https://github.com/fastify/fastify/pull/1841 -// All function parameters should be inferrable and should not produce 'any' -const thirdPartyPlugin: fastify.Plugin = (instance, options, callback) => {} -const thirdPartyPluginAsync: fastify.Plugin = async (instance, options) => {} - -server.register(thirdPartyPlugin) -server.register(thirdPartyPluginAsync) - -// Custom plugin -server.register((instance, options, callback) => {}) -server.register(async (instance, options) => {}) - -/** - * Test various hooks and different signatures - */ -server.addHook('preHandler', function (req, reply, next) { - this.log.debug('`this` is not `any`') - if (req.body.error) { - next(new Error('testing if middleware errors can be passed')) - } else { - // `stream` can be accessed correctly because `server` is an http2 server. - console.log('req stream', req.req.stream) - console.log('res stream', reply.res.stream) - reply.code(200).send('ok') - } -}) - -server.addHook('preHandler', async function (req, reply) { - this.log.debug('`this` is not `any`') - if (req.body.error) { - throw new Error('testing if middleware errors can be passed') - } else { - // `stream` can be accessed correctly because `server` is an http2 server. - console.log('req stream', req.req.stream) - console.log('res stream', reply.res.stream) - reply.code(200).send('ok') - } -}) - -server.addHook('onRequest', function (req, reply, next) { - this.log.debug('`this` is not `any`') - console.log(`${req.raw.method} ${req.raw.url}`) - next() -}) - -server.addHook('onResponse', function (req, reply, next) { - this.log.debug('`this` is not `any`') - this.log.debug({ code: reply.res.statusCode }, 'res has a statusCode') - setTimeout(function () { - console.log('response is finished after 100ms?', reply.res.finished) - next() - }, 100) -}) - -server.addHook('preSerialization', function (req, reply, payload, next) { - this.log.debug('`this` is not `any`') - console.log(`${req.req.method} ${req.req.url}`) - next(undefined, payload) -}) - -server.addHook('onSend', function (req, reply, payload, next) { - this.log.debug('`this` is not `any`') - console.log(`${req.req.method} ${req.req.url}`) - next() -}) - -server.addHook('onError', function (req, reply, error, next) { - this.log.debug('`this` is not `any`') - console.log(`${req.req.method} ${req.req.url}`) - next() -}) - -server.addHook('onClose', (instance, done) => { - done() -}) - -server.addHook('onRoute', (opts) => { -}) - -const schema: fastify.RouteSchema = { - body: { - type: 'object' - }, - querystring: { - type: 'object' - }, - params: { - type: 'object' - }, - headers: { - type: 'object' - }, - response: { - 200: { - type: 'object', - properties: { - hello: { - type: 'string' - } - } - }, - '2xx': { - type: 'object', - properties: { - hello: { - type: 'string' - } - } - } - } -} - -const opts: fastify.RouteShorthandOptions = { - schema, - preValidation: [ - (request, reply, next) => { - request.log.info(`pre validation for "${request.raw.url}" ${request.id}`) - next() - } - ], - preHandler: [ - (request, reply, next) => { - request.log.info(`pre handler for "${request.raw.url}" ${request.id}`) - next() - } - ], - schemaCompiler: (schema: Object) => () => {}, - bodyLimit: 5000, - logLevel: 'trace', - version: '1.0.0', - config: { } -} -const optsWithHandler: fastify.RouteShorthandOptions = { - ...opts, - handler (req, reply) { reply.send({ hello: 'route' }) } -} - -// Chaining and route definitions -server - .route({ - method: 'GET', - url: '/route', - handler: (req, reply) => { - reply.send({ hello: 'route' }) - }, - onRequest: (req, reply, done) => { - req.log.info(`onRequest for "${req.req.url}" ${req.id}`) - done() - }, - preParsing: (req, reply, done) => { - req.log.info(`preParsing for "${req.req.url}" ${req.id}`) - done() - }, - preValidation: (req, reply, done) => { - req.log.info(`preValidation for "${req.req.url}" ${req.id}`) - done() - }, - preSerialization: (req, reply, done) => { - req.log.info(`preSerialization for "${req.req.url}" ${req.id}`) - done() - }, - preHandler: (req, reply, done) => { - req.log.info(`pre handler for "${req.req.url}" ${req.id}`) - done() - } - }) - .get('/req', function (req, reply) { - reply.send(req.headers) - }) - .get<{ foo: number }>('/req', function ({ query, headers }, reply) { - const foo: number = query.foo - reply.send(headers) - }) - .get('/', opts, function (req, reply) { - reply.header('Content-Type', 'application/json').code(200) - reply.send({ hello: 'world' }) - }) - .get('/optsWithHandler', optsWithHandler) - .get('/status', function (req, reply) { - reply.status(204).send() - }) - .get('/promise', opts, function (req, reply) { - const promise = new Promise(function (resolve, reject) { - resolve({ hello: 'world' }) - }) - reply.header('content-type', 'application/json').code(200).send(promise) - }) - .get('/return-promise', opts, function (req, reply) { - const promise = new Promise(function (resolve, reject) { - resolve({ hello: 'world' }) - }) - return promise - }) - .get('/stream', function (req, reply) { - const fs = require('fs') - const stream = fs.createReadStream(process.cwd() + '/examples/plugin.js', 'utf8') - reply.code(200).send(stream) - }) - .get('/redirect', function (req, reply) { - reply.redirect('/other') - reply.redirect(301, '/something') - }) - .post('/', opts, function (req, reply) { - reply - .headers({ 'Content-Type': 'application/json' }) - .send({ hello: 'world' }) - }) - .post('/optsWithHandler', optsWithHandler) - .head('/', {}, function (req, reply) { - reply.send() - }) - .head('/optsWithHandler', optsWithHandler) - .delete('/', opts, function (req, reply) { - reply.send({ hello: 'world' }) - }) - .delete('/optsWithHandler', optsWithHandler) - .patch('/:id', opts, function (req, reply) { - req.log.info(`incoming id is ${req.params.id}`) - - reply.send({ hello: 'world' }) - }) - .patch('/optsWithHandler', optsWithHandler) - .route({ - method: ['GET', 'POST', 'PUT'], - url: '/multi-route', - handler: function (req, reply) { - reply.send({ hello: 'world' }) - } - }) - .route({ - method: 'GET', - url: '/with-config', - config: { foo: 'bar' }, - handler: function (req, reply) { - reply.send(reply.context.config) - } - }) - .register(function (instance, options, done) { - instance.get('/route', opts, function (req, reply) { - reply.send({ hello: 'world' }) - }) - done() - }, { prefix: 'v1', hello: 'world', schema: 'any string' }) - .all('/all/no-opts', function (req, reply) { - reply.send(req.headers) - }) - .all('/all/with-opts', opts, function (req, reply) { - reply.send(req.headers) - }) - .all('/optsWithHandler', optsWithHandler) - .route({ - method: 'GET', - url: '/headers', - preHandler: (_req, reply, done) => { - reply.header('E-tag', 'xB6392T=') - done() - }, - handler: (req, reply) => { - const lastModified = req.headers['last-modified'] - - if (reply.hasHeader('E-tag') && lastModified === reply.getHeader('E-tag')) { - reply.status(304).send() - return - } - - reply.status(200).send({ hello: 'world' }) - } - }) - .register((instance, _opts, done) => { - instance.setNotFoundHandler((req, reply) => { - reply - .status(404) - .type('text/plain') - .send('Route not found.') - }) - done() - }) - .get('/deprecatedpath/*', (req, reply) => { - reply.callNotFound() - }) - .get('/getResponseTime', function (req, reply) { - const milliseconds : number = reply.getResponseTime() - reply.send({ milliseconds }) - }) - -// Generics example -interface Query { - foo: string - bar: number -} - -interface Params { - foo: string -} - -interface Headers { - 'X-Access-Token': string -} - -interface Body { - foo: { - bar: { - baz: number - } - } -} - -// Query, Params, Headers, and Body can be provided as generics -server.get('/', ({ query, params, headers, body }, reply) => { - const bar: number = query.bar - const foo: string = params.foo - const xAccessToken: string = headers['X-Access-Token'] - const baz: number = body.foo.bar.baz - - reply.send({ hello: 'world' }) -}) - -// `this` points to FastifyInstance -server - .get('/', function (req, res) { - this.log.debug('`this` is not `any`') - }) - .route({ - url: '/', - method: 'GET', - handler: function (req, res) { - this.log.debug('`this` is not `any`') - } - }) - -// Default values are exported for each -server.get('/', ({ params }, reply) => { - const foo: string = params.foo - - reply.send({ hello: 'world' }) -}) - -// Using decorate requires casting so the compiler knows about new properties -server.decorate('utility', () => {}) -server.hasDecorator('utility') -server.hasRequestDecorator('utility') -server.hasReplyDecorator('utility') - -// Define a decorated instance -interface DecoratedInstance extends fastify.FastifyInstance { - utility: () => void -} - -// Use the custom decorator. Could also do "let f = server as DecoratedInstance" -(server as DecoratedInstance).utility() - -// Decorating a request or reply works in much the same way as decorate -interface DecoratedRequest extends fastify.FastifyRequest { - utility: () => void -} - -interface DecoratedReply extends fastify.FastifyReply { - utility: () => void -} - -server.get('/test-decorated-inputs', (req, reply) => { - (req as DecoratedRequest).utility(); - (reply as DecoratedReply).utility() -}) - -server.setNotFoundHandler((req, reply) => { -}) - -server.setErrorHandler((err, request, reply) => { - if (err.statusCode) { - reply.code(err.statusCode) - } - if (err.validation) { - reply.send(err.validation) - } else { - reply.send(err) - } -}) - -server.setReplySerializer((payload, statusCode) => { - if (statusCode === 201) { - return `Created ${payload}` - } - return JSON.stringify(payload) -}) - -server.listen(3000, err => { - if (err) throw err - const address = server.server.address() - if (address && typeof address === 'object') { - server.log.info(`server listening on ${address.port}`) - } else { - server.log.info(`server listening on ${address}`) - } -}) - -server.listen(3000, '127.0.0.1', err => { - if (err) throw err -}) - -server.listen(3000, '127.0.0.1', 511, err => { - if (err) throw err -}) - -server.listen('/tmp/sock', err => { - if (err) throw err -}) - -server.listen({ - port: 3000, - host: '127.0.0.1', - backlog: 511, - exclusive: false, - readableAll: false, - writableAll: false, - ipv6Only: false -}, err => { - if (err) throw err -}) - -server.listen(3000) - .then((address: string) => console.log(address)) - -server.listen(3000, '127.0.0.1') - .then((address: string) => console.log(address)) - -server.listen(3000, '127.0.0.1', 511) - .then((address: string) => console.log(address)) - -server.listen('/tmp/sock') - .then((address: string) => console.log(address)) - -server.listen({ - port: 3000, - host: '127.0.0.1', - backlog: 511, - exclusive: false, - readableAll: false, - writableAll: false, - ipv6Only: false -}).then((address: string) => console.log(address)) - -// http injections -server.inject({ url: '/test' }, (err: Error, res: fastify.HTTPInjectResponse) => { - server.log.debug(err) - server.log.debug(res.payload) -}) - -server.inject({ url: '/testAgain' }) - .then((res: fastify.HTTPInjectResponse) => console.log(res.payload)) - -server.setSchemaCompiler(function (schema: object) { - return () => true -}) - -server.setSchemaResolver(function (ref: string) { - return { - $id: ref, - type: 'string' - } -}) - -server.addSchema({}) - -server.addContentTypeParser('*', (req, done) => { - done(null, {}) -}) - -server.addContentTypeParser(['foo/bar'], (req, done) => { - done(null, {}) -}) - -server.addContentTypeParser('foo/bar', {}, (req, done) => { - done(null, {}) -}) - -server.addContentTypeParser(['foo/bar'], {}, (req, done) => { - done(null, {}) -}) - -server.addContentTypeParser('foo/bar', { bodyLimit: 20 }, (req, done) => { - done(null, {}) -}) - -server.addContentTypeParser('foo/bar', { parseAs: 'string' }, (req, body: string, done) => { - done(null, {}) -}) - -server.addContentTypeParser('foo/bar', { parseAs: 'buffer', bodyLimit: 20 }, (req, body: Buffer, done) => { - done(null, {}) -}) - -server.addContentTypeParser('foo/bar', async (req: http2.Http2ServerRequest) => []) - -if (typeof server.hasContentTypeParser('foo/bar') !== 'boolean') { - throw new Error('Invalid') -} - -server.printRoutes() - -server.ready(function (err) { - if (err) throw err -}) - -server.ready(function (err: Error, done: Function) { - done(err) -}) - -server.ready(function (err: Error, context: fastify.FastifyInstance, done: Function) { - server.log.debug(context) - done(err) -}) - -server.ready() - .then((context) => { - server.log.debug(context) - }) - .catch((err) => { - server.log.error(err) - }) - -server.after(function (err) { - if (err) throw err -}) - -server.after(function (err: Error, done: Function) { - done(err) -}) - -server.after(function (err: Error, context: fastify.FastifyInstance, done: Function) { - server.log.debug(context) - done(err) -}) - -{ - const server: fastify.FastifyInstance = fastify({ - logger: process.env.NODE_ENV === 'dev' - }) -} - -server.log.debug('Should log debug message', []) -server.log.debug({ log: 'object' }, 'Should log debug message', []) -server.log.error('Should log error message', []) -server.log.error({ log: 'object' }, 'Should log error message', []) -server.log.fatal('Should log fatal message', []) -server.log.fatal({ log: 'object' }, 'Should log fatal message', []) -server.log.info('Should log info message', []) -server.log.info({ log: 'object' }, 'Should log info message', []) -server.log.trace('Should log trace message', []) -server.log.trace({ log: 'object' }, 'Should log trace message', []) -server.log.warn('Should log warn message', []) -server.log.warn({ log: 'object' }, 'Should log warn message', []) - -const server2 = fastify() -server2.close().then(() => {}) -const server3 = fastify() -server3.close(() => {}) - -{ - // tests generics default values - const routeOptions: fastify.RouteOptions = { - method: 'GET', - url: '/', - handler: function (req, reply) { reply.send({}) } - } - - const genericHandler: fastify.RequestHandler = (req, reply) => { reply.send(reply) } - - const middleware: fastify.FastifyMiddleware = function middleware (req, reply, done) { - this.addHook('onClose', function (instance, done) { - done() - }) - done() - } -} - -type TestReplyDecoration = (this: fastify.FastifyReply) => void - -const server4 = fastify() -const testReplyDecoration: TestReplyDecoration = function () { - console.log('can access request from reply decorator', this.request.id) -} -server4.decorateReply('test-request-accessible-from-reply', testReplyDecoration) - -server4.get('/', (req, reply) => { - reply.removeHeader('x-foo').removeHeader('x-bar').send({}) -}) diff --git a/test/types/logger.test-d.ts b/test/types/logger.test-d.ts new file mode 100644 index 00000000000..ff004897a95 --- /dev/null +++ b/test/types/logger.test-d.ts @@ -0,0 +1,33 @@ +import { expectType, expectError } from 'tsd' +import fastify, { FastifyLoggerOptions, FastifyLogFn, LogLevels } from '../../fastify' +import { Server, IncomingMessage, ServerResponse } from 'http' + +expectType(fastify().log) + +;['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(logLevel => { + expectType(fastify().log[logLevel as LogLevels]) + expectType(fastify().log[logLevel as LogLevels]('')) + expectType(fastify().log[logLevel as LogLevels]({})) + expectError(fastify().log[logLevel as LogLevels](0)) +}) + +interface CustomLogger { + log: { + specialFunc: (...args: any[]) => void; + }; +} + +const customLogger: CustomLogger = { + log: { + specialFunc: (...args) => console.log(...args) + } +} + +const serverWithCustomLogger = fastify< +Server, +IncomingMessage, +ServerResponse, +CustomLogger +>({ logger: customLogger }) + +expectType(serverWithCustomLogger.log) diff --git a/test/types/plugin.test-d.ts b/test/types/plugin.test-d.ts new file mode 100644 index 00000000000..98f5ab1169e --- /dev/null +++ b/test/types/plugin.test-d.ts @@ -0,0 +1,15 @@ +import fastify, { FastifyPlugin } from '../../fastify' +import { expectType, expectError } from 'tsd' + +// FastifyPlugin & FastifyRegister +const plugin: FastifyPlugin<{ + option1: string; + option2: boolean; +}> = function (instance, opts, next) { } +expectError(fastify().register(plugin, {})) // error because missing required options from generic declaration +expectType(fastify().register(plugin, { option1: '', option2: true })) + +expectType(fastify().register(function (instace, opts, next) {})) +expectType(fastify().register(function (instace, opts, next) {}, () => {})) +expectType(fastify().register(function (instance, opts, next) {}, { logLevel: 'info', prefix: 'foobar' })) +expectError(fastify().register(function (instance, opts, next) {}, { logLevel: '' })) // must use a valid logLevel diff --git a/test/types/route.test-d.ts b/test/types/route.test-d.ts new file mode 100644 index 00000000000..1508435d27e --- /dev/null +++ b/test/types/route.test-d.ts @@ -0,0 +1,108 @@ +import fastify, { FastifyInstance, FastifyRequest, FastifyReply, RouteHandlerMethod } from '../../fastify' +import { expectType, expectError } from 'tsd' +import { HTTPMethods } from '../../types/utils' + +/* + * Testing Fastify HTTP Routes and Route Shorthands. + * Verifies Request and Reply types as well. + * For the route shorthand tests the argument orders are: + * - `(path, handler)` + * - `(path, options, handler)` + * - `(path, options)` + */ + +const routeHandler: RouteHandlerMethod = (request, reply) => { + expectType(request) + expectType(reply) +} + +type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' + +;['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE', 'OPTIONS'].forEach(method => { + // route method + expectType(fastify().route({ + method: method as HTTPMethods, + url: '/', + handler: routeHandler + })) + + const lowerCaseMethod: LowerCaseHTTPMethods = method.toLowerCase() as LowerCaseHTTPMethods + + // method as method + expectType(fastify()[lowerCaseMethod]('/', routeHandler)) + expectType(fastify()[lowerCaseMethod]('/', {}, routeHandler)) + expectType(fastify()[lowerCaseMethod]('/', { handler: routeHandler })) + + type BodyType = void + type QuerystringType = void + type ParamsType = void + type HeadersType = void + interface ContextConfigType { + foo: string; + bar: number; + } + fastify()[lowerCaseMethod]('/', { config: { foo: 'bar', bar: 100 } }, (req, res) => { + expectType(req.body) + expectType(req.query) + expectType(req.params) + expectType(req.headers) + expectType(res.context.config.foo) + expectType(res.context.config.bar) + }) + + fastify().route({ + url: '/', + method: method as HTTPMethods, + config: { foo: 'bar', bar: 100 }, + preHandler: (req, res) => { + expectType(req.body) + expectType(req.query) + expectType(req.params) + expectType(req.headers) + expectType(res.context.config.foo) + expectType(res.context.config.bar) + }, + preValidation: (req, res) => { + expectType(req.body) + expectType(req.query) + expectType(req.params) + expectType(req.headers) + expectType(res.context.config.foo) + expectType(res.context.config.bar) + }, + preSerialization: (req, res) => { + expectType(req.body) + expectType(req.query) + expectType(req.params) + expectType(req.headers) + expectType(res.context.config.foo) + expectType(res.context.config.bar) + }, + handler: (req, res) => { + expectType(req.body) + expectType(req.query) + expectType(req.params) + expectType(req.headers) + expectType(res.context.config.foo) + expectType(res.context.config.bar) + } + }) +}) + +expectError(fastify().route({ + url: '/', + method: 'CONNECT', // not a valid method + handler: routeHandler +})) + +expectType(fastify().route({ + url: '/', + method: ['GET', 'POST'], + handler: routeHandler +})) + +expectError(fastify().route({ + url: '/', + method: ['GET', 'POST', 'OPTION'], // OPTION is a typo for OPTIONS + handler: routeHandler +})) diff --git a/test/types/serverFactory.test-d.ts b/test/types/serverFactory.test-d.ts new file mode 100644 index 00000000000..cfa1609445e --- /dev/null +++ b/test/types/serverFactory.test-d.ts @@ -0,0 +1,38 @@ +import fastify, { FastifyServerFactory } from '../../fastify' +import * as http from 'http' +import { expectType } from 'tsd' + +// Custom Server +type CustomType = void; +interface CustomIncomingMessage extends http.IncomingMessage { + fakeMethod?: () => CustomType; +} + +interface CustomServerResponse extends http.ServerResponse { + fakeMethod?: () => CustomType; + +} + +const serverFactory: FastifyServerFactory = (handler, opts) => { + const server = http.createServer((req: CustomIncomingMessage, res: CustomServerResponse) => { + req.fakeMethod = () => {} + res.fakeMethod = () => {} + + handler(req, res) + }) + + return server +} + +// The request and reply objects should have the fakeMethods available (even though they may be undefined) +const customServer = fastify({ serverFactory }) + +customServer.get('/', function (request, reply) { + if (request.fakeMethod) { + expectType(request.fakeMethod()) + } + + if (reply.fakeMethod) { + expectType(reply.fakeMethod()) + } +}) diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json deleted file mode 100644 index 003ab032797..00000000000 --- a/test/types/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "noEmit": true, - "strict": true, - }, - "files": [ - "./index.ts" - ] -} diff --git a/types/.eslintrc.json b/types/.eslintrc.json new file mode 100644 index 00000000000..5e758136ebc --- /dev/null +++ b/types/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "standard" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "env": { "node": true }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "project": "./types/tsconfig.json" + }, + "rules": { + "no-console": "off", + "@typescript-eslint/indent": ["error", 2], + "semi": ["error", "never"], + "import/export": "off" // this errors on multiple exports (overload interfaces) + }, + "overrides": [ + { + "files": ["*.test-d.ts"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "off" + } + } + ] +} \ No newline at end of file diff --git a/types/content-type-parser.d.ts b/types/content-type-parser.d.ts new file mode 100644 index 00000000000..d80dc45e1d2 --- /dev/null +++ b/types/content-type-parser.d.ts @@ -0,0 +1,63 @@ +import { Buffer } from 'buffer' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression } from './utils' + +/** + * Body parser method that operatoes on request body + */ +export type FastifyBodyParser< + RawBody extends string | Buffer, + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, +> = ((req: RawRequest, rawBody: RawBody, done: (err: Error | null, body?: any) => void) => void) +| ((req: RawRequest, rawBody: RawBody) => Promise) + +/** + * Content Type Parser method that operates on request content + */ +export type FastifyContentTypeParser< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +> = ((req: RawRequest) => Promise) +| ((req: RawRequest, done: (err: Error | null, body?: any) => void) => void) + +/** + * Natively, Fastify only supports 'application/json' and 'text/plain' content types. The default charset is utf-8. If you need to support different content types, you can use the addContentTypeParser API. The default JSON and/or plain text parser can be changed. + */ +export interface AddContentTypeParser< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +> { + ( + contentType: string | string[], + opts: { + bodyLimit?: number; + }, + parser: FastifyContentTypeParser + ): void; +} + +export interface AddContentTypeParser< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +> { + (contentType: string | string[], parser: FastifyContentTypeParser): void; +} + +export interface AddContentTypeParser< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +>{ + ( + contentType: string | string[], + opts: { + parseAs: parseAs extends Buffer ? 'buffer' : 'string'; + bodyLimit?: number; + }, + parser: FastifyBodyParser + ): void; +} + +/** + * Checks for a type parser of a content type + */ +export type hasContentTypeParser = (contentType: string) => boolean diff --git a/types/context.d.ts b/types/context.d.ts new file mode 100644 index 00000000000..627b0bcae49 --- /dev/null +++ b/types/context.d.ts @@ -0,0 +1,8 @@ +import { ContextConfigDefault } from './utils' + +/** + * Route context object. Properties defined here will be available in the route's handler + */ +export interface FastifyContext { + config: ContextConfig; +} diff --git a/types/error.d.ts b/types/error.d.ts new file mode 100644 index 00000000000..700fcb19e7b --- /dev/null +++ b/types/error.d.ts @@ -0,0 +1,23 @@ +/** + * The route validation internally relies upon Ajv, which is a high-performance JSON schema validator. + */ +export interface ValidationResult { + keyword: string; + dataPath: string; + schemaPath: string; + params: { + [type: string]: string; + }; + message: string; +} + +/** + * FastifyError is a custom error object that includes status code and validation results. + */ +export interface FastifyError extends Error { + statusCode?: number; + /** + * Validation errors + */ + validation?: ValidationResult[]; +} diff --git a/types/hooks.d.ts b/types/hooks.d.ts new file mode 100644 index 00000000000..e3b6d73d54a --- /dev/null +++ b/types/hooks.d.ts @@ -0,0 +1,145 @@ +import { FastifyMiddleware } from './middleware' +import { FastifyInstance } from './instance' +import { RouteOptions } from './route' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './utils' +import { FastifyRequest } from './request' +import { FastifyReply } from './reply' +import { FastifyError } from './error' + +/** + * preParsing hook useful for authentication + */ +export type preParsingHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = FastifyMiddleware + +/** + * preValidation hook useful for authentication. Runs after request, middleware, and parsing steps. + */ +export type preValidationHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = FastifyMiddleware + +/** + * preSerialization hook that is triggered after a request is processed, but before the response is serialized + */ +export type preSerializationHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = ( + req: FastifyRequest, + reply: FastifyReply, + payload: unknown, + done: (err?: Error, value?: any) => void +) => FastifyMiddleware | void + +/** + * preHandler hook that is triggered before a request is processed, but after the preValidation hook + */ +export type preHandlerHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = FastifyMiddleware + +/** + * Adds a hook that is triggered when server.close is called. Useful for closing connections + * and performing cleanup tasks + */ +export type onCloseHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = (instance: FastifyInstance, done: () => void) => void + +/** + * Adds a hook that is triggered when a new route is registered. Listeners are passed a + * routeOptions object as the sole parameter. + * The interface is synchronous, and, as such, the listeners do not get passed a callback. + */ +export type onRouteHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = (opts: RouteOptions & { path: string; prefix: string }) => void + +/** + * Handler function for onRequest hook + */ +export type onRequestHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = FastifyMiddleware + +/** + * Handler function for onRequest hook + */ +export type onResponseHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = FastifyMiddleware + +/** + * Hook that is fired after a request is processed, but before the "onResponse" hook + */ +export type onSendHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = ( + req: FastifyRequest, + reply: FastifyReply, + payload: unknown, + done: (err?: Error, value?: any) => void +) => FastifyMiddleware | void + +/** + * Hook that is fired if `reply.send` is invoked with an Error + */ +export type onErrorHookHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = ( + req: FastifyRequest, + reply: FastifyReply, + error: FastifyError, + done: () => void +) => FastifyMiddleware | void + +export type Hooks = 'onError' | 'onSend' | 'onRequest' | 'onResponse' | 'onRoute' | 'onClose' | 'preHandler' | 'preSerialization' | 'preValidation' | 'preParsing' + +export interface AddHook< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> { + ( + name: Name, + hook: addHookHandler + ): FastifyInstance; +} + +type addHookHandler< + Name extends Hooks, + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, +> = Name extends 'onError' ? onErrorHookHandler + : Name extends 'onSend' ? onSendHookHandler + : Name extends 'onRequest' ? onRequestHookHandler + : Name extends 'onResponse' ? onResponseHookHandler + : Name extends 'onRoute' ? onRouteHookHandler + : Name extends 'onClose' ? onCloseHookHandler + : Name extends 'preHandler' ? preHandlerHookHandler + : Name extends 'preSerialization' ? preSerializationHookHandler + : Name extends 'preValidation' ? preValidationHookHandler + : Name extends 'preParsing' ? preParsingHookHandler + : never diff --git a/types/instance.d.ts b/types/instance.d.ts new file mode 100644 index 00000000000..09b07d8226b --- /dev/null +++ b/types/instance.d.ts @@ -0,0 +1,107 @@ +import { InjectOptions, InjectPayload } from 'light-my-request' +import { RouteOptions, RouteShorthandMethod } from './route' +import { FastifySchema, FastifySchemaCompiler } from './schema' +import { RawServerBase, RawRequestDefaultExpression, RawServerDefault, RawReplyDefaultExpression, ContextConfigDefault, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './utils' +import { FastifyLoggerOptions } from './logger' +import { FastifyRegister } from './register' +import { AddHook } from './hooks' +import { FastifyRequest } from './request' +import { FastifyReply } from './reply' +import { FastifyError } from './error' +import { AddContentTypeParser, hasContentTypeParser } from './content-type-parser' + +/** + * Fastify server instance. Returned by the core `fastify()` method. + */ +export interface FastifyInstance< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + Logger = FastifyLoggerOptions +> { + server: RawServer; + prefix: string; + log: Logger; + + addSchema(schema: FastifySchema): FastifyInstance; + + after(err: Error): FastifyInstance; + + close(closeListener?: () => void): void; + close(): Promise; // what is this use case? Not documented on Server#close + + // should be able to define something useful with the decorator getter/setter pattern using Generics to enfore the users function returns what they expect it to + decorate(property: string | symbol, value: any, dependencies?: string[]): FastifyInstance; + decorateRequest(property: string | symbol, value: any, dependencies?: string[]): FastifyInstance; + decorateReply(property: string | symbol, value: any, dependencies?: string[]): FastifyInstance; + + hasDecorator(decorator: string | symbol): boolean; + hasRequestDecorator(decorator: string | symbol): boolean; + hasReplyDecorator(decorator: string | symbol): boolean; + + inject(opts: InjectOptions | string, cb: (err: Error, response: InjectPayload) => void): void; + inject(opts: InjectOptions | string): Promise; + + listen(port: number, address: string, backlog: number, callback: (err: Error, address: string) => void): void; + listen(port: number, address: string, callback: (err: Error, address: string) => void): void; + listen(port: number, callback: (err: Error, address: string) => void): void; + listen(port: number, address?: string, backlog?: number): Promise; + + ready(): Promise>; + ready(readyListener: (err: Error) => void): void; + + register: FastifyRegister; + use: FastifyRegister; + + route< + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault + >(opts: RouteOptions): FastifyInstance; + + // Would love to implement something like the following: + // [key in RouteMethodsLower]: RouteShorthandMethod | RouteShorthandMethodWithOptions, + + get: RouteShorthandMethod; + head: RouteShorthandMethod; + post: RouteShorthandMethod; + put: RouteShorthandMethod; + delete: RouteShorthandMethod; + options: RouteShorthandMethod; + patch: RouteShorthandMethod; + all: RouteShorthandMethod; + + addHook: AddHook; + + /** + * Set the 404 handler + */ + setNotFoundHandler( + handler: (request: FastifyRequest, reply: FastifyReply) => void + ): void; + + /** + * Set a function that will be called whenever an error happens + */ + setErrorHandler( + handler: (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => void + ): void; + + /** + * Set the schema compiler for all routes. + */ + setSchemaCompiler(schemaCompiler: FastifySchemaCompiler): FastifyInstance; + + /** + * Add a content type parser + */ + addContentTypeParser: AddContentTypeParser; + hasContentTypeParser: hasContentTypeParser; + + /** + * Prints the representation of the internal radix tree used by the router + */ + printRoutes(): string; +} diff --git a/types/logger.d.ts b/types/logger.d.ts new file mode 100644 index 00000000000..533501eb117 --- /dev/null +++ b/types/logger.d.ts @@ -0,0 +1,49 @@ +import { FastifyError } from './error' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './utils' + +/** + * Standard Fastify logging function + */ +export interface FastifyLogFn { + (msg: string, ...args: any[]): void; + (obj: object, msg?: string, ...args: any[]): void; +} + +export type LogLevels = 'info' | 'error' | 'debug' | 'fatal' | 'warn' | 'trace' + +/** + * Fastify Custom Logger options. + */ +export interface FastifyLoggerOptions< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> { + serializers?: { + req: (req: RawRequest) => { + method: string; + url: string; + version: string; + hostname: string; + remoteAddress: string; + remotePort: number; + }; + err: (err: FastifyError) => { + type: string; + message: string; + stack: string; + [key: string]: any; + }; + res: (res: RawReply) => { + statusCode: string | number; + }; + }; + info: FastifyLogFn; + error: FastifyLogFn; + debug: FastifyLogFn; + fatal: FastifyLogFn; + warn: FastifyLogFn; + trace: FastifyLogFn; + child: FastifyLogFn | FastifyLoggerOptions; + genReqId?: string; +} diff --git a/types/middleware.d.ts b/types/middleware.d.ts new file mode 100644 index 00000000000..db3f3045543 --- /dev/null +++ b/types/middleware.d.ts @@ -0,0 +1,49 @@ +import { FastifyRequest } from './request' +import { FastifyReply } from './reply' +import { FastifyError } from './error' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault, ContextConfigDefault } from './utils' + +/** + * Fastify Middleware + * + * Fastify out of the box provides an asynchronous middleware engine compatible with Express and Restify middlewares. + */ +export interface FastifyMiddleware< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> { + ( + request: FastifyRequest, + reply: FastifyReply, + done: (err?: FastifyError) => void + ): void; +} + +/** + * Fastify Middleware + * + * Fastify out of the box provides an asynchronous middleware engine compatible with Express and Restify middlewares. + */ +export interface FastifyMiddlewareWithPayload< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> { + ( + request: FastifyRequest, + reply: FastifyReply, + payload: any, + done: (err?: FastifyError, value?: any) => void + ): void; +} diff --git a/types/plugin.d.ts b/types/plugin.d.ts new file mode 100644 index 00000000000..cd6b519170b --- /dev/null +++ b/types/plugin.d.ts @@ -0,0 +1,25 @@ +import { FastifyInstance } from './instance' +import { FastifyError } from './error' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './utils' + +/** + * FastifyPlugin + * + * Fastify allows the user to extend its functionalities with plugins. A plugin can be a set of routes, a server decorator or whatever. To activate plugins, use the `fastify.register()` method. + */ +export interface FastifyPlugin< + Options extends FastifyPluginOptions, + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> { + ( + instance: FastifyInstance, + opts: Options, + next: (err?: FastifyError) => void + ): void; +} + +export interface FastifyPluginOptions { + [key: string]: any; +} diff --git a/types/register.d.ts b/types/register.d.ts new file mode 100644 index 00000000000..32de3401315 --- /dev/null +++ b/types/register.d.ts @@ -0,0 +1,16 @@ +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression } from './utils' +import { FastifyPlugin, FastifyPluginOptions } from './plugin' +import { LogLevels } from './logger' + +export interface FastifyRegister< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> { + (plugin: FastifyPlugin, opts?: (RegisterOptions & Options) | (() => RegisterOptions & Options)): void; +} + +export interface RegisterOptions { + prefix?: string; + logLevel?: LogLevels; +} diff --git a/types/reply.d.ts b/types/reply.d.ts new file mode 100644 index 00000000000..7ba538e82aa --- /dev/null +++ b/types/reply.d.ts @@ -0,0 +1,30 @@ +import { RawReplyDefaultExpression, RawServerBase, RawServerDefault, ContextConfigDefault } from './utils' +import { FastifyContext } from './context' + +/** + * FastifyReply is an instance of the standard http or http2 reply types. + * It defaults to http.ServerResponse, and it also extends the relative reply object. + */ +export type FastifyReply< + RawServer extends RawServerBase = RawServerDefault, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + ContextConfig = ContextConfigDefault +> = RawReply & { + callNotFound(): void; + code(statusCode: number): FastifyReply; + hasHeader(key: string): boolean; + header(key: string, value: any): FastifyReply; + getHeader(key: string): string | undefined; + // Note: should consider refactoring the argument order for redirect. statusCode is optional so it should be after the required url param + redirect(statusCode: number, url: string): FastifyReply; + redirect(url: string): FastifyReply; + removeHeader(key: string): void; + send(payload?: any): FastifyReply; + serialize(payload: any): string; + serializer(fn: (payload: any) => string): FastifyReply; + status(statusCode: number): FastifyReply; + type(contentType: string): FastifyReply; + context: FastifyContext; + res: RawReply; + sent: boolean; +} diff --git a/types/request.d.ts b/types/request.d.ts new file mode 100644 index 00000000000..47af2174853 --- /dev/null +++ b/types/request.d.ts @@ -0,0 +1,23 @@ +import { FastifyLoggerOptions } from './logger' +import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './utils' + +/** + * FastifyRequest is an instance of the standard http or http2 request objects. + * It defaults to http.IncomingMessage, and it also extends the relative request object. + */ +export type FastifyRequest< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault +> = RawRequest & { + body: RequestBody; + id: any; + log: FastifyLoggerOptions; + params: RequestParams; + query: RequestQuerystring; + raw: RawRequest; + headers: RawRequest['headers'] & RequestHeaders; // this enables the developer to extend the existing http(s|2) headers list +} diff --git a/types/route.d.ts b/types/route.d.ts new file mode 100644 index 00000000000..4881c85b485 --- /dev/null +++ b/types/route.d.ts @@ -0,0 +1,127 @@ +import { FastifyInstance } from './instance' +import { FastifyMiddleware, FastifyMiddlewareWithPayload } from './middleware' +import { FastifyRequest } from './request' +import { FastifyReply } from './reply' +import { FastifySchema, FastifySchemaCompiler } from './schema' +import { HTTPMethods, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault, ContextConfigDefault } from './utils' +import { LogLevels } from './logger' + +/** + * Fastify Router Shorthand method type that is similar to the Express/Restify approach + */ +export interface RouteShorthandMethod< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, +> { + ( + path: string, + opts: RouteShorthandOptions, + handler: RouteHandlerMethod + ): FastifyInstance; +} + +/** + * Fastify Router Shorthand method type that is similar to the Express/Restify approach + */ +export interface RouteShorthandMethod< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, +> { + ( + path: string, + handler: RouteHandlerMethod + ): FastifyInstance; +} + +/** + * Fastify Router Shorthand method type that is similar to the Express/Restify approach + */ +export interface RouteShorthandMethod< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, +> { + ( + path: string, + opts: RouteShorthandOptionsWithHandler + ): FastifyInstance; +} + +/** + * Route shorthand options for the various shorthand methods + */ +export interface RouteShorthandOptions< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> { + schema?: FastifySchema; + attachValidation?: boolean; + preValidation?: FastifyMiddleware | FastifyMiddleware[]; + preHandler?: FastifyMiddleware | FastifyMiddleware[]; + preSerialization?: FastifyMiddlewareWithPayload | FastifyMiddlewareWithPayload[]; + schemaCompiler?: FastifySchemaCompiler; + bodyLimit?: number; + logLevel?: LogLevels; + config?: ContextConfig; + version?: string; + prefixTrailingSlash?: boolean; +} + +/** + * Fastify route method options. + */ +export interface RouteOptions< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> extends RouteShorthandOptions { + method: HTTPMethods | HTTPMethods[]; + url: string; + handler: RouteHandlerMethod; +} + +/** + * Shorthand options including the handler function property + */ +export interface RouteShorthandOptionsWithHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> extends RouteShorthandOptions { + handler: RouteHandlerMethod; +} + +/** + * Route handler method declaration. + */ +export type RouteHandlerMethod< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression, + RequestBody = RequestBodyDefault, + RequestQuerystring = RequestQuerystringDefault, + RequestParams = RequestParamsDefault, + RequestHeaders = RequestHeadersDefault, + ContextConfig = ContextConfigDefault +> = ( + request: FastifyRequest, + reply: FastifyReply +) => void | Promise diff --git a/types/schema.d.ts b/types/schema.d.ts new file mode 100644 index 00000000000..60815f5c6cd --- /dev/null +++ b/types/schema.d.ts @@ -0,0 +1,17 @@ +/** + * Schemas in Fastify follow the JSON-Schema standard. For this reason + * we have opted to not ship strict schema based types. Instead we provide + * an example in our documentation on how to solve this problem. Check it + * out here: + */ +export interface FastifySchema { + body?: unknown; + querystring?: unknown; + params?: unknown; + headers?: unknown; +} + +/** + * Compiler for FastifySchema Type + */ +export type FastifySchemaCompiler = (schema: FastifySchema) => unknown diff --git a/types/serverFactory.d.ts b/types/serverFactory.d.ts new file mode 100644 index 00000000000..0c2fd00ed9c --- /dev/null +++ b/types/serverFactory.d.ts @@ -0,0 +1,19 @@ +import { RawServerBase, RawServerDefault, RawReplyDefaultExpression, RawRequestDefaultExpression } from './utils' +import * as http from 'http' +import * as https from 'https' +import * as http2 from 'http2' + +export interface FastifyServerFactory< + RawServer extends RawServerBase = RawServerDefault +> { + (handler: FastifyServerFactoryHandler, opts: object): RawServer; +} + +export type FastifyServerFactoryHandler< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, + RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression +> = + RawServer extends http.Server | https.Server ? + (request: http.IncomingMessage & RawRequest, response: http.ServerResponse & RawReply) => void : + (request: http2.Http2ServerRequest & RawRequest, response: http2.Http2ServerResponse & RawReply) => void diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 00000000000..9d8d87551df --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ "es2015" ], + "module": "commonjs", + "noEmit": true, + "strict": true + }, + "includes": [ + "/test/types/*.test-d.ts", + "/types/*.d.ts" + ] +} diff --git a/types/utils.d.ts b/types/utils.d.ts new file mode 100644 index 00000000000..1c1e44c1fac --- /dev/null +++ b/types/utils.d.ts @@ -0,0 +1,34 @@ +import * as http from 'http' +import * as http2 from 'http2' +import * as https from 'https' + +/** + * Standard HTTP method strings + */ +export type HTTPMethods = 'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS' + +/** + * A union type of the Node.js server types from the http, https, and http2 modules. + */ +export type RawServerBase = http.Server | https.Server | http2.Http2Server | http2.Http2SecureServer + +/** + * The default server type + */ +export type RawServerDefault = http.Server + +/** + * The default request type based on the server type. Utilizes generic constraining. + */ +export type RawRequestDefaultExpression = RawServer extends http.Server | https.Server ? http.IncomingMessage : http2.Http2ServerRequest +/** + * The default reply type based on the server type. Utilizes generic constraining. + */ +export type RawReplyDefaultExpression = RawServer extends http.Server | https.Server ? http.ServerResponse : http2.Http2ServerResponse + +export type RequestBodyDefault = unknown +export type RequestQuerystringDefault = unknown +export type RequestParamsDefault = unknown +export type RequestHeadersDefault = unknown + +export type ContextConfigDefault = unknown From 259b83de4161c7a2f10cce6c2133a50ab884300d Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Sun, 24 Nov 2019 14:58:49 +0100 Subject: [PATCH 143/144] fix rebase: lint --- lib/errors.js | 2 +- lib/route.js | 7 ------- package.json | 4 ++-- test/async-await.test.js | 2 +- test/hooks-async.test.js | 6 +++--- test/internals/validation.test.js | 2 +- test/route.test.js | 2 +- 7 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/errors.js b/lib/errors.js index 9dc6b123152..46b7075c769 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -80,7 +80,7 @@ function createError (code, message, statusCode = 500, Base = Error) { function FastifyError (a, b, c) { Error.captureStackTrace(this, FastifyError) - this.name = `FastifyError` + this.name = 'FastifyError' this.code = code // more performant than spread (...) operator diff --git a/lib/route.js b/lib/route.js index 355dfbe3d92..7676a9547b6 100644 --- a/lib/route.js +++ b/lib/route.js @@ -10,7 +10,6 @@ const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler' const validation = require('./validation') const buildSchema = validation.build const { buildSchemaCompiler } = validation -const { beforeHandlerWarning } = require('./warnings') const { codes: { @@ -137,13 +136,7 @@ function buildRouting (options) { validateBodyLimitOption(opts.bodyLimit) - if (opts.preHandler == null && opts.beforeHandler != null) { - beforeHandlerWarning() - opts.preHandler = opts.beforeHandler - } - const prefix = this[kRoutePrefix] - this.after((notHandledErr, done) => { var path = opts.url || opts.path if (path === '/' && prefix.length > 0) { diff --git a/package.json b/package.json index f507708c0e3..0b7a914d8cc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "unit:junit": "tap-mocha-reporter xunit < out.tap > test/junit-testresults.xml", "test:typescript": "tsd", "test:report": "npm run lint && npm run unit:report && npm run test:typescript", - "test": "npm run lint && npm run unit && npm run test:typescript", + "test": "npm run lint && npm run unit", "coverage": "npm run unit -- --cov --coverage-report=html", "test:ci": "npm run lint && npm run unit -- --cov --coverage-report=lcovonly && npm run typescript", "bench": "branchcmp -r 2 -g -s \"npm run benchmark\"", @@ -173,4 +173,4 @@ "tsd": { "directory": "test/types" } -} \ No newline at end of file +} diff --git a/test/async-await.test.js b/test/async-await.test.js index babc97c5251..0d493798e4f 100644 --- a/test/async-await.test.js +++ b/test/async-await.test.js @@ -418,7 +418,7 @@ test('error is not logged because promise was fulfilled with undefined but respo var fastify = null var stream = split(JSON.parse) - var payload = { 'hello': 'world' } + var payload = { hello: 'world' } try { fastify = Fastify({ logger: { diff --git a/test/hooks-async.test.js b/test/hooks-async.test.js index 2416d56f625..0089dbe19f7 100644 --- a/test/hooks-async.test.js +++ b/test/hooks-async.test.js @@ -438,7 +438,7 @@ test('Should log a warning if is an async function with `done`', t => { try { fastify.addHook('onRequest', async (req, reply, done) => {}) } catch (e) { - t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + t.true(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.') } }) @@ -449,12 +449,12 @@ test('Should log a warning if is an async function with `done`', t => { try { fastify.addHook('onSend', async (req, reply, payload, done) => {}) } catch (e) { - t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + t.true(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.') } try { fastify.addHook('preSerialization', async (req, reply, payload, done) => {}) } catch (e) { - t.true(e.message === `Async function has too many arguments. Async hooks should not use the 'done' argument.`) + t.true(e.message === 'Async function has too many arguments. Async hooks should not use the \'done\' argument.') } }) diff --git a/test/internals/validation.test.js b/test/internals/validation.test.js index ea589f77fac..1cbe5c6a3e5 100644 --- a/test/internals/validation.test.js +++ b/test/internals/validation.test.js @@ -158,7 +158,7 @@ test('build schema - must throw if querystring and query schema exist', t => { validation.build(opts, schema => ajv.compile(schema), new Schemas()) } catch (err) { t.is(err.code, 'FST_ERR_SCH_DUPLICATE') - t.is(err.message, 'FST_ERR_SCH_DUPLICATE: Schema with \'querystring\' already present!') + t.is(err.message, 'Schema with \'querystring\' already present!') } }) diff --git a/test/route.test.js b/test/route.test.js index b29117252fe..40e572ee977 100644 --- a/test/route.test.js +++ b/test/route.test.js @@ -3,7 +3,7 @@ const t = require('tap') const test = t.test const sget = require('simple-get').concat -const joi = require('joi') +const joi = require('@hapi/joi') const Fastify = require('..') test('route', t => { From c5c4ad917a3380cf5abb8ed6d7b5599151cbb0ad Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Sun, 24 Nov 2019 15:01:59 +0100 Subject: [PATCH 144/144] removed node 6 from gh workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74326a15388..38d6685427e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [6.x, 8.x, 10.x, 12.x, 13.x] + node-version: [8.x, 10.x, 12.x, 13.x] os: [ubuntu-latest, windows-latest, macOS-latest] steps: