@browserless/function
v14.0.0
Published
Execute arbitrary JavaScript code in a secure browser sandbox with access to page DOM and network.
Maintainers
Readme
@browserless/function: Run arbitrary JavaScript inside a browser sandbox.
See the function section on our website for more information.
Install
Using npm:
npm install @browserless/function --saveAbout
This package provides a secure sandbox for running arbitrary JavaScript code with runtime access to a browser page. It executes user-provided functions in an isolated VM environment, with optional access to Puppeteer's page API.
What this package does
The @browserless/function package allows you to:
- Execute arbitrary JavaScript in a secure, isolated VM sandbox
- Access the browser page from within the sandbox for DOM manipulation
- Capture console output and execution profiling data
- Pass custom data to the sandboxed function at runtime
- Handle errors gracefully with structured result objects
Usage
First, create a function instance by calling the factory:
const createFunction = require('@browserless/function')()You can optionally pass a tmpdir for the sandbox working directory:
const createFunction = require('@browserless/function')({ tmpdir: '/tmp/functions' })Then use it to run arbitrary JavaScript:
// Simple function without page access
const code = ({ query }) => query.value * 2
const myFn = createFunction(code)
const result = await myFn('https://example.com', { query: { value: 21 } })
console.log(result)
// => { isFulfilled: true, value: 42, profiling: {...}, logging: {...} }Accessing the page
When your code references page, browserless automatically provides access to the Puppeteer page:
const createFunction = require('@browserless/function')()
// Function with page access
const code = async ({ page }) => {
const title = await page.title()
const content = await page.evaluate(() => document.body.innerText)
return { title, content }
}
const scraper = createFunction(code)
const result = await scraper('https://example.com')
console.log(result)
// => { isFulfilled: true, value: { title: 'Example', content: '...' }, ... }Available context
The sandboxed function receives these properties:
| Property | Description |
|----------|-------------|
| url | Target URL passed to the function (no browser required) |
| page | Puppeteer Page object (if referenced in code) |
| device | Device descriptor with userAgent and viewport |
| ...opts | Any custom options passed at runtime |
Result object
The function returns a structured result:
| Property | Description |
|----------|-------------|
| isFulfilled | true if execution succeeded, false if error |
| value | Return value (success) or error object (failure) |
| profiling | Execution timing and resource usage (see below) |
| logging | Captured console output (log, warn, error, etc.) |
Profiling
The profiling object contains phased timing and resource data:
| Property | Description |
|----------|-------------|
| phases.compile | Time to compile the sandbox script (ms) |
| phases.spawn | Time to spawn the child process (ms) |
| phases.run | Time executing the user function (ms) |
| phases.total | Total wall-clock time (ms) |
| cpu | CPU time consumed (ms) |
| memory | Peak memory usage (bytes) |
Teardown
Call .teardown() on the factory instance to clean up sandbox resources:
const createFunction = require('@browserless/function')({ tmpdir: '/tmp/functions' })
// ... use createFunction ...
await createFunction.teardown()extendPage
Attach extra methods on page. JSON values become async getters. Functions are inlined as async methods (this is the page; use a function, not an arrow, when you need this). If the user function only uses these methods, Chromium is not started:
const myFn = createFunction(({ page }) => page.html(), {
extendPage: { url, html }
})
const result = await myFn('https://example.com')
// => { isFulfilled: true, value: html, ... }hostPage
Attach methods on page that the host resolves when the user function calls them, rather than values decided before it runs. Work the function never reaches costs nothing:
const myFn = createFunction(({ page }) => page.content(), {
hostPage: { content: () => fetchThePage(url) }
})
const result = await myFn('https://example.com')
// => { isFulfilled: true, value: html, ... }, and fetchThePage ran oncefetchThePage runs because the function awaited content. A function returning 420 without touching page never triggers it, and neither does a branch it does not take:
const myFn = createFunction('async ({ page }) => (false ? await page.content() : "skipped")', {
hostPage: { content: () => fetchThePage(url) }
})
const result = await myFn('https://example.com')
// => { isFulfilled: true, value: 'skipped', ... }, and fetchThePage never ranUnlike extendPage, the method itself stays on the host rather than being serialized into the isolate; the call travels over the channel at the moment it is made. Its arguments and its result still cross that channel, so both have to be values the channel can carry: a BigInt, for instance, rejects the call rather than resolving it.
Both kinds can sit on the same page, and either counts as satisfying that method, so a function using only these does not start Chromium. A name present on both is the extendPage value, and that host method is not reachable.
The same method and arguments resolve once per run. 32 distinct calls are allowed per run; vmOpts.maxHostCalls changes the cap. The isolate runs untrusted code and can reach the channel directly, so treat every argument as untrusted input. Every hostPage value has to be a function.
Options
const myFn = createFunction(code, {
// Browserless instance factory
getBrowserless: () => require('browserless')(),
// Attempts on the `getPage` path, where there is no context to replace and
// this is the only retry. The default path takes its retry count from the
// browserless context instead.
retry: 2,
// Execution timeout in milliseconds
timeout: 30000,
// Extra methods on `page`. JSON values become async getters; functions
// are inlined as async methods. If the function only uses these methods,
// Chromium is not started.
extendPage: {
url,
html
},
// Methods on `page` the host resolves when the function calls them. The
// method stays on the host and one the function never reaches is never
// resolved, though arguments and results still cross the channel and must
// be values it can carry. Counts as satisfying that method, like extendPage.
hostPage: {
content: () => fetchThePage(url)
},
// Options passed to browserless.goto()
gotoOpts: {
scripts: ['https://cdn.example.com/library.js'],
waitUntil: 'networkidle0'
},
// VM sandbox options (passed to isolated-function)
vmOpts: { /* ... */ },
// Run against a page that is already navigated, instead of creating a
// context and navigating. It replaces that path, so `getBrowserless` is not
// consulted when it is set. No `goto` happens and the page is never closed:
// whoever supplied it owns its lifetime. Pass `response` when you have it,
// so the function still sees `_response`. `timeout` bounds how long the
// caller waits. It leaves the page open, and the snippet plus its isolate
// subprocess keep running until the snippet returns or the page is closed.
// `device` and `response` are optional. Without a device, the `{ userAgent,
// viewport }` a snippet sees is read off the page, so it matches what the
// default path reports; pass your own to override it.
getPage: async () => ({ page, device, response })
})Reusing a page the caller already loaded, so the target is fetched once.
getPage replaces the context-and-navigate path, so getBrowserless is not
consulted on this call:
const { page, response } = await somethingThatAlreadyNavigated(url)
const result = await createFunction(code, {
getPage: async () => ({ page, response })
})(url)
await page.close()The snippet runs in an isolate connected over browserWSEndpoint, so
page.browser() reaches every page on that browser, not only the one it was
handed. strictTarget decides which page that is, not what it can reach. This
was already true of the navigating path, and it matters more once one context is
shared across tasks: treat sharing as a trust boundary this package cannot
enforce for you.
timeout rejects the call and leaves the page open. The snippet and its
isolate subprocess keep running, so treat the page as busy: leave it alone
until that work finishes, or close it. Closing the page ends the snippet.
Retaining a page from browserless requires keepPage, since evaluate
closes its page as soon as it resolves:
let page
await context.evaluate(
currentPage => {
page = currentPage
return currentPage.content()
},
{ keepPage: true }
)(url)Retaining a page restarts its close watchdog. The new window is the
evaluate / withPage timeout, measured from handover. Finish or close the
page before that window ends. A later getPage call has its own timeout; the
watchdog still closes the page when the evaluate window ends, including while
that call is in progress. Set the evaluate timeout long enough to cover the
follow-up work.
Examples
Interact with page elements
const createFunction = require('@browserless/function')()
const code = async ({ page }) => {
await page.waitForSelector('button.submit')
await page.type('input', '[email protected]', { delay: 200 })
await page.click('button.submit')
await page.waitForNavigation()
return page.title()
}
const clickAndGetTitle = createFunction(code)
const result = await clickAndGetTitle('https://example.com')Inject external scripts
const createFunction = require('@browserless/function')()
const code = ({ page }) => page.evaluate('jQuery.fn.jquery')
const getjQueryVersion = createFunction(code, {
gotoOpts: {
scripts: ['https://code.jquery.com/jquery-3.6.0.min.js']
}
})
const result = await getjQueryVersion('https://example.com')
// => { isFulfilled: true, value: '3.6.0', ... }Use npm modules in sandbox
const createFunction = require('@browserless/function')()
const code = async ({ page }) => {
const _ = require('lodash')
const text = await page.evaluate(() => document.body.innerText)
return _.words(text).length
}
const countWords = createFunction(code)
const result = await countWords('https://example.com')Handle errors
const createFunction = require('@browserless/function')()
const code = () => {
throw new Error('Something went wrong')
}
const myFn = createFunction(code)
const result = await myFn('https://example.com')
console.log(result.isFulfilled) // => false
console.log(result.value.message) // => 'Something went wrong'How it fits in the monorepo
This is an extended functionality package. It is not wired into browserless core — install it when you need a sandbox that can touch a page.
Dependencies
| Package | Purpose |
|---------|---------|
| @browserless/errors | Error normalization and typed errors |
| isolated-function | Secure sandboxed execution via child processes |
| require-one-of | Auto-detects browserless installation |
| acorn / acorn-walk | AST parsing to detect page usage in code |
License
@browserless/function © Microlink, released under the MIT License. Authored and maintained by Microlink with help from contributors.
The logo has been designed by xinh studio.
microlink.io · GitHub microlinkhq · X @microlinkhq
