Repository: fastify/fastify-sensible Branch: main Commit: 3d0ec2bf01fd Files: 28 Total size: 65.6 KB Directory structure: gitextract__ix3j9hb/ ├── .borp.yaml ├── .gitattributes ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── lock-threads.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── lib/ │ ├── assert.js │ ├── cache-control.js │ ├── httpError.d.ts │ ├── httpErrors.js │ └── vary.js ├── package.json ├── test/ │ ├── assert.test.js │ ├── cache-control.test.js │ ├── forwarded.test.js │ ├── httpErrors.test.js │ ├── httpErrorsReply.test.js │ ├── is.test.js │ ├── schema.test.js │ ├── to.test.js │ └── vary.test.js └── types/ ├── index.d.ts └── index.test-d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .borp.yaml ================================================ files: - 'test/**/*.test.js' ================================================ FILE: .gitattributes ================================================ # Set the default behavior, in case people don't have core.autocrlf set * text=auto # Require Unix line endings * text eol=lf ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 - package-ecosystem: "npm" directory: "/" schedule: interval: "monthly" open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main - next - 'v*' paths-ignore: - 'docs/**' - '*.md' pull_request: paths-ignore: - 'docs/**' - '*.md' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" cancel-in-progress: true permissions: contents: read jobs: test: permissions: contents: write pull-requests: write uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6 with: license-check: true lint: true ================================================ FILE: .github/workflows/lock-threads.yml ================================================ name: Lock Threads on: schedule: - cron: '0 0 1 * *' workflow_dispatch: concurrency: group: lock permissions: contents: read jobs: lock-threads: permissions: issues: write pull-requests: write uses: fastify/workflows/.github/workflows/lock-threads.yml@v6 ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Vim swap files *.swp # macOS files .DS_Store # Clinic .clinic # lock files bun.lockb package-lock.json pnpm-lock.yaml yarn.lock # editor files .vscode .idea #tap files .tap/ ================================================ FILE: .npmrc ================================================ ignore-scripts=true package-lock=false ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-present Tomas Della Vedova and The Fastify team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # @fastify/sensible [![CI](https://github.com/fastify/fastify-sensible/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-sensible/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@fastify/sensible.svg?style=flat)](https://www.npmjs.com/package/@fastify/sensible) [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) Defaults for Fastify that everyone can agree on™.
This plugin adds some useful utilities to your Fastify instance, see the API section to learn more. *Why are these APIs here and not included with Fastify?
Because Fastify aims to be as small and focused as possible, every utility that is not essential should be shipped as a standalone plugin.* ## Install ``` npm i @fastify/sensible ``` ### Compatibility | Plugin version | Fastify version | | -------------- | --------------- | | `>=6.x` | `^5.x` | | `^5.x` | `^4.x` | | `^4.x` | `^3.x` | | `>=2.x <4.x` | `^2.x` | | `^1.x` | `^1.x` | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin in the table above. See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. ## Usage ```js const fastify = require('fastify')() fastify.register(require('@fastify/sensible')) fastify.get('/', (req, reply) => { reply.notFound() }) fastify.get('/async', async (req, reply) => { throw fastify.httpErrors.notFound() }) fastify.get('/async-return', async (req, reply) => { return reply.notFound() }) fastify.listen({ port: 3000 }) ``` ## Shared JSON Schema for HTTP errors If you set the `sharedSchemaId` option, a shared JSON Schema is added and can be used in your routes. ```js const fastify = require('fastify')() fastify.register(require('@fastify/sensible'), { sharedSchemaId: 'HttpError' }) fastify.get('/async', { schema: { response: { 404: { $ref: 'HttpError' } } }, handler: async (req, reply) => { return reply.notFound() } }) fastify.listen({ port: 3000 }) ``` ## API #### `fastify.httpErrors` Object that exposes `createError` and all of the `4xx` and `5xx` error constructors. Use of `4xx` and `5xx` error constructors follows the same structure as [`new createError[code || name]([msg]))`](https://github.com/jshttp/http-errors#new-createerrorcode--namemsg) in [http-errors](https://github.com/jshttp/http-errors): ```js // the custom message is optional const notFoundErr = fastify.httpErrors.notFound('custom message') ``` `4xx` - fastify.httpErrors.badRequest() - fastify.httpErrors.unauthorized() - fastify.httpErrors.paymentRequired() - fastify.httpErrors.forbidden() - fastify.httpErrors.notFound() - fastify.httpErrors.methodNotAllowed() - fastify.httpErrors.notAcceptable() - fastify.httpErrors.proxyAuthenticationRequired() - fastify.httpErrors.requestTimeout() - fastify.httpErrors.conflict() - fastify.httpErrors.gone() - fastify.httpErrors.lengthRequired() - fastify.httpErrors.preconditionFailed() - fastify.httpErrors.payloadTooLarge() - fastify.httpErrors.uriTooLong() - fastify.httpErrors.unsupportedMediaType() - fastify.httpErrors.rangeNotSatisfiable() - fastify.httpErrors.expectationFailed() - fastify.httpErrors.imateapot() - fastify.httpErrors.misdirectedRequest() - fastify.httpErrors.unprocessableEntity() - fastify.httpErrors.locked() - fastify.httpErrors.failedDependency() - fastify.httpErrors.tooEarly() - fastify.httpErrors.upgradeRequired() - fastify.httpErrors.preconditionRequired() - fastify.httpErrors.tooManyRequests() - fastify.httpErrors.requestHeaderFieldsTooLarge() - fastify.httpErrors.unavailableForLegalReasons() `5xx` - fastify.httpErrors.internalServerError() - fastify.httpErrors.notImplemented() - fastify.httpErrors.badGateway() - fastify.httpErrors.serviceUnavailable() - fastify.httpErrors.gatewayTimeout() - fastify.httpErrors.httpVersionNotSupported() - fastify.httpErrors.variantAlsoNegotiates() - fastify.httpErrors.insufficientStorage() - fastify.httpErrors.loopDetected() - fastify.httpErrors.bandwidthLimitExceeded() - fastify.httpErrors.notExtended() - fastify.httpErrors.networkAuthenticationRequired() `createError` Use of `createError` follows the same structure as [`createError([status], [message], [properties])`](https://github.com/jshttp/http-errors#createerrorstatus-message-properties) in [http-errors](https://github.com/jshttp/http-errors): ```js const err = fastify.httpErrors.createError(404, 'This video does not exist!') ``` #### `reply.[httpError]` The `reply` interface is decorated with all of the functions declared above, using it is easy: ```js fastify.get('/', (req, reply) => { reply.notFound() }) ``` #### `reply.vary` The `reply` interface is decorated with [`jshttp/vary`](https://github.com/jshttp/vary), the API is the same, but you do not need to pass the res object. ```js fastify.get('/', (req, reply) => { reply.vary('Accept') reply.send('ok') }) ``` #### `reply.cacheControl` The `reply` interface is decorated with a helper to configure cache control response headers. ```js // configure a single type fastify.get('/', (req, reply) => { reply.cacheControl('public') reply.send('ok') }) // configure multiple types fastify.get('/', (req, reply) => { reply.cacheControl('public') reply.cacheControl('immutable') reply.send('ok') }) // configure a type time fastify.get('/', (req, reply) => { reply.cacheControl('max-age', 42) reply.send('ok') }) // the time can be defined as string fastify.get('/', (req, reply) => { // all the formats of github.com/vercel/ms are supported reply.cacheControl('max-age', '1d') // will set to 'max-age=86400' reply.send('ok') }) ``` #### `reply.preventCache` The `reply` interface is decorated with a helper to set the cache control header to a no caching configuration. ```js fastify.get('/', (req, reply) => { // will set cache-control to 'no-store, max-age=0, private' // and for HTTP/1.0 compatibility // will set pragma to 'no-cache' and expires to 0 reply.preventCache() reply.send('ok') }) ``` #### `reply.revalidate` The `reply` interface is decorated with a helper to set the cache control header to a no caching configuration. ```js fastify.get('/', (req, reply) => { reply.revalidate() // will set to 'max-age=0, must-revalidate' reply.send('ok') }) ``` #### `reply.staticCache` The `reply` interface is decorated with a helper to set the cache control header to a public and immutable configuration. ```js fastify.get('/', (req, reply) => { // the time can be defined as a string reply.staticCache(42) // will set to 'public, max-age=42, immutable' reply.send('ok') }) ``` #### `reply.stale` The `reply` interface is decorated with a helper to set the cache control header for [stale content](https://tools.ietf.org/html/rfc5861). ```js fastify.get('/', (req, reply) => { // the time can be defined as a string reply.stale('while-revalidate', 42) reply.stale('if-error', 1) reply.send('ok') }) ``` #### `reply.maxAge` The `reply` interface is decorated with a helper to set max age of the response. It can be used in conjunction with `reply.stale`, see [here](https://web.dev/stale-while-revalidate/). ```js fastify.get('/', (req, reply) => { // the time can be defined as a string reply.maxAge(86400) reply.stale('while-revalidate', 42) reply.send('ok') }) ``` #### `request.forwarded` The `request` interface is decorated with [`jshttp/forwarded`](https://github.com/jshttp/forwarded), the API is the same, but you do not need to pass the request object: ```js fastify.get('/', (req, reply) => { reply.send(req.forwarded()) }) ``` #### `request.is` The `request` interface is decorated with [`jshttp/type-is`](https://github.com/jshttp/type-is), the API is the same but you do not need to pass the request object: ```js fastify.get('/', (req, reply) => { reply.send(req.is(['html', 'json'])) }) ``` #### `assert` Verify if a given condition is true, if not it throws the specified http error.
Useful if you work with *async* routes: ```js // the custom message is optional fastify.assert( req.headers.authorization, 400, 'Missing authorization header' ) ``` The `assert` API also exposes the following methods: - fastify.assert.ok() - fastify.assert.equal() - fastify.assert.notEqual() - fastify.assert.strictEqual() - fastify.assert.notStrictEqual() - fastify.assert.deepEqual() - fastify.assert.notDeepEqual() #### `to` Async await wrapper for easy error handling without try-catch, inspired by [`await-to-js`](https://github.com/scopsy/await-to-js): ```js const [err, user] = await fastify.to( db.findOne({ user: 'tyrion' }) ) ``` ## Contributing Do you feel there is some utility that *everyone can agree on* that is not present?
Open an issue and let's discuss it! Even better a pull request! ## Acknowledgments The project name is inspired by [`vim-sensible`](https://github.com/tpope/vim-sensible), an awesome package that if you use vim you should use too. ## License Licensed under [MIT](./LICENSE). ================================================ FILE: eslint.config.js ================================================ 'use strict' module.exports = require('neostandard')({ ignores: require('neostandard').resolveIgnoresFromGitignore(), ts: true }) ================================================ FILE: index.js ================================================ 'use strict' const fp = require('fastify-plugin') // External utilities const forwarded = require('forwarded') const typeis = require('type-is') // Internals Utilities const httpErrors = require('./lib/httpErrors') const assert = require('./lib/assert') const vary = require('./lib/vary') const cache = require('./lib/cache-control') /** @type {typeof import('./types/index').fastifySensible} */ function fastifySensible (fastify, opts, next) { fastify.decorate('httpErrors', httpErrors) fastify.decorate('assert', assert) fastify.decorate('to', to) fastify.decorateRequest('forwarded', function requestForwarded () { return forwarded(this.raw) }) fastify.decorateRequest('is', function requestIs (types) { return typeis(this.raw, Array.isArray(types) ? types : [types]) }) fastify.decorateReply('vary', vary) fastify.decorateReply('cacheControl', cache.cacheControl) fastify.decorateReply('preventCache', cache.preventCache) fastify.decorateReply('revalidate', cache.revalidate) fastify.decorateReply('staticCache', cache.staticCache) fastify.decorateReply('stale', cache.stale) fastify.decorateReply('maxAge', cache.maxAge) const httpErrorsKeys = Object.keys(httpErrors) const httpErrorsKeysLength = httpErrorsKeys.length for (let i = 0; i < httpErrorsKeysLength; ++i) { const httpError = httpErrorsKeys[i] switch (httpError) { case 'HttpError': // skip abstract class constructor break case 'getHttpError': fastify.decorateReply('getHttpError', function replyGetHttpError (errorCode, message) { this.send(httpErrors.getHttpError(errorCode, message)) return this }) break default: { const capitalizedMethodName = httpError.replace(/(?:^|\s)\S/gu, a => a.toUpperCase()) const replyMethodName = 'sensible' + capitalizedMethodName fastify.decorateReply(httpError, { [replyMethodName]: function (message) { this.send(httpErrors[httpError](message)) return this } }[replyMethodName]) } } } if (opts?.sharedSchemaId) { // The schema must be the same as: // https://github.com/fastify/fastify/blob/c08b67e0bfedc9935b51c787ae4cd6b250ad303c/build/build-error-serializer.js#L8-L16 fastify.addSchema({ $id: opts.sharedSchemaId, type: 'object', properties: { statusCode: { type: 'number' }, code: { type: 'string' }, error: { type: 'string' }, message: { type: 'string' } } }) } /** * Wraps a promise for easier error handling without try/catch. * @template T * @param {Promise} promise - The promise to wrap. * @returns {Promise<[Error, undefined] | [null, T]>} A promise that resolves to a tuple containing either an error or the resolved data. */ function to (promise) { return promise.then(data => [null, data], err => [err, undefined]) } next() } module.exports = fp(fastifySensible, { name: '@fastify/sensible', fastify: '5.x' }) module.exports.default = fastifySensible module.exports.fastifySensible = fastifySensible module.exports.httpErrors = httpErrors module.exports.HttpError = httpErrors.HttpError ================================================ FILE: lib/assert.js ================================================ /* eslint-disable eqeqeq */ 'use strict' const { dequal: deepEqual } = require('dequal') const { getHttpError } = require('./httpErrors') function assert (condition, code, message) { if (condition) return throw getHttpError(code, message) } assert.ok = assert assert.equal = function (a, b, code, message) { assert(a == b, code, message) } assert.notEqual = function (a, b, code, message) { assert(a != b, code, message) } assert.strictEqual = function (a, b, code, message) { assert(a === b, code, message) } assert.notStrictEqual = function (a, b, code, message) { assert(a !== b, code, message) } assert.deepEqual = function (a, b, code, message) { assert(deepEqual(a, b), code, message) } assert.notDeepEqual = function (a, b, code, message) { assert(!deepEqual(a, b), code, message) } module.exports = assert ================================================ FILE: lib/cache-control.js ================================================ 'use strict' // Cache control header utilities, for more info see: // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control // Useful reads: // - https://odino.org/http-cache-101-scaling-the-web/ // - https://web.dev/stale-while-revalidate/ // - https://csswizardry.com/2019/03/cache-control-for-civilians/ // - https://jakearchibald.com/2016/caching-best-practices/ const assert = require('node:assert') const ms = require('@lukeed/ms').parse const validSingletimes = [ 'must-revalidate', 'no-cache', 'no-store', 'no-transform', 'public', 'private', 'proxy-revalidate', 'immutable' ] const validMultitimes = [ 'max-age', 's-maxage', 'stale-while-revalidate', 'stale-if-error' ] function cacheControl (type, time) { const previoustime = this.getHeader('Cache-Control') if (time == null) { assert(validSingletimes.indexOf(type) !== -1, `Invalid Cache Control type: ${type}`) this.header('Cache-Control', previoustime ? `${previoustime}, ${type}` : type) } else { if (typeof time === 'string') { time = ms(time) / 1000 } assert(validMultitimes.indexOf(type) !== -1, `Invalid Cache Control type: ${type}`) assert(typeof time === 'number', 'The cache control time should be a number') this.header('Cache-Control', previoustime ? `${previoustime}, ${type}=${time}` : `${type}=${time}`) } return this } function preventCache () { this .header('Cache-Control', 'no-store, max-age=0, private') // compatibility support for HTTP/1.0 // see: https://owasp.org/www-community/OWASP_Application_Security_FAQ#how-do-i-ensure-that-sensitive-pages-are-not-cached-on-the-users-browser .header('Pragma', 'no-cache') .header('Expires', 0) return this } function maxAge (time) { return this.cacheControl('max-age', time) } function revalidate () { this.header('Cache-Control', 'max-age=0, must-revalidate') return this } function staticCache (time) { if (typeof time === 'string') { time = ms(time) / 1000 } assert(typeof time === 'number', 'The cache control time should be a number') this.header('Cache-Control', `public, max-age=${time}, immutable`) return this } function stale (type, time) { if (type === 'while-revalidate') { return this.cacheControl('stale-while-revalidate', time) } else if (type === 'if-error') { return this.cacheControl('stale-if-error', time) } else { throw new Error(`Invalid cache control stale time ${time}`) } } module.exports = { cacheControl, preventCache, revalidate, staticCache, stale, maxAge } ================================================ FILE: lib/httpError.d.ts ================================================ export declare class HttpError extends Error { status: N statusCode: N expose: boolean message: string headers?: { [key: string]: string; }; [key: string]: any; } type UnknownError = Error | string | number | { [key: string]: any } export type HttpErrorTypes = { badRequest: 400, unauthorized: 401, paymentRequired: 402, forbidden: 403, notFound: 404, methodNotAllowed: 405, notAcceptable: 406, proxyAuthenticationRequired: 407, requestTimeout: 408, conflict: 409, gone: 410, lengthRequired: 411, preconditionFailed: 412, payloadTooLarge: 413, uriTooLong: 414, unsupportedMediaType: 415, rangeNotSatisfiable: 416, expectationFailed: 417, imateapot: 418, misdirectedRequest: 421, unprocessableEntity: 422, locked: 423, failedDependency: 424, tooEarly: 425, upgradeRequired: 426, preconditionRequired: 428, tooManyRequests: 429, requestHeaderFieldsTooLarge: 431, unavailableForLegalReasons: 451, internalServerError: 500, notImplemented: 501, badGateway: 502, serviceUnavailable: 503, gatewayTimeout: 504, httpVersionNotSupported: 505, variantAlsoNegotiates: 506, insufficientStorage: 507, loopDetected: 508, bandwidthLimitExceeded: 509, notExtended: 510 networkAuthenticationRequired: 511 } type ValueOf = ObjectType[ValueType] export type HttpErrorNames = keyof HttpErrorTypes export type HttpErrorCodes = ValueOf // Permissive type for getHttpError lookups export type HttpErrorCodesLoose = HttpErrorCodes | `${HttpErrorCodes}` // Helper to go from stringified error codes back to numeric type AsCode = T extends `${infer N extends HttpErrorCodes}` ? N : never export type HttpErrors = { HttpError: typeof HttpError; getHttpError: (code: T, message?: string) => HttpError>; createError: (...args: UnknownError[]) => HttpError; } & { [Property in keyof HttpErrorTypes]: (...args: UnknownError[]) => HttpError } // eslint-disable-next-line @typescript-eslint/no-redeclare declare const HttpErrors: HttpErrors export default HttpErrors ================================================ FILE: lib/httpErrors.js ================================================ 'use strict' const createError = require('http-errors') const statusCodes = require('node:http').STATUS_CODES const statusCodesMap = Object.assign({}, statusCodes) Object.keys(statusCodesMap).forEach(code => { statusCodesMap[code] = normalize(code, statusCodesMap[code]) }) function normalize (code, msg) { if (code === '414') return 'uriTooLong' if (code === '505') return 'httpVersionNotSupported' msg = msg.split(' ').join('').replace(/'/g, '') msg = msg[0].toLowerCase() + msg.slice(1) return msg } const httpErrors = { badRequest: function badRequest (message) { return new createError.BadRequest(message) }, unauthorized: function unauthorized (message) { return new createError.Unauthorized(message) }, paymentRequired: function paymentRequired (message) { return new createError.PaymentRequired(message) }, forbidden: function forbidden (message) { return new createError.Forbidden(message) }, notFound: function notFound (message) { return new createError.NotFound(message) }, methodNotAllowed: function methodNotAllowed (message) { return new createError.MethodNotAllowed(message) }, notAcceptable: function notAcceptable (message) { return new createError.NotAcceptable(message) }, proxyAuthenticationRequired: function proxyAuthenticationRequired (message) { return new createError.ProxyAuthenticationRequired(message) }, requestTimeout: function requestTimeout (message) { return new createError.RequestTimeout(message) }, conflict: function conflict (message) { return new createError.Conflict(message) }, gone: function gone (message) { return new createError.Gone(message) }, lengthRequired: function lengthRequired (message) { return new createError.LengthRequired(message) }, preconditionFailed: function preconditionFailed (message) { return new createError.PreconditionFailed(message) }, payloadTooLarge: function payloadTooLarge (message) { return new createError.PayloadTooLarge(message) }, uriTooLong: function uriTooLong (message) { return new createError.URITooLong(message) }, unsupportedMediaType: function unsupportedMediaType (message) { return new createError.UnsupportedMediaType(message) }, rangeNotSatisfiable: function rangeNotSatisfiable (message) { return new createError.RangeNotSatisfiable(message) }, expectationFailed: function expectationFailed (message) { return new createError.ExpectationFailed(message) }, imateapot: function imateapot (message) { return new createError.ImATeapot(message) }, misdirectedRequest: function misdirectedRequest (message) { return new createError.MisdirectedRequest(message) }, unprocessableEntity: function unprocessableEntity (message) { return new createError.UnprocessableEntity(message) }, locked: function locked (message) { return new createError.Locked(message) }, failedDependency: function failedDependency (message) { return new createError.FailedDependency(message) }, tooEarly: function tooEarly (message) { return new createError.TooEarly(message) }, upgradeRequired: function upgradeRequired (message) { return new createError.UpgradeRequired(message) }, preconditionRequired: function preconditionRequired (message) { return new createError.PreconditionRequired(message) }, tooManyRequests: function tooManyRequests (message) { return new createError.TooManyRequests(message) }, requestHeaderFieldsTooLarge: function requestHeaderFieldsTooLarge (message) { return new createError.RequestHeaderFieldsTooLarge(message) }, unavailableForLegalReasons: function unavailableForLegalReasons (message) { return new createError.UnavailableForLegalReasons(message) }, internalServerError: function internalServerError (message) { return new createError.InternalServerError(message) }, notImplemented: function notImplemented (message) { return new createError.NotImplemented(message) }, badGateway: function badGateway (message) { return new createError.BadGateway(message) }, serviceUnavailable: function serviceUnavailable (message) { return new createError.ServiceUnavailable(message) }, gatewayTimeout: function gatewayTimeout (message) { return new createError.GatewayTimeout(message) }, httpVersionNotSupported: function httpVersionNotSupported (message) { return new createError.HTTPVersionNotSupported(message) }, variantAlsoNegotiates: function variantAlsoNegotiates (message) { return new createError.VariantAlsoNegotiates(message) }, insufficientStorage: function insufficientStorage (message) { return new createError.InsufficientStorage(message) }, loopDetected: function loopDetected (message) { return new createError.LoopDetected(message) }, bandwidthLimitExceeded: function bandwidthLimitExceeded (message) { return new createError.BandwidthLimitExceeded(message) }, notExtended: function notExtended (message) { return new createError.NotExtended(message) }, networkAuthenticationRequired: function networkAuthenticationRequired (message) { return new createError.NetworkAuthenticationRequired(message) } } function getHttpError (code, message) { return httpErrors[statusCodesMap[code + '']](message) } module.exports = httpErrors module.exports.getHttpError = getHttpError module.exports.HttpError = createError.HttpError module.exports.createError = createError ================================================ FILE: lib/vary.js ================================================ 'use strict' const append = require('vary').append // Same implementation of https://github.com/jshttp/vary // but adapted to the Fastify API function vary (field) { let value = this.getHeader('Vary') || '' const header = Array.isArray(value) ? value.join(', ') : String(value) // set new header value = append(header, field) this.header('Vary', value) } module.exports = vary module.exports.append = append ================================================ FILE: package.json ================================================ { "name": "@fastify/sensible", "version": "6.0.4", "description": "Defaults for Fastify that everyone can agree on", "main": "index.js", "type": "commonjs", "types": "types/index.d.ts", "scripts": { "lint": "eslint", "lint:fix": "eslint --fix", "test": "npm run test:unit && npm run test:typescript", "test:typescript": "tsd", "test:unit": "borp -C --check-coverage --reporter=@jsumners/line-reporter" }, "repository": { "type": "git", "url": "git+https://github.com/fastify/fastify-sensible.git" }, "keywords": [ "fastify", "http", "defaults", "helper" ], "author": "Tomas Della Vedova - @delvedor (http://delved.org)", "contributors": [ { "name": "Matteo Collina", "email": "hello@matteocollina.com" }, { "name": "Manuel Spigolon", "email": "behemoth89@gmail.com" }, { "name": "Cemre Mengu", "email": "cemremengu@gmail.com" }, { "name": "Frazer Smith", "email": "frazer.dev@icloud.com", "url": "https://github.com/fdawgs" } ], "license": "MIT", "bugs": { "url": "https://github.com/fastify/fastify-sensible/issues" }, "homepage": "https://github.com/fastify/fastify-sensible#readme", "funding": [ { "type": "github", "url": "https://github.com/sponsors/fastify" }, { "type": "opencollective", "url": "https://opencollective.com/fastify" } ], "devDependencies": { "@jsumners/line-reporter": "^1.0.1", "@types/node": "^25.0.3", "borp": "^1.0.0", "eslint": "^9.17.0", "fastify": "^5.0.0", "neostandard": "^0.13.0", "tsd": "^0.33.0" }, "dependencies": { "@lukeed/ms": "^2.0.2", "dequal": "^2.0.3", "fastify-plugin": "^5.0.0", "forwarded": "^0.2.0", "http-errors": "^2.0.0", "type-is": "^2.0.1", "vary": "^1.1.2" }, "publishConfig": { "access": "public" } } ================================================ FILE: test/assert.test.js ================================================ 'use strict' const { test } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') test('Should support basic assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.ok(true) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support ok assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.ok(true) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.equal(1, '1') t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support not equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notEqual(1, '2') t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support strict equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.strictEqual(1, 1) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support not strict equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notStrictEqual(1, 2) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support deep equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.deepEqual({ a: 1 }, { a: 1 }) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support not deep equal assert', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notDeepEqual({ hello: 'world' }, { hello: 'dlrow' }) t.assert.ok('Works correctly') } catch (err) { t.assert.fail(err) } done() }) }) test('Should support basic assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert(false) t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.equal(1, '2') t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support not equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notEqual(1, '1') t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support strict equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.equal(1, 2) t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support not strict equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notStrictEqual(1, 1) t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support deep equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.deepEqual({ hello: 'world' }, { hello: 'dlrow' }) t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should support not deep equal assert (throw)', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert.notDeepEqual({ hello: 'world' }, { hello: 'world' }) t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) } done() }) }) test('Should generate the correct http error', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) try { fastify.assert(false, 400, 'Wrong!') t.assert.fail('Should throw') } catch (err) { t.assert.strictEqual(err.message, 'Wrong!') t.assert.strictEqual(err.name, 'BadRequestError') t.assert.strictEqual(err.statusCode, 400) } done() }) }) ================================================ FILE: test/cache-control.test.js ================================================ 'use strict' const { test } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') test('reply.cacheControl API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.cacheControl('public') reply.send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'public') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.cacheControl API (multiple values)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply .cacheControl('public') .cacheControl('max-age', 604800) .cacheControl('immutable') .send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=604800, immutable') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.preventCache API', (t, done) => { t.plan(6) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.preventCache().send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'no-store, max-age=0, private') t.assert.strictEqual(res.headers.pragma, 'no-cache') t.assert.strictEqual(res.headers.expires, '0') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.stale API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.stale('while-revalidate', 42).send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'stale-while-revalidate=42') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.stale API (multiple values)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply .stale('while-revalidate', 42) .stale('if-error', 1) .send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'stale-while-revalidate=42, stale-if-error=1') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.stale API (bad value)', (t, done) => { t.plan(5) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { try { reply.stale('foo', 42).send('ok') t.assert.fail('Should throw') } catch (err) { t.assert.ok(err) reply.send('ok') } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.ok(!res.headers['cache-control']) t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.revalidate API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.revalidate().send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'max-age=0, must-revalidate') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.staticCache API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.staticCache(42).send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=42, immutable') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.staticCache API (as string)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.staticCache('42s').send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=42, immutable') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.maxAge and reply.stale API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply .maxAge(42) .stale('while-revalidate', 3) .send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'max-age=42, stale-while-revalidate=3') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('reply.cacheControl API (string time)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.cacheControl('max-age', '1d') reply.send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers['cache-control'], 'max-age=86400') t.assert.strictEqual(res.payload, 'ok') done() }) }) ================================================ FILE: test/forwarded.test.js ================================================ 'use strict' const { test } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') test('request.forwarded API', (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (req, reply) => { reply.send(req.forwarded()) }) fastify.inject({ method: 'GET', url: '/', headers: { 'x-forwarded-for': '10.0.0.2, 10.0.0.1' } }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.deepStrictEqual( JSON.parse(res.payload), ['127.0.0.1', '10.0.0.1', '10.0.0.2'] ) done() }) }) test('request.forwarded API (without header)', (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (req, reply) => { reply.send(req.forwarded()) }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.deepStrictEqual( JSON.parse(res.payload), ['127.0.0.1'] ) done() }) }) ================================================ FILE: test/httpErrors.test.js ================================================ 'use strict' const { test } = require('node:test') const createError = require('http-errors') const statusCodes = require('node:http').STATUS_CODES const Fastify = require('fastify') const Sensible = require('../index') const HttpError = require('../lib/httpErrors').HttpError test('Should generate the correct http error', (t, done) => { const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) Object.keys(statusCodes).forEach(code => { if (Number(code) < 400) return const name = normalize(code, statusCodes[code]) const err = fastify.httpErrors[name]() t.assert.ok(err instanceof HttpError) // `statusCodes` uses the capital T if (err.message === 'I\'m a Teapot') { t.assert.strictEqual(err.statusCode, 418) } else { t.assert.strictEqual(err.message, statusCodes[code]) } t.assert.strictEqual(typeof err.name, 'string') t.assert.strictEqual(err.statusCode, Number(code)) }) done() }) }) test('Should expose the createError method from http-errors', (t, done) => { t.plan(2) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) t.assert.strictEqual(fastify.httpErrors.createError, createError) done() }) }) test('Should generate the correct error using the properties given', (t, done) => { t.plan(5) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) const customError = fastify.httpErrors.createError(404, 'This video does not exist!') t.assert.ok(customError instanceof HttpError) t.assert.strictEqual(customError.message, 'This video does not exist!') t.assert.strictEqual(typeof customError.name, 'string') t.assert.strictEqual(customError.statusCode, 404) done() }) }) test('Should generate the correct http error (with custom message)', (t, done) => { const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) Object.keys(statusCodes).forEach(code => { if (Number(code) < 400) return const name = normalize(code, statusCodes[code]) const err = fastify.httpErrors[name]('custom') t.assert.ok(err instanceof HttpError) t.assert.strictEqual(err.message, 'custom') t.assert.strictEqual(typeof err.name, 'string') t.assert.strictEqual(err.statusCode, Number(code)) }) done() }) }) test('should throw error', (t) => { const err = Sensible.httpErrors.conflict('custom') t.assert.strictEqual(err.message, 'custom') }) function normalize (code, msg) { if (code === '414') return 'uriTooLong' if (code === '418') return 'imateapot' if (code === '505') return 'httpVersionNotSupported' msg = msg.split(' ').join('').replace(/'/g, '') msg = msg[0].toLowerCase() + msg.slice(1) return msg } ================================================ FILE: test/httpErrorsReply.test.js ================================================ 'use strict' const { test } = require('node:test') const statusCodes = require('node:http').STATUS_CODES const Fastify = require('fastify') const Sensible = require('../index') test('Should generate the correct http error', (t, rootDone) => { const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') let completedTests = 0 codes.forEach(code => { t.test(code, (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { const name = normalize(code, statusCodes[code]) t.assert.strictEqual(reply[name](), reply) }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, Number(code)) if (code === '425') { t.assert.deepStrictEqual(JSON.parse(res.payload), { error: 'Too Early', message: 'Too Early', statusCode: 425 }) } else { t.assert.deepStrictEqual(JSON.parse(res.payload), { error: statusCodes[code], message: statusCodes[code], statusCode: Number(code) }) } done() completedTests++ if (completedTests === codes.length) { rootDone() } }) }) }) }) test('Should generate the correct http error using getter', (t, rootDone) => { const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') let completedTests = 0 codes.forEach(code => { t.test(code, (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { t.assert.strictEqual(reply.getHttpError(code), reply) }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, Number(code)) t.assert.deepStrictEqual(JSON.parse(res.payload), { error: statusCodes[code], message: statusCodes[code], statusCode: Number(code) }) done() completedTests++ if (completedTests === codes.length) { rootDone() } }) }) }) }) test('Should generate the correct http error (with custom message)', (t, rootDone) => { const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') let completedTests = 0 codes.forEach(code => { t.test(code, (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { const name = normalize(code, statusCodes[code]) reply[name]('custom') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, Number(code)) t.assert.deepStrictEqual(JSON.parse(res.payload), { error: statusCodes[code], message: 'custom', statusCode: Number(code) }) done() completedTests++ if (completedTests === codes.length) { rootDone() } }) }) }) }) function normalize (code, msg) { if (code === '414') return 'uriTooLong' if (code === '505') return 'httpVersionNotSupported' msg = msg.split(' ').join('').replace(/'/g, '') msg = msg[0].toLowerCase() + msg.slice(1) return msg } ================================================ FILE: test/is.test.js ================================================ 'use strict' const { test } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') test('request.is API', (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (req, reply) => { reply.send(req.is('json')) }) fastify.inject({ method: 'GET', url: '/', payload: { foo: 'bar' } }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.deepStrictEqual( res.payload, 'json' ) done() }) }) test('request.is API (with array)', (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (req, reply) => { reply.send(req.is(['html', 'json'])) }) fastify.inject({ method: 'GET', url: '/', payload: { foo: 'bar' } }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.deepStrictEqual( res.payload, 'json' ) done() }) }) ================================================ FILE: test/schema.test.js ================================================ 'use strict' const { test } = require('node:test') const statusCodes = require('node:http').STATUS_CODES const Fastify = require('fastify') const Sensible = require('../index') test('Should add shared schema', (t, done) => { t.plan(3) const fastify = Fastify() fastify.register(Sensible, { sharedSchemaId: 'myError' }) fastify.get('/', { schema: { response: { 400: { $ref: 'myError' } } }, handler: (_req, reply) => { reply.badRequest() } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 400) t.assert.deepStrictEqual(JSON.parse(res.payload), { error: statusCodes[400], message: statusCodes[400], statusCode: 400 }) done() }) }) ================================================ FILE: test/to.test.js ================================================ 'use strict' const { test } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') test('Should nicely wrap promises (resolve)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) fastify.to(promise(true)) .then(val => { t.assert.ok(Array.isArray(val)) t.assert.ok(!val[0]) t.assert.ok(val[1]) done() }) }) }) test('Should nicely wrap promises (reject)', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.ready(err => { t.assert.ifError(err) fastify.to(promise(false)) .then(val => { t.assert.ok(Array.isArray(val)) t.assert.ok(val[0]) t.assert.ok(!val[1]) done() }) }) }) function promise (bool) { return new Promise((resolve, reject) => { if (bool) { resolve(true) } else { reject(new Error('kaboom')) } }) } ================================================ FILE: test/vary.test.js ================================================ 'use strict' const { test, describe } = require('node:test') const Fastify = require('fastify') const Sensible = require('../index') describe('reply.vary API', () => { test('accept string', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.vary('Accept') reply.vary('Origin') reply.vary('User-Agent') reply.send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers.vary, 'Accept, Origin, User-Agent') t.assert.strictEqual(res.payload, 'ok') done() }) }) test('accept array of strings', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { reply.header('Vary', ['Accept', 'Origin']) reply.vary('User-Agent') reply.send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.headers.vary, 'Accept, Origin, User-Agent') t.assert.strictEqual(res.payload, 'ok') done() }) }) }) test('reply.vary.append API', (t, done) => { t.plan(4) const fastify = Fastify() fastify.register(Sensible) fastify.get('/', (_req, reply) => { t.assert.strictEqual( reply.vary.append('', ['Accept', 'Accept-Language']), 'Accept, Accept-Language' ) reply.send('ok') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.assert.ifError(err) t.assert.strictEqual(res.statusCode, 200) t.assert.strictEqual(res.payload, 'ok') done() }) }) ================================================ FILE: types/index.d.ts ================================================ import { FastifyPluginCallback, FastifyReply } from 'fastify' import { HttpErrors, HttpError } from '../lib/httpError' import * as Errors from '../lib/httpError' type FastifySensible = FastifyPluginCallback type singleValueTypes = | 'must-revalidate' | 'no-cache' | 'no-store' | 'no-transform' | 'public' | 'private' | 'proxy-revalidate' | 'immutable' type multiValueTypes = | 'max-age' | 's-maxage' | 'stale-while-revalidate' | 'stale-if-error' type staleTypes = 'while-revalidate' | 'if-error' declare module 'fastify' { namespace SensibleTypes { type ToType = [Error, T] } interface Assert { (condition: unknown, code: number, message?: string): asserts condition; ok(condition: unknown, code: number, message?: string): asserts condition; equal(a: unknown, b: unknown, code: number, message?: string): void; notEqual(a: unknown, b: unknown, code: number, message?: string): void; strictEqual(a: unknown, b: T, code: number, message?: string): asserts a is T; notStrictEqual(a: unknown, b: unknown, code: number, message?: string): void; deepEqual(a: unknown, b: unknown, code: number, message?: string): void; notDeepEqual(a: unknown, b: unknown, code: number, message?: string): void; } interface FastifyInstance { assert: Assert; to(to: Promise): Promise>; httpErrors: HttpErrors; } interface FastifyReply extends fastifySensible.HttpErrorReplys { vary: { (field: string | string[]): void; append: (header: string, field: string | string[]) => string; }; cacheControl(type: singleValueTypes): this cacheControl(type: multiValueTypes, time: number | string): this preventCache(): this maxAge(type: number | string): this revalidate(): this staticCache(time: number | string): this stale(type: staleTypes, time: number | string): this } interface FastifyRequest { forwarded(): string[]; is(types: Array): string | false | null; is(...types: Array): string | false | null; } } declare namespace fastifySensible { export interface FastifySensibleOptions { /** * This option registers a shared JSON Schema to be used by all response schemas. * * @example * ```js * fastify.register(require('@fastify/sensible'), { * sharedSchemaId: 'HttpError' * }) * * fastify.get('/async', { * schema: { * response: { 404: { $ref: 'HttpError' } } * } * handler: async (req, reply) => { * return reply.notFound() * } * }) * ``` */ sharedSchemaId?: string | undefined; } export { HttpError } export type HttpErrors = Errors.HttpErrors export type HttpErrorCodes = Errors.HttpErrorCodes export type HttpErrorCodesLoose = Errors.HttpErrorCodesLoose export type HttpErrorNames = Errors.HttpErrorNames export type HttpErrorTypes = Errors.HttpErrorTypes export const httpErrors: typeof Errors.default export type HttpErrorReplys = { getHttpError: (code: HttpErrorCodesLoose, message?: string) => FastifyReply; } & { [Property in keyof HttpErrorTypes]: (msg?: string) => FastifyReply } export const fastifySensible: FastifySensible export { fastifySensible as default } } declare function fastifySensible (...params: Parameters): ReturnType export = fastifySensible ================================================ FILE: types/index.test-d.ts ================================================ import { expectType, expectAssignable, expectError, expectNotAssignable } from 'tsd' import fastify from 'fastify' import fastifySensible, { FastifySensibleOptions, httpErrors, HttpError } from '..' const app = fastify() app.register(fastifySensible) expectAssignable({}) expectAssignable({ sharedSchemaId: 'HttpError' }) expectAssignable({ sharedSchemaId: undefined }) expectNotAssignable({ notSharedSchemaId: 'HttpError' }) app.get('/', (_req, reply) => { expectAssignable(reply.badRequest()) expectAssignable(reply.unauthorized()) expectAssignable(reply.paymentRequired()) expectAssignable(reply.forbidden()) expectAssignable(reply.notFound()) expectAssignable(reply.methodNotAllowed()) expectAssignable(reply.notAcceptable()) expectAssignable(reply.proxyAuthenticationRequired()) expectAssignable(reply.requestTimeout()) expectAssignable(reply.gone()) expectAssignable(reply.lengthRequired()) expectAssignable(reply.preconditionFailed()) expectAssignable(reply.payloadTooLarge()) expectAssignable(reply.uriTooLong()) expectAssignable(reply.unsupportedMediaType()) expectAssignable(reply.rangeNotSatisfiable()) expectAssignable(reply.expectationFailed()) expectAssignable(reply.imateapot()) expectAssignable(reply.unprocessableEntity()) expectAssignable(reply.locked()) expectAssignable(reply.failedDependency()) expectAssignable(reply.tooEarly()) expectAssignable(reply.upgradeRequired()) expectAssignable(reply.preconditionFailed()) expectAssignable(reply.tooManyRequests()) expectAssignable(reply.requestHeaderFieldsTooLarge()) expectAssignable(reply.unavailableForLegalReasons()) expectAssignable(reply.internalServerError()) expectAssignable(reply.notImplemented()) expectAssignable(reply.badGateway()) expectAssignable(reply.serviceUnavailable()) expectAssignable(reply.gatewayTimeout()) expectAssignable(reply.httpVersionNotSupported()) expectAssignable(reply.variantAlsoNegotiates()) expectAssignable(reply.insufficientStorage()) expectAssignable(reply.loopDetected()) expectAssignable(reply.bandwidthLimitExceeded()) expectAssignable(reply.notExtended()) expectAssignable(reply.networkAuthenticationRequired()) }) app.get('/', (_req, reply) => { expectAssignable(reply.getHttpError(405, 'Method Not Allowed')) expectAssignable(reply.getHttpError('405', 'Method Not Allowed')) }) app.get('/', () => { expectAssignable(app.httpErrors.createError(405, 'Method Not Allowed')) }) app.get('/', () => { expectAssignable( app.httpErrors.createError(405, 'Method Not Allowed') ) expectAssignable( app.httpErrors.createError(405, 'Method Not Allowed') ) expectAssignable>(app.httpErrors.badRequest()) }) app.get('/', async () => { expectAssignable>(app.httpErrors.badRequest()) expectAssignable>(app.httpErrors.unauthorized()) expectAssignable>(app.httpErrors.paymentRequired()) expectAssignable>(app.httpErrors.forbidden()) expectAssignable>(app.httpErrors.notFound()) expectAssignable>(app.httpErrors.methodNotAllowed()) expectAssignable>(app.httpErrors.notAcceptable()) expectAssignable>(app.httpErrors.proxyAuthenticationRequired()) expectAssignable>(app.httpErrors.requestTimeout()) expectAssignable>(app.httpErrors.gone()) expectAssignable>(app.httpErrors.lengthRequired()) expectAssignable>(app.httpErrors.preconditionFailed()) expectAssignable>(app.httpErrors.payloadTooLarge()) expectAssignable>(app.httpErrors.uriTooLong()) expectAssignable>(app.httpErrors.unsupportedMediaType()) expectAssignable>(app.httpErrors.rangeNotSatisfiable()) expectAssignable>(app.httpErrors.expectationFailed()) expectAssignable>(app.httpErrors.imateapot()) expectAssignable>(app.httpErrors.unprocessableEntity()) expectAssignable>(app.httpErrors.locked()) expectAssignable>(app.httpErrors.failedDependency()) expectAssignable>(app.httpErrors.tooEarly()) expectAssignable>(app.httpErrors.upgradeRequired()) expectAssignable>(app.httpErrors.tooManyRequests()) expectAssignable>(app.httpErrors.requestHeaderFieldsTooLarge()) expectAssignable>(app.httpErrors.unavailableForLegalReasons()) expectAssignable>(app.httpErrors.internalServerError()) expectAssignable>(app.httpErrors.notImplemented()) expectAssignable>(app.httpErrors.badGateway()) expectAssignable>(app.httpErrors.serviceUnavailable()) expectAssignable>(app.httpErrors.gatewayTimeout()) expectAssignable>(app.httpErrors.httpVersionNotSupported()) expectAssignable>(app.httpErrors.variantAlsoNegotiates()) expectAssignable>(app.httpErrors.insufficientStorage()) expectAssignable>(app.httpErrors.loopDetected()) expectAssignable>(app.httpErrors.bandwidthLimitExceeded()) expectAssignable>(app.httpErrors.notExtended()) expectAssignable>(app.httpErrors.networkAuthenticationRequired()) }) app.get('/', async () => { expectError(app.assert(true)) expectType(app.assert(1, 400, 'Bad number')) expectType(app.assert.ok(true, 400)) expectType(app.assert.equal(1, 1, 400)) expectType(app.assert.notEqual(1, 2, 400)) expectType(app.assert.strictEqual(1, 1, 400)) expectType(app.assert.notStrictEqual(1, 2, 400)) expectType(app.assert.deepEqual({}, {}, 400)) expectType(app.assert.notDeepEqual({}, { a: 1 }, 400)) }) app.get('/', async () => { expectType>(app.to(new Promise(resolve => resolve()))) }) app.get('/', (_req, reply) => { expectAssignable(reply.cacheControl('public')) }) app.get('/', (_req, reply) => { expectAssignable(reply.preventCache()) }) app.get('/', (_req, reply) => { expectAssignable(reply.cacheControl('max-age', 42)) }) app.get('/', (_req, reply) => { expectError(reply.cacheControl('foobar')) }) app.get('/', (_req, reply) => { expectAssignable(reply.stale('while-revalidate', 42)) }) app.get('/', async (_req, reply) => { expectType(reply.vary('test')) expectType(reply.vary(['test'])) expectType(reply.vary.append('X-Header', 'field1')) expectType(reply.vary.append('X-Header', ['field1'])) }) app.get('/', async (req) => { expectType(req.forwarded()) expectType(req.is(['foo', 'bar'])) expectType(req.is('foo', 'bar')) }) httpErrors.forbidden('This type should be also available') httpErrors.createError('MyError')