browserless / browserless

Deploy headless browsers in Docker. Run on our cloud or bring your own. Free for non-commercial uses.
https://browserless.io
Other
8.44k stars 707 forks source link

SDK Docker Issue: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP" #4270

Closed openam closed 1 week ago

openam commented 2 weeks ago

Describe the bug Using the SDK/cli to create a custom image with custom endpoints creates an image that appears to be missing pieces. i.e. ChromeCDP appears to be missing upon building the new image.

To Reproduce Steps to reproduce the behavior:

  1. npx @browserless.io/browserless create # it used @browserless.io/browserless@2.18.0
  2. in that new directory ran npm run docker

    npm run docker
    
    > docker
    > DEBUG=browserless* browserless docker
    
      browserless.io:sdk:log Cleaning build directory +0ms
      browserless.io:sdk:log Compiling TypeScript +3ms
      browserless.io:sdk:log Building custom routes +1s
      browserless.io:sdk:log Building route runtime schema validation +1ms
      browserless.io:sdk:log Generating OpenAPI JSON file +454ms
      browserless.io:sdk:log All built assets complete +39ms
      browserless.io:sdk:log Generating Dockerfile at "/Users/michael/Developer/browserless/test-2-18-0/build/Dockerfile" +0ms
      browserless.io:prompt Which docker image do you want to use (defaults to: ghcr.io/browserless/multi)? +0ms
      > ghcr.io/browserless/chromium
      browserless.io:prompt Do you want to push the image or load it locally (defaults to load)? +0ms
      > load
      browserless.io:prompt What do you want to name the resulting image (eg, my-browserless:latest)? +0ms
      > my-browserless:arm
      browserless.io:prompt Which platform do you want to build for (defaults to linux/amd64)? +0ms
      > linux/arm64
      browserless.io:prompt Will execute "docker buildx build --build-arg FROM=ghcr.io/browserless/chromium --platform linux/arm64 --load -f ./build/Dockerfile -t my-browserless:arm ." Proceed (y/n)? +0ms
      > y
      browserless.io:sdk:log Starting docker build +2m
    [+] Building 487.4s (16/16) FINISHED
    ...<truncated docker build lines>...
  3. tried to run the built image docker run -it --rm --platform=linux/arm64 -p3000:3000 my-browserless:arm
  4. observe the error (see screenshot)
    Unhandled Rejection at: Promise {
      <rejected> Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP"
          at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23
          at Array.forEach (<anonymous>)
          at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19)
    }

Expected behavior A clear and concise description of what you expected to happen.

Screenshots image

If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

Smartphone (please complete the following information):

n/a

Additional context Just to make sure that it should be working on my laptop I tried to run the regular image docker run -it --rm --platform=linux/arm64 ghcr.io/browserless/chromium it appears to start just fine. To be fair I haven't tried to connect to it. It does start up and doesn't throw an error.

Is there something I'm missing? I'm not sure why it's looking for a ChromeCDP binary. I thought it was using the ghcr.io/browserless/chromium image. I also tried this specifying ghrc.io/browserless/chrome but that I had to specify linux/amd64 because there wasn't an arm version available.

joelgriffith commented 2 weeks ago

Is this from a fresh install or is this an existing one?

openam commented 2 weeks ago

When I did npx @browserless.io/browserless create it asked if it was ok to install @browserless.io/browserless@2.18.0. Is that what your asking about?

If not, a fresh install of what?

joelgriffith commented 2 weeks ago

I was just curious if this is a project you've already had running for some time or not, but your response has the context I need. Thanks!

zscumt123 commented 2 weeks ago

I have the same question, how to resolve it? @openam

openam commented 1 week ago

Did you try to install browsers ? : npm run install:browsers

No, I didn't try doing that. I was trying to have it the docker image via the create template. I did just look to see if that's an option in the scaffold/package.json, but it doesn't look like install:browsers is an available script to run inside the Dockerfile.

joelgriffith commented 1 week ago

