Skip to content

Commit

Permalink
fix: provide more detailed error messages for browser launch errors (#…
Browse files Browse the repository at this point in the history
…2157)

This enhances the default stack trace and messages for browser pool
launch errors to directly mention what went wrong (as some loggers may
not include the `cause` property to be logged, which is quite critical
for debugging what went wrong)
  • Loading branch information
vladfrangu committed Nov 2, 2023
1 parent c5a1b07 commit f188ebe
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 28 deletions.
44 changes: 44 additions & 0 deletions packages/browser-pool/src/abstract-classes/browser-plugin.ts
@@ -1,3 +1,4 @@
import { CriticalError } from '@crawlee/core';
import type { Dictionary } from '@crawlee/types';
import merge from 'lodash.merge';

Expand Down Expand Up @@ -206,6 +207,30 @@ export abstract class BrowserPlugin<
return originalArgs;
}

protected _throwAugmentedLaunchError(
cause: unknown,
executablePath: string | undefined,
dockerImage: string,
moduleInstallCommand: string,
): never {
const errorMessage = ['Failed to launch browser. Please check the following:'];

if (executablePath) {
errorMessage.push(`- Check whether the provided executable path "${executablePath}" is correct.`);
}

if (process.env.APIFY_IS_AT_HOME) {
errorMessage.push(`- Make sure your Dockerfile extends ${dockerImage}.`);
}

errorMessage.push(`- ${moduleInstallCommand}`);

errorMessage.push('', 'The original error is available in the `cause` property. Below is the error received when trying to launch a browser:', '');

// Add in a zero-width space so we can remove it later when printing the error stack
throw new BrowserLaunchError(`${errorMessage.join('\n')}\u200b`, { cause });
}

/**
* @private
*/
Expand Down Expand Up @@ -236,3 +261,22 @@ export abstract class BrowserPlugin<
throwImplementationNeeded('_createController');
}
}

export class BrowserLaunchError extends CriticalError {
public constructor(...args: ConstructorParameters<typeof CriticalError>) {
super(...args);
this.name = 'BrowserLaunchError';

const [, oldStack] = this.stack?.split('\u200b') ?? [null, ''];

Object.defineProperty(this, 'stack', {
get: () => {
if (this.cause instanceof Error) {
return `${this.message}\n${this.cause.stack}\nError thrown at:\n${oldStack}`;
}

return `${this.message}\n${oldStack}`;
},
});
}
}
2 changes: 2 additions & 0 deletions packages/browser-pool/src/index.ts
Expand Up @@ -41,6 +41,8 @@ export {
BrowserPlugin,
BrowserPluginOptions,
CreateLaunchContextOptions,
BrowserLaunchError,
DEFAULT_USER_AGENT,
} from './abstract-classes/browser-plugin';
export { LaunchContext, LaunchContextOptions } from './launch-context';
export {
Expand Down
18 changes: 5 additions & 13 deletions packages/browser-pool/src/playwright/playwright-plugin.ts
Expand Up @@ -3,7 +3,6 @@ import net from 'net';
import os from 'os';
import path from 'path';

import { CriticalError } from '@crawlee/core';
import type { Browser as PlaywrightBrowser, BrowserType } from 'playwright';

import { loadFirefoxAddon } from './load-firefox-addon';
Expand Down Expand Up @@ -185,19 +184,12 @@ export class PlaywrightPlugin extends BrowserPlugin<BrowserType, SafeParameters<
}

