browserless 14.0.19 → 14.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "browserless",
3
3
  "description": "The headless Chrome/Chromium driver on top of Puppeteer. Take screenshots, generate PDFs, extract text and HTML with a production-ready API.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "14.0.19",
5
+ "version": "14.0.22",
6
6
  "types": "index.d.ts",
7
7
  "main": "src/index.js",
8
8
  "author": {
@@ -35,10 +35,10 @@
35
35
  "web-scraping"
36
36
  ],
37
37
  "dependencies": {
38
- "@browserless/errors": "14.0.0",
39
- "@browserless/goto": "14.0.19",
40
- "@browserless/pdf": "14.0.19",
41
- "@browserless/screenshot": "14.0.19",
38
+ "@browserless/errors": "14.0.22",
39
+ "@browserless/goto": "14.0.22",
40
+ "@browserless/pdf": "14.0.22",
41
+ "@browserless/screenshot": "14.0.22",
42
42
  "debug-logfmt": "~1.4.15",
43
43
  "kill-process-group": "~1.0.16",
44
44
  "p-reflect": "~2.1.0",
@@ -48,7 +48,7 @@
48
48
  "superlock": "~1.3.5"
49
49
  },
50
50
  "devDependencies": {
51
- "ava": "5",
51
+ "ava": "8",
52
52
  "ps-list": "7",
53
53
  "puppeteer": "25",
54
54
  "tinyspawn": "latest"
@@ -69,5 +69,5 @@
69
69
  "timeout": "2m",
70
70
  "workerThreads": false
71
71
  },
72
- "gitHead": "1600991013bc8ed915b2effa9e74bbf1b97d1f02"
72
+ "gitHead": "22c8d447e55cfe1eb7b1e36af9a5cc4d9139bd23"
73
73
  }
package/src/driver.js CHANGED
@@ -69,26 +69,32 @@ const getPid = subprocess => {
69
69
  return 'pid' in browserProcess ? browserProcess.pid : undefined
70
70
  }
71
71
 
72
- const close = async (subprocess, { signal = 'SIGKILL', ...debugOpts } = {}) => {
72
+ const close = async (subprocess, { signal = 'SIGKILL', force = false, ...debugOpts } = {}) => {
73
73
  const pid = getPid(subprocess)
74
74
  const hasDisconnect = subprocess && typeof subprocess.disconnect === 'function'
75
75
  const hasClose = subprocess && typeof subprocess.close === 'function'
76
76
 
77
77
  if (pid === undefined && !hasDisconnect && !hasClose) return
78
78
 
79
- // It's necessary to call `browser.close` for removing temporal files associated
80
- // and remove listeners attached to the main process; check
81
- // - https://github.com/puppeteer/puppeteer/blob/778ac92469d66c542c3c12fe0aa23703dd6315c2/src/node/BrowserRunner.ts#L146
82
- // - https://github.com/puppeteer/puppeteer/blob/69d85e874416d62de6e821bef30e5cebcfd42f15/src/node/BrowserRunner.ts#L189
83
- await pReflect(
84
- pid === undefined
85
- ? hasDisconnect
86
- ? subprocess.disconnect()
87
- : subprocess.close()
88
- : hasClose
89
- ? subprocess.close()
90
- : killProcessGroup(subprocess, signal)
91
- )
79
+ const child = typeof subprocess.process === 'function' ? subprocess.process() : undefined
80
+ const kill = () => {
81
+ const proc = child && child.pid ? child : subprocess
82
+ return proc && proc.pid ? killProcessGroup(proc, { signal }) : undefined
83
+ }
84
+
85
+ // `browser.close` removes temp files and listeners on the main process:
86
+ // https://github.com/puppeteer/puppeteer/blob/778ac92469d66c542c3c12fe0aa23703dd6315c2/src/node/BrowserRunner.ts#L146
87
+ // https://github.com/puppeteer/puppeteer/blob/69d85e874416d62de6e821bef30e5cebcfd42f15/src/node/BrowserRunner.ts#L189
88
+ const graceful = () => {
89
+ if (pid === undefined) {
90
+ return hasDisconnect ? subprocess.disconnect() : subprocess.close()
91
+ }
92
+ if (hasClose) return subprocess.close()
93
+ return kill()
94
+ }
95
+
96
+ // A browser that has stopped answering CDP will not finish `browser.close()`.
97
+ await pReflect(force ? kill() : graceful())
92
98
 
93
99
  debug('close', { pid, signal, ...debugOpts })
94
100
  return pid === undefined ? {} : { pid }
package/src/index.js CHANGED
@@ -14,6 +14,19 @@ const { AbortError } = pRetry
14
14
 
15
15
  const driver = require('./driver')
16
16
 
17
+ // Chrome can stop answering CDP while `connected` stays true, so the
18
+ // disconnect respawn never runs. Puppeteer then waits out its 180s protocol
19
+ // timeout on every context, and each wait holds a pool slot.
20
+ const CREATE_CONTEXT_TIMEOUT = 10_000
21
+
22
+ const closeLateContext = pending => pending.then(context => context.close()).catch(() => {})
23
+
24
+ const isUnresponsiveBrowser = error =>
25
+ error?.name === 'TimeoutError' ||
26
+ (typeof error?.message === 'string' &&
27
+ error.message.includes('timed out') &&
28
+ error.message.includes('protocolTimeout'))
29
+
17
30
  module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
18
31
  const lock = withLock()
19
32
  const goto = createGoto({ timeout: globalTimeout, ...launchOpts })
@@ -26,10 +39,10 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
26
39
  return browserProcessPromise.then(browserProcess => driver.close(browserProcess, opts))
27
40
  }
28
41
 
29
- const respawn = () =>
42
+ const respawn = ({ force = false } = {}) =>
30
43
  !isClosed &&
31
44
  Promise.all([
32
- browserProcessPromise.then(driver.close),
45
+ browserProcessPromise.then(browser => driver.close(browser, { force })),
33
46
  (browserProcessPromise = spawn({ respawn: true }))
34
47
  ])
35
48
 
@@ -64,8 +77,24 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
64
77
 
65
78
  let browserProcessPromise = spawn()
66
79
 
67
- const createBrowserContext = contextOpts =>
68
- getBrowser().then(browser => browser.createBrowserContext(contextOpts))
80
+ const createBrowserContext = async contextOpts => {
81
+ for (let attempt = 0; attempt < 2; attempt++) {
82
+ const browser = await getBrowser()
83
+ const pending = browser.createBrowserContext(contextOpts)
84
+
85
+ try {
86
+ return await pTimeout(pending, Math.min(globalTimeout, CREATE_CONTEXT_TIMEOUT))
87
+ } catch (error) {
88
+ closeLateContext(pending)
89
+ if (attempt > 0 || !isUnresponsiveBrowser(error)) throw error
90
+ debug('respawn', { reason: 'createBrowserContext', message: error.message })
91
+ await lock(async () => {
92
+ const current = await browserProcessPromise.catch(() => {})
93
+ if (current === browser) await respawn({ force: true })
94
+ })
95
+ }
96
+ }
97
+ }
69
98
 
70
99
  const getBrowser = async () => {
71
100
  if (isClosed) return browserProcessPromise