Comparing version 4.18.0 to 4.19.0
@@ -251,2 +251,4 @@ <h1 align="center">Fastify</h1> | ||
generator & checker. | ||
- [`fastify-better-sqlite3`](https://github.com/punkish/fastify-better-sqlite3) | ||
Plugin for better-sqlite3. | ||
- [`fastify-blipp`](https://github.com/PavelPolyakov/fastify-blipp) Prints your | ||
@@ -262,2 +264,5 @@ routes to the console, so you definitely know which endpoints are available. | ||
to add support for [Bugsnag](https://www.bugsnag.com/) error reporting. | ||
- [`fastify-cacheman`](https://gitlab.com/aalfiann/fastify-cacheman) | ||
Small and efficient cache provider for Node.JS with In-memory, File, Redis | ||
and MongoDB engines for Fastify | ||
- [`fastify-casbin`](https://github.com/nearform/fastify-casbin) Casbin support | ||
@@ -264,0 +269,0 @@ for Fastify. |
@@ -31,2 +31,3 @@ <h1 align="center">Serverless</h1> | ||
- [Netlify Lambda](#netlify-lambda) | ||
- [Platformatic Cloud](#platformatic) | ||
- [Vercel](#vercel) | ||
@@ -457,3 +458,44 @@ | ||
## Platformatic Cloud | ||
[Platformatic](https://platformatic.dev) provides zero-configuration deployment | ||
for Node.js applications. | ||
To use it now, you should wrap your existing Fastify application inside a | ||
[Platformatic Service](https://oss.platformatic.dev/docs/reference/service/introduction), | ||
by running the following: | ||
```bash | ||
npm create platformatic@latest -- service | ||
``` | ||
The wizard would ask you to fill in a few answers: | ||
``` | ||
? Where would you like to create your project? . | ||
? Do you want to run npm install? yes | ||
? Do you want to use TypeScript? no | ||
? What port do you want to use? 3042 | ||
[13:04:14] INFO: Configuration file platformatic.service.json successfully created. | ||
[13:04:14] INFO: Environment file .env successfully created. | ||
[13:04:14] INFO: Plugins folder "plugins" successfully created. | ||
[13:04:14] INFO: Routes folder "routes" successfully created. | ||
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no | ||
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no | ||
``` | ||
Then, head to [Platformatic Cloud](https://platformatic.cloud) and sign in | ||
with your GitHub account. | ||
Create your first application and a static workspace: be careful to download the | ||
API key as an env file, e.g. `yourworkspace.txt`. | ||
Then, you can easily deploy your application with the following commmand: | ||
```bash | ||
platformatic deploy --keys `yuourworkspace.txt` | ||
``` | ||
Check out the [Full Guide](https://blog.platformatic.dev/how-to-migrate-a-fastify-app-to-platformatic-service) | ||
on how to wrap Fastify application in Platformatic. | ||
## Vercel | ||
@@ -511,2 +553,2 @@ | ||
export default routes; | ||
``` | ||
``` |
@@ -30,2 +30,4 @@ <h1 align="center">Fastify</h1> | ||
- `url` - the URL of the incoming request | ||
- `originalUrl` - similar to `url`, this allows you to access the | ||
original `url` in case of internal re-routing | ||
- `routerMethod` - the method defined for the router that is handling the | ||
@@ -32,0 +34,0 @@ request |
@@ -115,2 +115,7 @@ <h1 align="center">Fastify</h1> | ||
version of the endpoint. [Example](#version-constraints). | ||
* `constraints`: defines route restrictions based on request properties or | ||
values, enabling customized matching using | ||
[find-my-way](https://github.com/delvedor/find-my-way) constraints. Includes | ||
built-in `version` and `host` constraints, with support for custom constraint | ||
strategies. | ||
* `prefixTrailingSlash`: string used to determine how to handle passing `/` as a | ||
@@ -117,0 +122,0 @@ route with a prefix. |
@@ -530,4 +530,4 @@ <h1 align="center">Fastify</h1> | ||
Function for generating the request-id. It will receive the incoming request as | ||
a parameter. This function is expected to be error-free. | ||
Function for generating the request-id. It will receive the _raw_ incoming | ||
request as a parameter. This function is expected to be error-free. | ||
@@ -860,2 +860,5 @@ + Default: `value of 'request-id' header if provided or monotonically increasing | ||
>__Warning__: If utilized improperly, certain Fastify features could be disrupted. | ||
>It is recommended to only use it for attaching listeners. | ||
#### after | ||
@@ -862,0 +865,0 @@ <a id="after"></a> |
@@ -14,5 +14,5 @@ <h1 align="center">Fastify</h1> | ||
your project. Each provider uses a different inference library under the hood; | ||
allowing you to select the library most appropriate for your needs. Type | ||
allowing you to select the library most appropriate for your needs. Official Type | ||
Provider packages follow a `@fastify/type-provider-{provider-name}` naming | ||
convention. | ||
convention, and there are several community ones available as well. | ||
@@ -24,2 +24,3 @@ The following inference packages are supported: | ||
- `typebox` - [github](https://github.com/sinclairzx81/typebox) | ||
- `zod` - [github](https://github.com/colinhacks/zod) | ||
@@ -96,2 +97,8 @@ ### Json Schema to Ts | ||
### Zod | ||
See [official documentation](https://github.com/turkerdev/fastify-type-provider-zod) | ||
for Zod type provider instructions. | ||
### Scoped Type-Provider | ||
@@ -98,0 +105,0 @@ |
@@ -114,3 +114,3 @@ <h1 align="center">Fastify</h1> | ||
1. If you did not complete the previous example, follow steps 1-4 to get set up. | ||
2. Inside `index.ts`, define two interfaces `IQuerystring` and `IHeaders`: | ||
2. Inside `index.ts`, define three interfaces `IQuerystring`,`IHeaders` and `IReply`: | ||
```typescript | ||
@@ -125,4 +125,10 @@ interface IQuerystring { | ||
} | ||
interface IReply { | ||
200: { success: boolean }; | ||
302: { url: string }; | ||
'4xx': { error: string }; | ||
} | ||
``` | ||
3. Using the two interfaces, define a new API route and pass them as generics. | ||
3. Using the three interfaces, define a new API route and pass them as generics. | ||
The shorthand route methods (i.e. `.get`) accept a generic object | ||
@@ -137,3 +143,4 @@ `RouteGenericInterface` containing five named properties: `Body`, | ||
Querystring: IQuerystring, | ||
Headers: IHeaders | ||
Headers: IHeaders, | ||
Reply: IReply | ||
}>('/auth', async (request, reply) => { | ||
@@ -144,2 +151,9 @@ const { username, password } = request.query | ||
// chaining .statusCode/.code calls with .send allows type narrowing. For example: | ||
// this works | ||
reply.code(200).send({ success: true }); | ||
// but this gives a type error | ||
reply.code(200).send('uh-oh'); | ||
// it even works for wildcards | ||
reply.code(404).send({ error: 'Not found' }); | ||
return `logged in!` | ||
@@ -161,3 +175,4 @@ }) | ||
Querystring: IQuerystring, | ||
Headers: IHeaders | ||
Headers: IHeaders, | ||
Reply: IReply | ||
}>('/auth', { | ||
@@ -180,3 +195,3 @@ preValidation: (request, reply, done) => { | ||
🎉 Good work, now you can define interfaces for each route and have strictly | ||
typed request and reply instances. Other parts of the Fastify type system rely | ||
typed request and reply instances. Other parts of the Fastify type system rely | ||
on generic properties. Make sure to reference the detailed type system | ||
@@ -183,0 +198,0 @@ documentation below to learn more about what is available. |
@@ -112,3 +112,3 @@ import * as http from 'http' | ||
jsonShorthand?: boolean; | ||
genReqId?: <RequestGeneric extends RequestGenericInterface = RequestGenericInterface, TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault>(req: FastifyRequest<RequestGeneric, RawServer, RawRequestDefaultExpression<RawServer>, FastifySchema, TypeProvider>) => string, | ||
genReqId?: (req: RawRequestDefaultExpression<RawServer>) => string, | ||
trustProxy?: boolean | string | string[] | number | TrustProxyFunction, | ||
@@ -115,0 +115,0 @@ querystringParser?: (str: string) => { [key: string]: unknown }, |
'use strict' | ||
const VERSION = '4.18.0' | ||
const VERSION = '4.19.0' | ||
@@ -429,6 +429,9 @@ const Avvio = require('avvio') | ||
hookRunnerApplication('preClose', fastify[kAvvioBoot], fastify, function () { | ||
// No new TCP connections are accepted. | ||
// We must call close on the server even if we are not listening | ||
// otherwise memory will be leaked. | ||
// https://github.com/nodejs/node/issues/48604 | ||
instance.server.close(done) | ||
if (fastify[kState].listening) { | ||
// No new TCP connections are accepted | ||
instance.server.close(done) | ||
/* istanbul ignore next: Cannot test this without Node.js core support */ | ||
@@ -657,2 +660,7 @@ if (forceCloseConnections === 'idle') { | ||
errorLabel = 'timeout' | ||
} else if (err.code === 'HPE_HEADER_OVERFLOW') { | ||
errorCode = '431' | ||
errorStatus = http.STATUS_CODES[errorCode] | ||
body = `{"error":"${errorStatus}","message":"Exceeded maximum allowed HTTP header size","statusCode":431}` | ||
errorLabel = 'header_overflow' | ||
} else { | ||
@@ -697,6 +705,9 @@ errorCode = '400' | ||
childLogger.info({ req }, 'incoming request') | ||
const request = new Request(id, null, req, null, childLogger, onBadUrlContext) | ||
const reply = new Reply(res, request, childLogger) | ||
if (disableRequestLogging === false) { | ||
childLogger.info({ req: request }, 'incoming request') | ||
} | ||
return frameworkErrors(new FST_ERR_BAD_URL(path), request, reply) | ||
@@ -720,6 +731,9 @@ } | ||
childLogger.info({ req }, 'incoming request') | ||
const request = new Request(id, null, req, null, childLogger, onBadUrlContext) | ||
const reply = new Reply(res, request, childLogger) | ||
if (disableRequestLogging === false) { | ||
childLogger.info({ req: request }, 'incoming request') | ||
} | ||
return frameworkErrors(new FST_ERR_ASYNC_CONSTRAINT(), request, reply) | ||
@@ -800,2 +814,3 @@ } | ||
if (rewriteUrl) { | ||
req.originalUrl = req.url | ||
const url = rewriteUrl.call(fastify, req) | ||
@@ -802,0 +817,0 @@ if (typeof url === 'string') { |
@@ -284,3 +284,3 @@ 'use strict' | ||
function defaultJsonParser (req, body, done) { | ||
if (body === '' || body == null) { | ||
if (body === '' || body == null || (Buffer.isBuffer(body) && body.length === 0)) { | ||
return done(new FST_ERR_CTP_EMPTY_JSON_BODY(), undefined) | ||
@@ -287,0 +287,0 @@ } |
@@ -360,3 +360,3 @@ 'use strict' | ||
// We create a WeakMap to compile the schema only once | ||
// Its done leazily to avoid add overhead by creating the WeakMap | ||
// Its done lazily to avoid add overhead by creating the WeakMap | ||
// if it is not used | ||
@@ -863,3 +863,3 @@ // TODO: Explore a central cache for all the schemas shared across | ||
* @param {number} statusCode the http status code | ||
* @param {string} contentType the reply content type | ||
* @param {string} [contentType] the reply content type | ||
* @returns {string} the serialized payload | ||
@@ -866,0 +866,0 @@ */ |
@@ -16,3 +16,4 @@ 'use strict' | ||
kRouteContext, | ||
kPublicRouteContext | ||
kPublicRouteContext, | ||
kRequestOriginalUrl | ||
} = require('./symbols') | ||
@@ -153,2 +154,11 @@ const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION } = require('./errors') | ||
}, | ||
originalUrl: { | ||
get () { | ||
/* istanbul ignore else */ | ||
if (!this[kRequestOriginalUrl]) { | ||
this[kRequestOriginalUrl] = this.raw.originalUrl || this.raw.url | ||
} | ||
return this[kRequestOriginalUrl] | ||
} | ||
}, | ||
method: { | ||
@@ -167,3 +177,3 @@ get () { | ||
get () { | ||
return this[kRouteContext].config.url | ||
return this[kRouteContext].config?.url | ||
} | ||
@@ -178,4 +188,4 @@ }, | ||
const options = { | ||
method: context.config.method, | ||
url: context.config.url, | ||
method: context.config?.method, | ||
url: context.config?.url, | ||
bodyLimit: (routeLimit || serverLimit), | ||
@@ -193,3 +203,3 @@ attachValidation: context.attachValidation, | ||
get () { | ||
return this[kRouteContext].config.method | ||
return this[kRouteContext].config?.method | ||
} | ||
@@ -199,3 +209,3 @@ }, | ||
get () { | ||
return this[kRouteContext][kPublicRouteContext].config | ||
return this[kRouteContext][kPublicRouteContext]?.config | ||
} | ||
@@ -210,3 +220,3 @@ }, | ||
get () { | ||
return this[kRouteContext].config.url === undefined | ||
return this[kRouteContext].config?.url === undefined | ||
} | ||
@@ -293,3 +303,3 @@ }, | ||
// We create a WeakMap to compile the schema only once | ||
// Its done leazily to avoid add overhead by creating the WeakMap | ||
// Its done lazily to avoid add overhead by creating the WeakMap | ||
// if it is not used | ||
@@ -296,0 +306,0 @@ // TODO: Explore a central cache for all the schemas shared across |
@@ -471,3 +471,2 @@ 'use strict' | ||
const reply = new context.Reply(res, request, childLogger) | ||
if (disableRequestLogging === false) { | ||
@@ -474,0 +473,0 @@ childLogger.info({ req: request }, 'incoming request') |
@@ -103,5 +103,5 @@ 'use strict' | ||
* Do not setup the compiler more than once | ||
* @param {object} serverOptions: the fastify server option | ||
* @param {object} serverOptions the fastify server options | ||
*/ | ||
setupValidator (serverOption) { | ||
setupValidator (serverOptions) { | ||
const isReady = this.validatorCompiler !== undefined && !this.addedSchemas | ||
@@ -111,3 +111,3 @@ if (isReady) { | ||
} | ||
this.validatorCompiler = this.getValidatorBuilder()(this.schemaBucket.getSchemas(), serverOption.ajv) | ||
this.validatorCompiler = this.getValidatorBuilder()(this.schemaBucket.getSchemas(), serverOptions.ajv) | ||
} | ||
@@ -118,5 +118,5 @@ | ||
* Do not setup the compiler more than once | ||
* @param {object} serverOptions: the fastify server option | ||
* @param {object} serverOptions the fastify server options | ||
*/ | ||
setupSerializer (serverOption) { | ||
setupSerializer (serverOptions) { | ||
const isReady = this.serializerCompiler !== undefined && !this.addedSchemas | ||
@@ -127,3 +127,3 @@ if (isReady) { | ||
this.serializerCompiler = this.getSerializerBuilder()(this.schemaBucket.getSchemas(), serverOption.serializerOpts) | ||
this.serializerCompiler = this.getSerializerBuilder()(this.schemaBucket.getSchemas(), serverOptions.serializerOpts) | ||
} | ||
@@ -130,0 +130,0 @@ } |
@@ -150,4 +150,4 @@ 'use strict' | ||
* @param {number} statusCode the http status code | ||
* @param {string} contentType the reply content type | ||
* @returns {function|boolean} the right JSON Schema function to serialize | ||
* @param {string} [contentType] the reply content type | ||
* @returns {function|false} the right JSON Schema function to serialize | ||
* the reply or false if it is not set | ||
@@ -154,0 +154,0 @@ */ |
@@ -64,3 +64,2 @@ 'use strict' | ||
} | ||
if (host === 'localhost') { | ||
@@ -119,37 +118,64 @@ listenOptions.cb = (err, address) => { | ||
const isMainServerListening = mainServer.listening && serverOpts.serverFactory | ||
let binding = 0 | ||
let binded = 0 | ||
const primaryAddress = mainServer.address() | ||
for (const adr of addresses) { | ||
if (adr.address !== primaryAddress.address) { | ||
binding++ | ||
const secondaryOpts = Object.assign({}, listenOptions, { | ||
host: adr.address, | ||
port: primaryAddress.port, | ||
cb: (_ignoreErr) => { | ||
binded++ | ||
if (!isMainServerListening) { | ||
const primaryAddress = mainServer.address() | ||
for (const adr of addresses) { | ||
if (adr.address !== primaryAddress.address) { | ||
binding++ | ||
const secondaryOpts = Object.assign({}, listenOptions, { | ||
host: adr.address, | ||
port: primaryAddress.port, | ||
cb: (_ignoreErr) => { | ||
binded++ | ||
/* istanbul ignore next: the else won't be taken unless listening fails */ | ||
if (!_ignoreErr) { | ||
this[kServerBindings].push(secondaryServer) | ||
} | ||
/* istanbul ignore next: the else won't be taken unless listening fails */ | ||
if (!_ignoreErr) { | ||
this[kServerBindings].push(secondaryServer) | ||
// Due to the nature of the feature is not possible to know | ||
// ahead of time the number of bindings that will be made. | ||
// For instance, each binding is hooked to be closed at their own | ||
// pace through the `onClose` hook. | ||
// It also allows them to handle possible connections already | ||
// attached to them if any. | ||
this.onClose((instance, done) => { | ||
if (instance[kState].listening) { | ||
// No new TCP connections are accepted | ||
// We swallow any error from the secondary | ||
// server | ||
secondaryServer.close(() => done()) | ||
/* istanbul ignore next: Cannot test this without Node.js core support */ | ||
if (serverOpts.forceCloseConnections === 'idle') { | ||
// Not needed in Node 19 | ||
secondaryServer.closeIdleConnections() | ||
/* istanbul ignore next: Cannot test this without Node.js core support */ | ||
} else if (typeof secondaryServer.closeAllConnections === 'function' && serverOpts.forceCloseConnections) { | ||
secondaryServer.closeAllConnections() | ||
} | ||
} else { | ||
done() | ||
} | ||
}) | ||
} | ||
if (binded === binding) { | ||
// regardless of the error, we are done | ||
onListen() | ||
if (binded === binding) { | ||
// regardless of the error, we are done | ||
onListen() | ||
} | ||
} | ||
} | ||
}) | ||
}) | ||
const secondaryServer = getServerInstance(serverOpts, httpHandler) | ||
const closeSecondary = () => { secondaryServer.close(() => {}) } | ||
secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade')) | ||
mainServer.on('unref', closeSecondary) | ||
mainServer.on('close', closeSecondary) | ||
mainServer.on('error', closeSecondary) | ||
this[kState].listening = false | ||
listenCallback.call(this, secondaryServer, secondaryOpts)() | ||
const secondaryServer = getServerInstance(serverOpts, httpHandler) | ||
const closeSecondary = () => { secondaryServer.close(() => { }) } | ||
secondaryServer.on('upgrade', mainServer.emit.bind(mainServer, 'upgrade')) | ||
mainServer.on('unref', closeSecondary) | ||
mainServer.on('close', closeSecondary) | ||
mainServer.on('error', closeSecondary) | ||
this[kState].listening = false | ||
listenCallback.call(this, secondaryServer, secondaryOpts)() | ||
} | ||
} | ||
} | ||
// no extra bindings are necessary | ||
@@ -156,0 +182,0 @@ if (binding === 0) { |
@@ -33,2 +33,3 @@ 'use strict' | ||
kRequestCacheValidateFns: Symbol('fastify.request.cache.validateFns'), | ||
kRequestOriginalUrl: Symbol('fastify.request.originalUrl'), | ||
// 404 | ||
@@ -35,0 +36,0 @@ kFourOhFour: Symbol('fastify.404'), |
{ | ||
"name": "fastify", | ||
"version": "4.18.0", | ||
"version": "4.19.0", | ||
"description": "Fast and low overhead web framework, for Node.js", | ||
@@ -13,3 +13,3 @@ "main": "fastify.js", | ||
"build:validation": "node build/build-error-serializer.js && node build/build-validation.js", | ||
"coverage": "NODE_OPTIONS=no-network-family-autoselection npm run unit -- --cov --coverage-report=html", | ||
"coverage": "cross-env NODE_OPTIONS=no-network-family-autoselection npm run unit -- --cov --coverage-report=html", | ||
"coverage:ci": "npm run unit -- --cov --coverage-report=html --no-browser --no-check-coverage", | ||
@@ -23,4 +23,4 @@ "coverage:ci-check-coverage": "nyc check-coverage --branches 100 --functions 100 --lines 100 --statements 100", | ||
"lint:typescript": "eslint -c types/.eslintrc.json types/**/*.d.ts test/types/**/*.test-d.ts", | ||
"prepublishOnly": "PREPUBLISH=true tap --no-check-coverage test/build/**.test.js && npm run test:validator:integrity", | ||
"test": "npm run lint && NODE_OPTIONS=no-network-family-autoselection npm run unit && npm run test:typescript", | ||
"prepublishOnly": "cross-env PREPUBLISH=true tap --no-check-coverage test/build/**.test.js && npm run test:validator:integrity", | ||
"test": "npm run lint && cross-env NODE_OPTIONS=no-network-family-autoselection npm run unit && npm run test:typescript", | ||
"test:ci": "npm run unit -- --cov --coverage-report=lcovonly && npm run test:typescript", | ||
@@ -140,3 +140,3 @@ "test:report": "npm run lint && npm run unit:report && npm run test:typescript", | ||
"@sinclair/typebox": "^0.28.9", | ||
"@sinonjs/fake-timers": "^10.0.2", | ||
"@sinonjs/fake-timers": "^11.0.0", | ||
"@types/node": "^20.1.0", | ||
@@ -151,2 +151,3 @@ "@typescript-eslint/eslint-plugin": "^5.59.2", | ||
"branch-comparer": "^1.1.0", | ||
"cross-env": "^7.0.3", | ||
"eslint": "^8.39.0", | ||
@@ -156,3 +157,3 @@ "eslint-config-standard": "^17.0.0", | ||
"eslint-plugin-import": "^2.27.5", | ||
"eslint-plugin-n": "^15.7.0", | ||
"eslint-plugin-n": "^16.0.1", | ||
"eslint-plugin-promise": "^6.1.1", | ||
@@ -169,3 +170,3 @@ "fast-json-body": "^1.1.0", | ||
"license-checker": "^25.0.1", | ||
"markdownlint-cli2": "^0.7.1", | ||
"markdownlint-cli2": "^0.8.1", | ||
"proxyquire": "^2.1.3", | ||
@@ -190,6 +191,6 @@ "pump": "^3.0.0", | ||
"@fastify/fast-json-stringify-compiler": "^4.3.0", | ||
"fast-json-stringify": "^5.7.0", | ||
"abstract-logging": "^2.0.1", | ||
"avvio": "^8.2.1", | ||
"fast-content-type-parse": "^1.0.0", | ||
"fast-json-stringify": "^5.7.0", | ||
"find-my-way": "^7.6.0", | ||
@@ -196,0 +197,0 @@ "light-my-request": "^5.9.1", |
@@ -92,9 +92,12 @@ 'use strict' | ||
instance.request({ path: '/', method: 'GET' }), | ||
instance.request({ path: '/', method: 'GET' }), | ||
instance.request({ path: '/', method: 'GET' }) | ||
]) | ||
t.equal(responses[0].status, 'fulfilled') | ||
t.equal(responses[1].status, 'rejected') | ||
t.equal(responses[1].status, 'fulfilled') | ||
t.equal(responses[2].status, 'rejected') | ||
t.equal(responses[3].status, 'rejected') | ||
await instance.close() | ||
}) |
@@ -5,4 +5,3 @@ 'use strict' | ||
const http = require('http') | ||
const t = require('tap') | ||
const test = t.test | ||
const { test } = require('tap') | ||
const Fastify = require('..') | ||
@@ -209,3 +208,3 @@ const { Client } = require('undici') | ||
const isV19plus = semver.gte(process.version, '19.0.0') | ||
t.test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node >= v19.x', { skip: isV19plus }, t => { | ||
test('Current opened connection should continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node >= v19.x', { skip: isV19plus }, t => { | ||
const fastify = Fastify({ | ||
@@ -248,3 +247,3 @@ return503OnClosing: false, | ||
t.test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node < v19.x', { skip: !isV19plus }, t => { | ||
test('Current opened connection should NOT continue to work after closing and return "connection: close" header - return503OnClosing: false, skip Node < v19.x', { skip: !isV19plus }, t => { | ||
t.plan(4) | ||
@@ -288,3 +287,3 @@ const fastify = Fastify({ | ||
t.test('Current opened connection should not accept new incoming connections', t => { | ||
test('Current opened connection should not accept new incoming connections', t => { | ||
t.plan(3) | ||
@@ -311,3 +310,3 @@ const fastify = Fastify({ forceCloseConnections: false }) | ||
t.test('rejected incoming connections should be logged', t => { | ||
test('rejected incoming connections should be logged', t => { | ||
t.plan(2) | ||
@@ -457,2 +456,145 @@ const stream = split(JSON.parse) | ||
test('triggers on-close hook in the right order with multiple bindings', async t => { | ||
const expectedOrder = [1, 2, 3] | ||
const order = [] | ||
const fastify = Fastify() | ||
t.plan(1) | ||
// Follows LIFO | ||
fastify.addHook('onClose', () => { | ||
order.push(2) | ||
}) | ||
fastify.addHook('onClose', () => { | ||
order.push(1) | ||
}) | ||
await fastify.listen({ port: 0 }) | ||
await new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
fastify.close(err => { | ||
order.push(3) | ||
t.match(order, expectedOrder) | ||
if (err) t.error(err) | ||
else resolve() | ||
}) | ||
}, 2000) | ||
}) | ||
}) | ||
test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - idle)', { skip: noSupport }, async t => { | ||
const expectedPayload = { hello: 'world' } | ||
const timeoutTime = 2 * 60 * 1000 | ||
const expectedOrder = [1, 2] | ||
const order = [] | ||
const fastify = Fastify({ forceCloseConnections: 'idle' }) | ||
fastify.server.setTimeout(timeoutTime) | ||
fastify.server.keepAliveTimeout = timeoutTime | ||
fastify.get('/', async (req, reply) => { | ||
await new Promise((resolve) => { | ||
setTimeout(resolve, 1000) | ||
}) | ||
return expectedPayload | ||
}) | ||
fastify.addHook('onClose', () => { | ||
order.push(1) | ||
}) | ||
await fastify.listen({ port: 0 }) | ||
const addresses = fastify.addresses() | ||
const testPlan = (addresses.length * 2) + 1 | ||
t.plan(testPlan) | ||
for (const addr of addresses) { | ||
const { family, address, port } = addr | ||
const host = family === 'IPv6' ? `[${address}]` : address | ||
const client = new Client(`http://${host}:${port}`, { | ||
keepAliveTimeout: 1 * 60 * 1000 | ||
}) | ||
client.request({ path: '/', method: 'GET' }) | ||
.then((res) => res.body.json(), err => t.error(err)) | ||
.then(json => { | ||
t.match(json, expectedPayload, 'should payload match') | ||
t.notOk(client.closed, 'should client not be closed') | ||
}, err => t.error(err)) | ||
} | ||
await new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
fastify.close(err => { | ||
order.push(2) | ||
t.match(order, expectedOrder) | ||
if (err) t.error(err) | ||
else resolve() | ||
}) | ||
}, 2000) | ||
}) | ||
}) | ||
test('triggers on-close hook in the right order with multiple bindings (forceCloseConnections - true)', { skip: noSupport }, async t => { | ||
const expectedPayload = { hello: 'world' } | ||
const timeoutTime = 2 * 60 * 1000 | ||
const expectedOrder = [1, 2] | ||
const order = [] | ||
const fastify = Fastify({ forceCloseConnections: true }) | ||
fastify.server.setTimeout(timeoutTime) | ||
fastify.server.keepAliveTimeout = timeoutTime | ||
fastify.get('/', async (req, reply) => { | ||
await new Promise((resolve) => { | ||
setTimeout(resolve, 1000) | ||
}) | ||
return expectedPayload | ||
}) | ||
fastify.addHook('onClose', () => { | ||
order.push(1) | ||
}) | ||
await fastify.listen({ port: 0 }) | ||
const addresses = fastify.addresses() | ||
const testPlan = (addresses.length * 2) + 1 | ||
t.plan(testPlan) | ||
for (const addr of addresses) { | ||
const { family, address, port } = addr | ||
const host = family === 'IPv6' ? `[${address}]` : address | ||
const client = new Client(`http://${host}:${port}`, { | ||
keepAliveTimeout: 1 * 60 * 1000 | ||
}) | ||
client.request({ path: '/', method: 'GET' }) | ||
.then((res) => res.body.json(), err => t.error(err)) | ||
.then(json => { | ||
t.match(json, expectedPayload, 'should payload match') | ||
t.notOk(client.closed, 'should client not be closed') | ||
}, err => t.error(err)) | ||
} | ||
await new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
fastify.close(err => { | ||
order.push(2) | ||
t.match(order, expectedOrder) | ||
if (err) t.error(err) | ||
else resolve() | ||
}) | ||
}, 2000) | ||
}) | ||
}) | ||
test('shutsdown while keep-alive connections are active (non-async, custom)', t => { | ||
@@ -459,0 +601,0 @@ t.plan(5) |
@@ -14,6 +14,6 @@ 'use strict' | ||
t.plan(localAddresses.length + 3) | ||
t.plan((localAddresses.length - 1) + 3) | ||
const serverFactory = (handler, opts) => { | ||
t.ok(opts.serverFactory, 'it is called twice for every HOST interface') | ||
t.ok(opts.serverFactory, 'it is called once for localhost') | ||
@@ -75,1 +75,12 @@ const server = http.createServer((req, res) => { | ||
}) | ||
test('Should accept user defined serverFactory and ignore secondary server creation', async t => { | ||
const server = http.createServer(() => {}) | ||
t.teardown(() => new Promise(resolve => server.close(resolve))) | ||
const app = await Fastify({ | ||
serverFactory: () => server | ||
}) | ||
t.resolves(async () => { | ||
await app.listen({ port: 0 }) | ||
}) | ||
}) |
'use strict' | ||
const { Readable } = require('stream') | ||
const { test } = require('tap') | ||
@@ -33,1 +34,42 @@ const Fastify = require('..') | ||
}) | ||
test('Custom genReqId function gets raw request as argument', t => { | ||
t.plan(9) | ||
const REQUEST_ID = 'REQ-1234' | ||
const fastify = Fastify({ | ||
genReqId: function (req) { | ||
t.notOk('id' in req) | ||
t.notOk('raw' in req) | ||
t.ok(req instanceof Readable) | ||
// http.IncomingMessage does have `rawHeaders` property, but FastifyRequest does not | ||
const index = req.rawHeaders.indexOf('x-request-id') | ||
const xReqId = req.rawHeaders[index + 1] | ||
t.equal(xReqId, REQUEST_ID) | ||
t.equal(req.headers['x-request-id'], REQUEST_ID) | ||
return xReqId | ||
} | ||
}) | ||
fastify.get('/', (req, reply) => { | ||
t.equal(req.id, REQUEST_ID) | ||
reply.send({ id: req.id }) | ||
}) | ||
fastify.listen({ port: 0 }, err => { | ||
t.error(err) | ||
fastify.inject({ | ||
method: 'GET', | ||
headers: { | ||
'x-request-id': REQUEST_ID | ||
}, | ||
url: 'http://localhost:' + fastify.server.address().port | ||
}, (err, res) => { | ||
t.error(err) | ||
const payload = JSON.parse(res.payload) | ||
t.equal(payload.id, REQUEST_ID) | ||
fastify.close() | ||
}) | ||
}) | ||
}) |
@@ -16,6 +16,6 @@ 'use strict' | ||
t.plan(localAddresses.length + 3) | ||
t.plan((localAddresses.length - 1) + 3) | ||
const serverFactory = (handler, opts) => { | ||
t.ok(opts.serverFactory, 'it is called twice for every HOST interface') | ||
t.ok(opts.serverFactory, 'it is called once for localhost') | ||
@@ -22,0 +22,0 @@ const options = { |
@@ -9,2 +9,3 @@ 'use strict' | ||
const FormData = require('form-data') | ||
const { Readable } = require('stream') | ||
@@ -459,1 +460,30 @@ test('inject should exist', t => { | ||
}) | ||
test('Should not throw on access to routeConfig frameworkErrors handler - FST_ERR_BAD_URL', t => { | ||
t.plan(6) | ||
const fastify = Fastify({ | ||
frameworkErrors: function (err, req, res) { | ||
t.ok(typeof req.id === 'string') | ||
t.ok(req.raw instanceof Readable) | ||
t.same(req.routerPath, undefined) | ||
t.same(req.routeConfig, undefined) | ||
res.send(`${err.message} - ${err.code}`) | ||
} | ||
}) | ||
fastify.get('/test/:id', (req, res) => { | ||
res.send('{ hello: \'world\' }') | ||
}) | ||
fastify.inject( | ||
{ | ||
method: 'GET', | ||
url: '/test/%world' | ||
}, | ||
(err, res) => { | ||
t.error(err) | ||
t.equal(res.body, '\'/test/%world\' is not a valid url component - FST_ERR_BAD_URL') | ||
} | ||
) | ||
}) |
@@ -304,3 +304,3 @@ 'use strict' | ||
test('auto status code shoud be 200', t => { | ||
test('auto status code should be 200', t => { | ||
t.plan(3) | ||
@@ -317,3 +317,3 @@ sget({ | ||
test('auto type shoud be text/plain', t => { | ||
test('auto type should be text/plain', t => { | ||
t.plan(3) | ||
@@ -320,0 +320,0 @@ sget({ |
@@ -63,2 +63,3 @@ 'use strict' | ||
t.equal(request.url, '/') | ||
t.equal(request.originalUrl, '/') | ||
t.equal(request.socket, req.socket) | ||
@@ -78,2 +79,61 @@ t.equal(request.protocol, 'http') | ||
test('Request with undefined config', t => { | ||
const headers = { | ||
host: 'hostname' | ||
} | ||
const req = { | ||
method: 'GET', | ||
url: '/', | ||
socket: { remoteAddress: 'ip' }, | ||
headers | ||
} | ||
const context = new Context({ | ||
schema: { | ||
body: { | ||
type: 'object', | ||
required: ['hello'], | ||
properties: { | ||
hello: { type: 'string' } | ||
} | ||
} | ||
}, | ||
server: { | ||
[kReply]: {}, | ||
[kRequest]: Request | ||
} | ||
}) | ||
req.connection = req.socket | ||
const request = new Request('id', 'params', req, 'query', 'log', context) | ||
t.type(request, Request) | ||
t.type(request.validateInput, Function) | ||
t.type(request.getValidationFunction, Function) | ||
t.type(request.compileValidationSchema, Function) | ||
t.equal(request.id, 'id') | ||
t.equal(request.params, 'params') | ||
t.equal(request.raw, req) | ||
t.equal(request.query, 'query') | ||
t.equal(request.headers, headers) | ||
t.equal(request.log, 'log') | ||
t.equal(request.ip, 'ip') | ||
t.equal(request.ips, undefined) | ||
t.equal(request.hostname, 'hostname') | ||
t.equal(request.body, undefined) | ||
t.equal(request.method, 'GET') | ||
t.equal(request.url, '/') | ||
t.equal(request.originalUrl, '/') | ||
t.equal(request.socket, req.socket) | ||
t.equal(request.protocol, 'http') | ||
t.equal(request.routeSchema, context[kPublicRouteContext].schema) | ||
t.equal(request.routerPath, undefined) | ||
t.equal(request.routerMethod, undefined) | ||
t.equal(request.routeConfig, undefined) | ||
// Aim to not bad property keys (including Symbols) | ||
t.notOk('undefined' in request) | ||
// This will be removed, it's deprecated | ||
t.equal(request.connection, req.connection) | ||
t.end() | ||
}) | ||
test('Regular request - hostname from authority', t => { | ||
@@ -80,0 +140,0 @@ t.plan(2) |
'use strict' | ||
const split = require('split2') | ||
const test = require('tap').test | ||
@@ -172,2 +173,91 @@ const Fastify = require('../') | ||
test('Should supply Fastify request to the logger in frameworkErrors wrapper - FST_ERR_BAD_URL', t => { | ||
t.plan(8) | ||
const REQ_ID = 'REQ-1234' | ||
const logStream = split(JSON.parse) | ||
const fastify = Fastify({ | ||
frameworkErrors: function (err, req, res) { | ||
t.same(req.id, REQ_ID) | ||
t.same(req.raw.httpVersion, '1.1') | ||
res.send(`${err.message} - ${err.code}`) | ||
}, | ||
logger: { | ||
stream: logStream, | ||
serializers: { | ||
req (request) { | ||
t.same(request.id, REQ_ID) | ||
return { httpVersion: request.raw.httpVersion } | ||
} | ||
} | ||
}, | ||
genReqId: () => REQ_ID | ||
}) | ||
fastify.get('/test/:id', (req, res) => { | ||
res.send('{ hello: \'world\' }') | ||
}) | ||
logStream.on('data', (json) => { | ||
t.same(json.msg, 'incoming request') | ||
t.same(json.reqId, REQ_ID) | ||
t.same(json.req.httpVersion, '1.1') | ||
}) | ||
fastify.inject( | ||
{ | ||
method: 'GET', | ||
url: '/test/%world' | ||
}, | ||
(err, res) => { | ||
t.error(err) | ||
t.equal(res.body, '\'/test/%world\' is not a valid url component - FST_ERR_BAD_URL') | ||
} | ||
) | ||
}) | ||
test('Should honor disableRequestLogging option in frameworkErrors wrapper - FST_ERR_BAD_URL', t => { | ||
t.plan(2) | ||
const logStream = split(JSON.parse) | ||
const fastify = Fastify({ | ||
disableRequestLogging: true, | ||
frameworkErrors: function (err, req, res) { | ||
res.send(`${err.message} - ${err.code}`) | ||
}, | ||
logger: { | ||
stream: logStream, | ||
serializers: { | ||
req () { | ||
t.fail('should not be called') | ||
}, | ||
res () { | ||
t.fail('should not be called') | ||
} | ||
} | ||
} | ||
}) | ||
fastify.get('/test/:id', (req, res) => { | ||
res.send('{ hello: \'world\' }') | ||
}) | ||
logStream.on('data', (json) => { | ||
t.fail('should not be called') | ||
}) | ||
fastify.inject( | ||
{ | ||
method: 'GET', | ||
url: '/test/%world' | ||
}, | ||
(err, res) => { | ||
t.error(err) | ||
t.equal(res.body, '\'/test/%world\' is not a valid url component - FST_ERR_BAD_URL') | ||
} | ||
) | ||
}) | ||
test('Should honor frameworkErrors option - FST_ERR_ASYNC_CONSTRAINT', t => { | ||
@@ -223,1 +313,132 @@ t.plan(3) | ||
}) | ||
test('Should supply Fastify request to the logger in frameworkErrors wrapper - FST_ERR_ASYNC_CONSTRAINT', t => { | ||
t.plan(8) | ||
const constraint = { | ||
name: 'secret', | ||
storage: function () { | ||
const secrets = {} | ||
return { | ||
get: (secret) => { return secrets[secret] || null }, | ||
set: (secret, store) => { secrets[secret] = store } | ||
} | ||
}, | ||
deriveConstraint: (req, ctx, done) => { | ||
done(Error('kaboom')) | ||
}, | ||
validate () { return true } | ||
} | ||
const REQ_ID = 'REQ-1234' | ||
const logStream = split(JSON.parse) | ||
const fastify = Fastify({ | ||
constraints: { secret: constraint }, | ||
frameworkErrors: function (err, req, res) { | ||
t.same(req.id, REQ_ID) | ||
t.same(req.raw.httpVersion, '1.1') | ||
res.send(`${err.message} - ${err.code}`) | ||
}, | ||
logger: { | ||
stream: logStream, | ||
serializers: { | ||
req (request) { | ||
t.same(request.id, REQ_ID) | ||
return { httpVersion: request.raw.httpVersion } | ||
} | ||
} | ||
}, | ||
genReqId: () => REQ_ID | ||
}) | ||
fastify.route({ | ||
method: 'GET', | ||
url: '/', | ||
constraints: { secret: 'alpha' }, | ||
handler: (req, reply) => { | ||
reply.send({ hello: 'from alpha' }) | ||
} | ||
}) | ||
logStream.on('data', (json) => { | ||
t.same(json.msg, 'incoming request') | ||
t.same(json.reqId, REQ_ID) | ||
t.same(json.req.httpVersion, '1.1') | ||
}) | ||
fastify.inject( | ||
{ | ||
method: 'GET', | ||
url: '/' | ||
}, | ||
(err, res) => { | ||
t.error(err) | ||
t.equal(res.body, 'Unexpected error from async constraint - FST_ERR_ASYNC_CONSTRAINT') | ||
} | ||
) | ||
}) | ||
test('Should honor disableRequestLogging option in frameworkErrors wrapper - FST_ERR_ASYNC_CONSTRAINT', t => { | ||
t.plan(2) | ||
const constraint = { | ||
name: 'secret', | ||
storage: function () { | ||
const secrets = {} | ||
return { | ||
get: (secret) => { return secrets[secret] || null }, | ||
set: (secret, store) => { secrets[secret] = store } | ||
} | ||
}, | ||
deriveConstraint: (req, ctx, done) => { | ||
done(Error('kaboom')) | ||
}, | ||
validate () { return true } | ||
} | ||
const logStream = split(JSON.parse) | ||
const fastify = Fastify({ | ||
constraints: { secret: constraint }, | ||
disableRequestLogging: true, | ||
frameworkErrors: function (err, req, res) { | ||
res.send(`${err.message} - ${err.code}`) | ||
}, | ||
logger: { | ||
stream: logStream, | ||
serializers: { | ||
req () { | ||
t.fail('should not be called') | ||
}, | ||
res () { | ||
t.fail('should not be called') | ||
} | ||
} | ||
} | ||
}) | ||
fastify.route({ | ||
method: 'GET', | ||
url: '/', | ||
constraints: { secret: 'alpha' }, | ||
handler: (req, reply) => { | ||
reply.send({ hello: 'from alpha' }) | ||
} | ||
}) | ||
logStream.on('data', (json) => { | ||
t.fail('should not be called') | ||
}) | ||
fastify.inject( | ||
{ | ||
method: 'GET', | ||
url: '/' | ||
}, | ||
(err, res) => { | ||
t.error(err) | ||
t.equal(res.body, 'Unexpected error from async constraint - FST_ERR_ASYNC_CONSTRAINT') | ||
} | ||
) | ||
}) |
@@ -11,2 +11,3 @@ import fastify, { | ||
InjectOptions, FastifyBaseLogger, | ||
RawRequestDefaultExpression, | ||
RouteGenericInterface, | ||
@@ -131,3 +132,8 @@ ValidationResult, | ||
expectAssignable<FastifyInstance>(fastify({ requestIdHeader: false })) | ||
expectAssignable<FastifyInstance>(fastify({ genReqId: () => 'request-id' })) | ||
expectAssignable<FastifyInstance>(fastify({ | ||
genReqId: (req) => { | ||
expectType<RawRequestDefaultExpression>(req) | ||
return 'foo' | ||
} | ||
})) | ||
expectAssignable<FastifyInstance>(fastify({ trustProxy: true })) | ||
@@ -134,0 +140,0 @@ expectAssignable<FastifyInstance>(fastify({ querystringParser: () => ({ foo: 'bar' }) })) |
@@ -29,2 +29,4 @@ import { expectAssignable, expectDeprecated, expectError, expectNotDeprecated, expectType } from 'tsd' | ||
expectType<string>(server.pluginName) | ||
expectType<Record<string, unknown>>(server.getSchemas()) | ||
@@ -301,2 +303,18 @@ expectType<AddressInfo[]>(server.addresses()) | ||
}) | ||
server.decorate<string>('test', { | ||
getter () { | ||
expectType<FastifyInstance>(this) | ||
return 'foo' | ||
} | ||
}) | ||
server.decorate<string>('test', { | ||
getter () { | ||
expectType<FastifyInstance>(this) | ||
return 'foo' | ||
}, | ||
setter (x) { | ||
expectType<string>(x) | ||
expectType<FastifyInstance>(this) | ||
} | ||
}) | ||
@@ -321,3 +339,55 @@ server.decorateRequest<(x: string, y: number) => void>('test', function (x: string, y: number): void { | ||
})) | ||
expectError(server.decorate<string>('test', { | ||
getter () { | ||
return true | ||
} | ||
})) | ||
expectError(server.decorate<string>('test', { | ||
setter (x) {} | ||
})) | ||
declare module '../../fastify' { | ||
interface FastifyInstance { | ||
typedTestProperty: boolean | ||
typedTestPropertyGetterSetter: string | ||
typedTestMethod (x: string): string | ||
} | ||
} | ||
server.decorate('typedTestProperty', false) | ||
server.decorate('typedTestProperty', { | ||
getter () { | ||
return false | ||
} | ||
}) | ||
server.decorate('typedTestProperty', { | ||
getter (): boolean { | ||
return true | ||
}, | ||
setter (x) { | ||
expectType<boolean>(x) | ||
expectType<FastifyInstance>(this) | ||
} | ||
}) | ||
expectError(server.decorate('typedTestProperty', 'foo')) | ||
expectError(server.decorate('typedTestProperty', { | ||
getter () { | ||
return 'foo' | ||
} | ||
})) | ||
server.decorate('typedTestMethod', function (x) { | ||
expectType<string>(x) | ||
expectType<FastifyInstance>(this) | ||
return 'foo' | ||
}) | ||
server.decorate('typedTestMethod', x => x) | ||
expectError(server.decorate('typedTestMethod', function (x: boolean) { | ||
return 'foo' | ||
})) | ||
expectError(server.decorate('typedTestMethod', function (x) { | ||
return true | ||
})) | ||
expectError(server.decorate('typedTestMethod', async function (x) { | ||
return 'foo' | ||
})) | ||
const versionConstraintStrategy = { | ||
@@ -324,0 +394,0 @@ name: 'version', |
import { expectType, expectError, expectAssignable } from 'tsd' | ||
import fastify, { RouteHandlerMethod, RouteHandler, RawRequestDefaultExpression, FastifyContext, FastifyContextConfig, FastifyRequest, FastifyReply } from '../../fastify' | ||
import fastify, { RouteHandlerMethod, RouteHandler, RawRequestDefaultExpression, FastifyContext, FastifyContextConfig, FastifyRequest, FastifyReply, FastifySchema, FastifyTypeProviderDefault } from '../../fastify' | ||
import { RawServerDefault, RawReplyDefaultExpression, ContextConfigDefault } from '../../types/utils' | ||
@@ -8,4 +8,6 @@ import { FastifyLoggerInstance } from '../../types/logger' | ||
import { Buffer } from 'buffer' | ||
import { ReplyTypeConstrainer } from '../../types/reply' | ||
type DefaultSerializationFunction = (payload: {[key: string]: unknown}) => string | ||
type DefaultSerializationFunction = (payload: { [key: string]: unknown }) => string | ||
type DefaultFastifyReplyWithCode<Code extends number> = FastifyReply<RawServerDefault, RawRequestDefaultExpression, RawReplyDefaultExpression, RouteGenericInterface, ContextConfigDefault, FastifySchema, FastifyTypeProviderDefault, ReplyTypeConstrainer<RouteGenericInterface['Reply'], Code>> | ||
@@ -18,4 +20,4 @@ const getHandler: RouteHandlerMethod = function (_request, reply) { | ||
expectType<FastifyRequest<RouteGenericInterface, RawServerDefault, RawRequestDefaultExpression>>(reply.request) | ||
expectType<(statusCode: number) => FastifyReply>(reply.code) | ||
expectType<(statusCode: number) => FastifyReply>(reply.status) | ||
expectType<<Code extends number>(statusCode: Code) => DefaultFastifyReplyWithCode<Code>>(reply.code) | ||
expectType<<Code extends number>(statusCode: Code) => DefaultFastifyReplyWithCode<Code>>(reply.status) | ||
expectType<number>(reply.statusCode) | ||
@@ -63,2 +65,11 @@ expectType<boolean>(reply.sent) | ||
interface ReplyHttpCodes { | ||
Reply: { | ||
'1xx': number, | ||
200: 'abc', | ||
201: boolean, | ||
300: { foo: string }, | ||
} | ||
} | ||
const typedHandler: RouteHandler<ReplyPayload> = async (request, reply) => { | ||
@@ -109,1 +120,22 @@ expectType<((payload?: ReplyPayload['Reply']) => FastifyReply<RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, ReplyPayload>)>(reply.send) | ||
})) | ||
server.get<ReplyHttpCodes>('/get-generic-http-codes-send', async function handler (request, reply) { | ||
reply.code(200).send('abc') | ||
reply.code(201).send(true) | ||
reply.code(300).send({ foo: 'bar' }) | ||
reply.code(101).send(123) | ||
}) | ||
expectError(server.get<ReplyHttpCodes>('/get-generic-http-codes-send-error-1', async function handler (request, reply) { | ||
reply.code(200).send('def') | ||
})) | ||
expectError(server.get<ReplyHttpCodes>('/get-generic-http-codes-send-error-2', async function handler (request, reply) { | ||
reply.code(201).send(0) | ||
})) | ||
expectError(server.get<ReplyHttpCodes>('/get-generic-http-codes-send-error-3', async function handler (request, reply) { | ||
reply.code(300).send({ foo: 123 }) | ||
})) | ||
expectError(server.get<ReplyHttpCodes>('/get-generic-http-codes-send-error-4', async function handler (request, reply) { | ||
reply.code(100).send('asdasd') | ||
})) | ||
expectError(server.get<ReplyHttpCodes>('/get-generic-http-codes-send-error-5', async function handler (request, reply) { | ||
reply.code(401).send({ foo: 123 }) | ||
})) |
@@ -66,2 +66,3 @@ import { expectAssignable, expectType } from 'tsd' | ||
expectType<string>(request.url) | ||
expectType<string>(request.originalUrl) | ||
expectType<string>(request.method) | ||
@@ -68,0 +69,0 @@ expectType<string>(request.routerPath) |
@@ -77,3 +77,3 @@ import fastify, { FastifyInstance, FastifyRequest, FastifyReply, RouteHandlerMethod } from '../../fastify' | ||
fastify()[lowerCaseMethod]<RouteGeneric, RouteSpecificContextConfigType>('/', { config: { foo: 'bar', bar: 100, extra: true } }, (req, res) => { | ||
fastify()[lowerCaseMethod]<RouteGeneric, RouteSpecificContextConfigType>('/', { config: { foo: 'bar', bar: 100, extra: true, url: '/', method: lowerCaseMethod } }, (req, res) => { | ||
expectType<BodyInterface>(req.body) | ||
@@ -86,5 +86,9 @@ expectType<QuerystringInterface>(req.query) | ||
expectType<boolean>(req.context.config.extra) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<boolean>(res.context.config.extra) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}) | ||
@@ -95,3 +99,3 @@ | ||
method: method as HTTPMethods, | ||
config: { foo: 'bar', bar: 100 }, | ||
config: { foo: 'bar', bar: 100, url: '/', method: method as HTTPMethods }, | ||
prefixTrailingSlash: 'slash', | ||
@@ -105,4 +109,8 @@ onRequest: (req, res, done) => { // these handlers are tested in `hooks.test-d.ts` | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -116,4 +124,8 @@ preParsing: (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
expectType<RequestPayload>(payload) | ||
@@ -130,4 +142,8 @@ expectAssignable<(err?: FastifyError | null, res?: RequestPayload) => void>(done) | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -141,4 +157,8 @@ preHandler: (req, res, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -152,4 +172,8 @@ onResponse: (req, res, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
expectType<number>(res.statusCode) | ||
@@ -164,4 +188,8 @@ }, | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -175,4 +203,8 @@ preSerialization: (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -186,4 +218,8 @@ onSend: (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -197,4 +233,8 @@ handler: (req, res) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
} | ||
@@ -206,3 +246,3 @@ }) | ||
method: method as HTTPMethods, | ||
config: { foo: 'bar', bar: 100 }, | ||
config: { foo: 'bar', bar: 100, url: '/', method: method as HTTPMethods }, | ||
prefixTrailingSlash: 'slash', | ||
@@ -216,4 +256,8 @@ onRequest: async (req, res, done) => { // these handlers are tested in `hooks.test-d.ts` | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -227,4 +271,8 @@ preParsing: async (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
expectType<RequestPayload>(payload) | ||
@@ -241,4 +289,8 @@ expectAssignable<(err?: FastifyError | null, res?: RequestPayload) => void>(done) | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -252,4 +304,8 @@ preHandler: async (req, res, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -263,4 +319,8 @@ onResponse: async (req, res, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
expectType<number>(res.statusCode) | ||
@@ -275,4 +335,8 @@ }, | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -286,4 +350,8 @@ preSerialization: async (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -297,4 +365,8 @@ onSend: async (req, res, payload, done) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
}, | ||
@@ -308,4 +380,8 @@ handler: (req, res) => { | ||
expectType<number>(req.context.config.bar) | ||
expectType<string>(req.context.config.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.context.config.method) | ||
expectType<string>(res.context.config.foo) | ||
expectType<number>(res.context.config.bar) | ||
expectType<string>(req.routeConfig.url) | ||
expectType<HTTPMethods | HTTPMethods[]>(req.routeConfig.method) | ||
} | ||
@@ -312,0 +388,0 @@ }) |
@@ -107,1 +107,36 @@ 'use strict' | ||
}) | ||
test('Should rewrite url but keep originalUrl unchanged', t => { | ||
t.plan(7) | ||
const fastify = Fastify({ | ||
rewriteUrl (req) { | ||
t.equal(req.url, '/this-would-404-without-url-rewrite') | ||
t.equal(req.originalUrl, '/this-would-404-without-url-rewrite') | ||
return '/' | ||
} | ||
}) | ||
fastify.route({ | ||
method: 'GET', | ||
url: '/', | ||
handler: (req, reply) => { | ||
t.equal(req.originalUrl, '/this-would-404-without-url-rewrite') | ||
reply.send({ hello: 'world' }) | ||
} | ||
}) | ||
fastify.listen({ port: 0 }, function (err) { | ||
t.error(err) | ||
sget({ | ||
method: 'GET', | ||
url: 'http://localhost:' + fastify.server.address().port + '/this-would-404-without-url-rewrite' | ||
}, (err, response, body) => { | ||
t.error(err) | ||
t.same(JSON.parse(body), { hello: 'world' }) | ||
t.equal(response.statusCode, 200) | ||
}) | ||
}) | ||
t.teardown(() => fastify.close()) | ||
}) |
@@ -397,3 +397,3 @@ 'use strict' | ||
test('Bas accept version (server)', t => { | ||
test('Bad accept version (server)', t => { | ||
t.plan(5) | ||
@@ -400,0 +400,0 @@ const fastify = Fastify() |
import { ContextConfigDefault } from './utils' | ||
import { FastifyRouteConfig } from './route' | ||
// eslint-disable-next-line @typescript-eslint/no-empty-interface | ||
export interface FastifyContextConfig { | ||
export interface FastifyContextConfig extends FastifyRouteConfig { | ||
} | ||
@@ -6,0 +7,0 @@ |
@@ -88,2 +88,22 @@ import { FastifyError } from '@fastify/error' | ||
type GetterSetter<This, T> = T | { | ||
getter: (this: This) => T, | ||
setter?: (this: This, value: T) => void | ||
} | ||
type DecorationMethod<This, Return = This> = { | ||
< | ||
// Need to disable "no-use-before-define" to maintain backwards compatibility, as else decorate<Foo> would suddenly mean something new | ||
// eslint-disable-next-line no-use-before-define | ||
T extends (P extends keyof This ? This[P] : unknown), | ||
P extends string | symbol = string | symbol | ||
>(property: P, | ||
value: GetterSetter<This, T extends (...args: any[]) => any | ||
? (this: This, ...args: Parameters<T>) => ReturnType<T> | ||
: T | ||
>, | ||
dependencies?: string[] | ||
): Return; | ||
} | ||
/** | ||
@@ -100,2 +120,3 @@ * Fastify server instance. Returned by the core `fastify()` method. | ||
server: RawServer; | ||
pluginName: string; | ||
prefix: string; | ||
@@ -119,23 +140,6 @@ version: string; | ||
// should be able to define something useful with the decorator getter/setter pattern using Generics to enforce the users function returns what they expect it to | ||
decorate<T>(property: string | symbol, | ||
value: T extends (...args: any[]) => any | ||
? (this: FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>, ...args: Parameters<T>) => ReturnType<T> | ||
: T, | ||
dependencies?: string[] | ||
): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>; | ||
decorate: DecorationMethod<FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>>; | ||
decorateRequest: DecorationMethod<FastifyRequest, FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>>; | ||
decorateReply: DecorationMethod<FastifyReply, FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>>; | ||
decorateRequest<T>(property: string | symbol, | ||
value: T extends (...args: any[]) => any | ||
? (this: FastifyRequest, ...args: Parameters<T>) => ReturnType<T> | ||
: T, | ||
dependencies?: string[] | ||
): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>; | ||
decorateReply<T>(property: string | symbol, | ||
value: T extends (...args: any[]) => any | ||
? (this: FastifyReply, ...args: Parameters<T>) => ReturnType<T> | ||
: T, | ||
dependencies?: string[] | ||
): FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>; | ||
hasDecorator(decorator: string | symbol): boolean; | ||
@@ -142,0 +146,0 @@ hasRequestDecorator(decorator: string | symbol): boolean; |
@@ -1,10 +0,10 @@ | ||
import { RawReplyDefaultExpression, RawServerBase, RawServerDefault, ContextConfigDefault, RawRequestDefaultExpression, ReplyDefault } from './utils' | ||
import { FastifyReplyType, ResolveFastifyReplyType, FastifyTypeProvider, FastifyTypeProviderDefault } from './type-provider' | ||
import { Buffer } from 'buffer' | ||
import { FastifyContext } from './context' | ||
import { FastifyInstance } from './instance' | ||
import { FastifyBaseLogger } from './logger' | ||
import { FastifyRequest } from './request' | ||
import { RouteGenericInterface } from './route' | ||
import { FastifyInstance } from './instance' | ||
import { FastifySchema } from './schema' | ||
import { Buffer } from 'buffer' | ||
import { FastifyReplyType, FastifyTypeProvider, FastifyTypeProviderDefault, ResolveFastifyReplyType } from './type-provider' | ||
import { CodeToReplyKey, ContextConfigDefault, ReplyKeysToCodes, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase, RawServerDefault, ReplyDefault } from './utils' | ||
@@ -15,2 +15,6 @@ export interface ReplyGenericInterface { | ||
export type ReplyTypeConstrainer<RouteGenericReply, Code extends ReplyKeysToCodes<keyof RouteGenericReply>> = | ||
Code extends keyof RouteGenericReply ? RouteGenericReply[Code] : | ||
CodeToReplyKey<Code> extends keyof RouteGenericReply ? RouteGenericReply[CodeToReplyKey<Code>] : | ||
unknown; | ||
/** | ||
@@ -35,4 +39,4 @@ * FastifyReply is an instance of the standard http or http2 reply types. | ||
server: FastifyInstance; | ||
code(statusCode: number): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>; | ||
status(statusCode: number): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>; | ||
code<Code extends ReplyKeysToCodes<keyof RouteGeneric['Reply']>>(statusCode: Code): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReplyTypeConstrainer<RouteGeneric['Reply'], Code>>; | ||
status<Code extends ReplyKeysToCodes<keyof RouteGeneric['Reply']>>(statusCode: Code): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, ReplyTypeConstrainer<RouteGeneric['Reply'], Code>>; | ||
statusCode: number; | ||
@@ -39,0 +43,0 @@ sent: boolean; |
@@ -76,2 +76,3 @@ import { ErrorObject } from '@fastify/ajv-compiler' | ||
readonly url: string; | ||
readonly originalUrl: string; | ||
readonly protocol: 'http' | 'https'; | ||
@@ -78,0 +79,0 @@ readonly method: string; |
@@ -16,2 +16,7 @@ import { FastifyInstance } from './instance' | ||
export interface FastifyRouteConfig { | ||
url: string; | ||
method: HTTPMethods | HTTPMethods[]; | ||
} | ||
export interface RouteGenericInterface extends RequestGenericInterface, ReplyGenericInterface {} | ||
@@ -18,0 +23,0 @@ |
@@ -48,1 +48,25 @@ import * as http from 'http' | ||
export type ReplyDefault = unknown | ||
/** | ||
* Helpers for determining the type of the response payload based on the code | ||
*/ | ||
type StringAsNumber<T extends string> = T extends `${infer N extends number}` ? N : never; | ||
type CodeClasses = 1 | 2 | 3 | 4 | 5; | ||
type Digit = 0 |1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; | ||
type HttpCodes = StringAsNumber<`${CodeClasses}${Digit}${Digit}`>; | ||
type HttpKeys = HttpCodes | `${Digit}xx`; | ||
export type StatusCodeReply = { | ||
// eslint-disable-next-line no-unused-vars | ||
[Key in HttpKeys]?: unknown; | ||
}; | ||
// weird TS quirk: https://stackoverflow.com/questions/58977876/generic-conditional-type-resolves-to-never-when-the-generic-type-is-set-to-never | ||
export type ReplyKeysToCodes<Key> = [Key] extends [never] ? number : | ||
Key extends HttpCodes ? Key : | ||
Key extends `${infer X extends CodeClasses}xx` ? | ||
StringAsNumber<`${X}${Digit}${Digit}`> : number; | ||
export type CodeToReplyKey<Code extends number> = `${Code}` extends `${infer FirstDigit extends CodeClasses}${number}` | ||
? `${FirstDigit}xx` | ||
: never; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2160915
286
51752
44