private _throwOnFailedLaunch(launchContext: LaunchContext<BrowserType>, cause: unknown): never {
let debugMessage = `Failed to launch browser.`
+ `${launchContext.launchOptions?.executablePath
? ` Check whether the provided executable path is correct: ${launchContext.launchOptions?.executablePath}.` : ''}`;
if (process.env.APIFY_IS_AT_HOME) {
debugMessage += ' Make sure your Dockerfile extends apify/actor-node-playwright-*` (with a correct browser name). Or install';
} else {
debugMessage += ' Try installing';
}
debugMessage += ' the required dependencies by running `npx playwright install --with-deps` (https://playwright.dev/docs/browsers).'
+ ' The original error will be displayed at the bottom as the [cause].';
throw new CriticalError(debugMessage, {
this._throwAugmentedLaunchError(
cause,
});
launchContext.launchOptions?.executablePath,
'`apify/actor-node-playwright-*` (with a correct browser name)',
'Try installing the required dependencies by running `npx playwright install --with-deps` (https://playwright.dev/docs/browsers).',
);
}

protected _createController(): BrowserController<BrowserType, SafeParameters<BrowserType['launch']>[0], PlaywrightBrowser> {
Expand Down
22 changes: 7 additions & 15 deletions packages/browser-pool/src/puppeteer/puppeteer-plugin.ts
@@ -1,4 +1,3 @@
import { CriticalError } from '@crawlee/core';
import type { Dictionary } from '@crawlee/types';
import type Puppeteer from 'puppeteer';
import type * as PuppeteerTypes from 'puppeteer';
Expand Down Expand Up @@ -75,20 +74,13 @@ export class PuppeteerPlugin extends BrowserPlugin<
}
} catch (error: any) {
await close();
let debugMessage = `Failed to launch browser.`
+ `${launchContext.launchOptions?.executablePath
? ` Check whether the provided executable path is correct: ${launchContext.launchOptions?.executablePath}.` : ''}`;
if (process.env.APIFY_IS_AT_HOME) {
debugMessage += ' Make sure your Dockerfile extends `apify/actor-node-puppeteer-chrome. Or install';
} else {
debugMessage += ' Try installing';
}
debugMessage += ` a browser, if it's missing, by running \`npx @puppeteer/browsers install chromium --path [path]\``
+ ` and pointing \`executablePath\` to the downloaded executable (https://pptr.dev/browsers-api).`
+ ` The original error will be displayed at the bottom as the [cause].`;
throw new CriticalError(debugMessage, {
cause: error,
});

this._throwAugmentedLaunchError(
error,
launchContext.launchOptions?.executablePath,
'`apify/actor-node-puppeteer-chrome`',
"Try installing a browser, if it's missing, by running `npx @puppeteer/browsers install chromium --path [path]` and pointing `executablePath` to the downloaded executable (https://pptr.dev/browsers-api)",
);
}
}

Expand Down
38 changes: 38 additions & 0 deletions packages/browser-pool/test/new-errors.test.ts
@@ -0,0 +1,38 @@
import { BrowserLaunchError, BrowserPool, PuppeteerPlugin } from '@crawlee/browser-pool';
import puppeteer from 'puppeteer';

describe('New errors in BrowserPool', () => {
const pool = new BrowserPool({
browserPlugins: [new PuppeteerPlugin(puppeteer, { launchOptions: { executablePath: '/dev/null' } })],
});

afterEach(() => {
delete process.env.APIFY_IS_AT_HOME;
});

test('they should log more information', async () => {
const error = await pool.newPage().catch((err) => err);

expect(error).toBeInstanceOf(BrowserLaunchError);

// Must include the executable path
expect(error.message).toMatch(/\/dev\/null/);
// Must include the install command
expect(error.message).toMatch(/npx @puppeteer\/browsers/);
});

test('when running on Apify, it should also log Docker image suggestion', async () => {
process.env.APIFY_IS_AT_HOME = '1';

const error = await pool.newPage().catch((err) => err);

expect(error).toBeInstanceOf(BrowserLaunchError);

// Must include the executable path
expect(error.message).toMatch(/\/dev\/null/);
// Must include the docker image suggestion
expect(error.message).toMatch(/apify\/actor-node-puppeteer-chrome/);
// Must include the install command
expect(error.message).toMatch(/npx @puppeteer\/browsers/);
});
});

0 comments on commit f188ebe

Please sign in to comment.