Skip to content

Commit 1ae9eb8

Browse files
[Reporting] Handle error gracefully when browser driver unexpectedly exits or crashes. (#71989) (#74190)
* [Reporting] Handle page error event directly with Puppeteer * clean up logger.error that was stringifying error * use fromEvent * better handling and error messaging if browser was closed in the middle of the screenshot pipeline * fix pdf functional api tests * fix i18n error * fix jest * fix ts * fix i18n * tweaks * ok to throw error in callback * fix ts Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 4075200 commit 1ae9eb8

File tree

16 files changed

+2358
-63
lines changed

16 files changed

+2358
-63
lines changed

x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { map, truncate } from 'lodash';
99
import open from 'opn';
1010
import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer';
1111
import { parse as parseUrl } from 'url';
12+
import { getDisallowedOutgoingUrlError } from '../';
1213
import { LevelLogger } from '../../../lib';
1314
import { ViewZoomWidthHeight } from '../../../lib/layouts/layout';
1415
import { ConditionalHeaders, ElementPosition } from '../../../types';
@@ -76,6 +77,9 @@ export class HeadlessChromiumDriver {
7677
});
7778
}
7879

80+
/*
81+
* Call Page.goto and wait to see the Kibana DOM content
82+
*/
7983
public async open(
8084
url: string,
8185
{
@@ -113,6 +117,16 @@ export class HeadlessChromiumDriver {
113117
logger.info(`handled ${this.interceptedCount} page requests`);
114118
}
115119

120+
/*
121+
* Let modules poll if Chrome is still running so they can short circuit if needed
122+
*/
123+
public isPageOpen() {
124+
return !this.page.isClosed();
125+
}
126+
127+
/*
128+
* Call Page.screenshot and return a base64-encoded string of the image
129+
*/
116130
public async screenshot(elementPosition: ElementPosition): Promise<string> {
117131
const { boundingClientRect, scroll } = elementPosition;
118132
const screenshot = await this.page.screenshot({
@@ -220,18 +234,13 @@ export class HeadlessChromiumDriver {
220234

221235
// We should never ever let file protocol requests go through
222236
if (!allowed || !this.allowRequest(interceptedUrl)) {
223-
logger.error(`Got bad URL: "${interceptedUrl}", closing browser.`);
224237
await client.send('Fetch.failRequest', {
225238
errorReason: 'Aborted',
226239
requestId,
227240
});
228241
this.page.browser().close();
229-
throw new Error(
230-
i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', {
231-
defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}", exiting`,
232-
values: { interceptedUrl },
233-
})
234-
);
242+
logger.error(getDisallowedOutgoingUrlError(interceptedUrl));
243+
return;
235244
}
236245

237246
if (this._shouldUseCustomHeaders(conditionalHeaders.conditions, interceptedUrl)) {
@@ -292,9 +301,9 @@ export class HeadlessChromiumDriver {
292301
}
293302

294303
if (!allowed || !this.allowRequest(interceptedUrl)) {
295-
logger.error(`Got disallowed URL "${interceptedUrl}", closing browser.`);
296304
this.page.browser().close();
297-
throw new Error(`Received disallowed URL in response: ${interceptedUrl}`);
305+
logger.error(getDisallowedOutgoingUrlError(interceptedUrl));
306+
throw getDisallowedOutgoingUrlError(interceptedUrl);
298307
}
299308
});
300309

x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import * as Rx from 'rxjs';
2020
import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber';
2121
import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators';
22+
import { getChromiumDisconnectedError } from '../';
2223
import { BROWSER_TYPE } from '../../../../common/constants';
2324
import { CaptureConfig } from '../../../../server/types';
2425
import { LevelLogger } from '../../../lib';
@@ -115,13 +116,19 @@ export class HeadlessChromiumDriverFactory {
115116

116117
logger.debug(`Browser page driver created`);
117118
} catch (err) {
118-
observer.error(new Error(`Error spawning Chromium browser: [${err}]`));
119+
observer.error(new Error(`Error spawning Chromium browser!`));
120+
observer.error(err);
119121
throw err;
120122
}
121123

122124
const childProcess = {
123125
async kill() {
124-
await browser.close();
126+
try {
127+
await browser.close();
128+
} catch (err) {
129+
// do not throw
130+
logger.error(err);
131+
}
125132
},
126133
};
127134
const { terminate$ } = safeChildProcess(logger, childProcess);
@@ -167,7 +174,8 @@ export class HeadlessChromiumDriverFactory {
167174
// the unsubscribe function isn't `async` so we're going to make our best effort at
168175
// deleting the userDataDir and if it fails log an error.
169176
del(userDataDir, { force: true }).catch((error) => {
170-
logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`);
177+
logger.error(`error deleting user data directory at [${userDataDir}]!`);
178+
logger.error(error);
171179
});
172180
});
173181
});
@@ -219,7 +227,7 @@ export class HeadlessChromiumDriverFactory {
219227
mergeMap((err) => {
220228
return Rx.throwError(
221229
i18n.translate('xpack.reporting.browsers.chromium.errorDetected', {
222-
defaultMessage: 'Reporting detected an error: {err}',
230+
defaultMessage: 'Reporting encountered an error: {err}',
223231
values: { err: err.toString() },
224232
})
225233
);
@@ -230,23 +238,15 @@ export class HeadlessChromiumDriverFactory {
230238
mergeMap((err) => {
231239
return Rx.throwError(
232240
i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', {
233-
defaultMessage: `Reporting detected an error on the page: {err}`,
241+
defaultMessage: `Reporting encountered an error on the page: {err}`,
234242
values: { err: err.toString() },
235243
})
236244
);
237245
})
238246
);
239247

240248
const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe(
241-
mergeMap(() =>
242-
Rx.throwError(
243-
new Error(
244-
i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', {
245-
defaultMessage: `Reporting detected that Chromium has closed.`,
246-
})
247-
)
248-
)
249-
)
249+
mergeMap(() => Rx.throwError(getChromiumDisconnectedError()))
250250
);
251251

252252
return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$);

x-pack/plugins/reporting/server/browsers/chromium/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { i18n } from '@kbn/i18n';
78
import { BrowserDownload } from '../';
89
import { CaptureConfig } from '../../../server/types';
910
import { LevelLogger } from '../../lib';
@@ -15,3 +16,18 @@ export const chromium: BrowserDownload = {
1516
createDriverFactory: (binaryPath: string, captureConfig: CaptureConfig, logger: LevelLogger) =>
1617
new HeadlessChromiumDriverFactory(binaryPath, captureConfig, logger),
1718
};
19+
20+
export const getChromiumDisconnectedError = () =>
21+
new Error(
22+
i18n.translate('xpack.reporting.screencapture.browserWasClosed', {
23+
defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.',
24+
})
25+
);
26+
27+
export const getDisallowedOutgoingUrlError = (interceptedUrl: string) =>
28+
new Error(
29+
i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', {
30+
defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`,
31+
values: { interceptedUrl },
32+
})
33+
);

x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory<ImmediateCreateJobFn>
105105
if (errPayload.statusCode === 404) {
106106
throw notFound(errPayload.message);
107107
}
108-
if (err.stack) {
109-
logger.error(err.stack);
110-
}
108+
logger.error(err);
111109
throw new Error(`Unable to create a job from saved object data! Error: ${err}`);
112110
});
113111

