Skip to content

Commit de8dad0

Browse files
authored
feat(functions): use isolated-function (#591)
* chore(functions): use isolated-function It drops vm2 which is considered unsecured in favour of isolated-function * refactor: only handle browser errors * refactor: drop unnecessary logic
1 parent 6e3fb40 commit de8dad0

File tree

7 files changed

+192
-336
lines changed

7 files changed

+192
-336
lines changed

packages/errors/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ const debug = require('debug-logfmt')('browserless:error')
44
const { serializeError } = require('serialize-error')
55
const whoops = require('whoops')
66

7-
const createErrorFactory = opts => whoops('BrowserlessError', opts)
7+
const ERROR_NAME = 'BrowserlessError'
8+
9+
const createErrorFactory = opts => whoops(ERROR_NAME, opts)
810

911
const markAsProcessed = error => {
1012
Object.defineProperty(error, '__parsed', {
@@ -80,4 +82,7 @@ browserlessError.ensureError = rawError => {
8082
return require('ensure-error')(error)
8183
}
8284

85+
const isBrowserlessError = error => error.name === ERROR_NAME
86+
8387
module.exports = browserlessError
88+
module.exports.isBrowserlessError = isBrowserlessError

packages/function/package.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,9 @@
2929
"serverless"
3030
],
3131
"dependencies": {
32-
"@browserless/errors": "^10.5.2",
33-
"debug-logfmt": "~1.2.1",
34-
"deepmerge": "~4.3.1",
35-
"p-reflect": "~2.1.0",
32+
"isolated-function": "~0.1.4",
3633
"p-retry": "~4.6.1",
37-
"p-timeout": "~4.1.0",
38-
"require-one-of": "~1.0.19",
39-
"serialize-error": "~8.1.0",
40-
"vm2": "~3.9.19"
34+
"require-one-of": "~1.0.19"
4135
},
4236
"devDependencies": {
4337
"@browserless/test": "^10.5.2",

packages/function/src/function.js

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,22 @@
11
'use strict'
22

3-
const path = require('path')
4-
5-
const createVm = require('./vm')
6-
7-
const scriptPath = path.resolve(__dirname, 'function.js')
3+
const isolatedFunction = require('isolated-function')
84

95
const createFn = code => `
10-
async ({ url, gotoOpts, browserWSEndpoint, ...opts }) => {
11-
const { serializeError } = require('serialize-error')
12-
13-
const getBrowserless = require('browserless')
14-
const browserless = await getBrowserless({ mode: 'connect', browserWSEndpoint }).createContext()
15-
const fnWrapper = fn => (page, response) => {
16-
if (!response && opts.response) {
17-
const { status, statusText, headers, html } = opts.response
18-
response = {
19-
ok: () => status === 0 || (status >= 200 && opts.status <= 299),
20-
fromCache: () => false,
21-
fromServiceWorker: () => false,
22-
url: () => url,
23-
text: () => html,
24-
statusText: () => statusText,
25-
json: () => JSON.parse(html),
26-
headers: () => headers,
27-
status: () => status
28-
}
29-
}
30-
31-
return fn({ ...opts, page, response, url })
32-
}
33-
const browserFn = browserless.evaluate(fnWrapper(${code}), gotoOpts)
34-
6+
async (url, browserWSEndpoint, opts) => {
7+
const puppeteer = require('@cloudflare/puppeteer')
8+
const browser = await puppeteer.connect({ browserWSEndpoint })
9+
const page = (await browser.pages())[1]
3510
try {
36-
const value = await browserFn(url)
37-
return { isFulfilled: true, isRejected: false, value }
38-
} catch (error) {
39-
return { isFulfilled: false, isRejected: true, reason: serializeError(error) }
11+
return await (${code})({ page, ...opts })
4012
} finally {
41-
await browserless.destroyContext()
42-
await browserless.browser().then(browser => browser.disconnect())
13+
await browser.disconnect()
4314
}
4415
}`
4516

46-
module.exports = ({ url, code, vmOpts, gotoOpts, browserWSEndpoint, ...opts }) => {
47-
const vm = createVm(vmOpts)
48-
const fn = createFn(code)
49-
const run = vm(fn, scriptPath)
50-
return run({ url, gotoOpts, browserWSEndpoint, ...opts })
17+
module.exports = async ({ url, code, vmOpts, browserWSEndpoint, ...opts }) => {
18+
const [fn, teardown] = isolatedFunction(createFn(code), { ...vmOpts, throwError: false })
19+
const result = await fn(url, browserWSEndpoint, opts)
20+
await teardown()
21+
return result
5122
}

packages/function/src/index.js

Lines changed: 33 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,41 @@
11
'use strict'
22

3-
const { ensureError, browserTimeout } = require('@browserless/errors')
4-
const debug = require('debug-logfmt')('browserless:function')
3+
const { isBrowserlessError, ensureError } = require('@browserless/errors')
54
const requireOneOf = require('require-one-of')
6-
const pTimeout = require('p-timeout')
7-
const pRetry = require('p-retry')
8-
9-
const { AbortError } = pRetry
10-
115
const runFunction = require('./function')
126

137
const stringify = fn => fn.toString().trim().replace(/;$/, '')
148

15-
module.exports = (
16-
fn,
17-
{ getBrowserless = requireOneOf(['browserless']), retry = 2, timeout = 30000, ...opts } = {}
18-
) => {
19-
return async (url, fnOpts = {}) => {
20-
const browserlessPromise = getBrowserless()
21-
let isRejected = false
22-
23-
async function run () {
24-
const browserless = await browserlessPromise
25-
const browser = await browserless.browser()
26-
const browserWSEndpoint = browser.wsEndpoint()
27-
28-
const { value, reason, isFulfilled } = await runFunction({
29-
url,
30-
code: stringify(fn),
31-
browserWSEndpoint,
32-
...opts,
33-
...fnOpts
34-
})
35-
36-
if (isFulfilled) return value
37-
throw ensureError(reason)
9+
module.exports =
10+
(
11+
fn,
12+
{
13+
getBrowserless = requireOneOf(['browserless']),
14+
retry = 2,
15+
timeout = 30000,
16+
gotoOpts,
17+
...opts
18+
} = {}
19+
) =>
20+
async (url, fnOpts = {}) => {
21+
const browserlessPromise = getBrowserless()
22+
const browser = await browserlessPromise
23+
const browserless = await browser.createContext()
24+
25+
return browserless.withPage((page, goto) => async () => {
26+
const { device } = await goto(page, { url, ...gotoOpts })
27+
const result = await runFunction({
28+
url,
29+
code: stringify(fn),
30+
browserWSEndpoint: (await browserless.browser()).wsEndpoint(),
31+
device,
32+
...opts,
33+
...fnOpts
34+
})
35+
36+
if (result.isFulfilled) return result
37+
const error = ensureError(result.value)
38+
if (isBrowserlessError(error)) throw error
39+
return result
40+
})()
3841
}
39-
40-
const task = () =>
41-
pRetry(run, {
42-
retries: retry,
43-
onFailedAttempt: async error => {
44-
if (error.name === 'AbortError') throw error
45-
if (isRejected) throw new AbortError()
46-
await (await browserlessPromise).respawn()
47-
const { message, attemptNumber, retriesLeft } = error
48-
debug('retry', { attemptNumber, retriesLeft, message })
49-
}
50-
})
51-
52-
// main
53-
const result = await pTimeout(task(), timeout, () => {
54-
isRejected = true
55-
throw browserTimeout({ timeout })
56-
})
57-
58-
return result
59-
}
60-
}

packages/function/src/vm.js

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)