├── .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 | [](https://github.com/fastify/fastify-sensible/actions/workflows/ci.yml)
4 | [](https://www.npmjs.com/package/@fastify/sensible)
5 | [](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 |
--------------------------------------------------------------------------------