x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
9090
logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`);
9191
tracker.endGetBuffer();
9292
} catch (err) {
93-
logger.error(`Could not generate the PDF buffer! ${err}`);
93+
logger.error(`Could not generate the PDF buffer!`);
94+
logger.error(err);
9495
}
9596

9697
tracker.end();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { HeadlessChromiumDriver } from '../../browsers';
8+
import { getChromiumDisconnectedError } from '../../browsers/chromium';
9+
10+
/*
11+
* Call this function within error-handling `catch` blocks while setup and wait
12+
* for the Kibana URL to be ready for screenshot. This detects if a block of
13+
* code threw an exception because the page is closed or crashed.
14+
*
15+
* Also call once after `setup$` fires in the screenshot pipeline
16+
*/
17+
export const checkPageIsOpen = (browser: HeadlessChromiumDriver) => {
18+
if (!browser.isPageOpen()) {
19+
throw getChromiumDisconnectedError();
20+
}
21+
};

x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ describe('Screenshot Observable Pipeline', () => {
281281
`);
282282
});
283283

284-
it('recovers if exit$ fires a timeout signal', async () => {
284+
it('observes page exit', async () => {
285285
// mocks
286286
const mockGetCreatePage = (driver: HeadlessChromiumDriver) =>
287287
jest
@@ -311,38 +311,7 @@ describe('Screenshot Observable Pipeline', () => {
311311
}).toPromise();
312312
};
313313

314-
await expect(getScreenshot()).resolves.toMatchInlineSnapshot(`
315-
Array [
316-
Object {
317-
"elementsPositionAndAttributes": Array [
318-
Object {
319-
"attributes": Object {},
320-
"position": Object {
321-
"boundingClientRect": Object {
322-
"height": 200,
323-
"left": 0,
324-
"top": 0,
325-
"width": 200,
326-
},
327-
"scroll": Object {
328-
"x": 0,
329-
"y": 0,
330-
},
331-
},
332-
},
333-
],
334-
"error": "Instant timeout has fired!",
335-
"screenshots": Array [
336-
Object {
337-
"base64EncodedData": "allyourBase64",
338-
"description": undefined,
339-
"title": undefined,
340-
},
341-
],
342-
"timeRange": null,
343-
},
344-
]
345-
`);
314+
await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`);
346315
});
347316

348317
it(`uses defaults for element positions and size when Kibana page is not ready`, async () => {

x-pack/plugins/reporting/server/lib/screenshots/observable.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ScreenshotResults,
2525
ScreenshotsObservableFn,
2626
} from '../../types';
27+
import { checkPageIsOpen } from './check_browser_open';
2728
import { DEFAULT_PAGELOAD_SELECTOR } from './constants';
2829
import { getElementPositionAndAttributes } from './get_element_position_data';
2930
import { getNumberOfItems } from './get_number_of_items';
@@ -68,7 +69,6 @@ export function screenshotsObservableFactory(
6869
return Rx.from(urls).pipe(
6970
concatMap((url, index) => {
7071
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
71-
takeUntil(exit$),
7272
mergeMap(() => {
7373
// If we're moving to another page in the app, we'll want to wait for the app to tell us
7474
// it's loaded the next page.
@@ -117,14 +117,19 @@ export function screenshotsObservableFactory(
117117
}));
118118
}),
119119
catchError((err) => {
120+
checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it
121+
120122
logger.error(err);
121123
return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err });
122124
})
123125
);
124126

125127
return setup$.pipe(
128+
takeUntil(exit$),
126129
mergeMap(
127130
async (data: ScreenSetupData): Promise<ScreenshotResults> => {
131+
checkPageIsOpen(driver); // re-check that the browser has not closed
132+
128133
const elements = data.elementsPositionAndAttributes
129134
? data.elementsPositionAndAttributes
130135
: getDefaultElementPosition(layout.getViewport(1));

x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export const createMockBrowserDriverFactory = async (
129129
mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate;
130130
mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot;
131131
mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open;
132+
mockBrowserDriver.isPageOpen = () => true;
132133

133134
mockBrowserDriverFactory.createPage = opts.getCreatePage
134135
? opts.getCreatePage(mockBrowserDriver)

x-pack/plugins/translations/translations/ja-JP.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14336,7 +14336,6 @@
1433614336
"xpack.remoteClusters.updateRemoteCluster.noRemoteClusterErrorMessage": "その名前のリモートクラスターはありません。",
1433714337
"xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。",
1433814338
"xpack.reporting.breadcrumb": "レポート",
14339-
"xpack.reporting.browsers.chromium.chromiumClosed": "レポート生成時に Chromium の終了を検出しました。",
1434014339
"xpack.reporting.browsers.chromium.errorDetected": "レポート生成時にエラーを検出しました: {err}",
1434114340
"xpack.reporting.browsers.chromium.pageErrorDetected": "レポート生成時にページでエラーを検出しました: {err}",
1434214341
"xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "無許可の着信 URL を受信しました: 「{interceptedUrl}」、終了します",

0 commit comments

Comments
 (0)