├── test ├── .gitkeep ├── decorator.js ├── plugin.mjs ├── plugin.js ├── no-compiler.test.js └── basic.test.js ├── esm-wrapper.js ├── .github ├── tests_checker.yml ├── dependabot.yml ├── .stale.yml └── workflows │ └── ci.yml ├── examples ├── plugin.js └── example.js ├── package.json ├── .gitignore ├── index.js └── README.md /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /esm-wrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = path => import(path) 4 | -------------------------------------------------------------------------------- /test/decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | 5 | module.exports = fp(async function (app, opts) { 6 | app.decorate('foo', 'bar') 7 | }) 8 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: | 2 | Hello! Thank you for contributing! 3 | It appears that you have changed the code, but the tests that verify your change are missing. Could you please add them? 4 | fileExtensions: 5 | - '.ts' 6 | - '.js' 7 | 8 | testDir: 'test' -------------------------------------------------------------------------------- /.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: "weekly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /examples/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sleep = require('timers/promises').setTimeout 4 | 5 | module.exports = async function (app) { 6 | app.get('/', async (req, res) => { 7 | await sleep(10) 8 | console.log('promise constructor is different', Object.getPrototypeOf(req.p).constructor === Promise) 9 | return 'Hello World!' 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const isolate = require('..') 5 | const path = require('path') 6 | 7 | const app = Fastify() 8 | 9 | app.addHook('onRequest', async function (req) { 10 | req.p = Promise.resolve('hello') 11 | console.log('promise constructor is the same', Object.getPrototypeOf(req.p).constructor === Promise) 12 | }) 13 | 14 | app.register(isolate, { 15 | path: path.join(__dirname, '/plugin.js') 16 | }) 17 | 18 | app.listen(3000) 19 | 20 | process.on('SIGINT', () => { 21 | app.close() 22 | }) 23 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /test/plugin.mjs: -------------------------------------------------------------------------------- 1 | export default async function (app) { 2 | app.get('/', async (req) => { 3 | const data = { 4 | check: Object.getPrototypeOf(req.p).constructor === Promise 5 | } 6 | return data 7 | }) 8 | 9 | app.get('/throw', () => { 10 | process.nextTick(() => { 11 | throw new Error('kaboom') 12 | }) 13 | return 'ok' 14 | }) 15 | 16 | app.get('/error', () => { 17 | throw new Error('kaboom') 18 | }) 19 | 20 | app.register(async function (app) { 21 | app.register(import('@fastify/cookie'), { 22 | secret: 'secret' 23 | }) 24 | 25 | app.get('/set-cookie', async (_, res) => { 26 | res.cookie('test', 'test') 27 | return { 28 | status: 'ok' 29 | } 30 | }) 31 | 32 | app.get('/get-cookie', async (req) => { 33 | return { 34 | status: req.cookies.test 35 | } 36 | }) 37 | }) 38 | 39 | app.get('/globalThis', async (req) => { 40 | return { value: globalThis.test || 'not set' } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 'on': 3 | push: 4 | paths-ignore: 5 | - docs/** 6 | - '*.md' 7 | pull_request: 8 | paths-ignore: 9 | - docs/** 10 | - '*.md' 11 | env: 12 | CI: true 13 | 14 | jobs: 15 | test: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | node-version: 20 | - 16 21 | - 18 22 | - 20 23 | os: 24 | - ubuntu-latest 25 | - windows-latest 26 | name: Node ${{ matrix.node-version }} - ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Use Node.js 30 | id: setup_node 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Install Dependencies 35 | id: install 36 | run: npm install 37 | - name: Tests 38 | id: test 39 | run: npm test 40 | 41 | automerge: 42 | needs: test 43 | runs-on: ubuntu-latest 44 | permissions: 45 | pull-requests: write 46 | contents: write 47 | steps: 48 | - uses: fastify/github-action-merge-dependabot@v3 49 | with: 50 | github-token: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-sandbox", 3 | "version": "0.13.1", 4 | "description": "Load Fastify plugins in a sandbox", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "standard | snazzy && tap --no-coverage test/*.test.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/mcollina/fastify-sandbox.git" 16 | }, 17 | "keywords": [ 18 | "fastify", 19 | "sandbox", 20 | "synchronous", 21 | "worker", 22 | "hot", 23 | "reload" 24 | ], 25 | "author": "Matteo Collina ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/mcollina/fastify-sandbox/issues" 29 | }, 30 | "homepage": "https://github.com/mcollina/fastify-sandbox#readme", 31 | "devDependencies": { 32 | "@fastify/cookie": "^9.0.4", 33 | "@fastify/pre-commit": "^2.0.2", 34 | "fastify": "^4.0.2", 35 | "fastify-plugin": "^4.3.0", 36 | "proxyquire": "^2.1.3", 37 | "snazzy": "^9.0.0", 38 | "standard": "^17.0.0", 39 | "tap": "^16.0.0" 40 | }, 41 | "dependencies": { 42 | "import-fresh": "^3.3.0" 43 | }, 44 | "optionalDependencies": { 45 | "@matteo.collina/worker": "^3.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async function (app, opts) { 4 | app.get('/', async (req) => { 5 | const data = { 6 | check: Object.getPrototypeOf(req.p).constructor === Promise 7 | } 8 | return data 9 | }) 10 | 11 | app.get('/throw', () => { 12 | process.nextTick(() => { 13 | throw new Error('kaboom') 14 | }) 15 | return 'ok' 16 | }) 17 | 18 | app.get('/error', () => { 19 | throw new Error('kaboom') 20 | }) 21 | 22 | app.get('/perror', () => { 23 | throw new Error('kaboom') 24 | }) 25 | 26 | app.get('/typeerror', () => { 27 | throw new TypeError('kaboom - typeerror') 28 | }) 29 | 30 | app.get('/rangeerror', () => { 31 | throw new RangeError('kaboom - rangeerror') 32 | }) 33 | 34 | app.get('/evalerror', () => { 35 | throw new EvalError('kaboom - evalerror') 36 | }) 37 | 38 | app.get('/referenceerror', () => { 39 | throw new ReferenceError('kaboom - referenceerror') 40 | }) 41 | 42 | app.get('/syntaxerror', () => { 43 | throw new SyntaxError('kaboom - syntaxerror') 44 | }) 45 | 46 | app.get('/urierror', () => { 47 | throw new URIError('kaboom - urierror') 48 | }) 49 | 50 | app.get('/errorcode', () => { 51 | const err = new Error('kaboom') 52 | err.code = 'MY_ERROR_CODE' 53 | throw err 54 | }) 55 | 56 | app.register(async function (app) { 57 | app.register(require('@fastify/cookie'), { 58 | secret: 'secret' 59 | }) 60 | 61 | app.get('/set-cookie', async (_, res) => { 62 | res.cookie('test', 'test') 63 | return { 64 | status: 'ok' 65 | } 66 | }) 67 | 68 | app.get('/get-cookie', async (req) => { 69 | return { 70 | status: req.cookies.test 71 | } 72 | }) 73 | }) 74 | 75 | app.get('/globalThis', async (req) => { 76 | return { value: globalThis.test || 'not set' } 77 | }) 78 | 79 | app.get('/options', async () => opts) 80 | } 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # Vim swap files 119 | *.swp 120 | 121 | # macOS files 122 | .DS_Store 123 | 124 | # lock files 125 | package-lock.json 126 | yarn.lock 127 | 128 | # editor files 129 | .vscode 130 | .idea.ccls-cache 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const importFresh = require('import-fresh') 4 | const { join } = require('path') 5 | const { pathToFileURL } = require('url') 6 | const fp = require('fastify-plugin') 7 | let SynchronousWorker 8 | 9 | const esmWrapperPath = join(__dirname, 'esm-wrapper.js') 10 | 11 | try { 12 | SynchronousWorker = require('@matteo.collina/worker') 13 | } catch { 14 | // do nothing 15 | } 16 | 17 | async function sandbox (app, opts) { 18 | const stopTimeout = opts.stopTimeout || 100 19 | 20 | const onError = opts.onError || routeToProcess 21 | const customizeGlobalThis = opts.customizeGlobalThis || noop 22 | const fallback = opts.fallback || !SynchronousWorker 23 | 24 | if (!fallback) { 25 | const worker = new SynchronousWorker({ 26 | sharedEventLoop: true, 27 | sharedMicrotaskQueue: true 28 | }) 29 | 30 | worker.process.on('uncaughtException', onError) 31 | worker.globalThis.Error = Error 32 | worker.globalThis.TypeError = TypeError 33 | worker.globalThis.RangeError = RangeError 34 | worker.globalThis.EvalError = EvalError 35 | worker.globalThis.ReferenceError = ReferenceError 36 | worker.globalThis.SyntaxError = SyntaxError 37 | worker.globalThis.URIError = URIError 38 | 39 | customizeGlobalThis(worker.globalThis) 40 | 41 | const _require = worker.createRequire(opts.path) 42 | let plugin 43 | try { 44 | plugin = _require(opts.path) 45 | } catch (err) { 46 | if (err.code === 'ERR_REQUIRE_ESM') { 47 | const _import = _require(esmWrapperPath) 48 | plugin = _import(pathToFileURL(opts.path)) 49 | } else { 50 | throw err 51 | } 52 | } 53 | 54 | app.register(plugin, opts.options) 55 | 56 | app.addHook('onClose', (_, done) => { 57 | // the immediate blocks are needed to ensure that the worker 58 | // has actually finished its work before closing 59 | setTimeout(() => { 60 | worker.stop().then(() => { 61 | setImmediate(done) 62 | }) 63 | }, stopTimeout) 64 | }) 65 | } else { 66 | app.log.warn('isolates are not available, relying on import-fresh instead. Support for ESM is not available.') 67 | app.register(importFresh(opts.path), opts.options) 68 | } 69 | 70 | function routeToProcess (err) { 71 | app.log.error({ 72 | err: { 73 | message: err.message, 74 | stack: err.stack 75 | } 76 | }, 'error encounterated within the isolate, routing to uncaughtException, use onError option to catch') 77 | process.emit('uncaughtException', err) 78 | } 79 | } 80 | 81 | function noop () {} 82 | 83 | module.exports = fp(sandbox, { 84 | fastify: '4.x', 85 | name: 'fastify-sandbox' 86 | }) 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-sandbox 2 | 3 | Loads a fastify plugin in a sandbox. 4 | It will have a different require.cache, so loaded modules 5 | could be safely gc'ed once the sandbox goes out of scope. 6 | 7 | The plugin can be both commonjs or esm. 8 | 9 | This modules pairs well with [`@fastify/restartable`](https://github.com/fastify/restartable) 10 | to provide hot-reloading mechanism for Fastify applications. 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm i fastify-sandbox 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | 'use strict' 22 | 23 | const Fastify = require('fastify') 24 | const sandbox = require('fastify-sandbox') 25 | 26 | const app = Fastify() 27 | 28 | app.addHook('onRequest', async function (req) { 29 | req.p = Promise.resolve('hello') 30 | console.log('promise constructor is the same', Object.getPrototypeOf(req.p).constructor === Promise) 31 | }) 32 | 33 | app.register(sandbox, { 34 | path: __dirname + '/plugin.js', 35 | options: { // this object will be passed as the options of the loaded plugin 36 | hello: "world" 37 | }, 38 | onError (err) { 39 | // uncaught exceptions within the sandbox will land inside this 40 | // callback 41 | } 42 | }) 43 | 44 | app.listen(3000) 45 | ``` 46 | 47 | Inside `plugin.js`: 48 | 49 | ```js 50 | 'use strict' 51 | 52 | // We are in a different V8 Context now 53 | const sleep = require('timers/promises').setTimeout 54 | 55 | module.exports = async function (app) { 56 | app.get('/', async (req, res) => { 57 | console.log('promise constructor is different', Object.getPrototypeOf(req.p).constructor === Promise) 58 | return 'Hello World!' 59 | }) 60 | } 61 | ``` 62 | 63 | ## Missing compiler support 64 | 65 | In case there is no compiler toolchain available in the system, 66 | compiling the code needed to support for the current Node.js version would 67 | be impossible. In this case we rely on [import-fresh](https://npm.im/import-fresh) 68 | instead. 69 | 70 | It's also possible to turn on the fallback mechanism with the `fallback: true` option: 71 | 72 | ```js 73 | 'use strict' 74 | 75 | const Fastify = require('fastify') 76 | const sandbox = require('fastify-sandbox') 77 | 78 | const app = Fastify() 79 | 80 | app.addHook('onRequest', async function (req) { 81 | req.p = Promise.resolve('hello') 82 | console.log('promise constructor is the same', Object.getPrototypeOf(req.p).constructor === Promise) 83 | }) 84 | 85 | app.register(sandbox, { 86 | path: __dirname + '/plugin.js', 87 | fallback: true 88 | }) 89 | 90 | app.listen(3000) 91 | ``` 92 | 93 | Note that ESM is only supported via the native code. 94 | 95 | ## Caveats 96 | 97 | This module does not offer any protection against malicious code. 98 | 99 | ## License 100 | 101 | MIT 102 | -------------------------------------------------------------------------------- /test/no-compiler.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const proxyquire = require('proxyquire') 5 | 6 | const Fastify = require('fastify') 7 | const path = require('path') 8 | 9 | test('different isolates', async ({ same, teardown }) => { 10 | const isolate = proxyquire('../', { 11 | '@matteo.collina/worker': null 12 | }) 13 | 14 | const app = Fastify() 15 | teardown(app.close.bind(app)) 16 | 17 | app.addHook('onRequest', async function (req) { 18 | req.p = Promise.resolve('hello') 19 | }) 20 | 21 | app.get('/check', async function (req) { 22 | return { 23 | check: Object.getPrototypeOf(req.p).constructor === Promise 24 | } 25 | }) 26 | 27 | app.register(isolate, { 28 | path: path.join(__dirname, '/plugin.js') 29 | }) 30 | 31 | { 32 | const res = await app.inject({ 33 | method: 'GET', 34 | url: '/check' 35 | }) 36 | const data = res.json() 37 | 38 | same(data, { check: true }) 39 | } 40 | 41 | { 42 | const res = await app.inject({ 43 | method: 'GET', 44 | url: '/' 45 | }) 46 | const data = res.json() 47 | 48 | // Bummer, we have do not have isolates 49 | same(data, { check: true }) 50 | } 51 | }) 52 | 53 | test('different isolates', async ({ same, teardown }) => { 54 | const isolate = require('../') 55 | const app = Fastify() 56 | teardown(app.close.bind(app)) 57 | 58 | app.addHook('onRequest', async function (req) { 59 | req.p = Promise.resolve('hello') 60 | }) 61 | 62 | app.get('/check', async function (req) { 63 | return { 64 | check: Object.getPrototypeOf(req.p).constructor === Promise 65 | } 66 | }) 67 | 68 | app.register(isolate, { 69 | path: path.join(__dirname, '/plugin.js'), 70 | fallback: true 71 | }) 72 | 73 | { 74 | const res = await app.inject({ 75 | method: 'GET', 76 | url: '/check' 77 | }) 78 | const data = res.json() 79 | 80 | same(data, { check: true }) 81 | } 82 | 83 | { 84 | const res = await app.inject({ 85 | method: 'GET', 86 | url: '/' 87 | }) 88 | const data = res.json() 89 | 90 | // Bummer, we have do not have isolates 91 | same(data, { check: true }) 92 | } 93 | }) 94 | 95 | test('pass options through', async ({ same, teardown }) => { 96 | const isolate = proxyquire('../', { 97 | '@matteo.collina/worker': null 98 | }) 99 | 100 | const app = Fastify() 101 | teardown(app.close.bind(app)) 102 | 103 | app.addHook('onRequest', async function (req) { 104 | req.p = Promise.resolve('hello') 105 | }) 106 | 107 | app.get('/check', async function (req) { 108 | return { 109 | check: Object.getPrototypeOf(req.p).constructor === Promise 110 | } 111 | }) 112 | 113 | app.register(isolate, { 114 | path: path.join(__dirname, '/plugin.js'), 115 | options: { 116 | something: 'else' 117 | } 118 | }) 119 | 120 | { 121 | const res = await app.inject({ 122 | method: 'GET', 123 | url: '/options' 124 | }) 125 | const data = res.json() 126 | 127 | same(data, { something: 'else' }) 128 | } 129 | }) 130 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const isolate = require('../') 5 | const Fastify = require('fastify') 6 | const path = require('path') 7 | const fs = require('fs').promises 8 | const os = require('os') 9 | const isWin = os.platform() === 'win32' 10 | 11 | test('different isolates', async ({ same, teardown }) => { 12 | const app = Fastify() 13 | teardown(app.close.bind(app)) 14 | 15 | app.addHook('onRequest', async function (req) { 16 | req.p = Promise.resolve('hello') 17 | }) 18 | 19 | app.get('/check', async function (req) { 20 | return { 21 | check: Object.getPrototypeOf(req.p).constructor === Promise 22 | } 23 | }) 24 | 25 | app.register(isolate, { 26 | path: path.join(__dirname, '/plugin.js') 27 | }) 28 | 29 | { 30 | const res = await app.inject({ 31 | method: 'GET', 32 | url: '/check' 33 | }) 34 | const data = res.json() 35 | 36 | same(data, { check: true }) 37 | } 38 | 39 | { 40 | const res = await app.inject({ 41 | method: 'GET', 42 | url: '/' 43 | }) 44 | const data = res.json() 45 | 46 | same(data, { check: false }) 47 | } 48 | }) 49 | 50 | test('skip-override works', async ({ same, teardown, equal }) => { 51 | const app = Fastify() 52 | teardown(app.close.bind(app)) 53 | 54 | app.register(isolate, { 55 | path: path.join(__dirname, '/plugin.js') 56 | }) 57 | 58 | const res = await app.inject({ 59 | method: 'GET', 60 | url: '/set-cookie' 61 | }) 62 | 63 | const cookie = res.headers['set-cookie'].split(';')[0] 64 | 65 | same(res.json(), { status: 'ok' }) 66 | 67 | const res2 = await app.inject({ 68 | method: 'GET', 69 | headers: { 70 | cookie 71 | }, 72 | url: '/get-cookie' 73 | }) 74 | 75 | same(res2.json(), { status: 'test' }) 76 | }) 77 | 78 | test('decorators works', async ({ same, teardown, equal }) => { 79 | const app = Fastify() 80 | teardown(app.close.bind(app)) 81 | 82 | app.register(isolate, { 83 | path: path.join(__dirname, '/decorator.js') 84 | }) 85 | 86 | await app 87 | 88 | equal(app.foo, 'bar') 89 | }) 90 | 91 | test('throw and onError option', ({ same, plan, teardown }) => { 92 | plan(1) 93 | 94 | const app = Fastify() 95 | teardown(app.close.bind(app)) 96 | 97 | app.register(isolate, { 98 | path: path.join(__dirname, '/plugin.js'), 99 | onError (err) { 100 | same(err.message, 'kaboom') 101 | } 102 | }) 103 | 104 | // this will never get a response 105 | app.inject({ 106 | method: 'GET', 107 | url: '/throw' 108 | }) 109 | }) 110 | 111 | test('throw and route to uncaughtException', ({ same, plan, teardown }) => { 112 | plan(1) 113 | 114 | const app = Fastify() 115 | teardown(app.close.bind(app)) 116 | 117 | const uncaughtException = process.listeners('uncaughtException') 118 | process.removeAllListeners('uncaughtException') 119 | teardown(() => { 120 | process.removeAllListeners('uncaughtException') 121 | for (const listener of uncaughtException) { 122 | process.on('uncaughtException', listener) 123 | } 124 | }) 125 | 126 | process.on('uncaughtException', (err) => { 127 | same(err.message, 'kaboom') 128 | }) 129 | 130 | app.register(isolate, { 131 | path: path.join(__dirname, '/plugin.js') 132 | }) 133 | 134 | // this will never get a response 135 | app.inject({ 136 | method: 'GET', 137 | url: '/throw' 138 | }) 139 | }) 140 | 141 | test('error is the same', async ({ equal, teardown }) => { 142 | const app = Fastify() 143 | teardown(app.close.bind(app)) 144 | 145 | app.register(isolate, { 146 | path: path.join(__dirname, '/plugin.js') 147 | }) 148 | 149 | const res = await app.inject({ 150 | method: 'GET', 151 | url: '/error' 152 | }) 153 | equal(res.statusCode, 500) 154 | const data = res.json() 155 | equal(data.message, 'kaboom') 156 | }) 157 | 158 | test('error code is propagated', async ({ equal, teardown }) => { 159 | const app = Fastify() 160 | teardown(app.close.bind(app)) 161 | 162 | app.register(isolate, { 163 | path: path.join(__dirname, '/plugin.js') 164 | }) 165 | 166 | const res = await app.inject({ 167 | method: 'GET', 168 | url: '/errorcode' 169 | }) 170 | equal(res.statusCode, 500) 171 | const data = res.json() 172 | equal(data.message, 'kaboom') 173 | equal(data.code, 'MY_ERROR_CODE') 174 | }) 175 | 176 | test('stopTimeout', async ({ equal, teardown }) => { 177 | const app = Fastify() 178 | teardown(app.close.bind(app)) 179 | 180 | app.addHook('onRequest', async function (req) { 181 | req.p = Promise.resolve('hello') 182 | }) 183 | 184 | app.get('/check', async function (req) { 185 | return { 186 | check: Object.getPrototypeOf(req.p).constructor === Promise 187 | } 188 | }) 189 | 190 | app.register(isolate, { 191 | path: path.join(__dirname, '/plugin.js'), 192 | stopTimeout: 500 193 | }) 194 | 195 | const timer = Date.now() 196 | 197 | await app.close() 198 | 199 | const timer2 = Date.now() 200 | 201 | equal(timer2 - timer >= 500, true) 202 | }) 203 | 204 | test('customize isolate globalThis', async ({ same, teardown }) => { 205 | const app = Fastify() 206 | teardown(app.close.bind(app)) 207 | 208 | app.register(isolate, { 209 | path: path.join(__dirname, '/plugin.js'), 210 | customizeGlobalThis (_globalThis) { 211 | _globalThis.test = 'test' 212 | } 213 | }) 214 | 215 | { 216 | const res = await app.inject({ 217 | method: 'GET', 218 | url: '/globalThis' 219 | }) 220 | const data = res.json() 221 | 222 | same(data, { value: 'test' }) 223 | } 224 | }) 225 | 226 | test('different isolates with ESM', async ({ same, teardown }) => { 227 | const app = Fastify() 228 | teardown(app.close.bind(app)) 229 | 230 | app.addHook('onRequest', async function (req) { 231 | req.p = Promise.resolve('hello') 232 | }) 233 | 234 | app.get('/check', async function (req) { 235 | return { 236 | check: Object.getPrototypeOf(req.p).constructor === Promise 237 | } 238 | }) 239 | 240 | app.register(isolate, { 241 | path: path.join(__dirname, '/plugin.mjs') 242 | }) 243 | 244 | { 245 | const res = await app.inject({ 246 | method: 'GET', 247 | url: '/check' 248 | }) 249 | const data = res.json() 250 | 251 | same(data, { check: true }) 252 | } 253 | 254 | { 255 | const res = await app.inject({ 256 | method: 'GET', 257 | url: '/' 258 | }) 259 | const data = res.json() 260 | 261 | same(data, { check: false }) 262 | } 263 | }) 264 | 265 | test('can load CommonJS plugin with filename requiring escape', { skip: isWin }, async ({ same, teardown }) => { 266 | const tmpFile = path.join(os.tmpdir(), 'plugin \'"`.js') 267 | await fs.writeFile(tmpFile, ` 268 | module.exports = async function (app) { 269 | app.get('/', async () => { 270 | return { check: true } 271 | }) 272 | } 273 | `) 274 | const app = Fastify() 275 | teardown(app.close.bind(app)) 276 | 277 | app.register(isolate, { 278 | path: tmpFile 279 | }) 280 | 281 | { 282 | const res = await app.inject({ 283 | method: 'GET', 284 | url: '/' 285 | }) 286 | const data = res.json() 287 | 288 | same(data, { check: true }) 289 | } 290 | }) 291 | 292 | test('can load ESM plugin with filename requiring escape', { skip: isWin }, async ({ same, teardown }) => { 293 | const tmpFile = path.join(os.tmpdir(), 'plugin \'"`.mjs') 294 | await fs.writeFile(tmpFile, ` 295 | export default async function (app) { 296 | app.get('/', async () => { 297 | return { check: true } 298 | }) 299 | } 300 | `) 301 | 302 | const app = Fastify() 303 | teardown(app.close.bind(app)) 304 | 305 | app.register(isolate, { 306 | path: tmpFile 307 | }) 308 | 309 | { 310 | const res = await app.inject({ 311 | method: 'GET', 312 | url: '/' 313 | }) 314 | const data = res.json() 315 | 316 | same(data, { check: true }) 317 | } 318 | }) 319 | 320 | test('pass options through', async ({ same, teardown }) => { 321 | const app = Fastify() 322 | teardown(app.close.bind(app)) 323 | 324 | app.addHook('onRequest', async function (req) { 325 | req.p = Promise.resolve('hello') 326 | }) 327 | 328 | app.get('/check', async function (req) { 329 | return { 330 | check: Object.getPrototypeOf(req.p).constructor === Promise 331 | } 332 | }) 333 | 334 | app.register(isolate, { 335 | path: path.join(__dirname, '/plugin.js'), 336 | options: { 337 | something: 'else' 338 | } 339 | }) 340 | 341 | { 342 | const res = await app.inject({ 343 | method: 'GET', 344 | url: '/options' 345 | }) 346 | const data = res.json() 347 | 348 | same(data, { something: 'else' }) 349 | } 350 | }) 351 | 352 | test('error is the same via promises', async ({ equal, teardown }) => { 353 | const app = Fastify() 354 | teardown(app.close.bind(app)) 355 | 356 | app.register(isolate, { 357 | path: path.join(__dirname, '/plugin.js') 358 | }) 359 | 360 | const res = await app.inject({ 361 | method: 'GET', 362 | url: '/perror' 363 | }) 364 | equal(res.statusCode, 500) 365 | const data = res.json() 366 | equal(data.message, 'kaboom') 367 | }) 368 | 369 | test('error is the same in hook', async ({ equal, teardown, plan }) => { 370 | plan(3) 371 | const app = Fastify() 372 | teardown(app.close.bind(app)) 373 | 374 | app.setErrorHandler(function (err, req, reply) { 375 | equal(err instanceof Error, true) 376 | return reply.send(err) 377 | }) 378 | 379 | app.register(isolate, { 380 | path: path.join(__dirname, '/plugin.js') 381 | }) 382 | 383 | const res = await app.inject({ 384 | method: 'GET', 385 | url: '/perror' 386 | }) 387 | equal(res.statusCode, 500) 388 | const data = res.json() 389 | equal(data.message, 'kaboom') 390 | }) 391 | 392 | test('TypeError', async ({ equal, teardown }) => { 393 | const app = Fastify() 394 | teardown(app.close.bind(app)) 395 | 396 | app.register(isolate, { 397 | path: path.join(__dirname, '/plugin.js') 398 | }) 399 | 400 | const res = await app.inject({ 401 | method: 'GET', 402 | url: '/typeerror' 403 | }) 404 | equal(res.statusCode, 500) 405 | const data = res.json() 406 | equal(data.message, 'kaboom - typeerror') 407 | }) 408 | 409 | test('RangeError', async ({ equal, teardown }) => { 410 | const app = Fastify() 411 | teardown(app.close.bind(app)) 412 | 413 | app.register(isolate, { 414 | path: path.join(__dirname, '/plugin.js') 415 | }) 416 | 417 | const res = await app.inject({ 418 | method: 'GET', 419 | url: '/rangeerror' 420 | }) 421 | equal(res.statusCode, 500) 422 | const data = res.json() 423 | equal(data.message, 'kaboom - rangeerror') 424 | }) 425 | 426 | test('EvalError', async ({ equal, teardown }) => { 427 | const app = Fastify() 428 | teardown(app.close.bind(app)) 429 | 430 | app.register(isolate, { 431 | path: path.join(__dirname, '/plugin.js') 432 | }) 433 | 434 | const res = await app.inject({ 435 | method: 'GET', 436 | url: '/evalerror' 437 | }) 438 | equal(res.statusCode, 500) 439 | const data = res.json() 440 | equal(data.message, 'kaboom - evalerror') 441 | }) 442 | 443 | test('ReferenceError', async ({ equal, teardown }) => { 444 | const app = Fastify() 445 | teardown(app.close.bind(app)) 446 | 447 | app.register(isolate, { 448 | path: path.join(__dirname, '/plugin.js') 449 | }) 450 | 451 | const res = await app.inject({ 452 | method: 'GET', 453 | url: '/referenceerror' 454 | }) 455 | equal(res.statusCode, 500) 456 | const data = res.json() 457 | equal(data.message, 'kaboom - referenceerror') 458 | }) 459 | 460 | test('SyntaxError', async ({ equal, teardown }) => { 461 | const app = Fastify() 462 | teardown(app.close.bind(app)) 463 | 464 | app.register(isolate, { 465 | path: path.join(__dirname, '/plugin.js') 466 | }) 467 | 468 | const res = await app.inject({ 469 | method: 'GET', 470 | url: '/syntaxerror' 471 | }) 472 | equal(res.statusCode, 500) 473 | const data = res.json() 474 | equal(data.message, 'kaboom - syntaxerror') 475 | }) 476 | 477 | test('URIError', async ({ equal, teardown }) => { 478 | const app = Fastify() 479 | teardown(app.close.bind(app)) 480 | 481 | app.register(isolate, { 482 | path: path.join(__dirname, '/plugin.js') 483 | }) 484 | 485 | const res = await app.inject({ 486 | method: 'GET', 487 | url: '/urierror' 488 | }) 489 | equal(res.statusCode, 500) 490 | const data = res.json() 491 | equal(data.message, 'kaboom - urierror') 492 | }) 493 | 494 | test('throwing a string', async ({ equal, teardown }) => { 495 | const app = Fastify() 496 | teardown(app.close.bind(app)) 497 | 498 | app.register(isolate, { 499 | path: path.join(__dirname, '/plugin.js') 500 | }) 501 | 502 | app.get('/throwstring', async () => { 503 | // eslint-disable-next-line no-throw-literal 504 | throw 'kaboom' 505 | }) 506 | 507 | const res = await app.inject({ 508 | method: 'GET', 509 | url: '/throwstring' 510 | }) 511 | equal(res.statusCode, 500) 512 | const data = res.body 513 | equal(data, 'kaboom') 514 | }) 515 | --------------------------------------------------------------------------------