diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 114bd51686..70b07fcbf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: fastify/github-action-merge-dependabot@v2.5.0 + - uses: fastify/github-action-merge-dependabot@v2.6.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} target: minor diff --git a/.npmignore b/.npmignore index 6ca338ae96..33caf9bf83 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,5 @@ .editorconfig .gitattributes -.git -.DS_Store .gitignore .github .nyc_output @@ -9,7 +7,6 @@ coverage/ tools/ CODE_OF_CONDUCT.md CONTRIBUTING.md -.dependabot .clinic # test certification diff --git a/build/build-validation.js b/build/build-validation.js index ea252e1b4d..84d34a8963 100644 --- a/build/build-validation.js +++ b/build/build-validation.js @@ -25,6 +25,7 @@ const defaultInitOptions = { connectionTimeout: 0, // 0 sec keepAliveTimeout: 72000, // 72 seconds maxRequestsPerSocket: 0, // no limit + requestTimeout: 0, // no limit bodyLimit: 1024 * 1024, // 1 MiB caseSensitive: true, disableRequestLogging: false, @@ -47,6 +48,7 @@ const schema = { connectionTimeout: { type: 'integer', default: defaultInitOptions.connectionTimeout }, keepAliveTimeout: { type: 'integer', default: defaultInitOptions.keepAliveTimeout }, maxRequestsPerSocket: { type: 'integer', default: defaultInitOptions.maxRequestsPerSocket, nullable: true }, + requestTimeout: { type: 'integer', default: defaultInitOptions.requestTimeout }, bodyLimit: { type: 'integer', default: defaultInitOptions.bodyLimit }, caseSensitive: { type: 'boolean', default: defaultInitOptions.caseSensitive }, http2: { type: 'boolean' }, diff --git a/build/sync-version.js b/build/sync-version.js new file mode 100644 index 0000000000..5d00f216ed --- /dev/null +++ b/build/sync-version.js @@ -0,0 +1,11 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +// package.json:version -> fastify.js:VERSION +const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString('utf8')) + +const fastifyJs = path.join(__dirname, '..', 'fastify.js') + +fs.writeFileSync(fastifyJs, fs.readFileSync(fastifyJs).toString('utf8').replace(/const\s*VERSION\s*=.*/, `const VERSION = '${version}'`)) diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index aa55b45de0..4cafa616be 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -91,6 +91,7 @@ Plugins maintained by the Fastify team are listed under [Core](#core) while plug - [`fastify-cloudevents`](https://github.com/smartiniOnGitHub/fastify-cloudevents) Fastify plugin to generate and forward Fastify events in the Cloudevents format. - [`fastify-cockroachdb`](https://github.com/alex-ppg/fastify-cockroachdb) Fastify plugin to connect to a CockroachDB PostgreSQL instance via the Sequelize ORM. - [`fastify-couchdb`](https://github.com/nigelhanlon/fastify-couchdb) Fastify plugin to add CouchDB support via [nano](https://github.com/apache/nano). +- [`fastify-crud-generator`](https://github.com/heply/fastify-crud-generator) A plugin to rapidly generate CRUD routes for any entity. - [`fastify-custom-healthcheck`](https://github.com/gkampitakis/fastify-custom-healthcheck) Fastify plugin to add health route in your server that asserts custom functions. - [`fastify-decorators`](https://github.com/L2jLiga/fastify-decorators) Fastify plugin that provides the set of TypeScript decorators. - [`fastify-disablecache`](https://github.com/Fdawgs/fastify-disablecache) Fastify plugin to disable client-side caching, inspired by [nocache](https://github.com/helmetjs/nocache). @@ -158,6 +159,7 @@ Plugins maintained by the Fastify team are listed under [Core](#core) while plug - [`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-piscina`](https://github.com/piscinajs/fastify-piscina) A worker thread pool plugin using [Piscina](https://github.com/piscinajs/piscina). - [`fastify-peekaboo`](https://github.com/simone-sanfratello/fastify-peekaboo) Fastify plugin for memoize responses by expressive settings. +- [`fastify-polyglot`](https://github.com/heply/fastify-polyglot) A plugin to handle i18n using [node-polyglot](https://www.npmjs.com/package/node-polyglot). - [`fastify-postgraphile`](https://github.com/alemagio/fastify-postgraphile) Plugin to integrate [PostGraphile](https://www.graphile.org/postgraphile/) in a Fastify project. - [`fastify-prettier`](https://github.com/hsynlms/fastify-prettier) A Fastify plugin that uses [prettier](https://github.com/prettier/prettier) under the hood to beautify outgoing responses and/or other things in the Fastify server. - [`fastify-print-routes`](https://github.com/ShogunPanda/fastify-print-routes) A Fastify plugin that prints all available routes. diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 5cf43ec090..fb53566038 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -262,6 +262,24 @@ async function routes (fastify, options) { } return result }) + + const animalBodyJsonSchema = { + type: 'object', + required: ['animal'], + properties: { + animal: { type: 'string' }, + }, + } + + const schema = { + body: animalBodyJsonSchema, + } + + fastify.post('/animals', { schema }, async (request, reply) => { + // we can use the `request.body` object to get the data sent by the client + const result = await collection.insertOne({ animal: request.body.animal }) + return result + }) } module.exports = routes @@ -363,6 +381,20 @@ fastify.get('/', opts, async (request, reply) => { 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](Validation-and-Serialization.md) to learn more. + +### Parsing request payloads +Fastify parses `'application/json'` and `'text/plain'` request payloads natively, with the result accessible from the [Fastify request](Request.md) object at `request.body`.
+The following example returns the parsed body of a request back to the client: + +```js +const opts = {} +fastify.post('/', opts, async (request, reply) => { + return request.body +}) +``` + +Read [Content Type Parser](ContentTypeParser.md) to learn more about Fastify's default parsing functionality and how to support other content types. + ### Extend your server 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.
diff --git a/docs/Hooks.md b/docs/Hooks.md index 4054903002..f12595cb44 100644 --- a/docs/Hooks.md +++ b/docs/Hooks.md @@ -220,7 +220,7 @@ fastify.addHook('onTimeout', async (request, reply) => { await asyncMethod() }) ``` -`onTimeout` is useful if you need to monitor the request timed out in your service (if the `connectionTimeout` property is set on the Fastify instance). The `onTimeout` hook is executed when a request is timed out and the HTTP socket has been hanged up. Therefore ,you will not be able to send data to the client. +`onTimeout` is useful if you need to monitor the request timed out in your service (if the `connectionTimeout` property is set on the Fastify instance). The `onTimeout` hook is executed when a request is timed out and the HTTP socket has been hanged up. Therefore, you will not be able to send data to the client. ### Manage Errors from a hook diff --git a/docs/Logging.md b/docs/Logging.md index baf44ed251..0cc496f217 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -2,6 +2,7 @@ ## Logging +### Enable logging Logging is disabled by default, and you can enable it by passing `{ logger: true }` or `{ logger: { level: 'info' } }` when you create a fastify instance. Note that if the logger is disabled, it is impossible to @@ -11,13 +12,34 @@ this purpose. As 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: +Enabling the production JSON logger: ```js const fastify = require('fastify')({ logger: true }) +``` + +Enabling the logger with appropriate configuration for both local development and production environment requires bit more configuration: +```js +const fastify = require('fastify')({ + logger: { + prettyPrint: + environment === 'development' + ? { + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname' + } + : false + } +}) +``` +⚠️ `pino-pretty` needs to be installed as a dev dependency, it is not included by default for performance reasons. +### Usage +You can use the logger like this in your route handlers: + +```js fastify.get('/', options, function (request, reply) { request.log.info('Some info about the current request') reply.send({ hello: 'world' }) diff --git a/docs/Plugins.md b/docs/Plugins.md index 2ba3e08f3b..e5fce2624e 100644 --- a/docs/Plugins.md +++ b/docs/Plugins.md @@ -4,7 +4,7 @@ Fastify allows the user to extend its functionalities with plugins. A plugin can be a set of routes, a server [decorator](Decorators.md), or whatever. The API that you will need to use one or more plugins, is `register`.
-By default, `register` creates a *new scope*, this means that if you make some changes to the Fastify instance (via `decorate`), this change will not be reflected by the current context ancestors, but only to its sons. This feature allows us to achieve plugin *encapsulation* and *inheritance*, in this way we create a *direct acyclic graph* (DAG) and we will not have issues caused by cross dependencies. +By default, `register` creates a *new scope*, this means that if you make some changes to the Fastify instance (via `decorate`), this change will not be reflected by the current context ancestors, but only to its descendants. This feature allows us to achieve plugin *encapsulation* and *inheritance*, in this way we create a *direct acyclic graph* (DAG) and we will not have issues caused by cross dependencies. You already see in the [getting started](Getting-Started.md#register) section how using this API is pretty straightforward. ``` diff --git a/docs/Recommendations.md b/docs/Recommendations.md index 8156db8b05..f85b823af9 100644 --- a/docs/Recommendations.md +++ b/docs/Recommendations.md @@ -166,62 +166,94 @@ backend static-backend ### Nginx ```nginx +# This upstream block groups 3 servers into one named backend fastify_app +# with 2 primary servers distributed via round-robin +# and one backup which is used when the first 2 are not reachable +# This also assumes your fastify servers are listening on port 80. +# more info: http://nginx.org/en/docs/http/ngx_http_upstream_module.html upstream fastify_app { - # more info: http://nginx.org/en/docs/http/ngx_http_upstream_module.html server 10.10.11.1:80; server 10.10.11.2:80; server 10.10.11.3:80 backup; } +# This server block asks NGINX to respond with a redirect when +# an incoming request from port 80 (typically plain HTTP), to +# the same request URL but with HTTPS as protocol. +# This block is optional, and usually used if you are handling +# SSL termination in NGINX, like in the example here. server { - # default server + # default server is a special parameter to ask NGINX + # to set this server block to the default for this address/port + # which in this case is any address and port 80 listen 80 default_server; listen [::]:80 default_server; - # specify host + # With a server_name directive you can also ask NGINX to + # use this server block only with matching server name(s) # listen 80; # listen [::]:80; # server_name example.tld; + # This matches all paths from the request and responds with + # the redirect mentioned above. location / { return 301 https://$host$request_uri; } } +# This server block asks NGINX to respond to requests from +# port 443 with SSL enabled and accept HTTP/2 connections. +# This is where the request is then proxied to the fastify_app +# server group via port 3000. server { - # default server + # This listen directive asks NGINX to accept requests + # coming to any address, port 443, with SSL, and HTTP/2 + # if possible. listen 443 ssl http2 default_server; listen [::]:443 ssl http2 default_server; - - # specify host + + # With a server_name directive you can also ask NGINX to + # use this server block only with matching server name(s) # listen 443 ssl http2; # listen [::]:443 ssl http2; # server_name example.tld; - # public private keys + # Your SSL/TLS certificate (chain) and secret key in the PEM format ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/private.pem; - ssl_trusted_certificate /path/to/chain.pem; - # use https://ssl-config.mozilla.org/ for best practice configuration + # A generic best practice baseline for based + # on https://ssl-config.mozilla.org/ ssl_session_timeout 1d; ssl_session_cache shared:FastifyApp:10m; ssl_session_tickets off; - - # modern configuration + + # This tells NGINX to only accept TLS 1.3, which should be fine + # with most modern browsers including IE 11 with certain updates. + # If you want to support older browsers you might need to add + # additional fallback protocols. ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; - - # HSTS (ngx_http_headers_module is required) (63072000 seconds) + + # This adds a header that tells browsers to only ever use HTTPS + # with this server. add_header Strict-Transport-Security "max-age=63072000" always; - - # OCSP stapling + + # The following directives are only necessary if you want to + # enable OCSP Stapling. ssl_stapling on; ssl_stapling_verify on; + ssl_trusted_certificate /path/to/chain.pem; - # custom resolver + # Custom nameserver to resolve upstream server names # resolver 127.0.0.1; - + + # This section matches all paths and proxies it to the backend server + # group specified above. Note the additional headers that forward + # information about the original request. You might want to set + # trustProxy to the address of your NGINX server so the X-Forwarded + * fields are used by fastify. location / { # more info: http://nginx.org/en/docs/http/ngx_http_proxy_module.html proxy_http_version 1.1; @@ -232,8 +264,12 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - - proxy_pass http://fastify_app:3000; + + # This is the directive that proxies requests to the specified server. + # If you are using an upstream group, then you do not need to specify a port. + # If you are directly proxying to a server e.g. + # proxy_pass http://127.0.0.1:3000 then specify a port. + proxy_pass http://fastify_app; } } ``` diff --git a/docs/Reply.md b/docs/Reply.md index a15fdaef6d..a4129f1fdd 100644 --- a/docs/Reply.md +++ b/docs/Reply.md @@ -7,6 +7,7 @@ - [.statusCode](#statusCode) - [.server](#server) - [.header(key, value)](#headerkey-value) + - [set-cookie](#set-cookie) - [.headers(object)](#headersobject) - [.getHeader(key)](#getheaderkey) - [.getHeaders()](#getheaders) @@ -109,6 +110,18 @@ fastify.get('/', async function (req, rep) { Sets a response header. If the value is omitted or undefined, it is coerced to `''`. + +- ### set-cookie + - While sending different values as cookie with `set-cookie` as the key, every value will be sent as cookie instead of replacing the previous value. + + ```js + reply.header('set-cookie', 'foo'); + reply.header('set-cookie', 'bar'); + ``` + - The browser will only consider the latest reference of a key for `set-cookie` header. The fact that this is done this way is to avoid parsing the set-cookie header when you add it in the reply and speeds up the serialization of the reply. + + - To reset the `set-cookie`, you need to make an explicit call to `reply.removeHeader('set-cookie')`, read more about `.removeHeader(key)` [here](#removeheaderkey). + For more information, see [`http.ServerResponse#setHeader`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_response_setheader_name_value). diff --git a/docs/Request.md b/docs/Request.md index 892661f91f..88b4c4b5b3 100644 --- a/docs/Request.md +++ b/docs/Request.md @@ -4,7 +4,7 @@ The first parameter of the handler function is `Request`.
Request is a core Fastify object containing the following fields: - `query` - the parsed querystring, its format is specified by [`querystringParser`](Server.md#querystringparser) -- `body` - the body +- `body` - the request payload, see [Content Type Parser](ContentTypeParser.md) for details on what request payloads Fastify natively parses and how to support other content types - `params` - the params matching the URL - [`headers`](#headers) - the headers getter and setter - `raw` - the incoming HTTP request from Node core diff --git a/docs/Routes.md b/docs/Routes.md index 9c8ffdabfc..557e6fb703 100644 --- a/docs/Routes.md +++ b/docs/Routes.md @@ -52,6 +52,8 @@ They need to be in * `preSerialization(request, reply, payload, done)`: a [function](Hooks.md#preserialization) called just before the serialization, it could also be an array of functions. * `onSend(request, reply, payload, done)`: a [function](Hooks.md#route-hooks) called right before a response is sent, it could also be an array of functions. * `onResponse(request, reply, done)`: a [function](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. +* `onTimeout(request, reply, done)`: a [function](Hooks.md#ontimeout) called when a request is timed out and the HTTP socket has been hanged up. +* `onError(request, reply, error, done)`: a [function](Hooks.md#onerror) called when an Error is thrown or send to the client by the route handler. * `handler(request, reply)`: the function that will handle this request. The [Fastify server](Server.md) will be bound to `this` when the handler is called. Note: using an arrow function will break the binding of `this`. * `errorHandler(error, request, reply)`: a custom error handler for the scope of the request. Overrides the default error global handler, and anything set by [`setErrorHandler`](Server.md#setErrorHandler), for requests to the route. To access the default handler, you can access `instance.errorHandler`. Note that this will point to fastify's default `errorHandler` only if a plugin hasn't overridden it already. * `validatorCompiler({ schema, method, url, httpPart })`: function that builds schemas for request validations. See the [Validation and Serialization](Validation-and-Serialization.md#schema-validator) documentation. diff --git a/docs/Server.md b/docs/Server.md index f40ffd5153..93dbab5cb7 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -13,6 +13,7 @@ document describes the properties available in that options object. - [connectionTimeout](./Server.md#connectiontimeout) - [keepAliveTimeout](./Server.md#keepalivetimeout) - [maxRequestsPerSocket](./Server.md#maxRequestsPerSocket) +- [requestTimeout](./Server.md#requestTimeout) - [ignoreTrailingSlash](./Server.md#ignoretrailingslash) - [maxParamLength](./Server.md#maxparamlength) - [onProtoPoisoning](./Server.md#onprotopoisoning) @@ -94,6 +95,17 @@ is in use. Also, when `serverFactory` option is specified, this option is ignore + Default: `0` (no limit) + +### `requestTimeout` + +Defines the maximum number of milliseconds for receiving the entire request from the client. +[`server.requestTimeout` property](https://nodejs.org/dist/latest/docs/api/http.html#http_server_requesttimeout) +to understand the effect of this option. Also, when `serverFactory` option is specified, this option is ignored. +It must be set to a non-zero value (e.g. 120 seconds) to protect against potential Denial-of-Service attacks in case the server is deployed without a reverse proxy in front. +> At the time of this writing, only node version greater or equal to 14.11.0 support this option. Check the Node.js documentation for availability in the version you are running. + ++ Default: `0` (no limit) + ### `ignoreTrailingSlash` diff --git a/docs/Serverless.md b/docs/Serverless.md index dd82ff2985..251cb5bbc0 100644 --- a/docs/Serverless.md +++ b/docs/Serverless.md @@ -22,6 +22,7 @@ choice with an additional snippet of code. ### Contents - [AWS Lambda](#aws-lambda) +- [Google Cloud Functions](#google-cloud-functions) - [Google Cloud Run](#google-cloud-run) - [Netlify Lambda](#netlify-lambda) - [Vercel](#vercel) @@ -100,6 +101,128 @@ An example deployable with [claudia.js](https://claudiajs.com/tutorials/serverle - API Gateway does not support streams yet, so you are not able to handle [streams](https://www.fastify.io/docs/latest/Reply/#streams). - API Gateway has a timeout of 29 seconds, so it is important to provide a reply during this time. +## Google Cloud Functions + +### Creation of Fastify instance +```js +const fastify = require("fastify")({ + logger: true // you can also define the level passing an object configuration to logger: {level: 'debug'} +}); +``` + +### Add Custom `contentTypeParser` to Fastify instance + +As explained [in issue #946](https://github.com/fastify/fastify/issues/946#issuecomment-766319521), since the Google Cloud Functions platform parses the body of the request before it arrives into Fastify instance, troubling the body request in case of `POST` and `PATCH` methods, you need to add a custom [`ContentTypeParser`](https://www.fastify.io/docs/latest/ContentTypeParser/) to mitigate this behavior. + +```js +fastify.addContentTypeParser('application/json', {}, (req, body, done) => { + done(null, body.body); +}); +``` + +### Define your endpoint (examples) + +A simple `GET` endpoint: +```js +fastify.get('/', async (request, reply) => { + reply.send({message: 'Hello World!'}) +}) +``` + +Or a more complete `POST` endpoint with schema validation: +```js +fastify.route({ + method: 'POST', + url: '/hello', + schema: { + body: { + type: 'object', + properties: { + name: { type: 'string'} + }, + required: ['name'] + }, + response: { + 200: { + type: 'object', + properties: { + message: {type: 'string'} + } + } + }, + }, + handler: async (request, reply) => { + const { name } = request.body; + reply.code(200).send({ + message: `Hello ${name}!` + }) + } +}) +``` + +### Implement and export the function + +Final step, implement the function to handle the request and pass it to Fastify by emitting `request` event to `fastify.server`: + +```js +const fastifyFunction = async (request, reply) => { + await fastify.ready(); + fastify.server.emit('request', request, reply) +} + +export.fastifyFunction = fastifyFunction; +``` + +### Local test + +Install [Google Functions Framework for Node.js](https://github.com/GoogleCloudPlatform/functions-framework-nodejs). + +You can install it globally: +```bash +npm i -g @google-cloud/functions-framework +``` + +Or as a development library: +```bash +npm i --save-dev @google-cloud/functions-framework +``` + +Than you can run your function locally with Functions Framework: +``` bash +npx @google-cloud/functions-framework --target=fastifyFunction +``` + +Or add this command to your `package.json` scripts: +```json +"scripts": { +... +"dev": "npx @google-cloud/functions-framework --target=fastifyFunction" +... +} +``` +and run it with `npm run dev`. + + +### Deploy +```bash +gcloud functions deploy fastifyFunction \ +--runtime nodejs14 --trigger-http --region $GOOGLE_REGION --allow-unauthenticated +``` + +#### Read logs +```bash +gcloud functions logs read +``` + +#### Example request to `/hello` endpoint +```bash +curl -X POST https://$GOOGLE_REGION-$GOOGLE_PROJECT.cloudfunctions.net/me -H "Content-Type: application/json" -d '{ "name": "Fastify" }' +{"message":"Hello Fastify!"} +``` + +### References +- [Google Cloud Functions - Node.js Quickstart ](https://cloud.google.com/functions/docs/quickstart-nodejs) + ## Google Cloud Run Unlike AWS Lambda or Google Cloud Functions, Google Cloud Run is a serverless **container** environment. Its 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. diff --git a/docs/TypeScript.md b/docs/TypeScript.md index 42d321440a..7f2fb5cf83 100644 --- a/docs/TypeScript.md +++ b/docs/TypeScript.md @@ -35,6 +35,9 @@ This example will get you up and running with Fastify and TypeScript. It results } } ``` + +*Note: Set `target` property in `tsconfig.json` to `es2017` or greater to avoid [FastifyDeprecation](https://github.com/fastify/fastify/issues/3284) warning.* + 3. Initialize a TypeScript configuration file: ```bash npx tsc --init diff --git a/docs/Validation-and-Serialization.md b/docs/Validation-and-Serialization.md index 50e25dd8cc..88b4a7c839 100644 --- a/docs/Validation-and-Serialization.md +++ b/docs/Validation-and-Serialization.md @@ -12,6 +12,10 @@ All the examples in this section are using the [JSON Schema Draft 7](https://jso > user-provided schemas. See [Ajv](https://npm.im/ajv) and > [fast-json-stringify](https://npm.im/fast-json-stringify) for more > details. +> +> Moreover, the [`$async` Ajv feature](https://ajv.js.org/guide/async-validation.html) should not be used as part of the first validation strategy. +> This option is used to access Databases and reading them during the validation process may lead to Denial of Service Attacks to your +> application. If you need to run `async` tasks, use [Fastify's hooks](./Hooks.md) instead after validation completes, such as `preHandler`. ### Core concepts @@ -259,7 +263,7 @@ curl -X GET "http://localhost:3000/?ids=1 You can also specify a custom schema validator for each parameter type (body, querystring, params, headers). -For example, the following code disable type cohercion only for the `body` parameters, changing the ajv default options: +For example, the following code disable type coercion only for the `body` parameters, changing the ajv default options: ```js const schemaCompilers = { @@ -644,6 +648,7 @@ fastify.setErrorHandler(function (error, request, reply) { ``` If you want custom error response in schema without headaches and quickly, you can take a look at [`ajv-errors`](https://github.com/epoberezkin/ajv-errors). Check out the [example](https://github.com/fastify/example/blob/HEAD/validation-messages/custom-errors-messages.js) usage. +> Make sure to install version 1.0.1 of `ajv-errors`, because later versions of it are not compatible with AJV v6 (the version shipped by Fastify v3). Below is an example showing how to add **custom error messages for each property** of a schema by supplying custom AJV options. Inline comments in the schema below describe how to configure it to show a different error message for each case: @@ -651,7 +656,10 @@ Inline comments in the schema below describe how to configure it to show a diffe ```js const fastify = Fastify({ ajv: { - customOptions: { allErrors: true }, + customOptions: { + jsonPointers: true, + allErrors: true // Warning: Enabling this option may lead to this security issue https://www.cvedetails.com/cve/CVE-2020-8192/ + }, plugins: [ require('ajv-errors') ] diff --git a/examples/typescript-server.ts b/examples/typescript-server.ts index 549213ac12..31eb3bc317 100644 --- a/examples/typescript-server.ts +++ b/examples/typescript-server.ts @@ -73,6 +73,6 @@ server.get<{ server.listen(8080, (err, address) => { if (err) { console.error(err); - process.exit(0); + process.exit(1); } }); diff --git a/fastify.d.ts b/fastify.d.ts index 3596157089..8c32cdd9e0 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -15,6 +15,8 @@ import { FastifySchemaValidationError } from './types/schema' import { ConstructorAction, ProtoAction } from "./types/content-type-parser"; import { Socket } from 'net' import { Options as FJSOptions } from 'fast-json-stringify' +import { ValidatorCompiler } from '@fastify/ajv-compiler' +import { FastifySerializerCompiler } from './types/schema'; import { FastifySchema } from './types/schema' import { FastifyContextConfig } from './types/context' import { FastifyTypeProvider, FastifyTypeProviderDefault } from './types/type-provider' @@ -106,6 +108,8 @@ export type FastifyServerOptions< ignoreTrailingSlash?: boolean, connectionTimeout?: number, keepAliveTimeout?: number, + maxRequestsPerSocket?: number, + requestTimeout?: number, pluginTimeout?: number, bodyLimit?: number, maxParamLength?: number, @@ -137,6 +141,17 @@ export type FastifyServerOptions< constraints?: { [name: string]: ConstraintStrategy, unknown>, }, + schemaController?: { + bucket?: (parentSchemas?: unknown) => { + addSchema(schema: unknown): FastifyInstance; + getSchema(schemaId: string): unknown; + getSchemas(): Record; + }; + compilersFactory?: { + buildValidator?: ValidatorCompiler; + buildSerializer?: (externalSchemas: unknown, serializerOptsServerOption: FastifyServerOptions["serializerOpts"]) => FastifySerializerCompiler; + }; + }; return503OnClosing?: boolean, ajv?: { customOptions?: AjvOptions, diff --git a/fastify.js b/fastify.js index f8c689cc42..a868a63ffb 100644 --- a/fastify.js +++ b/fastify.js @@ -1,11 +1,11 @@ 'use strict' +const VERSION = '4.0.0-dev' + const Avvio = require('avvio') const http = require('http') const querystring = require('querystring') let lightMyRequest -let version -let versionLoaded = false const { kAvvioBoot, @@ -121,6 +121,7 @@ function fastify (options) { options.connectionTimeout = options.connectionTimeout || defaultInitOptions.connectionTimeout options.keepAliveTimeout = options.keepAliveTimeout || defaultInitOptions.keepAliveTimeout options.maxRequestsPerSocket = options.maxRequestsPerSocket || defaultInitOptions.maxRequestsPerSocket + options.requestTimeout = options.requestTimeout || defaultInitOptions.requestTimeout options.logger = logger options.genReqId = genReqId options.requestIdHeader = requestIdHeader @@ -317,12 +318,7 @@ function fastify (options) { get () { return this[kSchemaController].getSerializerCompiler() } }, version: { - get () { - if (versionLoaded === false) { - version = loadVersion() - } - return version - } + get () { return VERSION } }, errorHandler: { get () { @@ -413,7 +409,7 @@ function fastify (options) { // If the server is not ready yet, this // utility will automatically force it. function inject (opts, cb) { - // lightMyRequest is dynamically laoded as it seems very expensive + // lightMyRequest is dynamically loaded as it seems very expensive // because of Ajv if (lightMyRequest === undefined) { lightMyRequest = require('light-my-request') @@ -642,7 +638,7 @@ function fastify (options) { function setSchemaController (schemaControllerOpts) { throwIfAlreadyStarted('Cannot call "setSchemaController" when fastify instance is already started!') const old = this[kSchemaController] - const schemaController = SchemaController.buildSchemaController(old.parent, Object.assign({}, old.opts, schemaControllerOpts)) + const schemaController = SchemaController.buildSchemaController(old, Object.assign({}, old.opts, schemaControllerOpts)) this[kSchemaController] = schemaController this.getSchema = schemaController.getSchema.bind(schemaController) this.getSchemas = schemaController.getSchemas.bind(schemaController) @@ -698,20 +694,6 @@ function wrapRouting (httpHandler, { rewriteUrl, logger }) { } } -function loadVersion () { - versionLoaded = true - const fs = require('fs') - const path = require('path') - try { - const pkgPath = path.join(__dirname, 'package.json') - fs.accessSync(pkgPath, fs.constants.R_OK) - const pkg = JSON.parse(fs.readFileSync(pkgPath)) - return pkg.name === 'fastify' ? pkg.version : undefined - } catch (e) { - return undefined - } -} - /** * These export configurations enable JS and TS developers * to consumer fastify in whatever way best suits their needs. diff --git a/lib/configValidator.js b/lib/configValidator.js index e21e853750..fb93ea9a85 100644 --- a/lib/configValidator.js +++ b/lib/configValidator.js @@ -3,7 +3,7 @@ "use strict"; module.exports = validate10; module.exports.default = validate10; -const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"type":"string","default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}}; +const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"requestTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"type":"string","default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}}; const func4 = Object.prototype.hasOwnProperty; function validate10(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){ @@ -20,6 +20,9 @@ data.keepAliveTimeout = 72000; if(data.maxRequestsPerSocket === undefined){ data.maxRequestsPerSocket = 0; } +if(data.requestTimeout === undefined){ +data.requestTimeout = 0; +} if(data.bodyLimit === undefined){ data.bodyLimit = 1048576; } @@ -141,7 +144,7 @@ data["maxRequestsPerSocket"] = coerced2; } var valid0 = _errs6 === errors; if(valid0){ -let data3 = data.bodyLimit; +let data3 = data.requestTimeout; const _errs9 = errors; if(!(((typeof data3 == "number") && (!(data3 % 1) && !isNaN(data3))) && (isFinite(data3)))){ let dataType3 = typeof data3; @@ -152,46 +155,44 @@ if(dataType3 === "boolean" || data3 === null coerced3 = +data3; } else { -validate10.errors = [{instancePath:instancePath+"/bodyLimit",schemaPath:"#/properties/bodyLimit/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; +validate10.errors = [{instancePath:instancePath+"/requestTimeout",schemaPath:"#/properties/requestTimeout/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; return false; } } if(coerced3 !== undefined){ data3 = coerced3; if(data !== undefined){ -data["bodyLimit"] = coerced3; +data["requestTimeout"] = coerced3; } } } var valid0 = _errs9 === errors; if(valid0){ -let data4 = data.caseSensitive; +let data4 = data.bodyLimit; const _errs11 = errors; -if(typeof data4 !== "boolean"){ +if(!(((typeof data4 == "number") && (!(data4 % 1) && !isNaN(data4))) && (isFinite(data4)))){ +let dataType4 = typeof data4; let coerced4 = undefined; if(!(coerced4 !== undefined)){ -if(data4 === "false" || data4 === 0 || data4 === null){ -coerced4 = false; -} -else if(data4 === "true" || data4 === 1){ -coerced4 = true; +if(dataType4 === "boolean" || data4 === null + || (dataType4 === "string" && data4 && data4 == +data4 && !(data4 % 1))){ +coerced4 = +data4; } else { -validate10.errors = [{instancePath:instancePath+"/caseSensitive",schemaPath:"#/properties/caseSensitive/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +validate10.errors = [{instancePath:instancePath+"/bodyLimit",schemaPath:"#/properties/bodyLimit/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; return false; } } if(coerced4 !== undefined){ data4 = coerced4; if(data !== undefined){ -data["caseSensitive"] = coerced4; +data["bodyLimit"] = coerced4; } } } var valid0 = _errs11 === errors; if(valid0){ -if(data.http2 !== undefined){ -let data5 = data.http2; +let data5 = data.caseSensitive; const _errs13 = errors; if(typeof data5 !== "boolean"){ let coerced5 = undefined; @@ -203,43 +204,69 @@ else if(data5 === "true" || data5 === 1){ coerced5 = true; } else { -validate10.errors = [{instancePath:instancePath+"/http2",schemaPath:"#/properties/http2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +validate10.errors = [{instancePath:instancePath+"/caseSensitive",schemaPath:"#/properties/caseSensitive/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; return false; } } if(coerced5 !== undefined){ data5 = coerced5; if(data !== undefined){ -data["http2"] = coerced5; +data["caseSensitive"] = coerced5; } } } var valid0 = _errs13 === errors; +if(valid0){ +if(data.http2 !== undefined){ +let data6 = data.http2; +const _errs15 = errors; +if(typeof data6 !== "boolean"){ +let coerced6 = undefined; +if(!(coerced6 !== undefined)){ +if(data6 === "false" || data6 === 0 || data6 === null){ +coerced6 = false; +} +else if(data6 === "true" || data6 === 1){ +coerced6 = true; +} +else { +validate10.errors = [{instancePath:instancePath+"/http2",schemaPath:"#/properties/http2/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +return false; +} +} +if(coerced6 !== undefined){ +data6 = coerced6; +if(data !== undefined){ +data["http2"] = coerced6; +} +} +} +var valid0 = _errs15 === errors; } else { var valid0 = true; } if(valid0){ if(data.https !== undefined){ -let data6 = data.https; -const _errs15 = errors; -const _errs16 = errors; -let valid1 = true; +let data7 = data.https; const _errs17 = errors; const _errs18 = errors; +let valid1 = true; const _errs19 = errors; const _errs20 = errors; +const _errs21 = errors; +const _errs22 = errors; let valid3 = false; let passing0 = null; -const _errs21 = errors; -if(typeof data6 !== "boolean"){ -let coerced6 = undefined; -if(!(coerced6 !== undefined)){ -if(data6 === "false" || data6 === 0 || data6 === null){ -coerced6 = false; +const _errs23 = errors; +if(typeof data7 !== "boolean"){ +let coerced7 = undefined; +if(!(coerced7 !== undefined)){ +if(data7 === "false" || data7 === 0 || data7 === null){ +coerced7 = false; } -else if(data6 === "true" || data6 === 1){ -coerced6 = true; +else if(data7 === "true" || data7 === 1){ +coerced7 = true; } else { const err0 = {}; @@ -252,24 +279,24 @@ vErrors.push(err0); errors++; } } -if(coerced6 !== undefined){ -data6 = coerced6; +if(coerced7 !== undefined){ +data7 = coerced7; if(data !== undefined){ -data["https"] = coerced6; +data["https"] = coerced7; } } } -var _valid1 = _errs21 === errors; +var _valid1 = _errs23 === errors; if(_valid1){ valid3 = true; passing0 = 0; } -const _errs23 = errors; -if(data6 !== null){ -let coerced7 = undefined; -if(!(coerced7 !== undefined)){ -if(data6 === "" || data6 === 0 || data6 === false){ -coerced7 = null; +const _errs25 = errors; +if(data7 !== null){ +let coerced8 = undefined; +if(!(coerced8 !== undefined)){ +if(data7 === "" || data7 === 0 || data7 === false){ +coerced8 = null; } else { const err1 = {}; @@ -282,14 +309,14 @@ vErrors.push(err1); errors++; } } -if(coerced7 !== undefined){ -data6 = coerced7; +if(coerced8 !== undefined){ +data7 = coerced8; if(data !== undefined){ -data["https"] = coerced7; +data["https"] = coerced8; } } } -var _valid1 = _errs23 === errors; +var _valid1 = _errs25 === errors; if(_valid1 && valid3){ valid3 = false; passing0 = [passing0, 1]; @@ -299,11 +326,11 @@ if(_valid1){ valid3 = true; passing0 = 1; } -const _errs25 = errors; -if(errors === _errs25){ -if(data6 && typeof data6 == "object" && !Array.isArray(data6)){ +const _errs27 = errors; +if(errors === _errs27){ +if(data7 && typeof data7 == "object" && !Array.isArray(data7)){ let missing0; -if((data6.allowHTTP1 === undefined) && (missing0 = "allowHTTP1")){ +if((data7.allowHTTP1 === undefined) && (missing0 = "allowHTTP1")){ const err2 = {}; if(vErrors === null){ vErrors = [err2]; @@ -314,23 +341,23 @@ vErrors.push(err2); errors++; } else { -const _errs27 = errors; -for(const key1 in data6){ +const _errs29 = errors; +for(const key1 in data7){ if(!(key1 === "allowHTTP1")){ -delete data6[key1]; +delete data7[key1]; } } -if(_errs27 === errors){ -if(data6.allowHTTP1 !== undefined){ -let data7 = data6.allowHTTP1; -if(typeof data7 !== "boolean"){ -let coerced8 = undefined; -if(!(coerced8 !== undefined)){ -if(data7 === "false" || data7 === 0 || data7 === null){ -coerced8 = false; +if(_errs29 === errors){ +if(data7.allowHTTP1 !== undefined){ +let data8 = data7.allowHTTP1; +if(typeof data8 !== "boolean"){ +let coerced9 = undefined; +if(!(coerced9 !== undefined)){ +if(data8 === "false" || data8 === 0 || data8 === null){ +coerced9 = false; } -else if(data7 === "true" || data7 === 1){ -coerced8 = true; +else if(data8 === "true" || data8 === 1){ +coerced9 = true; } else { const err3 = {}; @@ -343,10 +370,10 @@ vErrors.push(err3); errors++; } } -if(coerced8 !== undefined){ -data7 = coerced8; -if(data6 !== undefined){ -data6["allowHTTP1"] = coerced8; +if(coerced9 !== undefined){ +data8 = coerced9; +if(data7 !== undefined){ +data7["allowHTTP1"] = coerced9; } } } @@ -365,7 +392,7 @@ vErrors.push(err4); errors++; } } -var _valid1 = _errs25 === errors; +var _valid1 = _errs27 === errors; if(_valid1 && valid3){ valid3 = false; passing0 = [passing0, 2]; @@ -388,17 +415,17 @@ vErrors.push(err5); errors++; } else { -errors = _errs20; +errors = _errs22; if(vErrors !== null){ -if(_errs20){ -vErrors.length = _errs20; +if(_errs22){ +vErrors.length = _errs22; } else { vErrors = null; } } } -var valid2 = _errs19 === errors; +var valid2 = _errs21 === errors; if(valid2){ const err6 = {}; if(vErrors === null){ @@ -410,30 +437,30 @@ vErrors.push(err6); errors++; } else { -errors = _errs18; +errors = _errs20; if(vErrors !== null){ -if(_errs18){ -vErrors.length = _errs18; +if(_errs20){ +vErrors.length = _errs20; } else { vErrors = null; } } } -var _valid0 = _errs17 === errors; -errors = _errs16; +var _valid0 = _errs19 === errors; +errors = _errs18; if(vErrors !== null){ -if(_errs16){ -vErrors.length = _errs16; +if(_errs18){ +vErrors.length = _errs18; } else { vErrors = null; } } if(_valid0){ -const _errs30 = errors; +const _errs32 = errors; data["https"] = true; -var _valid0 = _errs30 === errors; +var _valid0 = _errs32 === errors; valid1 = _valid0; } if(!valid1){ @@ -448,38 +475,13 @@ errors++; validate10.errors = vErrors; return false; } -var valid0 = _errs15 === errors; +var valid0 = _errs17 === errors; } else { var valid0 = true; } if(valid0){ -let data8 = data.ignoreTrailingSlash; -const _errs31 = errors; -if(typeof data8 !== "boolean"){ -let coerced9 = undefined; -if(!(coerced9 !== undefined)){ -if(data8 === "false" || data8 === 0 || data8 === null){ -coerced9 = false; -} -else if(data8 === "true" || data8 === 1){ -coerced9 = true; -} -else { -validate10.errors = [{instancePath:instancePath+"/ignoreTrailingSlash",schemaPath:"#/properties/ignoreTrailingSlash/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; -return false; -} -} -if(coerced9 !== undefined){ -data8 = coerced9; -if(data !== undefined){ -data["ignoreTrailingSlash"] = coerced9; -} -} -} -var valid0 = _errs31 === errors; -if(valid0){ -let data9 = data.disableRequestLogging; +let data9 = data.ignoreTrailingSlash; const _errs33 = errors; if(typeof data9 !== "boolean"){ let coerced10 = undefined; @@ -491,20 +493,20 @@ else if(data9 === "true" || data9 === 1){ coerced10 = true; } else { -validate10.errors = [{instancePath:instancePath+"/disableRequestLogging",schemaPath:"#/properties/disableRequestLogging/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +validate10.errors = [{instancePath:instancePath+"/ignoreTrailingSlash",schemaPath:"#/properties/ignoreTrailingSlash/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; return false; } } if(coerced10 !== undefined){ data9 = coerced10; if(data !== undefined){ -data["disableRequestLogging"] = coerced10; +data["ignoreTrailingSlash"] = coerced10; } } } var valid0 = _errs33 === errors; if(valid0){ -let data10 = data.jsonShorthand; +let data10 = data.disableRequestLogging; const _errs35 = errors; if(typeof data10 !== "boolean"){ let coerced11 = undefined; @@ -516,70 +518,69 @@ else if(data10 === "true" || data10 === 1){ coerced11 = true; } else { -validate10.errors = [{instancePath:instancePath+"/jsonShorthand",schemaPath:"#/properties/jsonShorthand/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +validate10.errors = [{instancePath:instancePath+"/disableRequestLogging",schemaPath:"#/properties/disableRequestLogging/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; return false; } } if(coerced11 !== undefined){ data10 = coerced11; if(data !== undefined){ -data["jsonShorthand"] = coerced11; +data["disableRequestLogging"] = coerced11; } } } var valid0 = _errs35 === errors; if(valid0){ -let data11 = data.maxParamLength; +let data11 = data.jsonShorthand; const _errs37 = errors; -if(!(((typeof data11 == "number") && (!(data11 % 1) && !isNaN(data11))) && (isFinite(data11)))){ -let dataType12 = typeof data11; +if(typeof data11 !== "boolean"){ let coerced12 = undefined; if(!(coerced12 !== undefined)){ -if(dataType12 === "boolean" || data11 === null - || (dataType12 === "string" && data11 && data11 == +data11 && !(data11 % 1))){ -coerced12 = +data11; +if(data11 === "false" || data11 === 0 || data11 === null){ +coerced12 = false; +} +else if(data11 === "true" || data11 === 1){ +coerced12 = true; } else { -validate10.errors = [{instancePath:instancePath+"/maxParamLength",schemaPath:"#/properties/maxParamLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; +validate10.errors = [{instancePath:instancePath+"/jsonShorthand",schemaPath:"#/properties/jsonShorthand/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; return false; } } if(coerced12 !== undefined){ data11 = coerced12; if(data !== undefined){ -data["maxParamLength"] = coerced12; +data["jsonShorthand"] = coerced12; } } } var valid0 = _errs37 === errors; if(valid0){ -let data12 = data.onProtoPoisoning; +let data12 = data.maxParamLength; const _errs39 = errors; -if(typeof data12 !== "string"){ +if(!(((typeof data12 == "number") && (!(data12 % 1) && !isNaN(data12))) && (isFinite(data12)))){ let dataType13 = typeof data12; let coerced13 = undefined; if(!(coerced13 !== undefined)){ -if(dataType13 == "number" || dataType13 == "boolean"){ -coerced13 = "" + data12; -} -else if(data12 === null){ -coerced13 = ""; +if(dataType13 === "boolean" || data12 === null + || (dataType13 === "string" && data12 && data12 == +data12 && !(data12 % 1))){ +coerced13 = +data12; } else { -validate10.errors = [{instancePath:instancePath+"/onProtoPoisoning",schemaPath:"#/properties/onProtoPoisoning/type",keyword:"type",params:{type: "string"},message:"must be string"}]; +validate10.errors = [{instancePath:instancePath+"/maxParamLength",schemaPath:"#/properties/maxParamLength/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; return false; } } if(coerced13 !== undefined){ data12 = coerced13; if(data !== undefined){ -data["onProtoPoisoning"] = coerced13; +data["maxParamLength"] = coerced13; } } } var valid0 = _errs39 === errors; if(valid0){ -let data13 = data.onConstructorPoisoning; +let data13 = data.onProtoPoisoning; const _errs41 = errors; if(typeof data13 !== "string"){ let dataType14 = typeof data13; @@ -592,70 +593,70 @@ else if(data13 === null){ coerced14 = ""; } else { -validate10.errors = [{instancePath:instancePath+"/onConstructorPoisoning",schemaPath:"#/properties/onConstructorPoisoning/type",keyword:"type",params:{type: "string"},message:"must be string"}]; +validate10.errors = [{instancePath:instancePath+"/onProtoPoisoning",schemaPath:"#/properties/onProtoPoisoning/type",keyword:"type",params:{type: "string"},message:"must be string"}]; return false; } } if(coerced14 !== undefined){ data13 = coerced14; if(data !== undefined){ -data["onConstructorPoisoning"] = coerced14; +data["onProtoPoisoning"] = coerced14; } } } var valid0 = _errs41 === errors; if(valid0){ -let data14 = data.pluginTimeout; +let data14 = data.onConstructorPoisoning; const _errs43 = errors; -if(!(((typeof data14 == "number") && (!(data14 % 1) && !isNaN(data14))) && (isFinite(data14)))){ +if(typeof data14 !== "string"){ let dataType15 = typeof data14; let coerced15 = undefined; if(!(coerced15 !== undefined)){ -if(dataType15 === "boolean" || data14 === null - || (dataType15 === "string" && data14 && data14 == +data14 && !(data14 % 1))){ -coerced15 = +data14; +if(dataType15 == "number" || dataType15 == "boolean"){ +coerced15 = "" + data14; +} +else if(data14 === null){ +coerced15 = ""; } else { -validate10.errors = [{instancePath:instancePath+"/pluginTimeout",schemaPath:"#/properties/pluginTimeout/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; +validate10.errors = [{instancePath:instancePath+"/onConstructorPoisoning",schemaPath:"#/properties/onConstructorPoisoning/type",keyword:"type",params:{type: "string"},message:"must be string"}]; return false; } } if(coerced15 !== undefined){ data14 = coerced15; if(data !== undefined){ -data["pluginTimeout"] = coerced15; +data["onConstructorPoisoning"] = coerced15; } } } var valid0 = _errs43 === errors; if(valid0){ -let data15 = data.requestIdHeader; +let data15 = data.pluginTimeout; const _errs45 = errors; -if(typeof data15 !== "string"){ +if(!(((typeof data15 == "number") && (!(data15 % 1) && !isNaN(data15))) && (isFinite(data15)))){ let dataType16 = typeof data15; let coerced16 = undefined; if(!(coerced16 !== undefined)){ -if(dataType16 == "number" || dataType16 == "boolean"){ -coerced16 = "" + data15; -} -else if(data15 === null){ -coerced16 = ""; +if(dataType16 === "boolean" || data15 === null + || (dataType16 === "string" && data15 && data15 == +data15 && !(data15 % 1))){ +coerced16 = +data15; } else { -validate10.errors = [{instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/type",keyword:"type",params:{type: "string"},message:"must be string"}]; +validate10.errors = [{instancePath:instancePath+"/pluginTimeout",schemaPath:"#/properties/pluginTimeout/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; return false; } } if(coerced16 !== undefined){ data15 = coerced16; if(data !== undefined){ -data["requestIdHeader"] = coerced16; +data["pluginTimeout"] = coerced16; } } } var valid0 = _errs45 === errors; if(valid0){ -let data16 = data.requestIdLogLabel; +let data16 = data.requestIdHeader; const _errs47 = errors; if(typeof data16 !== "string"){ let dataType17 = typeof data16; @@ -668,75 +669,101 @@ else if(data16 === null){ coerced17 = ""; } else { -validate10.errors = [{instancePath:instancePath+"/requestIdLogLabel",schemaPath:"#/properties/requestIdLogLabel/type",keyword:"type",params:{type: "string"},message:"must be string"}]; +validate10.errors = [{instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/type",keyword:"type",params:{type: "string"},message:"must be string"}]; return false; } } if(coerced17 !== undefined){ data16 = coerced17; if(data !== undefined){ -data["requestIdLogLabel"] = coerced17; +data["requestIdHeader"] = coerced17; } } } var valid0 = _errs47 === errors; if(valid0){ -let data17 = data.http2SessionTimeout; +let data17 = data.requestIdLogLabel; const _errs49 = errors; -if(!(((typeof data17 == "number") && (!(data17 % 1) && !isNaN(data17))) && (isFinite(data17)))){ +if(typeof data17 !== "string"){ let dataType18 = typeof data17; let coerced18 = undefined; if(!(coerced18 !== undefined)){ -if(dataType18 === "boolean" || data17 === null - || (dataType18 === "string" && data17 && data17 == +data17 && !(data17 % 1))){ -coerced18 = +data17; +if(dataType18 == "number" || dataType18 == "boolean"){ +coerced18 = "" + data17; +} +else if(data17 === null){ +coerced18 = ""; } else { -validate10.errors = [{instancePath:instancePath+"/http2SessionTimeout",schemaPath:"#/properties/http2SessionTimeout/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; +validate10.errors = [{instancePath:instancePath+"/requestIdLogLabel",schemaPath:"#/properties/requestIdLogLabel/type",keyword:"type",params:{type: "string"},message:"must be string"}]; return false; } } if(coerced18 !== undefined){ data17 = coerced18; if(data !== undefined){ -data["http2SessionTimeout"] = coerced18; +data["requestIdLogLabel"] = coerced18; } } } var valid0 = _errs49 === errors; if(valid0){ -let data18 = data.exposeHeadRoutes; +let data18 = data.http2SessionTimeout; const _errs51 = errors; -if(typeof data18 !== "boolean"){ +if(!(((typeof data18 == "number") && (!(data18 % 1) && !isNaN(data18))) && (isFinite(data18)))){ +let dataType19 = typeof data18; let coerced19 = undefined; if(!(coerced19 !== undefined)){ -if(data18 === "false" || data18 === 0 || data18 === null){ -coerced19 = false; -} -else if(data18 === "true" || data18 === 1){ -coerced19 = true; +if(dataType19 === "boolean" || data18 === null + || (dataType19 === "string" && data18 && data18 == +data18 && !(data18 % 1))){ +coerced19 = +data18; } else { -validate10.errors = [{instancePath:instancePath+"/exposeHeadRoutes",schemaPath:"#/properties/exposeHeadRoutes/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +validate10.errors = [{instancePath:instancePath+"/http2SessionTimeout",schemaPath:"#/properties/http2SessionTimeout/type",keyword:"type",params:{type: "integer"},message:"must be integer"}]; return false; } } if(coerced19 !== undefined){ data18 = coerced19; if(data !== undefined){ -data["exposeHeadRoutes"] = coerced19; +data["http2SessionTimeout"] = coerced19; } } } var valid0 = _errs51 === errors; if(valid0){ -if(data.versioning !== undefined){ -let data19 = data.versioning; +let data19 = data.exposeHeadRoutes; const _errs53 = errors; -if(errors === _errs53){ -if(data19 && typeof data19 == "object" && !Array.isArray(data19)){ +if(typeof data19 !== "boolean"){ +let coerced20 = undefined; +if(!(coerced20 !== undefined)){ +if(data19 === "false" || data19 === 0 || data19 === null){ +coerced20 = false; +} +else if(data19 === "true" || data19 === 1){ +coerced20 = true; +} +else { +validate10.errors = [{instancePath:instancePath+"/exposeHeadRoutes",schemaPath:"#/properties/exposeHeadRoutes/type",keyword:"type",params:{type: "boolean"},message:"must be boolean"}]; +return false; +} +} +if(coerced20 !== undefined){ +data19 = coerced20; +if(data !== undefined){ +data["exposeHeadRoutes"] = coerced20; +} +} +} +var valid0 = _errs53 === errors; +if(valid0){ +if(data.versioning !== undefined){ +let data20 = data.versioning; +const _errs55 = errors; +if(errors === _errs55){ +if(data20 && typeof data20 == "object" && !Array.isArray(data20)){ let missing1; -if(((data19.storage === undefined) && (missing1 = "storage")) || ((data19.deriveVersion === undefined) && (missing1 = "deriveVersion"))){ +if(((data20.storage === undefined) && (missing1 = "storage")) || ((data20.deriveVersion === undefined) && (missing1 = "deriveVersion"))){ validate10.errors = [{instancePath:instancePath+"/versioning",schemaPath:"#/properties/versioning/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}]; return false; } @@ -746,49 +773,49 @@ validate10.errors = [{instancePath:instancePath+"/versioning",schemaPath:"#/prop return false; } } -var valid0 = _errs53 === errors; +var valid0 = _errs55 === errors; } else { var valid0 = true; } if(valid0){ if(data.constraints !== undefined){ -let data20 = data.constraints; -const _errs56 = errors; -if(errors === _errs56){ -if(data20 && typeof data20 == "object" && !Array.isArray(data20)){ -for(const key2 in data20){ -let data21 = data20[key2]; -const _errs59 = errors; -if(errors === _errs59){ +let data21 = data.constraints; +const _errs58 = errors; +if(errors === _errs58){ if(data21 && typeof data21 == "object" && !Array.isArray(data21)){ +for(const key2 in data21){ +let data22 = data21[key2]; +const _errs61 = errors; +if(errors === _errs61){ +if(data22 && typeof data22 == "object" && !Array.isArray(data22)){ let missing2; -if(((((data21.name === undefined) && (missing2 = "name")) || ((data21.storage === undefined) && (missing2 = "storage"))) || ((data21.validate === undefined) && (missing2 = "validate"))) || ((data21.deriveConstraint === undefined) && (missing2 = "deriveConstraint"))){ +if(((((data22.name === undefined) && (missing2 = "name")) || ((data22.storage === undefined) && (missing2 = "storage"))) || ((data22.validate === undefined) && (missing2 = "validate"))) || ((data22.deriveConstraint === undefined) && (missing2 = "deriveConstraint"))){ validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/constraints/additionalProperties/required",keyword:"required",params:{missingProperty: missing2},message:"must have required property '"+missing2+"'"}]; return false; } else { -if(data21.name !== undefined){ -let data22 = data21.name; -if(typeof data22 !== "string"){ -let dataType20 = typeof data22; -let coerced20 = undefined; -if(!(coerced20 !== undefined)){ -if(dataType20 == "number" || dataType20 == "boolean"){ -coerced20 = "" + data22; +if(data22.name !== undefined){ +let data23 = data22.name; +if(typeof data23 !== "string"){ +let dataType21 = typeof data23; +let coerced21 = undefined; +if(!(coerced21 !== undefined)){ +if(dataType21 == "number" || dataType21 == "boolean"){ +coerced21 = "" + data23; } -else if(data22 === null){ -coerced20 = ""; +else if(data23 === null){ +coerced21 = ""; } else { validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/~/g, "~0").replace(/\//g, "~1")+"/name",schemaPath:"#/properties/constraints/additionalProperties/properties/name/type",keyword:"type",params:{type: "string"},message:"must be string"}]; return false; } } -if(coerced20 !== undefined){ -data22 = coerced20; -if(data21 !== undefined){ -data21["name"] = coerced20; +if(coerced21 !== undefined){ +data23 = coerced21; +if(data22 !== undefined){ +data22["name"] = coerced21; } } } @@ -800,7 +827,7 @@ validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/ return false; } } -var valid5 = _errs59 === errors; +var valid5 = _errs61 === errors; if(!valid5){ break; } @@ -811,7 +838,7 @@ validate10.errors = [{instancePath:instancePath+"/constraints",schemaPath:"#/pro return false; } } -var valid0 = _errs56 === errors; +var valid0 = _errs58 === errors; } else { var valid0 = true; @@ -837,6 +864,7 @@ var valid0 = true; } } } +} else { validate10.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}]; return false; @@ -847,4 +875,4 @@ return errors === 0; } -module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"bodyLimit":1048576,"caseSensitive":true,"disableRequestLogging":false,"jsonShorthand":true,"ignoreTrailingSlash":false,"maxParamLength":100,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":"request-id","requestIdLogLabel":"reqId","http2SessionTimeout":72000,"exposeHeadRoutes":true} +module.exports.defaultInitOptions = {"connectionTimeout":0,"keepAliveTimeout":72000,"maxRequestsPerSocket":0,"requestTimeout":0,"bodyLimit":1048576,"caseSensitive":true,"disableRequestLogging":false,"jsonShorthand":true,"ignoreTrailingSlash":false,"maxParamLength":100,"onProtoPoisoning":"error","onConstructorPoisoning":"error","pluginTimeout":10000,"requestIdHeader":"request-id","requestIdLogLabel":"reqId","http2SessionTimeout":72000,"exposeHeadRoutes":true} diff --git a/lib/error-handler.js b/lib/error-handler.js index 1be85a592c..cd6ab2ca05 100644 --- a/lib/error-handler.js +++ b/lib/error-handler.js @@ -46,7 +46,7 @@ function handleError (reply, error, cb) { // In case the error handler throws, we set the next errorHandler so we can error again reply[kReplyNextErrorHandler] = Object.getPrototypeOf(errorHandler) - reply[kReplyHeaders]['content-length'] = undefined + delete reply[kReplyHeaders]['content-length'] const func = errorHandler.func diff --git a/lib/logger.js b/lib/logger.js index 592bb0f433..0a44150506 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -50,10 +50,10 @@ const serializers = { return { method: req.method, url: req.url, - version: req.headers['accept-version'], + version: req.headers && req.headers['accept-version'], hostname: req.hostname, remoteAddress: req.ip, - remotePort: req.socket.remotePort + remotePort: req.socket ? req.socket.remotePort : undefined } }, err: pino.stdSerializers.err, diff --git a/lib/pluginUtils.js b/lib/pluginUtils.js index 3c1712a45f..9b405c34e1 100644 --- a/lib/pluginUtils.js +++ b/lib/pluginUtils.js @@ -114,9 +114,7 @@ function registerPluginName (fn) { function registerPlugin (fn) { registerPluginName.call(this, fn) - if (this.version !== undefined) { - checkVersion.call(this, fn) - } + checkVersion.call(this, fn) checkDecorators.call(this, fn) checkDependencies.call(this, fn) return shouldSkipOverride(fn) diff --git a/lib/reply.js b/lib/reply.js index 7910457ee3..0eec1f3f6a 100644 --- a/lib/reply.js +++ b/lib/reply.js @@ -1,11 +1,13 @@ 'use strict' -const eos = require('readable-stream').finished +const eos = require('stream').finished + const { kFourOhFourContext, kReplyErrorHandlerCalled, kReplyHijacked, kReplyStartTime, + kReplyEndTime, kReplySerializer, kReplySerializerDefault, kReplyIsError, @@ -309,7 +311,7 @@ Reply.prototype.getResponseTime = function () { let responseTime = 0 if (this[kReplyStartTime] !== undefined) { - responseTime = now() - this[kReplyStartTime] + responseTime = (this[kReplyEndTime] || now()) - this[kReplyStartTime] } return responseTime @@ -519,6 +521,7 @@ function setupResponseListeners (reply) { reply[kReplyStartTime] = now() const onResFinished = err => { + reply[kReplyEndTime] = now() reply.raw.removeListener('finish', onResFinished) reply.raw.removeListener('error', onResFinished) @@ -579,6 +582,7 @@ function buildReply (R) { this.request = request this[kReplyHeaders] = {} this[kReplyStartTime] = undefined + this[kReplyEndTime] = undefined this.log = log // eslint-disable-next-line no-var diff --git a/lib/request.js b/lib/request.js index f30e240630..25f478651a 100644 --- a/lib/request.js +++ b/lib/request.js @@ -102,7 +102,9 @@ function buildRequestWithTrustProxy (R, trustProxy) { if (this.headers['x-forwarded-proto']) { return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto']) } - return this.socket.encrypted ? 'https' : 'http' + if (this.socket) { + return this.socket.encrypted ? 'https' : 'http' + } } } }) @@ -152,7 +154,9 @@ Object.defineProperties(Request.prototype, { }, ip: { get () { - return this.socket.remoteAddress + if (this.socket) { + return this.socket.remoteAddress + } } }, hostname: { @@ -162,7 +166,9 @@ Object.defineProperties(Request.prototype, { }, protocol: { get () { - return this.socket.encrypted ? 'https' : 'http' + if (this.socket) { + return this.socket.encrypted ? 'https' : 'http' + } } }, headers: { diff --git a/lib/schema-controller.js b/lib/schema-controller.js index 7f682ce2e3..34821486b1 100644 --- a/lib/schema-controller.js +++ b/lib/schema-controller.js @@ -79,6 +79,14 @@ class SchemaController { return this.serializerCompiler || (this.parent && this.parent.getSerializerCompiler()) } + getSerializerBuilder () { + return this.compilersFactory.buildSerializer || (this.parent && this.parent.getSerializerBuilder()) + } + + getValidatorBuilder () { + return this.compilersFactory.buildValidator || (this.parent && this.parent.getValidatorBuilder()) + } + /** * This method will be called when a validator must be setup. * Do not setup the compiler more than once @@ -89,7 +97,7 @@ class SchemaController { if (isReady) { return } - this.validatorCompiler = this.compilersFactory.buildValidator(this.schemaBucket.getSchemas(), serverOption.ajv) + this.validatorCompiler = this.getValidatorBuilder()(this.schemaBucket.getSchemas(), serverOption.ajv) } /** @@ -102,7 +110,8 @@ class SchemaController { if (isReady) { return } - this.serializerCompiler = this.compilersFactory.buildSerializer(this.schemaBucket.getSchemas(), serverOption.serializerOpts) + + this.serializerCompiler = this.getSerializerBuilder()(this.schemaBucket.getSchemas(), serverOption.serializerOpts) } } diff --git a/lib/server.js b/lib/server.js index d8510af995..adcfd06dd3 100644 --- a/lib/server.js +++ b/lib/server.js @@ -14,20 +14,22 @@ function createServer (options, httpHandler) { let server = null if (options.serverFactory) { server = options.serverFactory(httpHandler, options) - } else if (options.https) { - if (options.http2) { + } else if (options.http2) { + if (options.https) { server = http2().createSecureServer(options.https, httpHandler) - server.on('session', sessionTimeout(options.http2SessionTimeout)) } else { - server = https.createServer(options.https, httpHandler) - server.keepAliveTimeout = options.keepAliveTimeout + server = http2().createServer(httpHandler) } - } else if (options.http2) { - server = http2().createServer(httpHandler) server.on('session', sessionTimeout(options.http2SessionTimeout)) } else { - server = http.createServer(httpHandler) + // this is http1 + if (options.https) { + server = https.createServer(options.https, httpHandler) + } else { + server = http.createServer(httpHandler) + } server.keepAliveTimeout = options.keepAliveTimeout + server.requestTimeout = options.requestTimeout // we treat zero as null // and null is the default setting from nodejs // so we do not pass the option to server diff --git a/lib/symbols.js b/lib/symbols.js index 2a8d4c18be..80bffeb883 100644 --- a/lib/symbols.js +++ b/lib/symbols.js @@ -32,6 +32,8 @@ const keys = { kReplyHijacked: Symbol('fastify.reply.hijacked'), kReplyStartTime: Symbol('fastify.reply.startTime'), kReplyNextErrorHandler: Symbol('fastify.reply.nextErrorHandler'), + kReplyEndTime: Symbol('fastify.reply.endTime'), + kReplyErrorHandlerCalled: Symbol('fastify.reply.errorHandlerCalled'), kReplyIsRunningOnErrorHook: Symbol('fastify.reply.isRunningOnErrorHook'), kSchemaVisited: Symbol('fastify.schemas.visited'), kState: Symbol('fastify.state'), diff --git a/package.json b/package.json index 33e3cbd0cb..0417e02183 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint:fix": "standard --fix", "lint:standard": "standard --verbose | snazzy", "lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts", + "prepublishOnly": "tap --no-check-coverage test/internals/version.test.js", "test": "npm run lint && npm run unit && npm run test:typescript", "test:ci": "npm run lint && npm run unit -- --cov --coverage-report=lcovonly && npm run test:typescript", "test:report": "npm run lint && npm run unit:report && npm run test:typescript", @@ -159,13 +160,13 @@ "serve-static": "^1.14.1", "simple-get": "^4.0.0", "snazzy": "^9.0.0", - "split2": "^3.2.2", - "standard": "^16.0.3", - "tap": "^15.0.9", + "split2": "^4.1.0", + "standard": "^16.0.1", + "tap": "^15.0.5", "tap-mocha-reporter": "^5.0.1", "then-sleep": "^1.0.1", - "tsd": "^0.17.0", - "typescript": "^4.4.2", + "tsd": "^0.19.0", + "typescript": "^4.0.2", "undici": "^4.5.1", "x-xss-protection": "^2.0.0", "yup": "^0.32.9" diff --git a/test/bundler/README.md b/test/bundler/README.md index 1602469874..e4040bfb8b 100644 --- a/test/bundler/README.md +++ b/test/bundler/README.md @@ -1,16 +1,16 @@ # Bundlers test stack -In some cases developers bundle their apps for several targets, eg: serveless applications. +In some cases, developers bundle their apps for several targets such as serverless applications. Even if it's not recommended by Fastify team; we need to ensure we do not break the build process. -Please note this might result in feature behaving differently like the version handling check for plugins. +Please note this might result in features behaving differently, like the version handling check for plugins. ## Test bundlers -The bundler test stack has been set appart than the rest of the Unit testing stack because it's not a +The bundler test stack has been defined separately from the rest of the Unit testing stack because it's not a part of the fastify lib itself. Note that the tests run in CI only on NodeJs LTS version. -Developers does not need to install every bundler to run unit tests. +Developers do not need to install every bundler to run unit tests. -To run the bundler tests you'll need to first install the repository dependencies and after the bundler +To run the bundler tests you will need to install the repository dependencies followed by the bundler stack dependencies. See: ```bash @@ -24,6 +24,6 @@ stack dependencies. See: ## Bundler test development To not break the fastify unit testing stack please name test files like this `*-test.js` and not `*.test.js`, -otherwise it can be catched by unit-test regex of fastify. -Test need to ensure the build process works and the fastify application can be run, +otherwise it will be targeted by the regular expression used for unit tests for fastify. +Tests need to ensure the build process works and the fastify application can be run, no need to go in deep testing unless an issue is raised. diff --git a/test/bundler/webpack/bundler-test.js b/test/bundler/webpack/bundler-test.js index a945917e93..b3145a6163 100644 --- a/test/bundler/webpack/bundler-test.js +++ b/test/bundler/webpack/bundler-test.js @@ -12,13 +12,9 @@ test('Bundled package should work', t => { }) }) -// In the webpack bundle context the fastify package.json is not read -// Because of this the version is set to `undefined`, this makes the plugin -// version check not able to work properly. By then this test shouldn't work -// in non-bundled environment but works in bundled environment -test('Bundled package should work with bad plugin version, undefined version fallback', t => { +test('Bundled package should not work with bad plugin version', t => { t.plan(1) fastifyFailPlugin.ready((err) => { - t.error(err) + t.ok(err) }) }) diff --git a/test/hooks-async.test.js b/test/hooks-async.test.js index a8744a1b02..d830f08363 100644 --- a/test/hooks-async.test.js +++ b/test/hooks-async.test.js @@ -550,15 +550,11 @@ test('preHandler respond with a stream', t => { 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).then(() => { - reply.raw.once('finish', () => { - t.equal(order.shift(), 2) - resolve() - }) - }) + const stream = fs.createReadStream(process.cwd() + '/test/stream.test.js', 'utf8') + reply.raw.once('finish', () => { + t.equal(order.shift(), 2) }) + return reply.send(stream) }) fastify.addHook('preHandler', async (req, reply) => { diff --git a/test/internals/initialConfig.test.js b/test/internals/initialConfig.test.js index b132d59ece..6e36ca261e 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 connectionTimeout: 0, keepAliveTimeout: 72000, maxRequestsPerSocket: 0, + requestTimeout: 0, bodyLimit: 1024 * 1024, caseSensitive: true, disableRequestLogging: false, @@ -252,6 +253,7 @@ test('Should not have issues when passing stream options to Pino.js', t => { connectionTimeout: 0, keepAliveTimeout: 72000, maxRequestsPerSocket: 0, + requestTimeout: 0, bodyLimit: 1024 * 1024, caseSensitive: true, disableRequestLogging: false, diff --git a/test/internals/logger.test.js b/test/internals/logger.test.js index 3b3da210e9..5688b97796 100644 --- a/test/internals/logger.test.js +++ b/test/internals/logger.test.js @@ -112,3 +112,23 @@ test('The logger should error if both stream and file destination are given', t t.equal(err.message, 'Cannot specify both logger.stream and logger.file options') } }) + +test('The serializer prevent fails if the request socket is undefined', t => { + t.plan(1) + + const serialized = loggerUtils.serializers.req({ + method: 'GET', + url: '/', + socket: undefined, + headers: {} + }) + + t.same(serialized, { + method: 'GET', + url: '/', + version: undefined, + hostname: undefined, + remoteAddress: undefined, + remotePort: undefined + }) +}) diff --git a/test/internals/reply.test.js b/test/internals/reply.test.js index 3e645213b7..93ef38a088 100644 --- a/test/internals/reply.test.js +++ b/test/internals/reply.test.js @@ -7,7 +7,7 @@ const http = require('http') const NotFound = require('http-errors').NotFound const EventEmitter = require('events').EventEmitter const Reply = require('../../lib/reply') -const { Writable } = require('readable-stream') +const { Writable } = require('stream') const { kReplyErrorHandlerCalled, kReplyHeaders, @@ -1441,6 +1441,48 @@ test('reply.getResponseTime() should return a number greater than 0 after the ti fastify.inject({ method: 'GET', url: '/' }) }) +test('reply.getResponseTime() should return the time since a request started while inflight', t => { + t.plan(1) + const fastify = require('../..')() + fastify.route({ + method: 'GET', + url: '/', + handler: (req, reply) => { + reply.send('hello world') + } + }) + + fastify.addHook('preValidation', (req, reply, done) => { + t.not(reply.getResponseTime(), reply.getResponseTime()) + done() + }) + + fastify.addHook('onResponse', (req, reply) => { + t.end() + }) + + fastify.inject({ method: 'GET', url: '/' }) +}) + +test('reply.getResponseTime() should return the same value after a request is finished', 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.equal(reply.getResponseTime(), reply.getResponseTime()) + t.end() + }) + + fastify.inject({ method: 'GET', url: '/' }) +}) + test('reply should use the custom serializer', t => { t.plan(4) const fastify = require('../..')() diff --git a/test/internals/request.test.js b/test/internals/request.test.js index 445d3cbda0..44f8e48766 100644 --- a/test/internals/request.test.js +++ b/test/internals/request.test.js @@ -219,3 +219,50 @@ test('Request with trust proxy - plain', t => { const request = new TpRequest('id', 'params', req, 'query', 'log') t.same(request.protocol, 'http') }) + +test('Request with undefined socket', t => { + t.plan(15) + const headers = { + host: 'hostname' + } + const req = { + method: 'GET', + url: '/', + socket: undefined, + headers + } + const request = new Request('id', 'params', req, 'query', 'log') + t.type(request, Request) + t.equal(request.id, 'id') + t.equal(request.params, 'params') + t.same(request.raw, req) + t.equal(request.query, 'query') + t.equal(request.headers, headers) + t.equal(request.log, 'log') + t.equal(request.ip, undefined) + t.equal(request.ips, undefined) + t.equal(request.hostname, 'hostname') + t.same(request.body, null) + t.equal(request.method, 'GET') + t.equal(request.url, '/') + t.equal(request.protocol, undefined) + t.same(request.socket, req.socket) +}) + +test('Request with trust proxy and undefined socket', t => { + t.plan(1) + const headers = { + 'x-forwarded-for': '2.2.2.2, 1.1.1.1', + 'x-forwarded-host': 'example.com' + } + const req = { + method: 'GET', + url: '/', + socket: undefined, + headers + } + + const TpRequest = Request.buildRequest(Request, true) + const request = new TpRequest('id', 'params', req, 'query', 'log') + t.same(request.protocol, undefined) +}) diff --git a/test/internals/version.test.js b/test/internals/version.test.js index dfc73a2753..46730e7541 100644 --- a/test/internals/version.test.js +++ b/test/internals/version.test.js @@ -1,43 +1,15 @@ 'use strict' +const fs = require('fs') +const path = require('path') const t = require('tap') const test = t.test -const proxyquire = require('proxyquire') +const fastify = require('../..')() -test('should output an undefined version in case of package.json not available', t => { - const Fastify = proxyquire('../..', { fs: { accessSync: () => { throw Error('error') } } }) +test('should be the same as package.json', t => { t.plan(1) - const srv = Fastify() - t.equal(srv.version, undefined) -}) - -test('should output an undefined version in case of package.json is not the fastify one', t => { - const Fastify = proxyquire('../..', { fs: { accessSync: () => { }, readFileSync: () => JSON.stringify({ name: 'foo', version: '6.6.6' }) } }) - t.plan(1) - const srv = Fastify() - t.equal(srv.version, undefined) -}) - -test('should skip the version check if the version is undefined', t => { - const Fastify = proxyquire('../..', { fs: { accessSync: () => { }, readFileSync: () => JSON.stringify({ name: 'foo', version: '6.6.6' }) } }) - t.plan(3) - const srv = Fastify() - t.equal(srv.version, undefined) - - plugin[Symbol.for('skip-override')] = false - plugin[Symbol.for('plugin-meta')] = { - name: 'plugin', - fastify: '>=99.0.0' - } - - srv.register(plugin) - srv.ready((err) => { - t.error(err) - t.pass('everything right') - }) + const json = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json')).toString('utf8')) - function plugin (instance, opts, done) { - done() - } + t.equal(fastify.version, json.version) }) diff --git a/test/logger.test.js b/test/logger.test.js index 946bf8df2a..775e7792b8 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -1536,3 +1536,19 @@ test('should create a default logger if provided one is invalid', t => { t.pass() }) + +test('should not throw error when serializing custom req', t => { + t.plan(1) + + const lines = [] + const dest = new stream.Writable({ + write: function (chunk, enc, cb) { + lines.push(JSON.parse(chunk)) + cb() + } + }) + const fastify = Fastify({ logger: { level: 'info', stream: dest } }) + fastify.log.info({ req: {} }) + + t.same(lines[0].req, {}) +}) diff --git a/test/maxRequestsPerSocket.test.js b/test/maxRequestsPerSocket.test.js index eeba0a6d91..c61596401a 100644 --- a/test/maxRequestsPerSocket.test.js +++ b/test/maxRequestsPerSocket.test.js @@ -104,3 +104,13 @@ test('maxRequestsPerSocket should 0', async (t) => { const initialConfig = Fastify().initialConfig t.same(initialConfig.maxRequestsPerSocket, 0) }) + +test('requestTimeout passed to server', t => { + t.plan(2) + + const httpServer = Fastify({ maxRequestsPerSocket: 5 }).server + t.equal(httpServer.maxRequestsPerSocket, 5) + + const httpsServer = Fastify({ maxRequestsPerSocket: 5, https: true }).server + t.equal(httpsServer.maxRequestsPerSocket, 5) +}) diff --git a/test/reply-error.test.js b/test/reply-error.test.js index ae9702c7c0..b67b2a6bf0 100644 --- a/test/reply-error.test.js +++ b/test/reply-error.test.js @@ -6,6 +6,8 @@ const net = require('net') const Fastify = require('..') const statusCodes = require('http').STATUS_CODES const split = require('split2') +const fs = require('fs') +const path = require('path') const codes = Object.keys(statusCodes) codes.forEach(code => { @@ -707,3 +709,29 @@ test('setting content-type on reply object should not hang the server case 3', t t.equal(res.statusCode, 200) }) }) + +test('pipe stream inside error handler should not cause error', t => { + t.plan(3) + const location = path.join(__dirname, '..', 'package.json') + const json = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString('utf8')) + + const fastify = Fastify() + + fastify.setErrorHandler((_error, _request, reply) => { + const stream = fs.createReadStream(location) + reply.code(400).type('application/json; charset=utf-8').send(stream) + }) + + fastify.get('/', (request, reply) => { + throw new Error('This is an error.') + }) + + fastify.inject({ + url: '/', + method: 'GET' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 400) + t.same(JSON.parse(res.payload), json) + }) +}) diff --git a/test/requestTimeout.test.js b/test/requestTimeout.test.js new file mode 100644 index 0000000000..6f62a63bac --- /dev/null +++ b/test/requestTimeout.test.js @@ -0,0 +1,53 @@ +'use strict' + +const http = require('http') +const { test } = require('tap') +const Fastify = require('../fastify') + +test('requestTimeout passed to server', t => { + t.plan(5) + + try { + Fastify({ requestTimeout: 500.1 }) + t.fail('option must be an integer') + } catch (err) { + t.ok(err) + } + + try { + Fastify({ requestTimeout: [] }) + t.fail('option must be an integer') + } catch (err) { + t.ok(err) + } + + const httpServer = Fastify({ requestTimeout: 1000 }).server + t.equal(httpServer.requestTimeout, 1000) + + const httpsServer = Fastify({ requestTimeout: 1000, https: true }).server + t.equal(httpsServer.requestTimeout, 1000) + + const serverFactory = (handler, _) => { + const server = http.createServer((req, res) => { + handler(req, res) + }) + server.requestTimeout = 5000 + return server + } + const customServer = Fastify({ requestTimeout: 4000, serverFactory }).server + t.equal(customServer.requestTimeout, 5000) +}) + +test('requestTimeout should be set', async (t) => { + t.plan(1) + + const initialConfig = Fastify({ requestTimeout: 5000 }).initialConfig + t.same(initialConfig.requestTimeout, 5000) +}) + +test('requestTimeout should 0', async (t) => { + t.plan(1) + + const initialConfig = Fastify().initialConfig + t.same(initialConfig.requestTimeout, 0) +}) diff --git a/test/route-hooks.test.js b/test/route-hooks.test.js index 46cdcc6503..bfa16c42cc 100644 --- a/test/route-hooks.test.js +++ b/test/route-hooks.test.js @@ -2,6 +2,7 @@ const { Readable } = require('stream') const test = require('tap').test +const sget = require('simple-get').concat const Fastify = require('../') process.removeAllListeners('warning') @@ -496,3 +497,57 @@ test('onRequest option should be called before preParsing', t => { t.same(payload, { hello: 'world' }) }) }) + +test('onTimeout on route', t => { + t.plan(4) + const fastify = Fastify({ connectionTimeout: 500 }) + + fastify.get('/timeout', { + handler (request, reply) { }, + onTimeout (request, reply, done) { + t.pass('onTimeout called') + done() + } + }) + + fastify.listen(0, (err, address) => { + t.error(err) + t.teardown(() => fastify.close()) + + sget({ + method: 'GET', + url: `${address}/timeout` + }, (err, response, body) => { + t.type(err, Error) + t.equal(err.message, 'socket hang up') + }) + }) +}) + +test('onError on route', t => { + t.plan(3) + + const fastify = Fastify() + + const err = new Error('kaboom') + + fastify.get('/', + { + onError (request, reply, error, done) { + t.match(error, err) + done() + } + }, + (req, reply) => { + reply.send(err) + }) + + fastify.inject('/', (err, res) => { + t.error(err) + t.same(JSON.parse(res.payload), { + error: 'Internal Server Error', + message: 'kaboom', + statusCode: 500 + }) + }) +}) diff --git a/test/schema-feature.test.js b/test/schema-feature.test.js index 1026222158..1c8f5b6608 100644 --- a/test/schema-feature.test.js +++ b/test/schema-feature.test.js @@ -4,6 +4,7 @@ const { test } = require('tap') const Fastify = require('..') const fp = require('fastify-plugin') const deepClone = require('rfdc')({ circles: true, proto: false }) +const Ajv = require('ajv') const { kSchemaController } = require('../lib/symbols.js') const echoParams = (req, reply) => { reply.send(req.params) } @@ -1294,3 +1295,463 @@ test('setSchemaController per instance', t => { fastify.ready(err => { t.error(err) }) }) + +test('setSchemaController: Inherits correctly parent schemas with a customized validator instance', async t => { + t.plan(5) + const customAjv = new Ajv({ coerceTypes: false }) + const server = Fastify() + const someSchema = { + $id: 'some', + type: 'array', + items: { + type: 'string' + } + } + const errorResponseSchema = { + $id: 'error_response', + type: 'object', + properties: { + statusCode: { + type: 'integer' + }, + message: { + type: 'string' + } + } + } + + server.addSchema(someSchema) + server.addSchema(errorResponseSchema) + + server.register((instance, _, done) => { + instance.setSchemaController({ + compilersFactory: { + buildValidator: function (externalSchemas) { + const schemaKeys = Object.keys(externalSchemas) + t.equal(schemaKeys.length, 2, 'Contains same number of schemas') + t.hasStrict([someSchema, errorResponseSchema], Object.values(externalSchemas), 'Contains expected schemas') + for (const key of schemaKeys) { + if (customAjv.getSchema(key) == null) { + customAjv.addSchema(externalSchemas[key], key) + } + } + return function validatorCompiler ({ schema }) { + return customAjv.compile(schema) + } + } + } + }) + + instance.get( + '/', + { + schema: { + querystring: { + msg: { + $ref: 'some#' + } + }, + response: { + '4xx': { + $ref: 'error_response#' + } + } + } + }, + (req, reply) => { + reply.send({ noop: 'noop' }) + } + ) + + done() + }) + + const res = await server.inject({ + method: 'GET', + url: '/', + query: { + msg: 'string' + } + }) + const json = res.json() + + t.equal(json.message, 'querystring/msg must be array') + t.equal(json.statusCode, 400) + t.equal(res.statusCode, 400, 'Should not coearce the string into array') +}) + +test('setSchemaController: Inherits buildSerializer from parent if not present within the instance', async t => { + t.plan(6) + const customAjv = new Ajv({ coerceTypes: false }) + const someSchema = { + $id: 'some', + type: 'array', + items: { + type: 'string' + } + } + const errorResponseSchema = { + $id: 'error_response', + type: 'object', + properties: { + statusCode: { + type: 'integer' + }, + message: { + type: 'string' + } + } + } + let rootSerializerCalled = 0 + let rootValidatorCalled = 0 + let childValidatorCalled = 0 + const rootBuildSerializer = function (externalSchemas) { + rootSerializerCalled++ + return function serializer () { + return data => { + return JSON.stringify({ + statusCode: data.statusCode, + message: data.message + }) + } + } + } + const rootBuildValidator = function (externalSchemas) { + rootValidatorCalled++ + return function validatorCompiler ({ schema }) { + return customAjv.compile(schema) + } + } + const server = Fastify({ + schemaController: { + compilersFactory: { + buildValidator: rootBuildValidator, + buildSerializer: rootBuildSerializer + } + } + }) + + server.addSchema(someSchema) + server.addSchema(errorResponseSchema) + + server.register((instance, _, done) => { + instance.setSchemaController({ + compilersFactory: { + buildValidator: function (externalSchemas) { + childValidatorCalled++ + const schemaKeys = Object.keys(externalSchemas) + for (const key of schemaKeys) { + if (customAjv.getSchema(key) == null) { + customAjv.addSchema(externalSchemas[key], key) + } + } + return function validatorCompiler ({ schema }) { + return customAjv.compile(schema) + } + } + } + }) + + instance.get( + '/', + { + schema: { + querystring: { + msg: { + $ref: 'some#' + } + }, + response: { + '4xx': { + $ref: 'error_response#' + } + } + } + }, + (req, reply) => { + reply.send({ noop: 'noop' }) + } + ) + + done() + }) + + const res = await server.inject({ + method: 'GET', + url: '/', + query: { + msg: 'string' + } + }) + const json = res.json() + + t.equal(json.statusCode, 400) + t.equal(json.message, 'querystring/msg must be array') + t.equal(rootSerializerCalled, 1, 'Should be called from the child') + t.equal(rootValidatorCalled, 0, 'Should not be called from the child') + t.equal(childValidatorCalled, 1, 'Should be called from the child') + t.equal(res.statusCode, 400, 'Should not coerce the string into array') +}) + +test('setSchemaController: Inherits buildValidator from parent if not present within the instance', async t => { + t.plan(6) + const customAjv = new Ajv({ coerceTypes: false }) + const someSchema = { + $id: 'some', + type: 'array', + items: { + type: 'string' + } + } + const errorResponseSchema = { + $id: 'error_response', + type: 'object', + properties: { + statusCode: { + type: 'integer' + }, + message: { + type: 'string' + } + } + } + let rootSerializerCalled = 0 + let rootValidatorCalled = 0 + let childSerializerCalled = 0 + const rootBuildSerializer = function (externalSchemas) { + rootSerializerCalled++ + return function serializer () { + return data => JSON.stringify(data) + } + } + const rootBuildValidator = function (externalSchemas) { + rootValidatorCalled++ + const schemaKeys = Object.keys(externalSchemas) + for (const key of schemaKeys) { + if (customAjv.getSchema(key) == null) { + customAjv.addSchema(externalSchemas[key], key) + } + } + return function validatorCompiler ({ schema }) { + return customAjv.compile(schema) + } + } + const server = Fastify({ + schemaController: { + compilersFactory: { + buildValidator: rootBuildValidator, + buildSerializer: rootBuildSerializer + } + } + }) + + server.register((instance, _, done) => { + instance.register((subInstance, _, subDone) => { + subInstance.setSchemaController({ + compilersFactory: { + buildSerializer: function (externalSchemas) { + childSerializerCalled++ + return function serializerCompiler () { + return data => { + return JSON.stringify({ + statusCode: data.statusCode, + message: data.message + }) + } + } + } + } + }) + + subInstance.get( + '/', + { + schema: { + querystring: { + msg: { + $ref: 'some#' + } + }, + response: { + '4xx': { + $ref: 'error_response#' + } + } + } + }, + (req, reply) => { + reply.send({ noop: 'noop' }) + } + ) + + subDone() + }) + + done() + }) + + server.addSchema(someSchema) + server.addSchema(errorResponseSchema) + + const res = await server.inject({ + method: 'GET', + url: '/', + query: { + msg: ['string'] + } + }) + const json = res.json() + + t.equal(json.statusCode, 400) + t.equal(json.message, 'querystring/msg must be array') + t.equal(rootSerializerCalled, 0, 'Should be called from the child') + t.equal(rootValidatorCalled, 1, 'Should not be called from the child') + t.equal(childSerializerCalled, 1, 'Should be called from the child') + t.equal(res.statusCode, 400, 'Should not coearce the string into array') +}) + +test('Should throw if not default validator passed', async t => { + t.plan(4) + const customAjv = new Ajv({ coerceTypes: false }) + const someSchema = { + $id: 'some', + type: 'array', + items: { + type: 'string' + } + } + const anotherSchema = { + $id: 'another', + type: 'integer' + } + const plugin = fp(function (pluginInstance, _, pluginDone) { + pluginInstance.setSchemaController({ + compilersFactory: { + buildValidator: function (externalSchemas) { + const schemaKeys = Object.keys(externalSchemas) + t.equal(schemaKeys.length, 2) + t.same(schemaKeys, ['some', 'another']) + + for (const key of schemaKeys) { + if (customAjv.getSchema(key) == null) { + customAjv.addSchema(externalSchemas[key], key) + } + } + return function validatorCompiler ({ schema }) { + return customAjv.compile(schema) + } + } + } + }) + + pluginDone() + }) + const server = Fastify() + + server.addSchema(someSchema) + + server.register((instance, opts, done) => { + instance.addSchema(anotherSchema) + + instance.register(plugin, {}) + + instance.post( + '/', + { + schema: { + query: { + msg: { + $ref: 'some#' + } + }, + headers: { + 'x-another': { + $ref: 'another#' + } + } + } + }, + (req, reply) => { + reply.send({ noop: 'noop' }) + } + ) + + done() + }) + + try { + const res = await server.inject({ + method: 'POST', + url: '/', + query: { + msg: ['string'] + } + }) + + t.equal(res.json().message, 'querystring/msg must be array') + t.equal(res.statusCode, 400, 'Should not coearce the string into array') + } catch (err) { + t.error(err) + } +}) + +test('Should throw if not default validator passed', async t => { + t.plan(2) + const someSchema = { + $id: 'some', + type: 'array', + items: { + type: 'string' + } + } + const anotherSchema = { + $id: 'another', + type: 'integer' + } + + const server = Fastify() + + server.addSchema(someSchema) + + server.register((instance, opts, done) => { + instance.addSchema(anotherSchema) + + instance.post( + '/', + { + schema: { + query: { + msg: { + $ref: 'some#' + } + }, + headers: { + 'x-another': { + $ref: 'another#' + } + } + } + }, + (req, reply) => { + reply.send({ noop: 'noop' }) + } + ) + + done() + }) + + try { + const res = await server.inject({ + method: 'POST', + url: '/', + query: { + msg: ['string'] + } + }) + + t.equal(res.json().message, 'querystring/msg must be array') + t.equal(res.statusCode, 400, 'Should not coearce the string into array') + } catch (err) { + t.error(err) + } +}) diff --git a/test/schema-special-usage.test.js b/test/schema-special-usage.test.js index ba004c17a1..b60a76cff7 100644 --- a/test/schema-special-usage.test.js +++ b/test/schema-special-usage.test.js @@ -84,7 +84,7 @@ test('Ajv6 usage instead of the bundle one', t => { fastify.ready(err => { t.error(err) - t.pass('startup successfull') + t.pass('startup successful') }) }) }) diff --git a/test/stream.test.js b/test/stream.test.js index bad3f652dc..8cd02bbddc 100644 --- a/test/stream.test.js +++ b/test/stream.test.js @@ -155,7 +155,7 @@ test('onSend hook stream should work even if payload is not a proper stream', t t.plan(1) const reply = proxyquire('../lib/reply', { - 'readable-stream': { + stream: { finished: (...args) => { if (args.length === 2) { args[1](new Error('test-error')) } } @@ -196,7 +196,7 @@ test('onSend hook stream should work on payload with "close" ending function', t t.plan(1) const reply = proxyquire('../lib/reply', { - 'readable-stream': { + stream: { finished: (...args) => { if (args.length === 2) { args[1](new Error('test-error')) } } diff --git a/test/types/content-type-parser.test-d.ts b/test/types/content-type-parser.test-d.ts index 4e995084e9..a3f388bc2b 100644 --- a/test/types/content-type-parser.test-d.ts +++ b/test/types/content-type-parser.test-d.ts @@ -1,4 +1,4 @@ -import fastify, { FastifyBodyParser, FastifyContentTypeParser } from '../../fastify' +import fastify, { FastifyBodyParser } from '../../fastify' import { expectError, expectType } from 'tsd' import { IncomingMessage } from 'http' import { FastifyRequest } from '../../types/request' diff --git a/test/types/fastify.test-d.ts b/test/types/fastify.test-d.ts index 5cd9057f66..e17d3c42d7 100644 --- a/test/types/fastify.test-d.ts +++ b/test/types/fastify.test-d.ts @@ -26,9 +26,24 @@ expectType & PromiseLike>>(fastify({ http2: true, http2SessionTimeout: 1000 })) expectType & PromiseLike>>(fastify({ http2: true, https: {}, http2SessionTimeout: 1000 })) expectType(fastify({ http2: true, https: {} }).inject()) +expectType & PromiseLike>>(fastify({ schemaController: {} })) +expectType & PromiseLike>>( + fastify({ + schemaController: { + compilersFactory: {} + } + }) +) expectError(fastify({ http2: false })) // http2 option must be true expectError(fastify({ http2: false })) // http2 option must be true +expectError( + fastify({ + schemaController: { + bucket: () => ({}) // cannot be empty + } + }) +) // light-my-request expectAssignable({ query: '' }) diff --git a/test/types/instance.test-d.ts b/test/types/instance.test-d.ts index 0ee0fe0028..4285c006cb 100644 --- a/test/types/instance.test-d.ts +++ b/test/types/instance.test-d.ts @@ -5,7 +5,7 @@ import fastify, { RawReplyDefaultExpression, RawRequestDefaultExpression } from '../../fastify' -import { expectAssignable, expectError, expectNotAssignable, expectType } from 'tsd' +import { expectAssignable, expectError, expectType } from 'tsd' import { FastifyRequest } from '../../types/request' import { FastifyReply } from '../../types/reply' import { HookHandlerDoneFunction } from '../../types/hooks' @@ -90,6 +90,12 @@ expectAssignable(server.listen('3000', '', (err, address) => {})) expectAssignable(server.listen(3000, (err, address) => {})) expectAssignable(server.listen('3000', (err, address) => {})) +// test listen method callback types +expectAssignable(server.listen('3000', (err, address) => { + expectAssignable(err) + expectAssignable(address) +})) + // test listen method promise expectAssignable>(server.listen(3000)) expectAssignable>(server.listen('3000')) diff --git a/test/types/register.test-d.ts b/test/types/register.test-d.ts index 5de85c1872..ea265d2b58 100644 --- a/test/types/register.test-d.ts +++ b/test/types/register.test-d.ts @@ -1,4 +1,4 @@ -import { expectAssignable, expectError } from 'tsd' +import { expectAssignable, expectError, expectType } from 'tsd' import fastify, { FastifyInstance, FastifyPluginAsync } from '../../fastify' const testPluginOptsAsync: FastifyPluginAsync = async function (_instance, _opts) { } @@ -14,3 +14,15 @@ expectAssignable( testPluginOptsAsync, { prefix: '/example', logLevel: 'info', logSerializers: { key: (value: any) => `${value}` } } ) ) + +expectAssignable( + fastify().register(testPluginOptsAsync, () => { + return {} + }) +) + +expectAssignable( + fastify().register(testPluginOptsAsync, (instance) => { + expectType(instance) + }) +) diff --git a/test/types/request.test-d.ts b/test/types/request.test-d.ts index aa50150c1e..c5f1a2ffdc 100644 --- a/test/types/request.test-d.ts +++ b/test/types/request.test-d.ts @@ -1,5 +1,5 @@ import { expectType } from 'tsd' -import fastify, { RouteHandler, RawRequestDefaultExpression, RequestBodyDefault, RequestGenericInterface } from '../../fastify' +import fastify, { RouteHandler, RawRequestDefaultExpression, RequestBodyDefault, RequestGenericInterface, FastifyContext, ContextConfigDefault, FastifyContextConfig } from '../../fastify' import { RequestParamsDefault, RequestHeadersDefault, RequestQuerystringDefault } from '../../types/utils' import { FastifyLoggerInstance } from '../../types/logger' import { FastifyRequest } from '../../types/request' @@ -50,6 +50,8 @@ const getHandler: RouteHandler = function (request, _reply) { expectType(request.raw) expectType(request.body) expectType(request.params) + expectType>(request.context) + expectType(request.context.config) expectType(request.headers) request.headers = {} @@ -72,6 +74,8 @@ const postHandler: Handler = function (request) { expectType(request.params.id) expectType(request.headers['x-foobar']) expectType(request.server) + expectType>(request.context) + expectType(request.context.config) } function putHandler (request: CustomRequest, reply: FastifyReply) { @@ -84,6 +88,8 @@ function putHandler (request: CustomRequest, reply: FastifyReply) { expectType(request.params.id) expectType(request.headers['x-foobar']) expectType(request.server) + expectType>(request.context) + expectType(request.context.config) } const server = fastify() diff --git a/test/types/route.test-d.ts b/test/types/route.test-d.ts index 56b328b103..4a6cf664c6 100644 --- a/test/types/route.test-d.ts +++ b/test/types/route.test-d.ts @@ -1,5 +1,5 @@ import fastify, { FastifyInstance, FastifyRequest, FastifyReply, RouteHandlerMethod } from '../../fastify' -import { expectType, expectError, expectAssignable } from 'tsd' +import { expectType, expectError, expectAssignable, printType } from 'tsd' import { HTTPMethods } from '../../types/utils' import * as http from 'http' import { RequestPayload } from '../../types/hooks' @@ -65,6 +65,9 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) + expectType(req.context.config.extra) expectType(res.context.config.foo) expectType(res.context.config.bar) expectType(res.context.config.extra) @@ -80,6 +83,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -88,6 +93,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) expectType(payload) @@ -99,6 +106,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -107,6 +116,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -115,6 +126,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) expectType(res.statusCode) @@ -124,6 +137,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -132,6 +147,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -140,6 +157,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) }, @@ -148,6 +167,8 @@ type LowerCaseHTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' expectType(req.query) expectType(req.params) expectType(req.headers) + expectType(req.context.config.foo) + expectType(req.context.config.bar) expectType(res.context.config.foo) expectType(res.context.config.bar) } diff --git a/test/types/type-provider.test-d.ts b/test/types/type-provider.test-d.ts index 80d47a3fe0..c69d2bf5ae 100644 --- a/test/types/type-provider.test-d.ts +++ b/test/types/type-provider.test-d.ts @@ -1,5 +1,5 @@ import fastify, { FastifyTypeProvider } from '../../fastify' -import { expectAssignable, expectType } from 'tsd' +import { expectAssignable, expectType, printType } from 'tsd' import { IncomingHttpHeaders } from 'http' import { Type, TSchema, Static } from '@sinclair/typebox' import { FromSchema, JSONSchema } from 'json-schema-to-ts' diff --git a/types/instance.d.ts b/types/instance.d.ts index eaec08230b..58c964f1e3 100644 --- a/types/instance.d.ts +++ b/types/instance.d.ts @@ -31,7 +31,7 @@ export interface FastifyInstance< > { server: RawServer; prefix: string; - version: string | undefined; + version: string; log: Logger; withTypeProvider(): FastifyInstance; @@ -76,11 +76,11 @@ export interface FastifyInstance< inject(opts: InjectOptions | string): Promise; inject(): LightMyRequestChain; - listen(port: number | string, address: string, backlog: number, callback: (err: Error, address: string) => void): void; - listen(port: number | string, address: string, callback: (err: Error, address: string) => void): void; - listen(port: number | string, callback: (err: Error, address: string) => void): void; + listen(port: number | string, address: string, backlog: number, callback: (err: Error|null, address: string) => void): void; + listen(port: number | string, address: string, callback: (err: Error|null, address: string) => void): void; + listen(port: number | string, callback: (err: Error|null, address: string) => void): void; listen(port: number | string, address?: string, backlog?: number): Promise; - listen(opts: { port: number; host?: string; backlog?: number }, callback: (err: Error, address: string) => void): void; + listen(opts: { port: number; host?: string; backlog?: number }, callback: (err: Error|null, address: string) => void): void; listen(opts: { port: number; host?: string; backlog?: number }): Promise; ready(): FastifyInstance & PromiseLike; diff --git a/types/request.d.ts b/types/request.d.ts index 591453c06f..15743b6bcb 100644 --- a/types/request.d.ts +++ b/types/request.d.ts @@ -1,9 +1,10 @@ import { FastifyLoggerInstance } from './logger' -import { RawServerBase, RawServerDefault, RawRequestDefaultExpression, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './utils' +import { ContextConfigDefault, RawServerBase, RawServerDefault, RawRequestDefaultExpression, RequestBodyDefault, RequestQuerystringDefault, RequestParamsDefault, RequestHeadersDefault } from './utils' import { RouteGenericInterface } from './route' import { FastifyInstance } from './instance' import { FastifyTypeProvider, FastifyTypeProviderDefault, CallTypeProvider } from './type-provider' import { FastifySchema } from './schema' +import { FastifyContext } from './context' export interface RequestGenericInterface { Body?: RequestBodyDefault; @@ -50,6 +51,7 @@ export interface FastifyRequest< RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, SchemaCompiler extends FastifySchema = FastifySchema, TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, + ContextConfig = ContextConfigDefault, Context extends FastifyRequestContext = ResolveFastifyRequestContext > { id: any; @@ -60,6 +62,7 @@ export interface FastifyRequest< log: FastifyLoggerInstance; server: FastifyInstance; body: Context['body']; + context: FastifyContext; /** in order for this to be used the user should ensure they have set the attachValidation option. */ validationError?: Error & { validation: any; validationContext: string }; diff --git a/types/route.d.ts b/types/route.d.ts index f44af6accf..800a0f840a 100644 --- a/types/route.d.ts +++ b/types/route.d.ts @@ -64,7 +64,7 @@ export type RouteHandlerMethod< TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, > = ( this: FastifyInstance, - request: FastifyRequest, + request: FastifyRequest, reply: FastifyReply ) => void | Promise @@ -134,7 +134,7 @@ export type RouteHandler< TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, > = ( this: FastifyInstance, - request: FastifyRequest, + request: FastifyRequest, reply: FastifyReply ) => void | Promise