There were a few bugs worked out yesterday that might have contributed to this, as well as I see that we do try and install browser dependencies with npx after installing the required node_modules. There's a discrepancy where the wrong browser binaries are installed since npx installs the latest stable package versus what's installed in the SDK application.

Going to get a fix for this here shortly.

joelgriffith commented 1 week ago

Should now be fixed in 2.20.0!

openam commented 5 days ago

@joelgriffith I'm still getting this error. I tried to docker rmi --force all my old built images to try and force it not to use a cache. I also did a docker pull on the ghcr.io/browserless/chromium image to get the latest. The error has more detail in it this time, but still says Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP".

image

Full run output with error ``` docker run -it --rm --platform=linux/arm64 -p3000:3000 my-browserless:arm > start > DEBUG=browserless*,-**:verbose browserless start browserless.io:sdk:log Importing all class overrides if present +0ms browserless.io:sdk:log Starting Browserless +4ms browserless.io:limiter:info Concurrency: 10 queue: 10 timeout: 30000ms +0ms browserless.io:sdk:log Starting Browserless HTTP Service +2ms browserless.io:sdk:log Binding signal interruption handlers and uncaught errors +0ms browserless.io:index:info --------------------------------------------------------- | browserless.io | To read documentation and more, load in your browser: | | OpenAPI: http://0.0.0.0:3000/docs | Full Documentation: https://docs.browserless.io/ | Debbuger: http://0.0.0.0:3000/debugger/?token=xxx --------------------------------------------------------- █▓▒ ████▒ ████▒ ████▒ ▒██▓▒ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒██████▓▒ ████▒ ▒██████████▒ ████▒ ▒██████▓████ ████▒ ▒█▓▓▒ ▒████ ████▒ ▒████ ████▒ ▒▓██████ ████▒ ▒▓████████▓▒ ████▓▓████████▓▒ ██████████▓▒ ▓███▓▒ +0ms browserless.io:index:info Running as user "blessuser" +0ms browserless.io:index:info Starting import of HTTP Routes +0ms browserless.io:index:info Starting import of WebSocket Routes +52ms Unhandled Rejection at: Promise { Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) } reason: Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) Unhandled Rejection at: Promise { Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) } reason: Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) Process is finished, exiting npm notice npm notice New minor version of npm available! 10.5.0 -> 10.8.3 npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.3 npm notice Run npm install -g npm@10.8.3 to update! npm notice ```

To be fair I'm doing this on SDK v2.20.2, since that's what existed when I tried it this morning.

I don't see where it does npx install of browser dependencies. I'm guessing it's part the step with npm run build in it. Is that correct

