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