Skip to content

Commit e0c8d46

Browse files
yury-saslushnikov
authored andcommitted
fix: abort page.waitForRequest/Response when page closes (#4865)
We'd like to pass an abortion signal inside Helper.waitForEvent in order to interrupt it when browser/page closes. Several approaches have been considered: 1. Pass CDPSession instance as a another parameter to the helper method and listen to Disconnected event on it. It would introduce undesired dependency on the session object. 2. Listen to the CDPSession closure at the call sites (e.g. waitForRequest) and pass an abortion promise which would be fulfilled when such event is fired. The listeners would have to be removed from the session on successful completion of waitForEvent so we'd have to pass some kind of DisposablePromise which would be disposed during cleanup. Such parameter looked somewhat hairy. 3. Create DisconnectPromise on CDPSession. One potential risk with that is all chained promises would hang around until the event is fired which might inadvertently cause memory leaks. On the other hand, adding such promise to Promise.race will remove dependency as soon as the race is finished. So this is the approach we're taking with one tweak: the promise is created locally inside Page. Ideally the disconnectPromise would throw when the session is closed but it may lead to uncaught promise errors if all chained promises are resolved, to avoid that the promise is resolved with an Error and Helper.waitForEvent throws it later. Fix #4733
1 parent faa4527 commit e0c8d46

File tree

7 files changed

+75
-12
lines changed

7 files changed

+75
-12
lines changed

experimental/puppeteer-firefox/lib/Page.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ class Page extends EventEmitter {
241241
}
242242
}
243243

244+
_sessionClosePromise() {
245+
if (!this._disconnectPromise)
246+
this._disconnectPromise = new Promise(fulfill => this._session.once(Events.JugglerSession.Disconnected, () => fulfill(new Error('Target closed'))));
247+
return this._disconnectPromise;
248+
}
249+
244250
/**
245251
* @param {(string|Function)} urlOrPredicate
246252
* @param {!{timeout?: number}=} options
@@ -256,7 +262,7 @@ class Page extends EventEmitter {
256262
if (typeof urlOrPredicate === 'function')
257263
return !!(urlOrPredicate(request));
258264
return false;
259-
}, timeout);
265+
}, timeout, this._sessionClosePromise());
260266
}
261267

262268
/**
@@ -274,7 +280,7 @@ class Page extends EventEmitter {
274280
if (typeof urlOrPredicate === 'function')
275281
return !!(urlOrPredicate(response));
276282
return false;
277-
}, timeout);
283+
}, timeout, this._sessionClosePromise());
278284
}
279285

280286
/**

experimental/puppeteer-firefox/lib/TimeoutSettings.js

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class TimeoutSettings {
4747
return DEFAULT_TIMEOUT;
4848
}
4949

50+
/**
51+
* @return {number}
52+
*/
5053
timeout() {
5154
if (this._defaultTimeout !== null)
5255
return this._defaultTimeout;

experimental/puppeteer-firefox/lib/helper.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,11 @@ class Helper {
121121
* @param {!NodeJS.EventEmitter} emitter
122122
* @param {(string|symbol)} eventName
123123
* @param {function} predicate
124+
* @param {number} timeout
125+
* @param {!Promise<!Error>} abortPromise
124126
* @return {!Promise}
125127
*/
126-
static waitForEvent(emitter, eventName, predicate, timeout) {
128+
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
127129
let eventTimeout, resolveCallback, rejectCallback;
128130
const promise = new Promise((resolve, reject) => {
129131
resolveCallback = resolve;
@@ -132,20 +134,27 @@ class Helper {
132134
const listener = Helper.addEventListener(emitter, eventName, event => {
133135
if (!predicate(event))
134136
return;
135-
cleanup();
136137
resolveCallback(event);
137138
});
138139
if (timeout) {
139140
eventTimeout = setTimeout(() => {
140-
cleanup();
141141
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
142142
}, timeout);
143143
}
144144
function cleanup() {
145145
Helper.removeEventListeners([listener]);
146146
clearTimeout(eventTimeout);
147147
}
148-
return promise;
148+
const result = await Promise.race([promise, abortPromise]).then(r => {
149+
cleanup();
150+
return r;
151+
}, e => {
152+
cleanup();
153+
throw e;
154+
});
155+
if (result instanceof Error)
156+
throw result;
157+
return result;
149158
}
150159

151160
/**

lib/Page.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,12 @@ class Page extends EventEmitter {
694694
return await this._frameManager.mainFrame().waitForNavigation(options);
695695
}
696696

697+
_sessionClosePromise() {
698+
if (!this._disconnectPromise)
699+
this._disconnectPromise = new Promise(fulfill => this._client.once(Events.CDPSession.Disconnected, () => fulfill(new Error('Target closed'))));
700+
return this._disconnectPromise;
701+
}
702+
697703
/**
698704
* @param {(string|Function)} urlOrPredicate
699705
* @param {!{timeout?: number}=} options
@@ -709,7 +715,7 @@ class Page extends EventEmitter {
709715
if (typeof urlOrPredicate === 'function')
710716
return !!(urlOrPredicate(request));
711717
return false;
712-
}, timeout);
718+
}, timeout, this._sessionClosePromise());
713719
}
714720

715721
/**
@@ -727,7 +733,7 @@ class Page extends EventEmitter {
727733
if (typeof urlOrPredicate === 'function')
728734
return !!(urlOrPredicate(response));
729735
return false;
730-
}, timeout);
736+
}, timeout, this._sessionClosePromise());
731737
}
732738

733739
/**

lib/helper.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,11 @@ class Helper {
176176
* @param {!NodeJS.EventEmitter} emitter
177177
* @param {(string|symbol)} eventName
178178
* @param {function} predicate
179+
* @param {number} timeout
180+
* @param {!Promise<!Error>} abortPromise
179181
* @return {!Promise}
180182
*/
181-
static waitForEvent(emitter, eventName, predicate, timeout) {
183+
static async waitForEvent(emitter, eventName, predicate, timeout, abortPromise) {
182184
let eventTimeout, resolveCallback, rejectCallback;
183185
const promise = new Promise((resolve, reject) => {
184186
resolveCallback = resolve;
@@ -187,20 +189,27 @@ class Helper {
187189
const listener = Helper.addEventListener(emitter, eventName, event => {
188190
if (!predicate(event))
189191
return;
190-
cleanup();
191192
resolveCallback(event);
192193
});
193194
if (timeout) {
194195
eventTimeout = setTimeout(() => {
195-
cleanup();
196196
rejectCallback(new TimeoutError('Timeout exceeded while waiting for event'));
197197
}, timeout);
198198
}
199199
function cleanup() {
200200
Helper.removeEventListeners([listener]);
201201
clearTimeout(eventTimeout);
202202
}
203-
return promise;
203+
const result = await Promise.race([promise, abortPromise]).then(r => {
204+
cleanup();
205+
return r;
206+
}, e => {
207+
cleanup();
208+
throw e;
209+
});
210+
if (result instanceof Error)
211+
throw result;
212+
return result;
204213
}
205214

206215
/**

test/launcher.spec.js

+17
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p
8484
await browser.close();
8585
});
8686
});
87+
describe('Browser.close', function() {
88+
it('should terminate network waiters', async({context, server}) => {
89+
const browser = await puppeteer.launch(defaultBrowserOptions);
90+
const remote = await puppeteer.connect({browserWSEndpoint: browser.wsEndpoint()});
91+
const newPage = await remote.newPage();
92+
const results = await Promise.all([
93+
newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e),
94+
newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e),
95+
browser.close()
96+
]);
97+
for (let i = 0; i < 2; i++) {
98+
const message = results[i].message;
99+
expect(message).toContain('Target closed');
100+
expect(message).not.toContain('Timeout');
101+
}
102+
});
103+
});
87104
describe('Puppeteer.launch', function() {
88105
it('should reject all promises when browser is closed', async() => {
89106
const browser = await puppeteer.launch(defaultBrowserOptions);

test/page.spec.js

+13
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,19 @@ module.exports.addTests = function({testRunner, expect, headless, puppeteer, CHR
7777
await newPage.close();
7878
expect(newPage.isClosed()).toBe(true);
7979
});
80+
it('should terminate network waiters', async({context, server}) => {
81+
const newPage = await context.newPage();
82+
const results = await Promise.all([
83+
newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e),
84+
newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e),
85+
newPage.close()
86+
]);
87+
for (let i = 0; i < 2; i++) {
88+
const message = results[i].message;
89+
expect(message).toContain('Target closed');
90+
expect(message).not.toContain('Timeout');
91+
}
92+
});
8093
});
8194

8295
describe('Page.Events.Load', function() {

0 commit comments

Comments
 (0)