npm run docker output ``` npm run docker > docker > DEBUG=browserless* browserless docker browserless.io:sdk:log Cleaning build directory +0ms browserless.io:sdk:log Compiling TypeScript +6ms browserless.io:sdk:log Building custom routes +2s browserless.io:sdk:log Building route runtime schema validation +1ms browserless.io:sdk:log Generating OpenAPI JSON file +545ms browserless.io:sdk:log All built assets complete +47ms browserless.io:sdk:log Generating Dockerfile at "/Users/michael/Developer/browserless/user-data-dir-2-20-2/build/Dockerfile" +2ms browserless.io:prompt Which docker image do you want to use (defaults to: ghcr.io/browserless/multi)? +0ms > ghcr.io/browserless/chromium browserless.io:prompt Do you want to push the image or load it locally (defaults to load)? +0ms > load browserless.io:prompt What do you want to name the resulting image (eg, my-browserless:latest)? +0ms > my-browserless:arm browserless.io:prompt Which platform do you want to build for (defaults to linux/amd64)? +0ms > linux/arm64 browserless.io:prompt Will execute "docker buildx build --build-arg FROM=ghcr.io/browserless/chromium --platform linux/arm64 --load -f ./build/Dockerfile -t my-browserless:arm ." Proceed (y/n)? +0ms > y browserless.io:sdk:log Starting docker build +39s Command produced the following stderr entries: #0 building with "desktop-linux" instance using docker driver #1 [internal] load build definition from Dockerfile #1 transferring dockerfile: 463B done #1 DONE 0.0s #2 [internal] load metadata for ghcr.io/browserless/chromium:latest #2 DONE 0.0s #3 [internal] load .dockerignore #3 transferring context: 2B done #3 DONE 0.0s #4 [internal] load build context #4 ... #5 [ 1/11] FROM ghcr.io/browserless/chromium:latest #5 DONE 0.2s #4 [internal] load build context #4 transferring context: 200B 0.3s done #4 DONE 0.3s #6 [ 2/11] RUN rm -rf /usr/src/app #6 DONE 0.9s #7 [ 3/11] RUN mkdir -p /usr/src/app #7 DONE 0.2s #8 [ 4/11] WORKDIR /usr/src/app #8 DONE 0.0s #9 [ 5/11] COPY src src #9 DONE 0.0s #10 [ 6/11] COPY package.json . #10 DONE 0.0s #11 [ 7/11] COPY package-lock.json . #11 DONE 0.0s #12 [ 8/11] COPY tsconfig.json . #12 DONE 0.0s #13 [ 9/11] COPY *README.md . #13 DONE 0.0s #14 [10/11] RUN npm install #14 2.201 npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. #14 2.312 npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported #14 2.375 npm WARN deprecated glob@8.1.0: Glob versions prior to v9 are no longer supported #14 2.383 npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported #14 2.385 npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported #14 6.500 #14 6.500 added 489 packages, and audited 490 packages in 6s #14 6.500 #14 6.500 94 packages are looking for funding #14 6.500 run `npm fund` for details #14 6.501 #14 6.501 found 0 vulnerabilities #14 DONE 7.1s #15 [11/11] RUN npm run build #15 0.281 #15 0.281 > build #15 0.281 > DEBUG=browserless* browserless build #15 0.281 #15 1.308 browserless.io:sdk:log Cleaning build directory +0ms #15 1.310 browserless.io:sdk:log Compiling TypeScript +3ms #15 7.021 browserless.io:sdk:log Building custom routes +6s #15 7.022 browserless.io:sdk:log Building route runtime schema validation +1ms #15 7.996 browserless.io:sdk:log Generating OpenAPI JSON file +973ms #15 8.056 browserless.io:sdk:log All built assets complete +61ms #15 8.057 Process is finished, exiting #15 DONE 8.1s #16 exporting to image #16 exporting layers #16 exporting layers 1.7s done #16 writing image sha256:6bec008b3ece162026345fb79f4be7fef4370a60f8cbd03f348b54f014a09d83 done #16 naming to docker.io/library/my-browserless:arm done #16 DONE 1.7s View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/8gom2qzmufkshi7b2rwuanfd6 Process is finished, exiting ```

Here is the docker file contents that ends up in ./build/Dockerfile. This docker file, and the output above indicate that it would have to be the npm run build step that installs the browser.

Dockerfile contents ``` ARG FROM=ghcr.io/browserless/multi:latest FROM ${FROM} # Change to root for install USER root # Cleanup RUN rm -rf $APP_DIR RUN mkdir -p $APP_DIR WORKDIR $APP_DIR # Copy src COPY src src COPY package.json . COPY package-lock.json . COPY tsconfig.json . COPY *README.md . # Install dependencies RUN npm install # Build Source files RUN npm run build # Back to non-privileged user USER blessuser CMD ["npm", "start"] ```
natdm commented 5 days ago

I've followed Tuttles directions and got the exact same error.

History:

10013  nvm i 20
10014  npx @browserless.io/browserless create
10015  cd test-browserless-2-20-2
10018  nvm use 20
10019  npm run docker
10020  docker run -it --rm --platform=linux/arm64 -p3000:3000 my-browserless:arm

Exact same output error as his.

