• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microlinkhq / browserless / 30949979540

04 Aug 2026 08:54PM UTC coverage: 80.583% (+1.9%) from 78.732%
30949979540

push

github

web-flow
fix: context leaks, screenshot readiness, and prepare scroll (#869)

* fix(screenshot): hydrate white overflow viewports during readiness

tryHydrateScroll was gated on !isWhite, but the default isPageReady is
!isWhite — so a white viewport made the hydrate condition unreachable.
Overflow/lazy SPAs that stay blank until scrolled never got the one-shot
hydrate from #852, and fullPage/PDF then skipped prepareFullDocument and
returned a blank capture as success.

Drop the !isWhite guard so the budgeted hydrate can run when the viewport
is still white.

Co-authored-by: kikohumanbeatbox <kikohumanbeatbox@gmail.com>

* fix(screenshot): drop probe quality; tolerate prepare scroll nav

#852's fullPage readiness probe clears `path` so it does not write during
the white check. #858 keeps `quality` when that path looked lossy. Together
the probe becomes `{ quality }` on puppeteer's png default and throws
`png screenshots do not support 'quality'`. Drop quality with the path on
the probe; the final fullPage capture still receives both.

Also wrap prepareFullDocument's scroll in pReflect like its overflow wait
and expand siblings. A SPA that client-navigates during the multi-second
scroll was failing the whole PDF/fullPage job with a bare context-destroyed
throw from settleDom/evaluate.

Co-authored-by: kikohumanbeatbox <kikohumanbeatbox@gmail.com>

* fix(function): destroy browser context after each page invocation

runWithBrowser created an isolated BrowserContext per call but only
closed the page via withPage, so repeated @browserless/function runs
that reference page accumulated orphaned Chromium contexts until the
process exhausted memory. Tear the context down in a finally block,
including when withPage rejects.

Co-authored-by: kikohumanbeatbox <kikohumanbeatbox@gmail.com>

* test(function): assert context teardown after withPage failure

Lock in that destroyContext still runs when the page path rejects, so
orphaned BrowserContexts cannot accumulate on the ... (continued)

725 of 886 branches covered (81.83%)

Branch coverage included in aggregate %.

44 of 47 new or added lines in 4 files covered. (93.62%)

3836 of 4774 relevant lines covered (80.35%)

104958.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.05
/packages/function/src/index.js
1
'use strict'
9✔
2

9✔
3
const { isBrowserlessError, ensureError } = require('@browserless/errors')
9✔
4
const createIsolatedFunction = require('isolated-function')
9✔
5
const requireOneOf = require('require-one-of')
9✔
6
const createRunFunction = require('./function')
9✔
7
const path = require('path')
9✔
8

9✔
9
const cloudflareDir = path.dirname(require.resolve('@cloudflare/puppeteer/package.json'))
9✔
10
const nodePaths = [path.resolve(cloudflareDir, '..', '..')]
9✔
11

9✔
12
const stringify = fn => fn.toString().trim().replace(/;$/, '')
9✔
13

9✔
14
const getTargetId = async page => {
9✔
15
  try {
21✔
16
    const session = await page.createCDPSession()
21✔
17
    const { targetInfo } = await session.send('Target.getTargetInfo')
16✔
18
    await session.detach()
16✔
19
    return targetInfo.targetId
16✔
20
  } catch {
21✔
21
    return undefined
5✔
22
  }
5✔
23
}
21✔
24

9✔
25
const isHttpResponse = response => response != null && typeof response.status === 'function'
9✔
26

9✔
27
const serializeResponse = response => ({
9✔
28
  status: response.status(),
17✔
29
  statusText: response.statusText(),
17✔
30
  url: response.url(),
17✔
31
  ok: response.ok(),
17✔
32
  headers: response.headers(),
17✔
33
  remoteAddress: response.remoteAddress(),
17✔
34
  timing: response.timing(),
17✔
35
  fromCache: response.fromCache(),
17✔
36
  fromServiceWorker: response.fromServiceWorker()
17✔
37
})
17✔
38

9✔
39
module.exports = ({ tmpdir } = {}) => {
9✔
40
  const isolatedFunction = createIsolatedFunction({ tmpdir, nodePaths })
9✔
41
  const runFunction = createRunFunction(isolatedFunction)
9✔
42

9✔
43
  const createFunction = (
9✔
44
    fn,
31✔
45
    {
31✔
46
      getBrowserless = requireOneOf(['browserless']),
31✔
47
      retry = 2,
31✔
48
      timeout = 30000,
31✔
49
      gotoOpts,
31✔
50
      ...opts
31✔
51
    } = {}
31✔
52
  ) => {
31✔
53
    const code = stringify(fn)
31✔
54
    const needsNetwork = createRunFunction.isUsingPage(code)
31✔
55
    const source = createRunFunction.buildTemplate(code, needsNetwork)
31✔
56
    let browserPromise
31✔
57

31✔
58
    const getBrowser = async () => {
31✔
59
      if (!browserPromise) {
23✔
60
        browserPromise = Promise.resolve(getBrowserless()).catch(error => {
22✔
61
          browserPromise = undefined
1✔
62
          throw error
1✔
63
        })
22✔
64
      }
22✔
65
      return browserPromise
23✔
66
    }
23✔
67

31✔
68
    const runWithBrowser = async (url, fnOpts) => {
31✔
69
      const browser = await getBrowser()
23✔
70
      const browserless = await browser.createContext()
22✔
71

22✔
72
      try {
22✔
73
        return await browserless.withPage((page, goto) => async () => {
22✔
74
          const { device, response } = await goto(page, { url, timeout, ...gotoOpts })
21✔
75

21✔
76
          const targetId = await getTargetId(page)
21✔
77

21✔
78
          const runFunctionOpts = {
21✔
79
            url,
21✔
80
            code,
21✔
81
            device,
21✔
82
            ...opts,
21✔
83
            ...fnOpts,
21✔
84
            ...(isHttpResponse(response) && { _response: serializeResponse(response) })
21✔
85
          }
21✔
86

21✔
87
          if (runFunctionOpts.code === code) {
21✔
88
            runFunctionOpts.needsNetwork = needsNetwork
21✔
89
            runFunctionOpts.source = source
21✔
90
          }
21✔
91

21✔
92
          const browserFromPage = typeof page.browser === 'function' ? page.browser() : undefined
21!
93
          const browserWSEndpoint =
21✔
94
            browserFromPage && typeof browserFromPage.wsEndpoint === 'function'
21✔
95
              ? browserFromPage.wsEndpoint()
21✔
96
              : undefined
21!
97

21✔
98
          if (!browserWSEndpoint) throw new Error('Browser WebSocket endpoint not found')
21✔
99
          runFunctionOpts.browserWSEndpoint = browserWSEndpoint
20✔
100
          runFunctionOpts.targetId = targetId
20✔
101

20✔
102
          const result = await runFunction(runFunctionOpts)
20✔
103

20✔
104
          if (result.isFulfilled) return result
20✔
NEW
105
          const error = ensureError(result.value)
×
NEW
106
          if (isBrowserlessError(error)) throw error
×
NEW
107
          return result
×
108
        })()
22✔
109
      } finally {
22✔
110
        await browserless.destroyContext()
22✔
111
      }
22✔
112
    }
23✔
113

31✔
114
    const runWithoutBrowser = async (url, fnOpts) => {
31✔
115
      const runFunctionOpts = {
12✔
116
        url,
12✔
117
        code,
12✔
118
        ...opts,
12✔
119
        ...fnOpts
12✔
120
      }
12✔
121

12✔
122
      if (runFunctionOpts.code === code) {
12✔
123
        runFunctionOpts.needsNetwork = false
12✔
124
        runFunctionOpts.source = source
12✔
125
      }
12✔
126

12✔
127
      const result = await runFunction(runFunctionOpts)
12✔
128

12✔
129
      if (result.isFulfilled) return result
12✔
130
      const error = ensureError(result.value)
1✔
131
      if (isBrowserlessError(error)) throw error
7!
132
      return result
1✔
133
    }
12✔
134

31✔
135
    return async (url, fnOpts = {}) =>
31✔
136
      needsNetwork ? runWithBrowser(url, fnOpts) : runWithoutBrowser(url, fnOpts)
35✔
137
  }
31✔
138

9✔
139
  createFunction.teardown = () => isolatedFunction.teardown()
9✔
140

9✔
141
  return createFunction
9✔
142
}
9✔
143

9✔
144
module.exports.isHttpResponse = isHttpResponse
9✔
145
module.exports.serializeResponse = serializeResponse
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc