diff --git a/src/client/browser.ts b/src/client/browser.ts index 804ee010f6..26ee39c041 100644 --- a/src/client/browser.ts +++ b/src/client/browser.ts @@ -22,6 +22,7 @@ import { Events } from './events'; import { BrowserContextOptions } from './types'; import { validateHeaders } from './network'; import { headersObjectToArray } from '../utils/utils'; +import { isSafeCloseError } from '../utils/errors'; export class Browser extends ChannelOwner { readonly _contexts = new Set(); @@ -88,9 +89,7 @@ export class Browser extends ChannelOwner { _pages = new Set(); @@ -36,7 +37,6 @@ export class BrowserContext extends ChannelOwner(); _timeoutSettings = new TimeoutSettings(); _ownerPage: Page | undefined; - private _isClosedOrClosing = false; private _closedPromise: Promise; static from(context: channels.BrowserContextChannel): BrowserContext { @@ -222,19 +222,21 @@ export class BrowserContext extends ChannelOwner { - return this._wrapApiCall('browserContext.close', async () => { - if (!this._isClosedOrClosing) { - this._isClosedOrClosing = true; + try { + await this._wrapApiCall('browserContext.close', async () => { await this._channel.close(); - } - await this._closedPromise; - }); + await this._closedPromise; + }); + } catch (e) { + if (isSafeCloseError(e)) + return; + throw e; + } } } diff --git a/src/client/browserType.ts b/src/client/browserType.ts index 7e6fc9c751..f0d5ac8567 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -29,6 +29,7 @@ import { envObjectToArray } from './clientHelper'; import { validateHeaders } from './network'; import { assert, makeWaitForNextTask, headersObjectToArray } from '../utils/utils'; import { SelectorsOwner, sharedSelectors } from './selectors'; +import { kBrowserClosedError } from '../utils/errors'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; @@ -127,7 +128,7 @@ export class BrowserType extends ChannelOwner { if (ws.readyState !== WebSocket.OPEN) { setTimeout(() => { - connection.dispatch({ id: (message as any).id, error: serializeError(new Error('Browser has been closed')) }); + connection.dispatch({ id: (message as any).id, error: serializeError(new Error(kBrowserClosedError)) }); }, 0); return; } diff --git a/src/client/page.ts b/src/client/page.ts index 2b93e26ee6..5e6e102a71 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -43,6 +43,7 @@ import * as util from 'util'; import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types'; import { evaluationScript, urlMatches } from './clientHelper'; import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../utils/utils'; +import { isSafeCloseError } from '../utils/errors'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const mkdirAsync = util.promisify(fs.mkdir); @@ -451,11 +452,17 @@ export class Page extends ChannelOwner { - await this._channel.close(options); - if (this._ownedContext) - await this._ownedContext.close(); - }); + try { + await this._wrapApiCall('page.close', async () => { + await this._channel.close(options); + if (this._ownedContext) + await this._ownedContext.close(); + }); + } catch (e) { + if (isSafeCloseError(e)) + return; + throw e; + } } isClosed(): boolean { diff --git a/src/dispatchers/dispatcher.ts b/src/dispatchers/dispatcher.ts index 7c752abec0..e155472fcd 100644 --- a/src/dispatchers/dispatcher.ts +++ b/src/dispatchers/dispatcher.ts @@ -20,6 +20,7 @@ import { serializeError } from '../protocol/serializers'; import { createScheme, Validator, ValidationError } from '../protocol/validator'; import { assert, createGuid, debugAssert, isUnderTest } from '../utils/utils'; import { tOptional } from '../protocol/validatorPrimitives'; +import { kBrowserOrContextClosedError } from '../utils/errors'; export const dispatcherSymbol = Symbol('dispatcher'); @@ -167,7 +168,7 @@ export class DispatcherConnection { const { id, guid, method, params, metadata } = message as any; const dispatcher = this._dispatchers.get(guid); if (!dispatcher) { - this.onmessage({ id, error: serializeError(new Error('Target browser or context has been closed')) }); + this.onmessage({ id, error: serializeError(new Error(kBrowserOrContextClosedError)) }); return; } if (method === 'debugScopeState') { diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 3044091584..47dcc0d056 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -24,3 +24,10 @@ class CustomError extends Error { } export class TimeoutError extends CustomError {} + +export const kBrowserClosedError = 'Browser has been closed'; +export const kBrowserOrContextClosedError = 'Target browser or context has been closed'; + +export function isSafeCloseError(error: Error) { + return error.message.endsWith(kBrowserClosedError) || error.message.endsWith(kBrowserOrContextClosedError); +} diff --git a/test/browsertype-connect.spec.ts b/test/browsertype-connect.spec.ts index 7aa2ed0029..df180c103e 100644 --- a/test/browsertype-connect.spec.ts +++ b/test/browsertype-connect.spec.ts @@ -211,4 +211,25 @@ describe('connect', (suite, { wire }) => { ]); await remote.close(); }); + + it('should not throw on context.close after disconnect', async ({browserType, remoteServer, server}) => { + const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const context = await remote.newContext(); + await context.newPage(); + await Promise.all([ + new Promise(f => remote.on('disconnected', f)), + remoteServer.close() + ]); + await context.close(); + }); + + it('should not throw on page.close after disconnect', async ({browserType, remoteServer, server}) => { + const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await remote.newPage(); + await Promise.all([ + new Promise(f => remote.on('disconnected', f)), + remoteServer.close() + ]); + await page.close(); + }); });