Full error ```bash > start > DEBUG=browserless*,-**:verbose browserless start browserless.io:sdk:log Importing all class overrides if present +0ms browserless.io:sdk:log Starting Browserless +1ms browserless.io:limiter:info  Concurrency: 10 queue: 10 timeout: 30000ms +0ms browserless.io:sdk:log Starting Browserless HTTP Service +1ms browserless.io:sdk:log Binding signal interruption handlers and uncaught errors +1ms browserless.io:index:info  --------------------------------------------------------- | browserless.io | To read documentation and more, load in your browser: | | OpenAPI: http://0.0.0.0:3000/docs | Full Documentation: https://docs.browserless.io/ | Debbuger: http://0.0.0.0:3000/debugger/?token=xxx --------------------------------------------------------- █▓▒ ████▒ ████▒ ████▒ ▒██▓▒ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒████ ████▒ ▒██████▓▒ ████▒ ▒██████████▒ ████▒ ▒██████▓████ ████▒ ▒█▓▓▒ ▒████ ████▒ ▒████ ████▒ ▒▓██████ ████▒ ▒▓████████▓▒ ████▓▓████████▓▒ ██████████▓▒ ▓███▓▒ +0ms browserless.io:index:info  Running as user "blessuser" +0ms browserless.io:index:info  Starting import of HTTP Routes +0ms browserless.io:index:info  Starting import of WebSocket Routes +41ms Unhandled Rejection at: Promise {  Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) } reason: Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) Unhandled Rejection at: Promise {  Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) } reason: Error: Couldn't load route "/chrome/content?(/)" due to missing browser binary for "ChromeCDP". Installed Browsers: class ChromiumCDP extends EventEmitter { config; userDataDir; blockAds; running = false; browser = null; browserWSEndpoint = null; port; logger; proxy = httpProxy.createProxyServer(); executablePath = playwright.chromium.executablePath(); constructor({ blockAds, config, userDataDir, logger, }) { super(); this.userDataDir = userDataDir; this.config = config; this.blockAds = blockAds; this.logger = logger; this.logger.info(`Starting new ${this.constructor.name} instance`); } cleanListeners() { this.browser?.removeAllListeners(); this.removeAllListeners(); } keepUntil() { return 0; } getPageId(page) { // @ts-ignore return page.target()._targetId; } async onTargetCreated(target) { if (target.type() === 'page') { const page = await target.page().catch((e) => { this.logger.error(`Error in ${this.constructor.name} new page ${e}`); return null; }); if (page) { this.logger.trace(`Setting up file:// protocol request rejection`); page.on('error', (err) => { this.logger.error(err); }); page.on('pageerror', (err) => { this.logger.warn(err); }); page.on('framenavigated', (frame) => { this.logger.trace(`Navigation to ${frame.url()}`); }); page.on('console', (message) => { this.logger.trace(`${message.type()}: ${message.text()}`); }); page.on('requestfailed', (req) => { this.logger.warn(`"${req.failure()?.errorText}": ${req.url()}`); }); page.on('request', async (request) => { this.logger.trace(`${request.method()}: ${request.url()}`); if (!this.config.getAllowFileProtocol() && request.url().startsWith('file://')) { this.logger.error(`File protocol request found in request to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); page.on('response', async (response) => { this.logger.trace(`${response.status()}: ${response.url()}`); if (!this.config.getAllowFileProtocol() && response.url().startsWith('file://')) { this.logger.error(`File protocol request found in response to ${this.constructor.name}, terminating`); page.close().catch(noop); this.close(); } }); this.emit('newPage', page); } } } isRunning() { return this.running; } async newPage() { if (!this.browser) { throw new ServerError(`${this.constructor.name} hasn't been launched yet!`); } return this.browser.newPage(); } async close() { if (this.browser) { this.logger.info(`Closing ${this.constructor.name} process and all listeners`); this.emit('close'); this.cleanListeners(); this.browser.removeAllListeners(); this.browser.close(); this.running = false; this.browser = null; this.browserWSEndpoint = null; } } async pages() { return this.browser?.pages() || []; } process() { return this.browser?.process() || null; } async launch({ options, stealth, }) { this.port = await getPort(); this.logger.info(`${this.constructor.name} got open port ${this.port}`); const extensionLaunchArgs = options.args?.find((a) => a.startsWith('--load-extension')); // Remove extension flags as we recompile them below with our own options.args = options.args?.filter((a) => !a.startsWith('--load-extension') && !a.startsWith('--disable-extensions-except')); const extensions = [ this.blockAds ? ublockPath : null, extensionLaunchArgs ? extensionLaunchArgs.split('=')[1] : null, ].filter((_) => !!_); // Bypass the host we bind to so things like /function can work with proxies if (options.args?.some((arg) => arg.includes('--proxy-server'))) { const bypassList = [ this.config.getHost(), new URL(this.config.getExternalAddress()).hostname, ]; options.args.push(`--proxy-bypass-list=${bypassList.join(',')}`); } const finalOptions = { ...options, args: [ `--remote-debugging-port=${this.port}`, `--no-sandbox`, ...(options.args || []), this.userDataDir ? `--user-data-dir=${this.userDataDir}` : '', ].filter((_) => !!_), executablePath: this.executablePath, }; if (extensions.length) { finalOptions.args.push('--load-extension=' + extensions.join(','), '--disable-extensions-except=' + extensions.join(',')); } const launch = stealth ? puppeteerStealth.launch.bind(puppeteerStealth) : puppeteer.launch.bind(puppeteer); this.logger.info(finalOptions, `Launching ${this.constructor.name} Handler`); this.browser = (await launch(finalOptions)); this.browser.on('targetcreated', this.onTargetCreated.bind(this)); this.running = true; this.browserWSEndpoint = this.browser.wsEndpoint(); this.logger.info(`${this.constructor.name} is running on ${this.browserWSEndpoint}`); return this.browser; } wsEndpoint() { return this.browserWSEndpoint; } publicWSEndpoint(token) { if (!this.browserWSEndpoint) { return null; } const externalURL = new URL(this.config.getExternalWebSocketAddress()); const { pathname } = new URL(this.browserWSEndpoint); externalURL.pathname = path.join(externalURL.pathname, pathname); if (token) { externalURL.searchParams.set('token', token); } return externalURL.href; } async proxyPageWebSocket(req, socket, head) { return new Promise(async (resolve, reject) => { if (!this.browserWSEndpoint || !this.browser) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } socket.once('close', resolve); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name}`); const shouldMakePage = req.parsed.pathname.includes(BLESS_PAGE_IDENTIFIER); const page = shouldMakePage ? await this.browser.newPage() : null; const pathname = page ? path.join('/devtools', '/page', this.getPageId(page)) : req.parsed.pathname; const target = new URL(pathname, this.browserWSEndpoint).href; req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } async proxyWebSocket(req, socket, head) { return new Promise((resolve, reject) => { if (!this.browserWSEndpoint) { throw new ServerError(`No browserWSEndpoint found, did you launch first?`); } const close = once(() => { this.browser?.off('close', close); this.browser?.process()?.off('close', close); socket.off('close', close); return resolve(); }); this.browser?.once('close', close); this.browser?.process()?.once('close', close); socket.once('close', close); this.logger.info(`Proxying ${req.parsed.href} to ${this.constructor.name} ${this.browserWSEndpoint}`); req.url = ''; // Delete headers known to cause issues delete req.headers.origin; this.proxy.ws(req, socket, head, { changeOrigin: true, target: this.browserWSEndpoint, }, (error) => { this.logger.error(`Error proxying session to ${this.constructor.name}: ${error}`); this.close(); return reject(error); }); }); } }, class ChromiumPlaywright extends BasePlaywright { playwrightBrowserType = PlaywrightBrowserTypes.chromium; } at file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:202:23 at Array.forEach () at Browserless.start (file:///usr/src/app/node_modules/@browserless.io/browserless/build/browserless.js:197:19) Process is finished, exiting npm notice npm notice New minor version of npm available! 10.5.0 -> 10.8.3 npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.8.3 npm notice Run npm install -g npm@10.8.3 to update! npm notice  ```