├── .gitattributes
├── .github
├── dependabot.yml
├── stale.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── eslint.config.js
├── examples
└── example.js
├── index.js
├── package.json
├── test
└── index.test.js
└── types
├── index.d.ts
└── index.test-d.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set default behavior to automatically convert line endings
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.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
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Fastify
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 |
2 |
3 | # @fastify/circuit-breaker
4 |
5 | [](https://github.com/fastify/fastify-circuit-breaker/actions/workflows/ci.yml)
6 | [](https://www.npmjs.com/package/@fastify/circuit-breaker)
7 | [](https://github.com/neostandard/neostandard)
8 |
9 | A low overhead [circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html) for your routes.
10 |
11 | ## Install
12 | ```
13 | npm i @fastify/circuit-breaker
14 | ```
15 |
16 | ### Compatibility
17 | | Plugin version | Fastify version |
18 | | ---------------|-----------------|
19 | | `>=4.x` | `^5.x` |
20 | | `^3.x` | `^4.x` |
21 | | `>=1.x <3.x` | `^3.x` |
22 | | `^0.x` | `^2.x` |
23 | | `^0.x` | `^1.x` |
24 |
25 |
26 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
27 | in the table above.
28 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.
29 |
30 | ## Usage
31 | Register the plugin and, if needed, pass it custom options.
32 | This plugin will add an `onSend` hook and expose a `circuitBreaker` utility.
33 | Call `fastify.circuitBreaker()` when declaring the `preHandler` option of a route, in this way you will put that very specific route under the *circuit breaking* check.
34 | ```js
35 | const fastify = require('fastify')()
36 |
37 | fastify.register(require('@fastify/circuit-breaker'))
38 |
39 | fastify.register(function (instance, opts, next) {
40 | instance.route({
41 | method: 'GET',
42 | url: '/',
43 | schema: {
44 | querystring: {
45 | error: { type: 'boolean' },
46 | delay: { type: 'number' }
47 | }
48 | },
49 | preHandler: instance.circuitBreaker(),
50 | handler: function (req, reply) {
51 | setTimeout(() => {
52 | reply.send(
53 | req.query.error ? new Error('kaboom') : { hello: 'world' }
54 | )
55 | }, req.query.delay || 0)
56 | }
57 | })
58 | next()
59 | })
60 |
61 | fastify.listen({ port: 3000 }, err => {
62 | if (err) throw err
63 | console.log('Server listening at http://localhost:3000')
64 | })
65 | ```
66 |
67 | ### Options
68 | You can pass the following options during the plugin registration, this way the values will be used in all routes.
69 | ```js
70 | fastify.register(require('@fastify/circuit-breaker'), {
71 | threshold: 3, // default 5
72 | timeout: 5000, // default 10000
73 | resetTimeout: 5000, // default 10000
74 | onCircuitOpen: async (req, reply) => {
75 | reply.statusCode = 500
76 | throw new Error('a custom error')
77 | },
78 | onTimeout: async (req, reply) => {
79 | reply.statusCode = 504
80 | return 'timed out'
81 | }
82 | })
83 | ```
84 | - `threshold`: the maximum number of failures accepted before opening the circuit.
85 | - `timeout:` the maximum number of milliseconds you can wait before returning a `TimeoutError`.
86 | - `resetTimeout`: number of milliseconds before the circuit will move from `open` to `half-open`
87 | - `onCircuitOpen`: async function that gets called when the circuit is `open` due to errors. It can modify the reply and return a `string` | `Buffer` | `Stream` payload. If an `Error` is thrown it will be routed to your error handler.
88 | - `onTimeout`: async function that gets called when the circuit is `open` due to timeouts. It can modify the reply and return a `string` | `Buffer` | `Stream` | `Error` payload. If an `Error` is thrown it will be routed to your error handler.
89 |
90 | Otherwise, you can customize every single route by passing the same options to the `circuitBreaker` utility:
91 | ```js
92 | fastify.circuitBreaker({
93 | threshold: 3, // default 5
94 | timeout: 5000, // default 10000
95 | resetTimeout: 5000 // default 10000
96 | })
97 | ```
98 | If you pass the options directly to the utility, it will take precedence over the global configuration.
99 |
100 | ### Customize error messages
101 | If needed you can change the default error message for the *circuit open error* and the *timeout error*:
102 | ```js
103 | fastify.register(require('@fastify/circuit-breaker'), {
104 | timeoutErrorMessage: 'Ronf...', // default 'Timeout'
105 | circuitOpenErrorMessage: 'Oh gosh!' // default 'Circuit open'
106 | })
107 | ```
108 |
109 | ## Caveats
110 | Since it is not possible to apply the classic timeout feature of the pattern, in this case the timeout will measure the time that the route takes to execute and **once the route has finished** if the time taken is higher than the timeout it will return an error, even if the route has produced a successful response.
111 |
112 | If you need a classic circuit breaker to wrap around an API call consider using [`easy-breaker`](https://github.com/delvedor/easy-breaker).
113 |
114 | ## Acknowledgments
115 | Image courtesy of [Martin Fowler](https://martinfowler.com/bliki/CircuitBreaker.html).
116 |
117 |
118 | ## License
119 |
120 | Licensed under [MIT](./LICENSE).
121 |
122 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = require('neostandard')({
4 | ignores: require('neostandard').resolveIgnoresFromGitignore(),
5 | ts: true
6 | })
7 |
--------------------------------------------------------------------------------
/examples/example.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fastify = require('fastify')()
4 |
5 | fastify.register(require('..'), {
6 | threshold: 3,
7 | timeout: 5000,
8 | resetTimeout: 5000
9 | })
10 |
11 | fastify.register(function (instance, _opts, next) {
12 | instance.route({
13 | method: 'GET',
14 | url: '/',
15 | schema: {
16 | querystring: {
17 | error: { type: 'boolean' },
18 | delay: { type: 'number' }
19 | }
20 | },
21 | beforeHandler: fastify.circuitBreaker(),
22 | handler: function (req, reply) {
23 | setTimeout(() => {
24 | reply.send(
25 | req.query.error ? new Error('kaboom') : { hello: 'world' }
26 | )
27 | }, req.query.delay || 0)
28 | }
29 | })
30 | next()
31 | })
32 |
33 | fastify.listen({ port: 3000 }, err => {
34 | if (err) throw err
35 | console.log('Server listening at http://localhost:3000')
36 | })
37 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fp = require('fastify-plugin')
4 | const { FifoObject } = require('toad-cache')
5 | const createError = require('@fastify/error')
6 |
7 | const OPEN = Symbol('open')
8 | const HALFOPEN = Symbol('half-open')
9 | const CLOSE = Symbol('close')
10 |
11 | function fastifyCircuitBreaker (fastify, opts, next) {
12 | opts = opts || {}
13 | const timeout = opts.timeout || 1000 * 10
14 | const resetTimeout = opts.resetTimeout || 1000 * 10
15 | const threshold = opts.threshold || 5
16 | const timeoutErrorMessage = opts.timeoutErrorMessage || 'Timeout'
17 | const circuitOpenErrorMessage = opts.circuitOpenErrorMessage || 'Circuit open'
18 | const onCircuitOpen = opts.onCircuitOpen
19 | const onTimeout = opts.onTimeout
20 | const cache = new FifoObject(opts.cache || 500)
21 |
22 | let routeId = 0
23 |
24 | fastify.decorateRequest('_cbTime', 0)
25 | fastify.decorateRequest('_cbIsOpen', false)
26 | fastify.decorateRequest('_cbRouteId', 0)
27 | fastify.decorate('circuitBreaker', circuitBreaker)
28 | fastify.addHook('onSend', onSend)
29 |
30 | next()
31 |
32 | function circuitBreaker (opts) {
33 | opts = opts || {}
34 | const thisRouteId = ++routeId
35 | cache.set(thisRouteId, {
36 | status: CLOSE,
37 | failures: 0,
38 | currentlyRunningRequest: 0,
39 | isResetTimerRunning: false,
40 | threshold: opts.threshold || threshold,
41 | timeout: opts.timeout || timeout,
42 | resetTimeout: opts.resetTimeout || resetTimeout,
43 | onCircuitOpen: opts.onCircuitOpen || onCircuitOpen,
44 | onTimeout: opts.onTimeout || onTimeout
45 | })
46 | return async function beforeHandler (req, reply) {
47 | const route = cache.get(thisRouteId)
48 | if (route.status === OPEN) {
49 | req._cbIsOpen = true
50 | if (route.onCircuitOpen) {
51 | const errorPayload = await route.onCircuitOpen(req, reply)
52 | return reply.send(errorPayload)
53 | }
54 |
55 | return reply.send(new CircuitOpenError())
56 | }
57 |
58 | if (route.status === HALFOPEN && route.currentlyRunningRequest >= 1) {
59 | req._cbIsOpen = true
60 | if (route.onCircuitOpen) {
61 | const errorPayload = await route.onCircuitOpen(req, reply)
62 | return reply.send(errorPayload)
63 | }
64 |
65 | throw new CircuitOpenError()
66 | }
67 |
68 | route.currentlyRunningRequest++
69 | req._cbRouteId = thisRouteId
70 | req._cbTime = getTime()
71 | }
72 | }
73 |
74 | async function onSend (req, reply, _payload) {
75 | if (req._cbRouteId === 0 || req._cbIsOpen === true) {
76 | return
77 | }
78 | const route = cache.get(req._cbRouteId)
79 | route.currentlyRunningRequest--
80 |
81 | if (getTime() - req._cbTime > route.timeout) {
82 | route.failures++
83 | if (route.failures >= route.threshold) {
84 | route.status = OPEN
85 | runTimer(req._cbRouteId)
86 | }
87 | if (route.onTimeout) {
88 | const errorPayload = await route.onTimeout(req, reply)
89 | return errorPayload
90 | }
91 |
92 | const err = new TimeoutError()
93 | reply.code(err.statusCode)
94 | throw err
95 | }
96 |
97 | if (reply.raw.statusCode < 500) {
98 | route.status = CLOSE
99 | route.failures = 0
100 | return
101 | }
102 |
103 | route.failures++
104 | if (route.status === HALFOPEN) {
105 | route.status = OPEN
106 | runTimer(req._cbRouteId)
107 | return
108 | }
109 |
110 | if (route.failures >= route.threshold) {
111 | route.status = OPEN
112 | runTimer(req._cbRouteId)
113 | if (route.onCircuitOpen) {
114 | const errorPayload = await route.onCircuitOpen(req, reply)
115 | return errorPayload
116 | }
117 |
118 | const err = new CircuitOpenError()
119 | reply.code(err.statusCode)
120 | throw err
121 | }
122 | }
123 |
124 | function runTimer (routeId) {
125 | const route = cache.get(routeId)
126 | if (route.isResetTimerRunning === true) return
127 | route.isResetTimerRunning = true
128 | setTimeout(() => {
129 | route.isResetTimerRunning = false
130 | route.status = HALFOPEN
131 | }, route.resetTimeout)
132 | }
133 |
134 | const TimeoutError = createError(
135 | 'FST_ERR_CIRCUIT_BREAKER_TIMEOUT',
136 | timeoutErrorMessage,
137 | 503
138 | )
139 |
140 | const CircuitOpenError = createError(
141 | 'FST_ERR_CIRCUIT_BREAKER_OPEN',
142 | circuitOpenErrorMessage,
143 | 503
144 | )
145 |
146 | function getTime () {
147 | const ts = process.hrtime()
148 | return (ts[0] * 1e3) + (ts[1] / 1e6)
149 | }
150 | }
151 |
152 | module.exports = fp(fastifyCircuitBreaker, {
153 | fastify: '5.x',
154 | name: '@fastify/circuit-breaker'
155 | })
156 | module.exports.default = fastifyCircuitBreaker
157 | module.exports.fastifyCircuitBreaker = fastifyCircuitBreaker
158 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@fastify/circuit-breaker",
3 | "version": "4.0.2",
4 | "description": "A low overhead circuit breaker for your routes",
5 | "main": "index.js",
6 | "types": "types/index.d.ts",
7 | "type": "commonjs",
8 | "scripts": {
9 | "lint": "eslint",
10 | "lint:fix": "eslint --fix",
11 | "test": "npm run test:unit && npm run test:typescript",
12 | "test:unit": "c8 -100 node --test",
13 | "test:typescript": "tsd"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/fastify/fastify-circuit-breaker.git"
18 | },
19 | "keywords": [
20 | "fastify",
21 | "circuit breaker",
22 | "circuit",
23 | "breaker",
24 | "overhead",
25 | "speed"
26 | ],
27 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)",
28 | "contributors": [
29 | {
30 | "name": "Matteo Collina",
31 | "email": "hello@matteocollina.com"
32 | },
33 | {
34 | "name": "Manuel Spigolon",
35 | "email": "behemoth89@gmail.com"
36 | },
37 | {
38 | "name": "Aras Abbasi",
39 | "email": "aras.abbasi@gmail.com"
40 | },
41 | {
42 | "name": "Frazer Smith",
43 | "email": "frazer.dev@icloud.com",
44 | "url": "https://github.com/fdawgs"
45 | }
46 | ],
47 | "license": "MIT",
48 | "devDependencies": {
49 | "@fastify/pre-commit": "^2.1.0",
50 | "@types/node": "^22.0.0",
51 | "c8": "^10.1.2",
52 | "eslint": "^9.17.0",
53 | "fastify": "^5.0.0",
54 | "neostandard": "^0.12.0",
55 | "tsd": "^0.32.0"
56 | },
57 | "dependencies": {
58 | "@fastify/error": "^4.0.0",
59 | "fastify-plugin": "^5.0.0",
60 | "toad-cache": "^3.7.0"
61 | },
62 | "bugs": {
63 | "url": "https://github.com/fastify/fastify-circuit-breaker/issues"
64 | },
65 | "homepage": "https://github.com/fastify/fastify-circuit-breaker#readme",
66 | "funding": [
67 | {
68 | "type": "github",
69 | "url": "https://github.com/sponsors/fastify"
70 | },
71 | {
72 | "type": "opencollective",
73 | "url": "https://opencollective.com/fastify"
74 | }
75 | ],
76 | "pre-commit": [
77 | "lint",
78 | "test"
79 | ],
80 | "publishConfig": {
81 | "access": "public"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { test } = require('node:test')
4 | const Fastify = require('fastify')
5 | const circuitBreaker = require('..')
6 | const { setTimeout: sleep } = require('timers/promises')
7 |
8 | const opts = {
9 | schema: {
10 | querystring: {
11 | type: 'object',
12 | properties: {
13 | error: { type: 'boolean' },
14 | delay: { type: 'integer' }
15 | }
16 | }
17 | }
18 | }
19 |
20 | test('Should respond with a 503 once the threshold has been reached', async t => {
21 | t.plan(12)
22 |
23 | const fastify = Fastify()
24 | await fastify.register(circuitBreaker, {
25 | threshold: 3,
26 | timeout: 1000,
27 | resetTimeout: 1000
28 | })
29 |
30 | fastify.after(() => {
31 | opts.preHandler = fastify.circuitBreaker()
32 | fastify.get('/', opts, (req, reply) => {
33 | t.assert.strictEqual(typeof req._cbTime, 'number')
34 | setTimeout(() => {
35 | reply.send(
36 | req.query.error ? new Error('kaboom') : { hello: 'world' }
37 | )
38 | }, req.query.delay || 0)
39 | })
40 | })
41 |
42 | let res = await fastify.inject('/?error=true')
43 | t.assert.ok(res)
44 | t.assert.strictEqual(res.statusCode, 500)
45 | t.assert.deepStrictEqual({
46 | error: 'Internal Server Error',
47 | message: 'kaboom',
48 | statusCode: 500
49 | }, JSON.parse(res.payload))
50 |
51 | res = await fastify.inject('/?error=true')
52 | t.assert.ok(res)
53 | t.assert.strictEqual(res.statusCode, 500)
54 | t.assert.deepStrictEqual({
55 | error: 'Internal Server Error',
56 | message: 'kaboom',
57 | statusCode: 500
58 | }, JSON.parse(res.payload))
59 |
60 | res = await fastify.inject('/?error=true')
61 | t.assert.ok(res)
62 | t.assert.strictEqual(res.statusCode, 503)
63 | t.assert.deepStrictEqual({
64 | error: 'Service Unavailable',
65 | message: 'Circuit open',
66 | statusCode: 503,
67 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
68 | }, JSON.parse(res.payload))
69 | })
70 |
71 | test('Should respond with a 503 once the threshold has been reached (timeout)', async t => {
72 | t.plan(15)
73 |
74 | const fastify = Fastify()
75 | fastify.register(circuitBreaker, {
76 | threshold: 3,
77 | timeout: 50,
78 | resetTimeout: 1000
79 | })
80 |
81 | fastify.after(() => {
82 | opts.preHandler = fastify.circuitBreaker()
83 | fastify.get('/', opts, (req, reply) => {
84 | t.assert.strictEqual(typeof req._cbTime, 'number')
85 | setTimeout(() => {
86 | reply.send(
87 | req.query.error ? new Error('kaboom') : { hello: 'world' }
88 | )
89 | }, req.query.delay || 0)
90 | })
91 | })
92 |
93 | fastify.inject('/?error=false&delay=100', (err, res) => {
94 | t.assert.ifError(err)
95 | t.assert.strictEqual(res.statusCode, 503)
96 | t.assert.deepStrictEqual({
97 | error: 'Service Unavailable',
98 | message: 'Timeout',
99 | statusCode: 503,
100 | code: 'FST_ERR_CIRCUIT_BREAKER_TIMEOUT'
101 | }, JSON.parse(res.payload))
102 | })
103 |
104 | fastify.inject('/?error=false&delay=100', (err, res) => {
105 | t.assert.ifError(err)
106 | t.assert.strictEqual(res.statusCode, 503)
107 | t.assert.deepStrictEqual({
108 | error: 'Service Unavailable',
109 | message: 'Timeout',
110 | statusCode: 503,
111 | code: 'FST_ERR_CIRCUIT_BREAKER_TIMEOUT'
112 | }, JSON.parse(res.payload))
113 | })
114 |
115 | fastify.inject('/?error=false&delay=100', (err, res) => {
116 | t.assert.ifError(err)
117 | t.assert.strictEqual(res.statusCode, 503)
118 | t.assert.deepStrictEqual({
119 | error: 'Service Unavailable',
120 | message: 'Timeout',
121 | statusCode: 503,
122 | code: 'FST_ERR_CIRCUIT_BREAKER_TIMEOUT'
123 | }, JSON.parse(res.payload))
124 | })
125 |
126 | setTimeout(() => {
127 | fastify.inject('/?error=false&delay=100', (err, res) => {
128 | t.assert.ifError(err)
129 | t.assert.strictEqual(res.statusCode, 503)
130 | t.assert.deepStrictEqual({
131 | error: 'Service Unavailable',
132 | message: 'Circuit open',
133 | statusCode: 503,
134 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
135 | }, JSON.parse(res.payload))
136 | })
137 | }, 200)
138 |
139 | await sleep(200)
140 | })
141 |
142 | test('Should return 503 until the circuit is open', async t => {
143 | t.plan(12)
144 |
145 | const fastify = Fastify()
146 | await fastify.register(circuitBreaker, {
147 | threshold: 2,
148 | timeout: 1000,
149 | resetTimeout: 500
150 | })
151 |
152 | fastify.after(() => {
153 | opts.preHandler = fastify.circuitBreaker()
154 | fastify.get('/', opts, (req, reply) => {
155 | t.assert.strictEqual(typeof req._cbTime, 'number')
156 | setTimeout(() => {
157 | reply.send(
158 | req.query.error ? new Error('kaboom') : { hello: 'world' }
159 | )
160 | }, req.query.delay || 0)
161 | })
162 | })
163 |
164 | let res = await fastify.inject('/?error=true')
165 | t.assert.ok(res)
166 | t.assert.strictEqual(res.statusCode, 500)
167 | t.assert.deepStrictEqual({
168 | error: 'Internal Server Error',
169 | message: 'kaboom',
170 | statusCode: 500
171 | }, JSON.parse(res.payload))
172 |
173 | res = await fastify.inject('/?error=true')
174 | t.assert.ok(res)
175 | t.assert.strictEqual(res.statusCode, 503)
176 | t.assert.deepStrictEqual({
177 | error: 'Service Unavailable',
178 | message: 'Circuit open',
179 | statusCode: 503,
180 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
181 | }, JSON.parse(res.payload))
182 |
183 | await sleep(1000)
184 | res = await fastify.inject('/?error=false')
185 | t.assert.ok(res)
186 | t.assert.strictEqual(res.statusCode, 200)
187 | t.assert.deepStrictEqual({ hello: 'world' }, JSON.parse(res.payload))
188 | })
189 |
190 | test('If the staus is half-open and there is an error the state should be open again', async t => {
191 | t.plan(15)
192 |
193 | const fastify = Fastify()
194 | fastify.register(circuitBreaker, {
195 | threshold: 2,
196 | timeout: 1000,
197 | resetTimeout: 500
198 | })
199 |
200 | fastify.after(() => {
201 | opts.preHandler = fastify.circuitBreaker()
202 | fastify.get('/', opts, (req, reply) => {
203 | t.assert.strictEqual(typeof req._cbTime, 'number')
204 | setTimeout(() => {
205 | reply.send(
206 | req.query.error ? new Error('kaboom') : { hello: 'world' }
207 | )
208 | }, req.query.delay || 0)
209 | })
210 | })
211 |
212 | fastify.inject('/?error=true', (err, res) => {
213 | t.assert.ifError(err)
214 | t.assert.strictEqual(res.statusCode, 500)
215 | t.assert.deepStrictEqual({
216 | error: 'Internal Server Error',
217 | message: 'kaboom',
218 | statusCode: 500
219 | }, JSON.parse(res.payload))
220 | })
221 |
222 | fastify.inject('/?error=true', (err, res) => {
223 | t.assert.ifError(err)
224 | t.assert.strictEqual(res.statusCode, 503)
225 | t.assert.deepStrictEqual({
226 | error: 'Service Unavailable',
227 | message: 'Circuit open',
228 | statusCode: 503,
229 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
230 | }, JSON.parse(res.payload))
231 | })
232 |
233 | setTimeout(() => {
234 | fastify.inject('/?error=true', (err, res) => {
235 | t.assert.ifError(err)
236 | t.assert.strictEqual(res.statusCode, 500)
237 | t.assert.deepStrictEqual({
238 | error: 'Internal Server Error',
239 | message: 'kaboom',
240 | statusCode: 500
241 | }, JSON.parse(res.payload))
242 | })
243 |
244 | fastify.inject('/?error=true', (err, res) => {
245 | t.assert.ifError(err)
246 | t.assert.strictEqual(res.statusCode, 503)
247 | t.assert.deepStrictEqual({
248 | error: 'Service Unavailable',
249 | message: 'Circuit open',
250 | statusCode: 503,
251 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
252 | }, JSON.parse(res.payload))
253 | })
254 | }, 1000)
255 |
256 | await sleep(1200)
257 | })
258 |
259 | test('Should customize circuit open error message', async t => {
260 | t.plan(4)
261 |
262 | const fastify = Fastify()
263 | fastify.register(circuitBreaker, {
264 | threshold: 1,
265 | circuitOpenErrorMessage: 'Oh gosh!'
266 | })
267 |
268 | fastify.after(() => {
269 | opts.preHandler = fastify.circuitBreaker()
270 | fastify.get('/', opts, (req, reply) => {
271 | t.assert.strictEqual(typeof req._cbTime, 'number')
272 | setTimeout(() => {
273 | reply.send(
274 | req.query.error ? new Error('kaboom') : { hello: 'world' }
275 | )
276 | }, req.query.delay || 0)
277 | })
278 | })
279 |
280 | const res = await fastify.inject('/?error=true')
281 | t.assert.ok(res)
282 | t.assert.strictEqual(res.statusCode, 503)
283 | t.assert.deepStrictEqual({
284 | error: 'Service Unavailable',
285 | message: 'Oh gosh!',
286 | statusCode: 503,
287 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
288 | }, JSON.parse(res.payload))
289 | })
290 |
291 | test('Should customize timeout error message', async t => {
292 | t.plan(4)
293 |
294 | const fastify = Fastify()
295 | fastify.register(circuitBreaker, {
296 | threshold: 2,
297 | timeout: 100,
298 | timeoutErrorMessage: 'Oh gosh!'
299 | })
300 |
301 | fastify.after(() => {
302 | opts.preHandler = fastify.circuitBreaker()
303 | fastify.get('/', opts, (req, reply) => {
304 | t.assert.strictEqual(typeof req._cbTime, 'number')
305 | setTimeout(() => {
306 | reply.send(
307 | req.query.error ? new Error('kaboom') : { hello: 'world' }
308 | )
309 | }, req.query.delay || 0)
310 | })
311 | })
312 |
313 | const res = await fastify.inject('/?error=true&delay=200')
314 | t.assert.ok(res)
315 | t.assert.strictEqual(res.statusCode, 503)
316 | t.assert.deepStrictEqual({
317 | error: 'Service Unavailable',
318 | message: 'Oh gosh!',
319 | statusCode: 503,
320 | code: 'FST_ERR_CIRCUIT_BREAKER_TIMEOUT'
321 | }, JSON.parse(res.payload))
322 | })
323 |
324 | test('One route should not interfere with others', async t => {
325 | t.plan(7)
326 |
327 | const fastify = Fastify()
328 | fastify.register(circuitBreaker, {
329 | threshold: 1
330 | })
331 |
332 | fastify.after(() => {
333 | opts.preHandler = fastify.circuitBreaker()
334 | fastify.get('/', opts, (req, reply) => {
335 | t.assert.strictEqual(typeof req._cbTime, 'number')
336 | setTimeout(() => {
337 | reply.send(
338 | req.query.error ? new Error('kaboom') : { hello: 'world' }
339 | )
340 | }, req.query.delay || 0)
341 | })
342 |
343 | const options = { beforeHandler: fastify.circuitBreaker() }
344 | fastify.get('/other', options, (_req, reply) => {
345 | reply.send({ hello: 'world' })
346 | })
347 | })
348 |
349 | let res = await fastify.inject('/?error=true')
350 | t.assert.ok(res)
351 | t.assert.strictEqual(res.statusCode, 503)
352 | t.assert.deepStrictEqual({
353 | error: 'Service Unavailable',
354 | message: 'Circuit open',
355 | statusCode: 503,
356 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
357 | }, JSON.parse(res.payload))
358 |
359 | res = await fastify.inject('/other')
360 | t.assert.ok(res)
361 | t.assert.strictEqual(res.statusCode, 200)
362 | t.assert.deepStrictEqual({ hello: 'world' }, JSON.parse(res.payload))
363 | })
364 |
365 | test('Custom options should overwrite the globals', async t => {
366 | t.plan(4)
367 |
368 | const fastify = Fastify()
369 | await fastify.register(circuitBreaker, {
370 | threshold: 1
371 | })
372 |
373 | fastify.after(() => {
374 | opts.preHandler = fastify.circuitBreaker({ threshold: 2 })
375 | fastify.get('/', opts, (req, reply) => {
376 | t.assert.strictEqual(typeof req._cbTime, 'number')
377 | setTimeout(() => {
378 | reply.send(
379 | req.query.error ? new Error('kaboom') : { hello: 'world' }
380 | )
381 | }, req.query.delay || 0)
382 | })
383 | })
384 |
385 | const res = await fastify.inject('/?error=true')
386 | t.assert.ok(res)
387 | t.assert.strictEqual(res.statusCode, 500)
388 | t.assert.deepStrictEqual({
389 | error: 'Internal Server Error',
390 | message: 'kaboom',
391 | statusCode: 500
392 | }, JSON.parse(res.payload))
393 | })
394 |
395 | test('Should handle also errors with statusCode property', async t => {
396 | t.plan(6)
397 |
398 | const fastify = Fastify()
399 | fastify.register(circuitBreaker, {
400 | threshold: 2
401 | })
402 |
403 | fastify.after(() => {
404 | opts.preHandler = fastify.circuitBreaker()
405 | fastify.get('/', opts, (_req, reply) => {
406 | const error = new Error('kaboom')
407 | error.statusCode = 501
408 | reply.send(error)
409 | })
410 | })
411 |
412 | let res = await fastify.inject('/')
413 | t.assert.ok(res)
414 | t.assert.strictEqual(res.statusCode, 501)
415 | t.assert.deepStrictEqual({
416 | error: 'Not Implemented',
417 | message: 'kaboom',
418 | statusCode: 501
419 | }, JSON.parse(res.payload))
420 |
421 | res = await fastify.inject('/')
422 | t.assert.ok(res)
423 | t.assert.strictEqual(res.statusCode, 503)
424 | t.assert.deepStrictEqual({
425 | error: 'Service Unavailable',
426 | message: 'Circuit open',
427 | statusCode: 503,
428 | code: 'FST_ERR_CIRCUIT_BREAKER_OPEN'
429 | }, JSON.parse(res.payload))
430 | })
431 |
432 | test('If a route is not under the circuit breaker, _cbRouteId should always be equal to 0', async t => {
433 | t.plan(8)
434 |
435 | const fastify = Fastify()
436 | fastify.register(circuitBreaker)
437 |
438 | fastify.get('/first', (req, reply) => {
439 | t.assert.strictEqual(req._cbRouteId, 0)
440 | reply.send({ hello: 'world' })
441 | })
442 |
443 | fastify.get('/second', (req, reply) => {
444 | t.assert.strictEqual(req._cbRouteId, 0)
445 | reply.send({ hello: 'world' })
446 | })
447 |
448 | let res = await fastify.inject('/first')
449 | t.assert.ok(res)
450 | t.assert.strictEqual(res.statusCode, 200)
451 | t.assert.deepStrictEqual({ hello: 'world' }, JSON.parse(res.payload))
452 |
453 | res = await fastify.inject('/second')
454 | t.assert.ok(res)
455 | t.assert.strictEqual(res.statusCode, 200)
456 | t.assert.deepStrictEqual({ hello: 'world' }, JSON.parse(res.payload))
457 | })
458 |
459 | test('Should work only if the status code is >= 500', async t => {
460 | t.plan(6)
461 |
462 | const fastify = Fastify()
463 | fastify.register(circuitBreaker, {
464 | threshold: 1
465 | })
466 |
467 | fastify.after(() => {
468 | opts.preHandler = fastify.circuitBreaker()
469 | fastify.get('/first', opts, (_req, reply) => {
470 | const error = new Error('kaboom')
471 | error.statusCode = 400
472 | reply.send(error)
473 | })
474 |
475 | fastify.get('/second', opts, (_req, reply) => {
476 | reply.code(400).send(new Error('kaboom'))
477 | })
478 | })
479 |
480 | let res = await fastify.inject('/first')
481 | t.assert.ok(res)
482 | t.assert.strictEqual(res.statusCode, 400)
483 | t.assert.deepStrictEqual({
484 | error: 'Bad Request',
485 | message: 'kaboom',
486 | statusCode: 400
487 | }, JSON.parse(res.payload))
488 |
489 | res = await fastify.inject('/second')
490 | t.assert.ok(res)
491 | t.assert.strictEqual(res.statusCode, 400)
492 | t.assert.deepStrictEqual({
493 | error: 'Bad Request',
494 | message: 'kaboom',
495 | statusCode: 400
496 | }, JSON.parse(res.payload))
497 | })
498 |
499 | test('Should call onCircuitOpen when the threshold has been reached', async t => {
500 | t.plan(6)
501 |
502 | const fastify = Fastify()
503 | fastify.register(circuitBreaker, {
504 | threshold: 2,
505 | onCircuitOpen: (_req, reply) => {
506 | reply.statusCode = 503
507 | return JSON.stringify({ message: 'hi' })
508 | }
509 | })
510 |
511 | fastify.after(() => {
512 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
513 | reply.send(new Error('kaboom'))
514 | })
515 | })
516 |
517 | let res = await fastify.inject('/')
518 | t.assert.ok(res)
519 | t.assert.strictEqual(res.statusCode, 500)
520 | t.assert.deepStrictEqual({
521 | error: 'Internal Server Error',
522 | message: 'kaboom',
523 | statusCode: 500
524 | }, JSON.parse(res.payload))
525 |
526 | res = await fastify.inject('/')
527 | t.assert.ok(res)
528 | t.assert.strictEqual(res.statusCode, 503)
529 | t.assert.deepStrictEqual({
530 | message: 'hi'
531 | }, JSON.parse(res.payload))
532 | })
533 |
534 | test('Should call onTimeout when the timeout has been reached', async t => {
535 | t.plan(3)
536 |
537 | const fastify = Fastify()
538 | fastify.register(circuitBreaker, {
539 | timeout: 50,
540 | onTimeout: (_req, reply) => {
541 | reply.statusCode = 504
542 | return 'timed out'
543 | }
544 | })
545 |
546 | fastify.after(() => {
547 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
548 | setTimeout(() => {
549 | reply.send({ hello: 'world' })
550 | }, 100)
551 | })
552 | })
553 |
554 | const res = await fastify.inject('/')
555 | t.assert.ok(res)
556 | t.assert.strictEqual(res.statusCode, 504)
557 | t.assert.strictEqual(res.payload, 'timed out')
558 | })
559 |
560 | test('onCircuitOpen will handle a thrown error', async t => {
561 | t.plan(6)
562 |
563 | const fastify = Fastify()
564 | fastify.register(circuitBreaker, {
565 | threshold: 2,
566 | onCircuitOpen: (_req, reply) => {
567 | reply.statusCode = 503
568 | throw new Error('circuit open')
569 | }
570 | })
571 |
572 | fastify.after(() => {
573 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
574 | reply.send(new Error('kaboom'))
575 | })
576 | })
577 |
578 | let res = await fastify.inject('/')
579 | t.assert.ok(res)
580 | t.assert.strictEqual(res.statusCode, 500)
581 | t.assert.deepStrictEqual({
582 | error: 'Internal Server Error',
583 | message: 'kaboom',
584 | statusCode: 500
585 | }, JSON.parse(res.payload))
586 |
587 | res = await fastify.inject('/')
588 | t.assert.ok(res)
589 | t.assert.strictEqual(res.statusCode, 503)
590 | t.assert.deepStrictEqual({
591 | error: 'Service Unavailable',
592 | message: 'circuit open',
593 | statusCode: 503
594 | }, JSON.parse(res.payload))
595 | })
596 |
597 | test('onTimeout will handle a thrown error', async t => {
598 | t.plan(2)
599 |
600 | const fastify = Fastify()
601 | fastify.register(circuitBreaker, {
602 | timeout: 50,
603 | onTimeout: (_req, reply) => {
604 | reply.statusCode = 504
605 | throw new Error('timed out')
606 | }
607 | })
608 |
609 | fastify.after(() => {
610 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
611 | setTimeout(() => {
612 | reply.send({ hello: 'world' })
613 | }, 100)
614 | })
615 | })
616 |
617 | const res = await fastify.inject('/')
618 | t.assert.ok(res)
619 | t.assert.deepStrictEqual({
620 | error: 'Gateway Timeout',
621 | message: 'timed out',
622 | statusCode: 504
623 | }, JSON.parse(res.payload))
624 | })
625 |
626 | test('onCircuitOpen can be an async function', async t => {
627 | t.plan(6)
628 |
629 | const fastify = Fastify()
630 | fastify.register(circuitBreaker, {
631 | threshold: 2,
632 | onCircuitOpen: async (_req, reply) => {
633 | const statusCode = await Promise.resolve(503)
634 | reply.statusCode = statusCode
635 | throw new Error('circuit open')
636 | }
637 | })
638 |
639 | fastify.after(() => {
640 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
641 | reply.send(new Error('kaboom'))
642 | })
643 | })
644 |
645 | let res = await fastify.inject('/')
646 | t.assert.ok(res)
647 | t.assert.strictEqual(res.statusCode, 500)
648 | t.assert.deepStrictEqual({
649 | error: 'Internal Server Error',
650 | message: 'kaboom',
651 | statusCode: 500
652 | }, JSON.parse(res.payload))
653 |
654 | res = await fastify.inject('/')
655 | t.assert.ok(res)
656 | t.assert.strictEqual(res.statusCode, 503)
657 | t.assert.deepStrictEqual({
658 | error: 'Service Unavailable',
659 | message: 'circuit open',
660 | statusCode: 503
661 | }, JSON.parse(res.payload))
662 | })
663 |
664 | test('onTimeout can be an async function', async t => {
665 | t.plan(2)
666 |
667 | const fastify = Fastify()
668 | fastify.register(circuitBreaker, {
669 | timeout: 50,
670 | onTimeout: async (_req, reply) => {
671 | const statusCode = await Promise.resolve(504)
672 | reply.statusCode = statusCode
673 | throw new Error('timed out')
674 | }
675 | })
676 |
677 | fastify.after(() => {
678 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (_req, reply) => {
679 | setTimeout(() => {
680 | reply.send({ hello: 'world' })
681 | }, 100)
682 | })
683 | })
684 |
685 | const res = await fastify.inject('/')
686 | t.assert.ok(res)
687 | t.assert.deepStrictEqual({
688 | error: 'Gateway Timeout',
689 | message: 'timed out',
690 | statusCode: 504
691 | }, JSON.parse(res.payload))
692 | })
693 |
694 | test('Should not throw error if no options is passed', async t => {
695 | t.plan(3)
696 | const fastify = Fastify()
697 | const fastify2 = Fastify()
698 | const fastify3 = Fastify()
699 | t.assert.strictEqual(circuitBreaker(fastify, undefined, () => {}), undefined)
700 | t.assert.strictEqual(circuitBreaker(fastify2, null, () => {}), undefined)
701 | t.assert.strictEqual(circuitBreaker(fastify3, {}, () => {}), undefined)
702 | })
703 |
704 | test('Should throw error on route status open and circuit open', async t => {
705 | t.plan(5)
706 |
707 | const fastify = Fastify()
708 | await fastify.register(circuitBreaker, {
709 | threshold: 1,
710 | timeout: 1000,
711 | resetTimeout: 1500,
712 | onCircuitOpen: async (_req, reply) => {
713 | reply.statusCode = 500
714 | return JSON.stringify({ err: 'custom error' })
715 | }
716 | })
717 |
718 | fastify.after(() => {
719 | fastify.get('/', { preHandler: fastify.circuitBreaker() }, (req, reply) => {
720 | t.assert.strictEqual(typeof req._cbTime, 'number')
721 | setTimeout(() => {
722 | reply.send(new Error('kaboom'))
723 | }, 0)
724 | })
725 | })
726 |
727 | let res = await fastify.inject('/?error=true')
728 | t.assert.ok(res)
729 |
730 | await sleep(1000)
731 | res = await fastify.inject('/?error=false')
732 | t.assert.ok(res)
733 | t.assert.strictEqual(res.statusCode, 500)
734 | t.assert.deepStrictEqual(res.json(), { err: 'custom error' })
735 | })
736 |
737 | test('Should throw error on route status half open and circuit open', async t => {
738 | t.plan(15)
739 |
740 | const fastify = Fastify()
741 | fastify.register(circuitBreaker, {
742 | threshold: 2,
743 | timeout: 1000,
744 | resetTimeout: 500,
745 | onCircuitOpen: async (_req, reply) => {
746 | reply.statusCode = 500
747 | return JSON.stringify({ err: 'custom error' })
748 | }
749 | })
750 |
751 | fastify.after(() => {
752 | opts.preHandler = fastify.circuitBreaker()
753 |
754 | fastify.get('/', opts, (req, reply) => {
755 | t.assert.strictEqual(typeof req._cbTime, 'number')
756 | setTimeout(() => {
757 | reply.send(new Error('kaboom'))
758 | }, 0)
759 | })
760 | })
761 |
762 | fastify.inject('/?error=true', (err, res) => {
763 | t.assert.ifError(err)
764 | t.assert.strictEqual(res.statusCode, 500)
765 | t.assert.deepStrictEqual({
766 | error: 'Internal Server Error',
767 | message: 'kaboom',
768 | statusCode: 500
769 | }, JSON.parse(res.payload))
770 | })
771 |
772 | fastify.inject('/?error=true', (err, res) => {
773 | t.assert.ifError(err)
774 | t.assert.strictEqual(res.statusCode, 500)
775 | t.assert.deepStrictEqual(res.json(), { err: 'custom error' })
776 | })
777 |
778 | setTimeout(() => {
779 | fastify.inject('/?error=true', (err, res) => {
780 | t.assert.ifError(err)
781 | t.assert.strictEqual(res.statusCode, 500)
782 | t.assert.deepStrictEqual({
783 | error: 'Internal Server Error',
784 | message: 'kaboom',
785 | statusCode: 500
786 | }, JSON.parse(res.payload))
787 | })
788 |
789 | fastify.inject('/?error=true', (err, res) => {
790 | t.assert.strictEqual(null, err)
791 | t.assert.strictEqual(res.statusCode, 500)
792 | t.assert.deepStrictEqual(res.json(), { err: 'custom error' })
793 | })
794 | }, 1000)
795 |
796 | await sleep(1200)
797 | })
798 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FastifyPluginCallback,
3 | FastifyRequest,
4 | FastifyReply,
5 | HookHandlerDoneFunction,
6 | } from 'fastify'
7 | import { Stream } from 'node:stream'
8 |
9 | declare module 'fastify' {
10 | interface FastifyInstance {
11 | circuitBreaker(
12 | options?: fastifyCircuitBreaker.FastifyCircuitBreakerOptions
13 | ): fastifyCircuitBreaker.FastifyCircuitBreakerBeforeHandler;
14 | }
15 | }
16 |
17 | type FastifyCircuitBreaker = FastifyPluginCallback
18 |
19 | declare namespace fastifyCircuitBreaker {
20 | export interface FastifyCircuitBreakerBeforeHandler {
21 | (
22 | req: FastifyRequest,
23 | reply: FastifyReply,
24 | next: HookHandlerDoneFunction
25 | ): Promise | void;
26 | }
27 |
28 | export type FastifyCircuitBreakerOptions = {
29 | /**
30 | * The maximum numbers of failures you accept to have before opening the circuit.
31 | * @default 5
32 | */
33 | threshold?: number;
34 |
35 | /**
36 | * The maximum number of milliseconds you can wait before return a `TimeoutError`.
37 | * @default 10000
38 | */
39 | timeout?: number;
40 |
41 | /**
42 | * The number of milliseconds before the circuit will move from `open` to `half-open`.
43 | * @default 10000
44 | */
45 | resetTimeout?: number;
46 |
47 | /**
48 | * Gets called when the circuit is `open` due to timeouts.
49 | * It can modify the reply and return a `string` | `Buffer` | `Stream` |
50 | * `Error` payload. If an `Error` is thrown it will be routed to your error
51 | * handler.
52 | */
53 | onTimeout?: (request: FastifyRequest, reply: FastifyReply) => void | string | Buffer | Stream | Error | Promise;
54 |
55 | /**
56 | *
57 | * @default 'Timeout'
58 | */
59 | timeoutErrorMessage?: string;
60 |
61 | /**
62 | * Gets called when the circuit is `open` due to errors.
63 | * It can modify the reply and return a `string` | `Buffer` | `Stream`
64 | * payload. If an `Error` is thrown it will be routed to your error handler.
65 | */
66 | onCircuitOpen?: (request: FastifyRequest, reply: FastifyReply) => void | string | Buffer | Stream | Promise;
67 |
68 | /**
69 | * @default 'Circuit open'
70 | */
71 | circuitOpenErrorMessage?: string;
72 |
73 | /**
74 | * The amount of cached requests.
75 | * @default 500
76 | */
77 | cache?: number;
78 | }
79 | export const fastifyCircuitBreaker: FastifyCircuitBreaker
80 | export { fastifyCircuitBreaker as default }
81 | }
82 |
83 | declare function fastifyCircuitBreaker (...params: Parameters): ReturnType
84 | export = fastifyCircuitBreaker
85 |
--------------------------------------------------------------------------------
/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import fastify, { FastifyReply, FastifyRequest } from 'fastify'
2 | import { expectType } from 'tsd'
3 | import FastifyCircuitBreaker, { FastifyCircuitBreakerOptions } from '..'
4 |
5 | const app = fastify()
6 |
7 | app.register(FastifyCircuitBreaker)
8 | app.register(FastifyCircuitBreaker, {})
9 | app.register(FastifyCircuitBreaker, { timeout: 5000 })
10 | app.register(FastifyCircuitBreaker, { threshold: 5 })
11 | app.register(FastifyCircuitBreaker, { resetTimeout: 10000 })
12 | app.register(FastifyCircuitBreaker, {
13 | timeout: 5000,
14 | threshold: 5,
15 | resetTimeout: 10000,
16 | })
17 |
18 | const fastifyCircuitBreakerOptions: FastifyCircuitBreakerOptions = {
19 | timeout: 5000,
20 | threshold: 5,
21 | resetTimeout: 10000,
22 | }
23 | app.register(FastifyCircuitBreaker, fastifyCircuitBreakerOptions)
24 |
25 | app.get(
26 | '/',
27 | {
28 | preHandler: app.circuitBreaker(),
29 | },
30 | () => { }
31 | )
32 |
33 | app.register(FastifyCircuitBreaker, { timeoutErrorMessage: 'Timeon' })
34 | app.register(FastifyCircuitBreaker, {
35 | onTimeout: async (req, reply) => {
36 | expectType(req)
37 | expectType(reply)
38 | const statusCode = await Promise.resolve(504)
39 | reply.statusCode = statusCode
40 | throw new Error('timed out')
41 | }
42 | })
43 | app.register(FastifyCircuitBreaker, {
44 | onTimeout: (req, reply) => {
45 | expectType(req)
46 | expectType(reply)
47 | reply.statusCode = 504
48 | return 'timed out'
49 | }
50 | })
51 | app.register(FastifyCircuitBreaker, {
52 | onTimeout: async (req, reply) => {
53 | expectType(req)
54 | expectType(reply)
55 | reply.statusCode = 504
56 | return 'timed out'
57 | }
58 | })
59 |
60 | app.register(FastifyCircuitBreaker, { circuitOpenErrorMessage: 'circus open' })
61 | app.register(FastifyCircuitBreaker, {
62 | onCircuitOpen: async (req, reply) => {
63 | expectType(req)
64 | expectType(reply)
65 | const statusCode = await Promise.resolve(504)
66 | reply.statusCode = statusCode
67 | throw new Error('circuit open')
68 | }
69 | })
70 | app.register(FastifyCircuitBreaker, {
71 | onCircuitOpen: (req, reply) => {
72 | expectType(req)
73 | expectType(reply)
74 | reply.statusCode = 504
75 | return 'circuit open'
76 | }
77 | })
78 | app.register(FastifyCircuitBreaker, {
79 | onCircuitOpen: async (req, reply) => {
80 | expectType(req)
81 | expectType(reply)
82 | reply.statusCode = 504
83 | return 'circuit open'
84 | }
85 | })
86 |
--------------------------------------------------------------------------------