├── .borp.yaml ├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.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 /.borp.yaml: -------------------------------------------------------------------------------- 1 | files: 2 | - 'test/**/*.test.js' 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set 2 | * text=auto 3 | 4 | # Require Unix line endings 5 | * text eol=lf 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tomas Della Vedova and Fastify Collaborators 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/sensible 2 | 3 | [![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) 4 | [![NPM version](https://img.shields.io/npm/v/@fastify/sensible.svg?style=flat)](https://www.npmjs.com/package/@fastify/sensible) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | Defaults for Fastify that everyone can agree on™.
8 | This plugin adds some useful utilities to your Fastify instance, see the API section to learn more. 9 | 10 | *Why are these APIs here and not included with Fastify?
11 | Because Fastify aims to be as small and focused as possible, every utility that is not essential should be shipped as a standalone plugin.* 12 | 13 | ## Install 14 | ``` 15 | npm i @fastify/sensible 16 | ``` 17 | 18 | ### Compatibility 19 | 20 | | Plugin version | Fastify version | 21 | | -------------- | --------------- | 22 | | `>=6.x` | `^5.x` | 23 | | `^5.x` | `^4.x` | 24 | | `^4.x` | `^3.x` | 25 | | `>=2.x <4.x` | `^2.x` | 26 | | `^1.x` | `^1.x` | 27 | 28 | 29 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 30 | in the table above. 31 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 32 | 33 | ## Usage 34 | ```js 35 | const fastify = require('fastify')() 36 | fastify.register(require('@fastify/sensible')) 37 | 38 | fastify.get('/', (req, reply) => { 39 | reply.notFound() 40 | }) 41 | 42 | fastify.get('/async', async (req, reply) => { 43 | throw fastify.httpErrors.notFound() 44 | }) 45 | 46 | fastify.get('/async-return', async (req, reply) => { 47 | return reply.notFound() 48 | }) 49 | 50 | fastify.listen({ port: 3000 }) 51 | ``` 52 | 53 | ## Shared JSON Schema for HTTP errors 54 | If you set the `sharedSchemaId` option, a shared JSON Schema is added and can be used in your routes. 55 | ```js 56 | const fastify = require('fastify')() 57 | fastify.register(require('@fastify/sensible'), { 58 | sharedSchemaId: 'HttpError' 59 | }) 60 | 61 | fastify.get('/async', { 62 | schema: { 63 | response: { 64 | 404: { $ref: 'HttpError' } 65 | } 66 | }, 67 | handler: async (req, reply) => { 68 | return reply.notFound() 69 | } 70 | }) 71 | 72 | fastify.listen({ port: 3000 }) 73 | ``` 74 | 75 | ## API 76 | #### `fastify.httpErrors` 77 | Object that exposes `createError` and all of the `4xx` and `5xx` error constructors. 78 | 79 | 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): 80 | 81 | ```js 82 | // the custom message is optional 83 | const notFoundErr = fastify.httpErrors.notFound('custom message') 84 | ``` 85 | 86 | `4xx` 87 | - fastify.httpErrors.badRequest() 88 | - fastify.httpErrors.unauthorized() 89 | - fastify.httpErrors.paymentRequired() 90 | - fastify.httpErrors.forbidden() 91 | - fastify.httpErrors.notFound() 92 | - fastify.httpErrors.methodNotAllowed() 93 | - fastify.httpErrors.notAcceptable() 94 | - fastify.httpErrors.proxyAuthenticationRequired() 95 | - fastify.httpErrors.requestTimeout() 96 | - fastify.httpErrors.conflict() 97 | - fastify.httpErrors.gone() 98 | - fastify.httpErrors.lengthRequired() 99 | - fastify.httpErrors.preconditionFailed() 100 | - fastify.httpErrors.payloadTooLarge() 101 | - fastify.httpErrors.uriTooLong() 102 | - fastify.httpErrors.unsupportedMediaType() 103 | - fastify.httpErrors.rangeNotSatisfiable() 104 | - fastify.httpErrors.expectationFailed() 105 | - fastify.httpErrors.imateapot() 106 | - fastify.httpErrors.misdirectedRequest() 107 | - fastify.httpErrors.unprocessableEntity() 108 | - fastify.httpErrors.locked() 109 | - fastify.httpErrors.failedDependency() 110 | - fastify.httpErrors.tooEarly() 111 | - fastify.httpErrors.upgradeRequired() 112 | - fastify.httpErrors.preconditionRequired() 113 | - fastify.httpErrors.tooManyRequests() 114 | - fastify.httpErrors.requestHeaderFieldsTooLarge() 115 | - fastify.httpErrors.unavailableForLegalReasons() 116 | 117 | `5xx` 118 | - fastify.httpErrors.internalServerError() 119 | - fastify.httpErrors.notImplemented() 120 | - fastify.httpErrors.badGateway() 121 | - fastify.httpErrors.serviceUnavailable() 122 | - fastify.httpErrors.gatewayTimeout() 123 | - fastify.httpErrors.httpVersionNotSupported() 124 | - fastify.httpErrors.variantAlsoNegotiates() 125 | - fastify.httpErrors.insufficientStorage() 126 | - fastify.httpErrors.loopDetected() 127 | - fastify.httpErrors.bandwidthLimitExceeded() 128 | - fastify.httpErrors.notExtended() 129 | - fastify.httpErrors.networkAuthenticationRequired() 130 | 131 | `createError` 132 | 133 | 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): 134 | 135 | ```js 136 | const err = fastify.httpErrors.createError(404, 'This video does not exist!') 137 | ``` 138 | 139 | #### `reply.[httpError]` 140 | The `reply` interface is decorated with all of the functions declared above, using it is easy: 141 | ```js 142 | fastify.get('/', (req, reply) => { 143 | reply.notFound() 144 | }) 145 | ``` 146 | 147 | #### `reply.vary` 148 | 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. 149 | ```js 150 | fastify.get('/', (req, reply) => { 151 | reply.vary('Accept') 152 | reply.send('ok') 153 | }) 154 | ``` 155 | 156 | #### `reply.cacheControl` 157 | The `reply` interface is decorated with a helper to configure cache control response headers. 158 | ```js 159 | // configure a single type 160 | fastify.get('/', (req, reply) => { 161 | reply.cacheControl('public') 162 | reply.send('ok') 163 | }) 164 | 165 | // configure multiple types 166 | fastify.get('/', (req, reply) => { 167 | reply.cacheControl('public') 168 | reply.cacheControl('immutable') 169 | reply.send('ok') 170 | }) 171 | 172 | // configure a type time 173 | fastify.get('/', (req, reply) => { 174 | reply.cacheControl('max-age', 42) 175 | reply.send('ok') 176 | }) 177 | 178 | // the time can be defined as string 179 | fastify.get('/', (req, reply) => { 180 | // all the formats of github.com/vercel/ms are supported 181 | reply.cacheControl('max-age', '1d') // will set to 'max-age=86400' 182 | reply.send('ok') 183 | }) 184 | ``` 185 | 186 | #### `reply.preventCache` 187 | The `reply` interface is decorated with a helper to set the cache control header to a no caching configuration. 188 | ```js 189 | fastify.get('/', (req, reply) => { 190 | // will set cache-control to 'no-store, max-age=0, private' 191 | // and for HTTP/1.0 compatibility 192 | // will set pragma to 'no-cache' and expires to 0 193 | reply.preventCache() 194 | reply.send('ok') 195 | }) 196 | ``` 197 | 198 | #### `reply.revalidate` 199 | The `reply` interface is decorated with a helper to set the cache control header to a no caching configuration. 200 | ```js 201 | fastify.get('/', (req, reply) => { 202 | reply.revalidate() // will set to 'max-age=0, must-revalidate' 203 | reply.send('ok') 204 | }) 205 | ``` 206 | 207 | #### `reply.staticCache` 208 | The `reply` interface is decorated with a helper to set the cache control header to a public and immutable configuration. 209 | ```js 210 | fastify.get('/', (req, reply) => { 211 | // the time can be defined as a string 212 | reply.staticCache(42) // will set to 'public, max-age=42, immutable' 213 | reply.send('ok') 214 | }) 215 | ``` 216 | 217 | #### `reply.stale` 218 | The `reply` interface is decorated with a helper to set the cache control header for [stale content](https://tools.ietf.org/html/rfc5861). 219 | ```js 220 | fastify.get('/', (req, reply) => { 221 | // the time can be defined as a string 222 | reply.stale('while-revalidate', 42) 223 | reply.stale('if-error', 1) 224 | reply.send('ok') 225 | }) 226 | ``` 227 | 228 | #### `reply.maxAge` 229 | 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/). 230 | ```js 231 | fastify.get('/', (req, reply) => { 232 | // the time can be defined as a string 233 | reply.maxAge(86400) 234 | reply.stale('while-revalidate', 42) 235 | reply.send('ok') 236 | }) 237 | ``` 238 | 239 | #### `request.forwarded` 240 | 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: 241 | ```js 242 | fastify.get('/', (req, reply) => { 243 | reply.send(req.forwarded()) 244 | }) 245 | ``` 246 | 247 | #### `request.is` 248 | 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: 249 | ```js 250 | fastify.get('/', (req, reply) => { 251 | reply.send(req.is(['html', 'json'])) 252 | }) 253 | ``` 254 | 255 | #### `assert` 256 | Verify if a given condition is true, if not it throws the specified http error.
Useful if you work with *async* routes: 257 | ```js 258 | // the custom message is optional 259 | fastify.assert( 260 | req.headers.authorization, 400, 'Missing authorization header' 261 | ) 262 | ``` 263 | The `assert` API also exposes the following methods: 264 | - fastify.assert.ok() 265 | - fastify.assert.equal() 266 | - fastify.assert.notEqual() 267 | - fastify.assert.strictEqual() 268 | - fastify.assert.notStrictEqual() 269 | - fastify.assert.deepEqual() 270 | - fastify.assert.notDeepEqual() 271 | 272 | #### `to` 273 | Async await wrapper for easy error handling without try-catch, inspired by [`await-to-js`](https://github.com/scopsy/await-to-js): 274 | 275 | ```js 276 | const [err, user] = await fastify.to( 277 | db.findOne({ user: 'tyrion' }) 278 | ) 279 | ``` 280 | 281 | ## Contributing 282 | Do you feel there is some utility that *everyone can agree on* that is not present?
283 | Open an issue and let's discuss it! Even better a pull request! 284 | 285 | ## Acknowledgments 286 | 287 | 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. 288 | 289 | ## License 290 | 291 | Licensed under [MIT](./LICENSE). 292 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | // External utilities 5 | const forwarded = require('forwarded') 6 | const typeis = require('type-is') 7 | // Internals Utilities 8 | const httpErrors = require('./lib/httpErrors') 9 | const assert = require('./lib/assert') 10 | const vary = require('./lib/vary') 11 | const cache = require('./lib/cache-control') 12 | 13 | function fastifySensible (fastify, opts, next) { 14 | fastify.decorate('httpErrors', httpErrors) 15 | fastify.decorate('assert', assert) 16 | fastify.decorate('to', to) 17 | 18 | fastify.decorateRequest('forwarded', function () { 19 | return forwarded(this.raw) 20 | }) 21 | 22 | fastify.decorateRequest('is', function (types) { 23 | return typeis(this.raw, Array.isArray(types) ? types : [types]) 24 | }) 25 | 26 | fastify.decorateReply('vary', vary) 27 | fastify.decorateReply('cacheControl', cache.cacheControl) 28 | fastify.decorateReply('preventCache', cache.preventCache) 29 | fastify.decorateReply('revalidate', cache.revalidate) 30 | fastify.decorateReply('staticCache', cache.staticCache) 31 | fastify.decorateReply('stale', cache.stale) 32 | fastify.decorateReply('maxAge', cache.maxAge) 33 | 34 | const httpErrorsKeys = Object.keys(httpErrors) 35 | for (let i = 0; i < httpErrorsKeys.length; ++i) { 36 | const httpError = httpErrorsKeys[i] 37 | 38 | switch (httpError) { 39 | case 'HttpError': 40 | // skip abstract class constructor 41 | break 42 | case 'getHttpError': 43 | fastify.decorateReply('getHttpError', function (errorCode, message) { 44 | this.send(httpErrors.getHttpError(errorCode, message)) 45 | return this 46 | }) 47 | break 48 | default: 49 | fastify.decorateReply(httpError, function (message) { 50 | this.send(httpErrors[httpError](message)) 51 | return this 52 | }) 53 | } 54 | } 55 | 56 | if (opts?.sharedSchemaId) { 57 | // The schema must be the same as: 58 | // https://github.com/fastify/fastify/blob/c08b67e0bfedc9935b51c787ae4cd6b250ad303c/build/build-error-serializer.js#L8-L16 59 | fastify.addSchema({ 60 | $id: opts.sharedSchemaId, 61 | type: 'object', 62 | properties: { 63 | statusCode: { type: 'number' }, 64 | code: { type: 'string' }, 65 | error: { type: 'string' }, 66 | message: { type: 'string' } 67 | } 68 | }) 69 | } 70 | 71 | function to (promise) { 72 | return promise.then(data => [null, data], err => [err, undefined]) 73 | } 74 | 75 | next() 76 | } 77 | 78 | module.exports = fp(fastifySensible, { 79 | name: '@fastify/sensible', 80 | fastify: '5.x' 81 | }) 82 | module.exports.default = fastifySensible 83 | module.exports.fastifySensible = fastifySensible 84 | module.exports.httpErrors = httpErrors 85 | module.exports.HttpError = httpErrors.HttpError 86 | -------------------------------------------------------------------------------- /lib/assert.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eqeqeq */ 2 | 'use strict' 3 | 4 | const { dequal: deepEqual } = require('dequal') 5 | const { getHttpError } = require('./httpErrors') 6 | 7 | function assert (condition, code, message) { 8 | if (condition) return 9 | throw getHttpError(code, message) 10 | } 11 | 12 | assert.ok = assert 13 | 14 | assert.equal = function (a, b, code, message) { 15 | assert(a == b, code, message) 16 | } 17 | 18 | assert.notEqual = function (a, b, code, message) { 19 | assert(a != b, code, message) 20 | } 21 | 22 | assert.strictEqual = function (a, b, code, message) { 23 | assert(a === b, code, message) 24 | } 25 | 26 | assert.notStrictEqual = function (a, b, code, message) { 27 | assert(a !== b, code, message) 28 | } 29 | 30 | assert.deepEqual = function (a, b, code, message) { 31 | assert(deepEqual(a, b), code, message) 32 | } 33 | 34 | assert.notDeepEqual = function (a, b, code, message) { 35 | assert(!deepEqual(a, b), code, message) 36 | } 37 | 38 | module.exports = assert 39 | -------------------------------------------------------------------------------- /lib/cache-control.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Cache control header utilities, for more info see: 4 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control 5 | // Useful reads: 6 | // - https://odino.org/http-cache-101-scaling-the-web/ 7 | // - https://web.dev/stale-while-revalidate/ 8 | // - https://csswizardry.com/2019/03/cache-control-for-civilians/ 9 | // - https://jakearchibald.com/2016/caching-best-practices/ 10 | 11 | const assert = require('node:assert') 12 | const ms = require('@lukeed/ms').parse 13 | 14 | const validSingletimes = [ 15 | 'must-revalidate', 16 | 'no-cache', 17 | 'no-store', 18 | 'no-transform', 19 | 'public', 20 | 'private', 21 | 'proxy-revalidate', 22 | 'immutable' 23 | ] 24 | 25 | const validMultitimes = [ 26 | 'max-age', 27 | 's-maxage', 28 | 'stale-while-revalidate', 29 | 'stale-if-error' 30 | ] 31 | 32 | function cacheControl (type, time) { 33 | const previoustime = this.getHeader('Cache-Control') 34 | if (time == null) { 35 | assert(validSingletimes.indexOf(type) !== -1, `Invalid Cache Control type: ${type}`) 36 | this.header('Cache-Control', previoustime ? `${previoustime}, ${type}` : type) 37 | } else { 38 | if (typeof time === 'string') { 39 | time = ms(time) / 1000 40 | } 41 | assert(validMultitimes.indexOf(type) !== -1, `Invalid Cache Control type: ${type}`) 42 | assert(typeof time === 'number', 'The cache control time should be a number') 43 | this.header('Cache-Control', previoustime ? `${previoustime}, ${type}=${time}` : `${type}=${time}`) 44 | } 45 | return this 46 | } 47 | 48 | function preventCache () { 49 | this 50 | .header('Cache-Control', 'no-store, max-age=0, private') 51 | // compatibility support for HTTP/1.0 52 | // see: https://owasp.org/www-community/OWASP_Application_Security_FAQ#how-do-i-ensure-that-sensitive-pages-are-not-cached-on-the-users-browser 53 | .header('Pragma', 'no-cache') 54 | .header('Expires', 0) 55 | 56 | return this 57 | } 58 | 59 | function maxAge (time) { 60 | return this.cacheControl('max-age', time) 61 | } 62 | 63 | function revalidate () { 64 | this.header('Cache-Control', 'max-age=0, must-revalidate') 65 | return this 66 | } 67 | 68 | function staticCache (time) { 69 | if (typeof time === 'string') { 70 | time = ms(time) / 1000 71 | } 72 | assert(typeof time === 'number', 'The cache control time should be a number') 73 | this.header('Cache-Control', `public, max-age=${time}, immutable`) 74 | return this 75 | } 76 | 77 | function stale (type, time) { 78 | if (type === 'while-revalidate') { 79 | return this.cacheControl('stale-while-revalidate', time) 80 | } else if (type === 'if-error') { 81 | return this.cacheControl('stale-if-error', time) 82 | } else { 83 | throw new Error(`Invalid cache control stale time ${time}`) 84 | } 85 | } 86 | 87 | module.exports = { 88 | cacheControl, 89 | preventCache, 90 | revalidate, 91 | staticCache, 92 | stale, 93 | maxAge 94 | } 95 | -------------------------------------------------------------------------------- /lib/httpError.d.ts: -------------------------------------------------------------------------------- 1 | export declare class HttpError extends Error { 2 | status: N 3 | statusCode: N 4 | expose: boolean 5 | message: string 6 | headers?: { 7 | [key: string]: string; 8 | }; 9 | 10 | [key: string]: any; 11 | } 12 | 13 | type UnknownError = Error | string | number | { [key: string]: any } 14 | 15 | export type HttpErrorTypes = { 16 | badRequest: 400, 17 | unauthorized: 401, 18 | paymentRequired: 402, 19 | forbidden: 403, 20 | notFound: 404, 21 | methodNotAllowed: 405, 22 | notAcceptable: 406, 23 | proxyAuthenticationRequired: 407, 24 | requestTimeout: 408, 25 | conflict: 409, 26 | gone: 410, 27 | lengthRequired: 411, 28 | preconditionFailed: 412, 29 | payloadTooLarge: 413, 30 | uriTooLong: 414, 31 | unsupportedMediaType: 415, 32 | rangeNotSatisfiable: 416, 33 | expectationFailed: 417, 34 | imateapot: 418, 35 | misdirectedRequest: 421, 36 | unprocessableEntity: 422, 37 | locked: 423, 38 | failedDependency: 424, 39 | tooEarly: 425, 40 | upgradeRequired: 426, 41 | preconditionRequired: 428, 42 | tooManyRequests: 429, 43 | requestHeaderFieldsTooLarge: 431, 44 | unavailableForLegalReasons: 451, 45 | internalServerError: 500, 46 | notImplemented: 501, 47 | badGateway: 502, 48 | serviceUnavailable: 503, 49 | gatewayTimeout: 504, 50 | httpVersionNotSupported: 505, 51 | variantAlsoNegotiates: 506, 52 | insufficientStorage: 507, 53 | loopDetected: 508, 54 | bandwidthLimitExceeded: 509, 55 | notExtended: 510 56 | networkAuthenticationRequired: 511 57 | } 58 | 59 | type ValueOf = ObjectType[ValueType] 60 | 61 | export type HttpErrorNames = keyof HttpErrorTypes 62 | export type HttpErrorCodes = ValueOf 63 | // Permissive type for getHttpError lookups 64 | export type HttpErrorCodesLoose = HttpErrorCodes | `${HttpErrorCodes}` 65 | // Helper to go from stringified error codes back to numeric 66 | type AsCode = T extends `${infer N extends HttpErrorCodes}` ? N : never 67 | 68 | export type HttpErrors = { 69 | HttpError: typeof HttpError; 70 | getHttpError: (code: T, message?: string) => HttpError>; 71 | createError: (...args: UnknownError[]) => HttpError; 72 | } & { 73 | [Property in keyof HttpErrorTypes]: (...args: UnknownError[]) => HttpError 74 | } 75 | 76 | // eslint-disable-next-line @typescript-eslint/no-redeclare 77 | declare const HttpErrors: HttpErrors 78 | export default HttpErrors 79 | -------------------------------------------------------------------------------- /lib/httpErrors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const createError = require('http-errors') 4 | const statusCodes = require('node:http').STATUS_CODES 5 | 6 | const statusCodesMap = Object.assign({}, statusCodes) 7 | Object.keys(statusCodesMap).forEach(code => { 8 | statusCodesMap[code] = normalize(code, statusCodesMap[code]) 9 | }) 10 | 11 | function normalize (code, msg) { 12 | if (code === '414') return 'uriTooLong' 13 | if (code === '505') return 'httpVersionNotSupported' 14 | msg = msg.split(' ').join('').replace(/'/g, '') 15 | msg = msg[0].toLowerCase() + msg.slice(1) 16 | return msg 17 | } 18 | 19 | const httpErrors = { 20 | badRequest: function badRequest (message) { 21 | return new createError.BadRequest(message) 22 | }, 23 | 24 | unauthorized: function unauthorized (message) { 25 | return new createError.Unauthorized(message) 26 | }, 27 | 28 | paymentRequired: function paymentRequired (message) { 29 | return new createError.PaymentRequired(message) 30 | }, 31 | 32 | forbidden: function forbidden (message) { 33 | return new createError.Forbidden(message) 34 | }, 35 | 36 | notFound: function notFound (message) { 37 | return new createError.NotFound(message) 38 | }, 39 | 40 | methodNotAllowed: function methodNotAllowed (message) { 41 | return new createError.MethodNotAllowed(message) 42 | }, 43 | 44 | notAcceptable: function notAcceptable (message) { 45 | return new createError.NotAcceptable(message) 46 | }, 47 | 48 | proxyAuthenticationRequired: function proxyAuthenticationRequired (message) { 49 | return new createError.ProxyAuthenticationRequired(message) 50 | }, 51 | 52 | requestTimeout: function requestTimeout (message) { 53 | return new createError.RequestTimeout(message) 54 | }, 55 | 56 | conflict: function conflict (message) { 57 | return new createError.Conflict(message) 58 | }, 59 | 60 | gone: function gone (message) { 61 | return new createError.Gone(message) 62 | }, 63 | 64 | lengthRequired: function lengthRequired (message) { 65 | return new createError.LengthRequired(message) 66 | }, 67 | 68 | preconditionFailed: function preconditionFailed (message) { 69 | return new createError.PreconditionFailed(message) 70 | }, 71 | 72 | payloadTooLarge: function payloadTooLarge (message) { 73 | return new createError.PayloadTooLarge(message) 74 | }, 75 | 76 | uriTooLong: function uriTooLong (message) { 77 | return new createError.URITooLong(message) 78 | }, 79 | 80 | unsupportedMediaType: function unsupportedMediaType (message) { 81 | return new createError.UnsupportedMediaType(message) 82 | }, 83 | 84 | rangeNotSatisfiable: function rangeNotSatisfiable (message) { 85 | return new createError.RangeNotSatisfiable(message) 86 | }, 87 | 88 | expectationFailed: function expectationFailed (message) { 89 | return new createError.ExpectationFailed(message) 90 | }, 91 | 92 | imateapot: function imateapot (message) { 93 | return new createError.ImATeapot(message) 94 | }, 95 | 96 | misdirectedRequest: function misdirectedRequest (message) { 97 | return new createError.MisdirectedRequest(message) 98 | }, 99 | 100 | unprocessableEntity: function unprocessableEntity (message) { 101 | return new createError.UnprocessableEntity(message) 102 | }, 103 | 104 | locked: function locked (message) { 105 | return new createError.Locked(message) 106 | }, 107 | 108 | failedDependency: function failedDependency (message) { 109 | return new createError.FailedDependency(message) 110 | }, 111 | 112 | tooEarly: function tooEarly (message) { 113 | return new createError.TooEarly(message) 114 | }, 115 | 116 | upgradeRequired: function upgradeRequired (message) { 117 | return new createError.UpgradeRequired(message) 118 | }, 119 | 120 | preconditionRequired: function preconditionRequired (message) { 121 | return new createError.PreconditionRequired(message) 122 | }, 123 | 124 | tooManyRequests: function tooManyRequests (message) { 125 | return new createError.TooManyRequests(message) 126 | }, 127 | 128 | requestHeaderFieldsTooLarge: function requestHeaderFieldsTooLarge (message) { 129 | return new createError.RequestHeaderFieldsTooLarge(message) 130 | }, 131 | 132 | unavailableForLegalReasons: function unavailableForLegalReasons (message) { 133 | return new createError.UnavailableForLegalReasons(message) 134 | }, 135 | 136 | internalServerError: function internalServerError (message) { 137 | return new createError.InternalServerError(message) 138 | }, 139 | 140 | notImplemented: function notImplemented (message) { 141 | return new createError.NotImplemented(message) 142 | }, 143 | 144 | badGateway: function badGateway (message) { 145 | return new createError.BadGateway(message) 146 | }, 147 | 148 | serviceUnavailable: function serviceUnavailable (message) { 149 | return new createError.ServiceUnavailable(message) 150 | }, 151 | 152 | gatewayTimeout: function gatewayTimeout (message) { 153 | return new createError.GatewayTimeout(message) 154 | }, 155 | 156 | httpVersionNotSupported: function httpVersionNotSupported (message) { 157 | return new createError.HTTPVersionNotSupported(message) 158 | }, 159 | 160 | variantAlsoNegotiates: function variantAlsoNegotiates (message) { 161 | return new createError.VariantAlsoNegotiates(message) 162 | }, 163 | 164 | insufficientStorage: function insufficientStorage (message) { 165 | return new createError.InsufficientStorage(message) 166 | }, 167 | 168 | loopDetected: function loopDetected (message) { 169 | return new createError.LoopDetected(message) 170 | }, 171 | 172 | bandwidthLimitExceeded: function bandwidthLimitExceeded (message) { 173 | return new createError.BandwidthLimitExceeded(message) 174 | }, 175 | 176 | notExtended: function notExtended (message) { 177 | return new createError.NotExtended(message) 178 | }, 179 | 180 | networkAuthenticationRequired: function networkAuthenticationRequired (message) { 181 | return new createError.NetworkAuthenticationRequired(message) 182 | } 183 | } 184 | 185 | function getHttpError (code, message) { 186 | return httpErrors[statusCodesMap[code + '']](message) 187 | } 188 | 189 | module.exports = httpErrors 190 | module.exports.getHttpError = getHttpError 191 | module.exports.HttpError = createError.HttpError 192 | module.exports.createError = createError 193 | -------------------------------------------------------------------------------- /lib/vary.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const append = require('vary').append 4 | 5 | // Same implementation of https://github.com/jshttp/vary 6 | // but adapted to the Fastify API 7 | function vary (field) { 8 | let value = this.getHeader('Vary') || '' 9 | const header = Array.isArray(value) 10 | ? value.join(', ') 11 | : String(value) 12 | 13 | // set new header 14 | value = append(header, field) 15 | this.header('Vary', value) 16 | } 17 | 18 | module.exports = vary 19 | module.exports.append = append 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/sensible", 3 | "version": "6.0.3", 4 | "description": "Defaults for Fastify that everyone can agree on", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "npm run test:unit && npm run test:typescript", 12 | "test:typescript": "tsd", 13 | "test:unit": "borp -C --check-coverage --reporter=@jsumners/line-reporter" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fastify/fastify-sensible.git" 18 | }, 19 | "keywords": [ 20 | "fastify", 21 | "http", 22 | "defaults", 23 | "helper" 24 | ], 25 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 26 | "contributors": [ 27 | { 28 | "name": "Matteo Collina", 29 | "email": "hello@matteocollina.com" 30 | }, 31 | { 32 | "name": "Manuel Spigolon", 33 | "email": "behemoth89@gmail.com" 34 | }, 35 | { 36 | "name": "Cemre Mengu", 37 | "email": "cemremengu@gmail.com" 38 | }, 39 | { 40 | "name": "Frazer Smith", 41 | "email": "frazer.dev@icloud.com", 42 | "url": "https://github.com/fdawgs" 43 | } 44 | ], 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/fastify/fastify-sensible/issues" 48 | }, 49 | "homepage": "https://github.com/fastify/fastify-sensible#readme", 50 | "funding": [ 51 | { 52 | "type": "github", 53 | "url": "https://github.com/sponsors/fastify" 54 | }, 55 | { 56 | "type": "opencollective", 57 | "url": "https://opencollective.com/fastify" 58 | } 59 | ], 60 | "devDependencies": { 61 | "@fastify/pre-commit": "^2.1.0", 62 | "@jsumners/line-reporter": "^1.0.1", 63 | "@types/node": "^22.0.0", 64 | "borp": "^0.20.0", 65 | "eslint": "^9.17.0", 66 | "fastify": "^5.0.0", 67 | "neostandard": "^0.12.0", 68 | "tsd": "^0.32.0" 69 | }, 70 | "dependencies": { 71 | "@lukeed/ms": "^2.0.2", 72 | "dequal": "^2.0.3", 73 | "fastify-plugin": "^5.0.0", 74 | "forwarded": "^0.2.0", 75 | "http-errors": "^2.0.0", 76 | "type-is": "^2.0.1", 77 | "vary": "^1.1.2" 78 | }, 79 | "publishConfig": { 80 | "access": "public" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/assert.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | 5 | const Fastify = require('fastify') 6 | const Sensible = require('../index') 7 | 8 | test('Should support basic assert', (t, done) => { 9 | t.plan(2) 10 | const fastify = Fastify() 11 | fastify.register(Sensible) 12 | 13 | fastify.ready(err => { 14 | t.assert.ifError(err) 15 | try { 16 | fastify.assert.ok(true) 17 | t.assert.ok('Works correctly') 18 | } catch (err) { 19 | t.assert.fail(err) 20 | } 21 | done() 22 | }) 23 | }) 24 | 25 | test('Should support ok assert', (t, done) => { 26 | t.plan(2) 27 | const fastify = Fastify() 28 | fastify.register(Sensible) 29 | 30 | fastify.ready(err => { 31 | t.assert.ifError(err) 32 | try { 33 | fastify.assert.ok(true) 34 | t.assert.ok('Works correctly') 35 | } catch (err) { 36 | t.assert.fail(err) 37 | } 38 | done() 39 | }) 40 | }) 41 | 42 | test('Should support equal assert', (t, done) => { 43 | t.plan(2) 44 | const fastify = Fastify() 45 | fastify.register(Sensible) 46 | 47 | fastify.ready(err => { 48 | t.assert.ifError(err) 49 | try { 50 | fastify.assert.equal(1, '1') 51 | t.assert.ok('Works correctly') 52 | } catch (err) { 53 | t.assert.fail(err) 54 | } 55 | done() 56 | }) 57 | }) 58 | 59 | test('Should support not equal assert', (t, done) => { 60 | t.plan(2) 61 | const fastify = Fastify() 62 | fastify.register(Sensible) 63 | 64 | fastify.ready(err => { 65 | t.assert.ifError(err) 66 | try { 67 | fastify.assert.notEqual(1, '2') 68 | t.assert.ok('Works correctly') 69 | } catch (err) { 70 | t.assert.fail(err) 71 | } 72 | done() 73 | }) 74 | }) 75 | 76 | test('Should support strict equal assert', (t, done) => { 77 | t.plan(2) 78 | const fastify = Fastify() 79 | fastify.register(Sensible) 80 | 81 | fastify.ready(err => { 82 | t.assert.ifError(err) 83 | try { 84 | fastify.assert.strictEqual(1, 1) 85 | t.assert.ok('Works correctly') 86 | } catch (err) { 87 | t.assert.fail(err) 88 | } 89 | done() 90 | }) 91 | }) 92 | 93 | test('Should support not strict equal assert', (t, done) => { 94 | t.plan(2) 95 | const fastify = Fastify() 96 | fastify.register(Sensible) 97 | 98 | fastify.ready(err => { 99 | t.assert.ifError(err) 100 | try { 101 | fastify.assert.notStrictEqual(1, 2) 102 | t.assert.ok('Works correctly') 103 | } catch (err) { 104 | t.assert.fail(err) 105 | } 106 | done() 107 | }) 108 | }) 109 | 110 | test('Should support deep equal assert', (t, done) => { 111 | t.plan(2) 112 | const fastify = Fastify() 113 | fastify.register(Sensible) 114 | 115 | fastify.ready(err => { 116 | t.assert.ifError(err) 117 | try { 118 | fastify.assert.deepEqual({ a: 1 }, { a: 1 }) 119 | t.assert.ok('Works correctly') 120 | } catch (err) { 121 | t.assert.fail(err) 122 | } 123 | done() 124 | }) 125 | }) 126 | 127 | test('Should support not deep equal assert', (t, done) => { 128 | t.plan(2) 129 | const fastify = Fastify() 130 | fastify.register(Sensible) 131 | 132 | fastify.ready(err => { 133 | t.assert.ifError(err) 134 | try { 135 | fastify.assert.notDeepEqual({ hello: 'world' }, { hello: 'dlrow' }) 136 | t.assert.ok('Works correctly') 137 | } catch (err) { 138 | t.assert.fail(err) 139 | } 140 | done() 141 | }) 142 | }) 143 | 144 | test('Should support basic assert (throw)', (t, done) => { 145 | t.plan(2) 146 | const fastify = Fastify() 147 | fastify.register(Sensible) 148 | 149 | fastify.ready(err => { 150 | t.assert.ifError(err) 151 | try { 152 | fastify.assert(false) 153 | t.assert.fail('Should throw') 154 | } catch (err) { 155 | t.assert.ok(err) 156 | } 157 | done() 158 | }) 159 | }) 160 | 161 | test('Should support equal assert (throw)', (t, done) => { 162 | t.plan(2) 163 | const fastify = Fastify() 164 | fastify.register(Sensible) 165 | 166 | fastify.ready(err => { 167 | t.assert.ifError(err) 168 | try { 169 | fastify.assert.equal(1, '2') 170 | t.assert.fail('Should throw') 171 | } catch (err) { 172 | t.assert.ok(err) 173 | } 174 | done() 175 | }) 176 | }) 177 | 178 | test('Should support not equal assert (throw)', (t, done) => { 179 | t.plan(2) 180 | const fastify = Fastify() 181 | fastify.register(Sensible) 182 | 183 | fastify.ready(err => { 184 | t.assert.ifError(err) 185 | try { 186 | fastify.assert.notEqual(1, '1') 187 | t.assert.fail('Should throw') 188 | } catch (err) { 189 | t.assert.ok(err) 190 | } 191 | done() 192 | }) 193 | }) 194 | 195 | test('Should support strict equal assert (throw)', (t, done) => { 196 | t.plan(2) 197 | const fastify = Fastify() 198 | fastify.register(Sensible) 199 | 200 | fastify.ready(err => { 201 | t.assert.ifError(err) 202 | try { 203 | fastify.assert.equal(1, 2) 204 | t.assert.fail('Should throw') 205 | } catch (err) { 206 | t.assert.ok(err) 207 | } 208 | done() 209 | }) 210 | }) 211 | 212 | test('Should support not strict equal assert (throw)', (t, done) => { 213 | t.plan(2) 214 | const fastify = Fastify() 215 | fastify.register(Sensible) 216 | 217 | fastify.ready(err => { 218 | t.assert.ifError(err) 219 | try { 220 | fastify.assert.notStrictEqual(1, 1) 221 | t.assert.fail('Should throw') 222 | } catch (err) { 223 | t.assert.ok(err) 224 | } 225 | done() 226 | }) 227 | }) 228 | 229 | test('Should support deep equal assert (throw)', (t, done) => { 230 | t.plan(2) 231 | const fastify = Fastify() 232 | fastify.register(Sensible) 233 | 234 | fastify.ready(err => { 235 | t.assert.ifError(err) 236 | try { 237 | fastify.assert.deepEqual({ hello: 'world' }, { hello: 'dlrow' }) 238 | t.assert.fail('Should throw') 239 | } catch (err) { 240 | t.assert.ok(err) 241 | } 242 | done() 243 | }) 244 | }) 245 | 246 | test('Should support not deep equal assert (throw)', (t, done) => { 247 | t.plan(2) 248 | const fastify = Fastify() 249 | fastify.register(Sensible) 250 | 251 | fastify.ready(err => { 252 | t.assert.ifError(err) 253 | try { 254 | fastify.assert.notDeepEqual({ hello: 'world' }, { hello: 'world' }) 255 | t.assert.fail('Should throw') 256 | } catch (err) { 257 | t.assert.ok(err) 258 | } 259 | done() 260 | }) 261 | }) 262 | 263 | test('Should generate the correct http error', (t, done) => { 264 | t.plan(4) 265 | const fastify = Fastify() 266 | fastify.register(Sensible) 267 | 268 | fastify.ready(err => { 269 | t.assert.ifError(err) 270 | try { 271 | fastify.assert(false, 400, 'Wrong!') 272 | t.assert.fail('Should throw') 273 | } catch (err) { 274 | t.assert.strictEqual(err.message, 'Wrong!') 275 | t.assert.strictEqual(err.name, 'BadRequestError') 276 | t.assert.strictEqual(err.statusCode, 400) 277 | } 278 | done() 279 | }) 280 | }) 281 | -------------------------------------------------------------------------------- /test/cache-control.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const Sensible = require('../index') 6 | 7 | test('reply.cacheControl API', (t, done) => { 8 | t.plan(4) 9 | 10 | const fastify = Fastify() 11 | fastify.register(Sensible) 12 | 13 | fastify.get('/', (_req, reply) => { 14 | reply.cacheControl('public') 15 | reply.send('ok') 16 | }) 17 | 18 | fastify.inject({ 19 | method: 'GET', 20 | url: '/' 21 | }, (err, res) => { 22 | t.assert.ifError(err) 23 | t.assert.strictEqual(res.statusCode, 200) 24 | t.assert.strictEqual(res.headers['cache-control'], 'public') 25 | t.assert.strictEqual(res.payload, 'ok') 26 | done() 27 | }) 28 | }) 29 | 30 | test('reply.cacheControl API (multiple values)', (t, done) => { 31 | t.plan(4) 32 | 33 | const fastify = Fastify() 34 | fastify.register(Sensible) 35 | 36 | fastify.get('/', (_req, reply) => { 37 | reply 38 | .cacheControl('public') 39 | .cacheControl('max-age', 604800) 40 | .cacheControl('immutable') 41 | .send('ok') 42 | }) 43 | 44 | fastify.inject({ 45 | method: 'GET', 46 | url: '/' 47 | }, (err, res) => { 48 | t.assert.ifError(err) 49 | t.assert.strictEqual(res.statusCode, 200) 50 | t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=604800, immutable') 51 | t.assert.strictEqual(res.payload, 'ok') 52 | done() 53 | }) 54 | }) 55 | 56 | test('reply.preventCache API', (t, done) => { 57 | t.plan(6) 58 | 59 | const fastify = Fastify() 60 | fastify.register(Sensible) 61 | 62 | fastify.get('/', (_req, reply) => { 63 | reply.preventCache().send('ok') 64 | }) 65 | 66 | fastify.inject({ 67 | method: 'GET', 68 | url: '/' 69 | }, (err, res) => { 70 | t.assert.ifError(err) 71 | t.assert.strictEqual(res.statusCode, 200) 72 | t.assert.strictEqual(res.headers['cache-control'], 'no-store, max-age=0, private') 73 | t.assert.strictEqual(res.headers.pragma, 'no-cache') 74 | t.assert.strictEqual(res.headers.expires, '0') 75 | t.assert.strictEqual(res.payload, 'ok') 76 | done() 77 | }) 78 | }) 79 | 80 | test('reply.stale API', (t, done) => { 81 | t.plan(4) 82 | 83 | const fastify = Fastify() 84 | fastify.register(Sensible) 85 | 86 | fastify.get('/', (_req, reply) => { 87 | reply.stale('while-revalidate', 42).send('ok') 88 | }) 89 | 90 | fastify.inject({ 91 | method: 'GET', 92 | url: '/' 93 | }, (err, res) => { 94 | t.assert.ifError(err) 95 | t.assert.strictEqual(res.statusCode, 200) 96 | t.assert.strictEqual(res.headers['cache-control'], 'stale-while-revalidate=42') 97 | t.assert.strictEqual(res.payload, 'ok') 98 | done() 99 | }) 100 | }) 101 | 102 | test('reply.stale API (multiple values)', (t, done) => { 103 | t.plan(4) 104 | 105 | const fastify = Fastify() 106 | fastify.register(Sensible) 107 | 108 | fastify.get('/', (_req, reply) => { 109 | reply 110 | .stale('while-revalidate', 42) 111 | .stale('if-error', 1) 112 | .send('ok') 113 | }) 114 | 115 | fastify.inject({ 116 | method: 'GET', 117 | url: '/' 118 | }, (err, res) => { 119 | t.assert.ifError(err) 120 | t.assert.strictEqual(res.statusCode, 200) 121 | t.assert.strictEqual(res.headers['cache-control'], 'stale-while-revalidate=42, stale-if-error=1') 122 | t.assert.strictEqual(res.payload, 'ok') 123 | done() 124 | }) 125 | }) 126 | 127 | test('reply.stale API (bad value)', (t, done) => { 128 | t.plan(5) 129 | 130 | const fastify = Fastify() 131 | fastify.register(Sensible) 132 | 133 | fastify.get('/', (_req, reply) => { 134 | try { 135 | reply.stale('foo', 42).send('ok') 136 | t.assert.fail('Should throw') 137 | } catch (err) { 138 | t.assert.ok(err) 139 | reply.send('ok') 140 | } 141 | }) 142 | 143 | fastify.inject({ 144 | method: 'GET', 145 | url: '/' 146 | }, (err, res) => { 147 | t.assert.ifError(err) 148 | t.assert.strictEqual(res.statusCode, 200) 149 | t.assert.ok(!res.headers['cache-control']) 150 | t.assert.strictEqual(res.payload, 'ok') 151 | done() 152 | }) 153 | }) 154 | 155 | test('reply.revalidate API', (t, done) => { 156 | t.plan(4) 157 | 158 | const fastify = Fastify() 159 | fastify.register(Sensible) 160 | 161 | fastify.get('/', (_req, reply) => { 162 | reply.revalidate().send('ok') 163 | }) 164 | 165 | fastify.inject({ 166 | method: 'GET', 167 | url: '/' 168 | }, (err, res) => { 169 | t.assert.ifError(err) 170 | t.assert.strictEqual(res.statusCode, 200) 171 | t.assert.strictEqual(res.headers['cache-control'], 'max-age=0, must-revalidate') 172 | t.assert.strictEqual(res.payload, 'ok') 173 | done() 174 | }) 175 | }) 176 | 177 | test('reply.staticCache API', (t, done) => { 178 | t.plan(4) 179 | 180 | const fastify = Fastify() 181 | fastify.register(Sensible) 182 | 183 | fastify.get('/', (_req, reply) => { 184 | reply.staticCache(42).send('ok') 185 | }) 186 | 187 | fastify.inject({ 188 | method: 'GET', 189 | url: '/' 190 | }, (err, res) => { 191 | t.assert.ifError(err) 192 | t.assert.strictEqual(res.statusCode, 200) 193 | t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=42, immutable') 194 | t.assert.strictEqual(res.payload, 'ok') 195 | done() 196 | }) 197 | }) 198 | 199 | test('reply.staticCache API (as string)', (t, done) => { 200 | t.plan(4) 201 | 202 | const fastify = Fastify() 203 | fastify.register(Sensible) 204 | 205 | fastify.get('/', (_req, reply) => { 206 | reply.staticCache('42s').send('ok') 207 | }) 208 | 209 | fastify.inject({ 210 | method: 'GET', 211 | url: '/' 212 | }, (err, res) => { 213 | t.assert.ifError(err) 214 | t.assert.strictEqual(res.statusCode, 200) 215 | t.assert.strictEqual(res.headers['cache-control'], 'public, max-age=42, immutable') 216 | t.assert.strictEqual(res.payload, 'ok') 217 | done() 218 | }) 219 | }) 220 | 221 | test('reply.maxAge and reply.stale API', (t, done) => { 222 | t.plan(4) 223 | 224 | const fastify = Fastify() 225 | fastify.register(Sensible) 226 | 227 | fastify.get('/', (_req, reply) => { 228 | reply 229 | .maxAge(42) 230 | .stale('while-revalidate', 3) 231 | .send('ok') 232 | }) 233 | 234 | fastify.inject({ 235 | method: 'GET', 236 | url: '/' 237 | }, (err, res) => { 238 | t.assert.ifError(err) 239 | t.assert.strictEqual(res.statusCode, 200) 240 | t.assert.strictEqual(res.headers['cache-control'], 'max-age=42, stale-while-revalidate=3') 241 | t.assert.strictEqual(res.payload, 'ok') 242 | done() 243 | }) 244 | }) 245 | 246 | test('reply.cacheControl API (string time)', (t, done) => { 247 | t.plan(4) 248 | 249 | const fastify = Fastify() 250 | fastify.register(Sensible) 251 | 252 | fastify.get('/', (_req, reply) => { 253 | reply.cacheControl('max-age', '1d') 254 | reply.send('ok') 255 | }) 256 | 257 | fastify.inject({ 258 | method: 'GET', 259 | url: '/' 260 | }, (err, res) => { 261 | t.assert.ifError(err) 262 | t.assert.strictEqual(res.statusCode, 200) 263 | t.assert.strictEqual(res.headers['cache-control'], 'max-age=86400') 264 | t.assert.strictEqual(res.payload, 'ok') 265 | done() 266 | }) 267 | }) 268 | -------------------------------------------------------------------------------- /test/forwarded.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const Sensible = require('../index') 6 | 7 | test('request.forwarded API', (t, done) => { 8 | t.plan(3) 9 | 10 | const fastify = Fastify() 11 | fastify.register(Sensible) 12 | 13 | fastify.get('/', (req, reply) => { 14 | reply.send(req.forwarded()) 15 | }) 16 | 17 | fastify.inject({ 18 | method: 'GET', 19 | url: '/', 20 | headers: { 21 | 'x-forwarded-for': '10.0.0.2, 10.0.0.1' 22 | } 23 | }, (err, res) => { 24 | t.assert.ifError(err) 25 | t.assert.strictEqual(res.statusCode, 200) 26 | t.assert.deepStrictEqual( 27 | JSON.parse(res.payload), 28 | ['127.0.0.1', '10.0.0.1', '10.0.0.2'] 29 | ) 30 | done() 31 | }) 32 | }) 33 | 34 | test('request.forwarded API (without header)', (t, done) => { 35 | t.plan(3) 36 | 37 | const fastify = Fastify() 38 | fastify.register(Sensible) 39 | 40 | fastify.get('/', (req, reply) => { 41 | reply.send(req.forwarded()) 42 | }) 43 | 44 | fastify.inject({ 45 | method: 'GET', 46 | url: '/' 47 | }, (err, res) => { 48 | t.assert.ifError(err) 49 | t.assert.strictEqual(res.statusCode, 200) 50 | t.assert.deepStrictEqual( 51 | JSON.parse(res.payload), 52 | ['127.0.0.1'] 53 | ) 54 | done() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/httpErrors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const createError = require('http-errors') 5 | const statusCodes = require('node:http').STATUS_CODES 6 | const Fastify = require('fastify') 7 | const Sensible = require('../index') 8 | const HttpError = require('../lib/httpErrors').HttpError 9 | 10 | test('Should generate the correct http error', (t, done) => { 11 | const fastify = Fastify() 12 | fastify.register(Sensible) 13 | 14 | fastify.ready(err => { 15 | t.assert.ifError(err) 16 | 17 | Object.keys(statusCodes).forEach(code => { 18 | if (Number(code) < 400) return 19 | const name = normalize(code, statusCodes[code]) 20 | const err = fastify.httpErrors[name]() 21 | t.assert.ok(err instanceof HttpError) 22 | // `statusCodes` uses the capital T 23 | if (err.message === 'I\'m a Teapot') { 24 | t.assert.strictEqual(err.statusCode, 418) 25 | } else { 26 | t.assert.strictEqual(err.message, statusCodes[code]) 27 | } 28 | t.assert.strictEqual(typeof err.name, 'string') 29 | t.assert.strictEqual(err.statusCode, Number(code)) 30 | }) 31 | 32 | done() 33 | }) 34 | }) 35 | 36 | test('Should expose the createError method from http-errors', (t, done) => { 37 | t.plan(2) 38 | const fastify = Fastify() 39 | fastify.register(Sensible) 40 | 41 | fastify.ready(err => { 42 | t.assert.ifError(err) 43 | t.assert.strictEqual(fastify.httpErrors.createError, createError) 44 | done() 45 | }) 46 | }) 47 | 48 | test('Should generate the correct error using the properties given', (t, done) => { 49 | t.plan(5) 50 | const fastify = Fastify() 51 | fastify.register(Sensible) 52 | 53 | fastify.ready(err => { 54 | t.assert.ifError(err) 55 | const customError = fastify.httpErrors.createError(404, 'This video does not exist!') 56 | t.assert.ok(customError instanceof HttpError) 57 | t.assert.strictEqual(customError.message, 'This video does not exist!') 58 | t.assert.strictEqual(typeof customError.name, 'string') 59 | t.assert.strictEqual(customError.statusCode, 404) 60 | done() 61 | }) 62 | }) 63 | 64 | test('Should generate the correct http error (with custom message)', (t, done) => { 65 | const fastify = Fastify() 66 | fastify.register(Sensible) 67 | 68 | fastify.ready(err => { 69 | t.assert.ifError(err) 70 | 71 | Object.keys(statusCodes).forEach(code => { 72 | if (Number(code) < 400) return 73 | const name = normalize(code, statusCodes[code]) 74 | const err = fastify.httpErrors[name]('custom') 75 | t.assert.ok(err instanceof HttpError) 76 | t.assert.strictEqual(err.message, 'custom') 77 | t.assert.strictEqual(typeof err.name, 'string') 78 | t.assert.strictEqual(err.statusCode, Number(code)) 79 | }) 80 | 81 | done() 82 | }) 83 | }) 84 | 85 | test('should throw error', (t) => { 86 | const err = Sensible.httpErrors.conflict('custom') 87 | t.assert.strictEqual(err.message, 'custom') 88 | }) 89 | 90 | function normalize (code, msg) { 91 | if (code === '414') return 'uriTooLong' 92 | if (code === '418') return 'imateapot' 93 | if (code === '505') return 'httpVersionNotSupported' 94 | msg = msg.split(' ').join('').replace(/'/g, '') 95 | msg = msg[0].toLowerCase() + msg.slice(1) 96 | return msg 97 | } 98 | -------------------------------------------------------------------------------- /test/httpErrorsReply.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const statusCodes = require('node:http').STATUS_CODES 5 | const Fastify = require('fastify') 6 | const Sensible = require('../index') 7 | 8 | test('Should generate the correct http error', (t, rootDone) => { 9 | const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') 10 | let completedTests = 0 11 | 12 | codes.forEach(code => { 13 | t.test(code, (t, done) => { 14 | t.plan(4) 15 | const fastify = Fastify() 16 | fastify.register(Sensible) 17 | 18 | fastify.get('/', (_req, reply) => { 19 | const name = normalize(code, statusCodes[code]) 20 | t.assert.strictEqual(reply[name](), reply) 21 | }) 22 | 23 | fastify.inject({ 24 | method: 'GET', 25 | url: '/' 26 | }, (err, res) => { 27 | t.assert.ifError(err) 28 | t.assert.strictEqual(res.statusCode, Number(code)) 29 | if (code === '425') { 30 | t.assert.deepStrictEqual(JSON.parse(res.payload), { 31 | error: 'Too Early', 32 | message: 'Too Early', 33 | statusCode: 425 34 | }) 35 | } else { 36 | t.assert.deepStrictEqual(JSON.parse(res.payload), { 37 | error: statusCodes[code], 38 | message: statusCodes[code], 39 | statusCode: Number(code) 40 | }) 41 | } 42 | done() 43 | completedTests++ 44 | 45 | if (completedTests === codes.length) { 46 | rootDone() 47 | } 48 | }) 49 | }) 50 | }) 51 | }) 52 | 53 | test('Should generate the correct http error using getter', (t, rootDone) => { 54 | const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') 55 | let completedTests = 0 56 | 57 | codes.forEach(code => { 58 | t.test(code, (t, done) => { 59 | t.plan(4) 60 | const fastify = Fastify() 61 | fastify.register(Sensible) 62 | 63 | fastify.get('/', (_req, reply) => { 64 | t.assert.strictEqual(reply.getHttpError(code), reply) 65 | }) 66 | 67 | fastify.inject({ 68 | method: 'GET', 69 | url: '/' 70 | }, (err, res) => { 71 | t.assert.ifError(err) 72 | t.assert.strictEqual(res.statusCode, Number(code)) 73 | t.assert.deepStrictEqual(JSON.parse(res.payload), { 74 | error: statusCodes[code], 75 | message: statusCodes[code], 76 | statusCode: Number(code) 77 | }) 78 | done() 79 | completedTests++ 80 | 81 | if (completedTests === codes.length) { 82 | rootDone() 83 | } 84 | }) 85 | }) 86 | }) 87 | }) 88 | 89 | test('Should generate the correct http error (with custom message)', (t, rootDone) => { 90 | const codes = Object.keys(statusCodes).filter(code => Number(code) >= 400 && code !== '418') 91 | let completedTests = 0 92 | 93 | codes.forEach(code => { 94 | t.test(code, (t, done) => { 95 | t.plan(3) 96 | const fastify = Fastify() 97 | fastify.register(Sensible) 98 | 99 | fastify.get('/', (_req, reply) => { 100 | const name = normalize(code, statusCodes[code]) 101 | reply[name]('custom') 102 | }) 103 | 104 | fastify.inject({ 105 | method: 'GET', 106 | url: '/' 107 | }, (err, res) => { 108 | t.assert.ifError(err) 109 | t.assert.strictEqual(res.statusCode, Number(code)) 110 | t.assert.deepStrictEqual(JSON.parse(res.payload), { 111 | error: statusCodes[code], 112 | message: 'custom', 113 | statusCode: Number(code) 114 | }) 115 | done() 116 | completedTests++ 117 | 118 | if (completedTests === codes.length) { 119 | rootDone() 120 | } 121 | }) 122 | }) 123 | }) 124 | }) 125 | 126 | function normalize (code, msg) { 127 | if (code === '414') return 'uriTooLong' 128 | if (code === '505') return 'httpVersionNotSupported' 129 | msg = msg.split(' ').join('').replace(/'/g, '') 130 | msg = msg[0].toLowerCase() + msg.slice(1) 131 | return msg 132 | } 133 | -------------------------------------------------------------------------------- /test/is.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const Sensible = require('../index') 6 | 7 | test('request.is API', (t, done) => { 8 | t.plan(3) 9 | 10 | const fastify = Fastify() 11 | fastify.register(Sensible) 12 | 13 | fastify.get('/', (req, reply) => { 14 | reply.send(req.is('json')) 15 | }) 16 | 17 | fastify.inject({ 18 | method: 'GET', 19 | url: '/', 20 | payload: { foo: 'bar' } 21 | }, (err, res) => { 22 | t.assert.ifError(err) 23 | t.assert.strictEqual(res.statusCode, 200) 24 | t.assert.deepStrictEqual( 25 | res.payload, 26 | 'json' 27 | ) 28 | done() 29 | }) 30 | }) 31 | 32 | test('request.is API (with array)', (t, done) => { 33 | t.plan(3) 34 | 35 | const fastify = Fastify() 36 | fastify.register(Sensible) 37 | 38 | fastify.get('/', (req, reply) => { 39 | reply.send(req.is(['html', 'json'])) 40 | }) 41 | 42 | fastify.inject({ 43 | method: 'GET', 44 | url: '/', 45 | payload: { foo: 'bar' } 46 | }, (err, res) => { 47 | t.assert.ifError(err) 48 | t.assert.strictEqual(res.statusCode, 200) 49 | t.assert.deepStrictEqual( 50 | res.payload, 51 | 'json' 52 | ) 53 | done() 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/schema.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const statusCodes = require('node:http').STATUS_CODES 5 | const Fastify = require('fastify') 6 | const Sensible = require('../index') 7 | 8 | test('Should add shared schema', (t, done) => { 9 | t.plan(3) 10 | 11 | const fastify = Fastify() 12 | fastify.register(Sensible, { sharedSchemaId: 'myError' }) 13 | 14 | fastify.get('/', { 15 | schema: { 16 | response: { 17 | 400: { $ref: 'myError' } 18 | } 19 | }, 20 | handler: (_req, reply) => { 21 | reply.badRequest() 22 | } 23 | }) 24 | 25 | fastify.inject({ 26 | method: 'GET', 27 | url: '/' 28 | }, (err, res) => { 29 | t.assert.ifError(err) 30 | t.assert.strictEqual(res.statusCode, 400) 31 | t.assert.deepStrictEqual(JSON.parse(res.payload), { 32 | error: statusCodes[400], 33 | message: statusCodes[400], 34 | statusCode: 400 35 | }) 36 | done() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/to.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const Sensible = require('../index') 6 | 7 | test('Should nicely wrap promises (resolve)', (t, done) => { 8 | t.plan(4) 9 | 10 | const fastify = Fastify() 11 | fastify.register(Sensible) 12 | 13 | fastify.ready(err => { 14 | t.assert.ifError(err) 15 | 16 | fastify.to(promise(true)) 17 | .then(val => { 18 | t.assert.ok(Array.isArray(val)) 19 | t.assert.ok(!val[0]) 20 | t.assert.ok(val[1]) 21 | done() 22 | }) 23 | }) 24 | }) 25 | 26 | test('Should nicely wrap promises (reject)', (t, done) => { 27 | t.plan(4) 28 | 29 | const fastify = Fastify() 30 | fastify.register(Sensible) 31 | 32 | fastify.ready(err => { 33 | t.assert.ifError(err) 34 | 35 | fastify.to(promise(false)) 36 | .then(val => { 37 | t.assert.ok(Array.isArray(val)) 38 | t.assert.ok(val[0]) 39 | t.assert.ok(!val[1]) 40 | done() 41 | }) 42 | }) 43 | }) 44 | 45 | function promise (bool) { 46 | return new Promise((resolve, reject) => { 47 | if (bool) { 48 | resolve(true) 49 | } else { 50 | reject(new Error('kaboom')) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /test/vary.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, describe } = require('node:test') 4 | const Fastify = require('fastify') 5 | const Sensible = require('../index') 6 | 7 | describe('reply.vary API', () => { 8 | test('accept string', (t, done) => { 9 | t.plan(4) 10 | 11 | const fastify = Fastify() 12 | fastify.register(Sensible) 13 | 14 | fastify.get('/', (_req, reply) => { 15 | reply.vary('Accept') 16 | reply.vary('Origin') 17 | reply.vary('User-Agent') 18 | reply.send('ok') 19 | }) 20 | 21 | fastify.inject({ 22 | method: 'GET', 23 | url: '/' 24 | }, (err, res) => { 25 | t.assert.ifError(err) 26 | t.assert.strictEqual(res.statusCode, 200) 27 | t.assert.strictEqual(res.headers.vary, 'Accept, Origin, User-Agent') 28 | t.assert.strictEqual(res.payload, 'ok') 29 | done() 30 | }) 31 | }) 32 | 33 | test('accept array of strings', (t, done) => { 34 | t.plan(4) 35 | 36 | const fastify = Fastify() 37 | fastify.register(Sensible) 38 | 39 | fastify.get('/', (_req, reply) => { 40 | reply.header('Vary', ['Accept', 'Origin']) 41 | reply.vary('User-Agent') 42 | reply.send('ok') 43 | }) 44 | 45 | fastify.inject({ 46 | method: 'GET', 47 | url: '/' 48 | }, (err, res) => { 49 | t.assert.ifError(err) 50 | t.assert.strictEqual(res.statusCode, 200) 51 | t.assert.strictEqual(res.headers.vary, 'Accept, Origin, User-Agent') 52 | t.assert.strictEqual(res.payload, 'ok') 53 | done() 54 | }) 55 | }) 56 | }) 57 | 58 | test('reply.vary.append API', (t, done) => { 59 | t.plan(4) 60 | 61 | const fastify = Fastify() 62 | fastify.register(Sensible) 63 | 64 | fastify.get('/', (_req, reply) => { 65 | t.assert.strictEqual( 66 | reply.vary.append('', ['Accept', 'Accept-Language']), 'Accept, Accept-Language' 67 | ) 68 | reply.send('ok') 69 | }) 70 | 71 | fastify.inject({ 72 | method: 'GET', 73 | url: '/' 74 | }, (err, res) => { 75 | t.assert.ifError(err) 76 | t.assert.strictEqual(res.statusCode, 200) 77 | t.assert.strictEqual(res.payload, 'ok') 78 | done() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginCallback, FastifyReply } from 'fastify' 2 | import { HttpErrors, HttpError } from '../lib/httpError' 3 | import * as Errors from '../lib/httpError' 4 | 5 | type FastifySensible = FastifyPluginCallback 6 | 7 | type singleValueTypes = 8 | | 'must-revalidate' 9 | | 'no-cache' 10 | | 'no-store' 11 | | 'no-transform' 12 | | 'public' 13 | | 'private' 14 | | 'proxy-revalidate' 15 | | 'immutable' 16 | 17 | type multiValueTypes = 18 | | 'max-age' 19 | | 's-maxage' 20 | | 'stale-while-revalidate' 21 | | 'stale-if-error' 22 | 23 | type staleTypes = 'while-revalidate' | 'if-error' 24 | 25 | declare module 'fastify' { 26 | namespace SensibleTypes { 27 | type ToType = [Error, T] 28 | } 29 | 30 | interface Assert { 31 | (condition: unknown, code?: number | string, message?: string): asserts condition; 32 | ok(condition: unknown, code?: number | string, message?: string): asserts condition; 33 | equal(a: unknown, b: unknown, code?: number | string, message?: string): void; 34 | notEqual(a: unknown, b: unknown, code?: number | string, message?: string): void; 35 | strictEqual(a: unknown, b: T, code?: number | string, message?: string): asserts a is T; 36 | notStrictEqual(a: unknown, b: unknown, code?: number | string, message?: string): void; 37 | deepEqual(a: unknown, b: unknown, code?: number | string, message?: string): void; 38 | notDeepEqual(a: unknown, b: unknown, code?: number | string, message?: string): void; 39 | } 40 | 41 | interface FastifyInstance { 42 | assert: Assert; 43 | to(to: Promise): Promise>; 44 | httpErrors: HttpErrors; 45 | } 46 | 47 | interface FastifyReply extends fastifySensible.HttpErrorReplys { 48 | vary: { 49 | (field: string | string[]): void; 50 | append: (header: string, field: string | string[]) => string; 51 | }; 52 | cacheControl(type: singleValueTypes): this 53 | cacheControl(type: multiValueTypes, time: number | string): this 54 | preventCache(): this 55 | maxAge(type: number | string): this 56 | revalidate(): this 57 | staticCache(time: number | string): this 58 | stale(type: staleTypes, time: number | string): this 59 | } 60 | 61 | interface FastifyRequest { 62 | forwarded(): string[]; 63 | is(types: Array): string | false | null; 64 | is(...types: Array): string | false | null; 65 | } 66 | } 67 | 68 | declare namespace fastifySensible { 69 | export interface FastifySensibleOptions { 70 | /** 71 | * This option registers a shared JSON Schema to be used by all response schemas. 72 | * 73 | * @example 74 | * ```js 75 | * fastify.register(require('@fastify/sensible'), { 76 | * sharedSchemaId: 'HttpError' 77 | * }) 78 | * 79 | * fastify.get('/async', { 80 | * schema: { 81 | * response: { 404: { $ref: 'HttpError' } } 82 | * } 83 | * handler: async (req, reply) => { 84 | * return reply.notFound() 85 | * } 86 | * }) 87 | * ``` 88 | */ 89 | sharedSchemaId?: string | undefined; 90 | } 91 | 92 | export { HttpError } 93 | 94 | export type HttpErrors = Errors.HttpErrors 95 | export type HttpErrorCodes = Errors.HttpErrorCodes 96 | export type HttpErrorCodesLoose = Errors.HttpErrorCodesLoose 97 | export type HttpErrorNames = Errors.HttpErrorNames 98 | export type HttpErrorTypes = Errors.HttpErrorTypes 99 | 100 | export const httpErrors: typeof Errors.default 101 | 102 | export type HttpErrorReplys = { 103 | getHttpError: (code: HttpErrorCodesLoose, message?: string) => FastifyReply; 104 | } & { 105 | [Property in keyof HttpErrorTypes]: (msg?: string) => FastifyReply 106 | } 107 | 108 | export const fastifySensible: FastifySensible 109 | export { fastifySensible as default } 110 | } 111 | 112 | declare function fastifySensible (...params: Parameters): ReturnType 113 | export = fastifySensible 114 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType, expectAssignable, expectError, expectNotAssignable } from 'tsd' 2 | import fastify from 'fastify' 3 | import fastifySensible, { FastifySensibleOptions, httpErrors, HttpError } from '..' 4 | 5 | const app = fastify() 6 | 7 | app.register(fastifySensible) 8 | 9 | expectAssignable({}) 10 | expectAssignable({ sharedSchemaId: 'HttpError' }) 11 | expectAssignable({ sharedSchemaId: undefined }) 12 | expectNotAssignable({ notSharedSchemaId: 'HttpError' }) 13 | 14 | app.get('/', (_req, reply) => { 15 | expectAssignable(reply.badRequest()) 16 | expectAssignable(reply.unauthorized()) 17 | expectAssignable(reply.paymentRequired()) 18 | expectAssignable(reply.forbidden()) 19 | expectAssignable(reply.notFound()) 20 | expectAssignable(reply.methodNotAllowed()) 21 | expectAssignable(reply.notAcceptable()) 22 | expectAssignable(reply.proxyAuthenticationRequired()) 23 | expectAssignable(reply.requestTimeout()) 24 | expectAssignable(reply.gone()) 25 | expectAssignable(reply.lengthRequired()) 26 | expectAssignable(reply.preconditionFailed()) 27 | expectAssignable(reply.payloadTooLarge()) 28 | expectAssignable(reply.uriTooLong()) 29 | expectAssignable(reply.unsupportedMediaType()) 30 | expectAssignable(reply.rangeNotSatisfiable()) 31 | expectAssignable(reply.expectationFailed()) 32 | expectAssignable(reply.imateapot()) 33 | expectAssignable(reply.unprocessableEntity()) 34 | expectAssignable(reply.locked()) 35 | expectAssignable(reply.failedDependency()) 36 | expectAssignable(reply.tooEarly()) 37 | expectAssignable(reply.upgradeRequired()) 38 | expectAssignable(reply.preconditionFailed()) 39 | expectAssignable(reply.tooManyRequests()) 40 | expectAssignable(reply.requestHeaderFieldsTooLarge()) 41 | expectAssignable(reply.unavailableForLegalReasons()) 42 | expectAssignable(reply.internalServerError()) 43 | expectAssignable(reply.notImplemented()) 44 | expectAssignable(reply.badGateway()) 45 | expectAssignable(reply.serviceUnavailable()) 46 | expectAssignable(reply.gatewayTimeout()) 47 | expectAssignable(reply.httpVersionNotSupported()) 48 | expectAssignable(reply.variantAlsoNegotiates()) 49 | expectAssignable(reply.insufficientStorage()) 50 | expectAssignable(reply.loopDetected()) 51 | expectAssignable(reply.bandwidthLimitExceeded()) 52 | expectAssignable(reply.notExtended()) 53 | expectAssignable(reply.networkAuthenticationRequired()) 54 | }) 55 | 56 | app.get('/', (_req, reply) => { 57 | expectAssignable(reply.getHttpError(405, 'Method Not Allowed')) 58 | expectAssignable(reply.getHttpError('405', 'Method Not Allowed')) 59 | }) 60 | 61 | app.get('/', () => { 62 | expectAssignable(app.httpErrors.createError(405, 'Method Not Allowed')) 63 | }) 64 | 65 | app.get('/', () => { 66 | expectAssignable( 67 | app.httpErrors.createError(405, 'Method Not Allowed') 68 | ) 69 | expectAssignable( 70 | app.httpErrors.createError(405, 'Method Not Allowed') 71 | ) 72 | expectAssignable>(app.httpErrors.badRequest()) 73 | }) 74 | 75 | app.get('/', async () => { 76 | expectAssignable>(app.httpErrors.badRequest()) 77 | expectAssignable>(app.httpErrors.unauthorized()) 78 | expectAssignable>(app.httpErrors.paymentRequired()) 79 | expectAssignable>(app.httpErrors.forbidden()) 80 | expectAssignable>(app.httpErrors.notFound()) 81 | expectAssignable>(app.httpErrors.methodNotAllowed()) 82 | expectAssignable>(app.httpErrors.notAcceptable()) 83 | expectAssignable>(app.httpErrors.proxyAuthenticationRequired()) 84 | expectAssignable>(app.httpErrors.requestTimeout()) 85 | expectAssignable>(app.httpErrors.gone()) 86 | expectAssignable>(app.httpErrors.lengthRequired()) 87 | expectAssignable>(app.httpErrors.preconditionFailed()) 88 | expectAssignable>(app.httpErrors.payloadTooLarge()) 89 | expectAssignable>(app.httpErrors.uriTooLong()) 90 | expectAssignable>(app.httpErrors.unsupportedMediaType()) 91 | expectAssignable>(app.httpErrors.rangeNotSatisfiable()) 92 | expectAssignable>(app.httpErrors.expectationFailed()) 93 | expectAssignable>(app.httpErrors.imateapot()) 94 | expectAssignable>(app.httpErrors.unprocessableEntity()) 95 | expectAssignable>(app.httpErrors.locked()) 96 | expectAssignable>(app.httpErrors.failedDependency()) 97 | expectAssignable>(app.httpErrors.tooEarly()) 98 | expectAssignable>(app.httpErrors.upgradeRequired()) 99 | expectAssignable>(app.httpErrors.tooManyRequests()) 100 | expectAssignable>(app.httpErrors.requestHeaderFieldsTooLarge()) 101 | expectAssignable>(app.httpErrors.unavailableForLegalReasons()) 102 | expectAssignable>(app.httpErrors.internalServerError()) 103 | expectAssignable>(app.httpErrors.notImplemented()) 104 | expectAssignable>(app.httpErrors.badGateway()) 105 | expectAssignable>(app.httpErrors.serviceUnavailable()) 106 | expectAssignable>(app.httpErrors.gatewayTimeout()) 107 | expectAssignable>(app.httpErrors.httpVersionNotSupported()) 108 | expectAssignable>(app.httpErrors.variantAlsoNegotiates()) 109 | expectAssignable>(app.httpErrors.insufficientStorage()) 110 | expectAssignable>(app.httpErrors.loopDetected()) 111 | expectAssignable>(app.httpErrors.bandwidthLimitExceeded()) 112 | expectAssignable>(app.httpErrors.notExtended()) 113 | expectAssignable>(app.httpErrors.networkAuthenticationRequired()) 114 | }) 115 | 116 | app.get('/', async () => { 117 | expectType(app.assert(1)) 118 | expectType(app.assert.ok(true)) 119 | expectType(app.assert.equal(1, 1)) 120 | expectType(app.assert.notEqual(1, 2)) 121 | expectType(app.assert.strictEqual(1, 1)) 122 | expectType(app.assert.notStrictEqual(1, 2)) 123 | expectType(app.assert.deepEqual({}, {})) 124 | expectType(app.assert.notDeepEqual({}, { a: 1 })) 125 | }) 126 | 127 | app.get('/', async () => { 128 | expectType>(app.to(new Promise(resolve => resolve()))) 129 | }) 130 | 131 | app.get('/', (_req, reply) => { 132 | expectAssignable(reply.cacheControl('public')) 133 | }) 134 | 135 | app.get('/', (_req, reply) => { 136 | expectAssignable(reply.preventCache()) 137 | }) 138 | 139 | app.get('/', (_req, reply) => { 140 | expectAssignable(reply.cacheControl('max-age', 42)) 141 | }) 142 | 143 | app.get('/', (_req, reply) => { 144 | expectError(reply.cacheControl('foobar')) 145 | }) 146 | 147 | app.get('/', (_req, reply) => { 148 | expectAssignable(reply.stale('while-revalidate', 42)) 149 | }) 150 | 151 | app.get('/', async (_req, reply) => { 152 | expectType(reply.vary('test')) 153 | expectType(reply.vary(['test'])) 154 | expectType(reply.vary.append('X-Header', 'field1')) 155 | expectType(reply.vary.append('X-Header', ['field1'])) 156 | }) 157 | 158 | app.get('/', async (req) => { 159 | expectType(req.forwarded()) 160 | expectType(req.is(['foo', 'bar'])) 161 | expectType(req.is('foo', 'bar')) 162 | }) 163 | 164 | httpErrors.forbidden('This type should be also available') 165 | httpErrors.createError('MyError') 166 | --------------------------------------------------------------------------------