Do not leave dangling promises when sending messages over protocol (#742)

It's very bad to have 'unhandled promise rejection' that can't be
handled in user code. These errors will exit node process in a near
future.

This patch avoids 'unhandled promise rejection' while sending protocol
messages.

This patch:
- introduces `puppeteer:error` debug scope and starts using it for all
  swalloed errors.
- makes sure that every `client.send` method is either awaited or its
  errors are handled.
- starts return promises from Request.continue() and Request.abort().
- starts swallow errors from Request.contine() and Request.abort().

The last is the most important part of the patch. Since
`Request.continue()` might try to continue canceled request, we should
disregard the error.

Fixes #627.
This commit is contained in:
Andrey Lushnikov 2017-09-11 16:21:51 -07:00 committed by GitHub
parent f11e09d0d4
commit 0bea42bd8c
14 changed files with 61 additions and 29 deletions

View File

@ -1314,6 +1314,7 @@ If request fails at some point, then instead of 'requestfinished' event (and pos
If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new request is issued to a redirected url. If request gets a 'redirect' response, the request is successfully finished with the 'requestfinished' event, and a new request is issued to a redirected url.
#### request.abort() #### request.abort()
- returns: <[Promise]>
Aborts request. To use this, request interception should be enabled with `page.setRequestInterceptionEnabled`. Aborts request. To use this, request interception should be enabled with `page.setRequestInterceptionEnabled`.
Exception is immediately thrown if the request interception is not enabled. Exception is immediately thrown if the request interception is not enabled.
@ -1324,6 +1325,7 @@ Exception is immediately thrown if the request interception is not enabled.
- `method` <[string]> If set changes the request method (e.g. `GET` or `POST`) - `method` <[string]> If set changes the request method (e.g. `GET` or `POST`)
- `postData` <[string]> If set changes the post data of request - `postData` <[string]> If set changes the post data of request
- `headers` <[Object]> If set changes the request HTTP headers - `headers` <[Object]> If set changes the request HTTP headers
- returns: <[Promise]>
Continues request with optional request overrides. To use this, request interception should be enabled with `page.setRequestInterceptionEnabled`. Continues request with optional request overrides. To use this, request interception should be enabled with `page.setRequestInterceptionEnabled`.
Exception is immediately thrown if the request interception is not enabled. Exception is immediately thrown if the request interception is not enabled.

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const helper = require('./helper'); const {helper} = require('./helper');
const Page = require('./Page'); const Page = require('./Page');
class Browser { class Browser {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const helper = require('./helper'); const {helper} = require('./helper');
class Dialog { class Dialog {
/** /**

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const path = require('path'); const path = require('path');
const helper = require('./helper'); const {helper} = require('./helper');
class ElementHandle { class ElementHandle {
/** /**

View File

@ -16,7 +16,7 @@
const fs = require('fs'); const fs = require('fs');
const EventEmitter = require('events'); const EventEmitter = require('events');
const helper = require('./helper'); const {helper} = require('./helper');
const ElementHandle = require('./ElementHandle'); const ElementHandle = require('./ElementHandle');
class FrameManager extends EventEmitter { class FrameManager extends EventEmitter {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const helper = require('./helper'); const {helper} = require('./helper');
class Keyboard { class Keyboard {
/** /**

View File

@ -22,7 +22,7 @@ const Connection = require('./Connection');
const Browser = require('./Browser'); const Browser = require('./Browser');
const readline = require('readline'); const readline = require('readline');
const fs = require('fs'); const fs = require('fs');
const helper = require('./helper'); const {helper} = require('./helper');
const ChromiumRevision = require('../package.json').puppeteer.chromium_revision; const ChromiumRevision = require('../package.json').puppeteer.chromium_revision;
const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-'); const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'puppeteer_dev_profile-');

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const helper = require('./helper'); const {helper} = require('./helper');
class NavigatorWatcher { class NavigatorWatcher {
/** /**

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const EventEmitter = require('events'); const EventEmitter = require('events');
const helper = require('./helper'); const {helper, debugError} = require('./helper');
const Multimap = require('./Multimap'); const Multimap = require('./Multimap');
const url = require('url'); const url = require('url');
@ -66,7 +66,7 @@ class NetworkManager extends EventEmitter {
* @param {string} userAgent * @param {string} userAgent
*/ */
async setUserAgent(userAgent) { async setUserAgent(userAgent) {
return this._client.send('Network.setUserAgentOverride', { userAgent }); await this._client.send('Network.setUserAgentOverride', { userAgent });
} }
/** /**
@ -240,32 +240,40 @@ class Request {
/** /**
* @param {!Object=} overrides * @param {!Object=} overrides
*/ */
continue(overrides = {}) { async continue(overrides = {}) {
// DataURL's are not interceptable. In this case, do nothing. // DataURL's are not interceptable. In this case, do nothing.
if (this.url.startsWith('data:')) if (this.url.startsWith('data:'))
return; return;
console.assert(this._interceptionId, 'Request Interception is not enabled!'); console.assert(this._interceptionId, 'Request Interception is not enabled!');
console.assert(!this._interceptionHandled, 'Request is already handled!'); console.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true; this._interceptionHandled = true;
this._client.send('Network.continueInterceptedRequest', { await this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId, interceptionId: this._interceptionId,
url: overrides.url, url: overrides.url,
method: overrides.method, method: overrides.method,
postData: overrides.postData, postData: overrides.postData,
headers: overrides.headers, headers: overrides.headers,
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
}); });
} }
abort() { async abort() {
// DataURL's are not interceptable. In this case, do nothing. // DataURL's are not interceptable. In this case, do nothing.
if (this.url.startsWith('data:')) if (this.url.startsWith('data:'))
return; return;
console.assert(this._interceptionId, 'Request Interception is not enabled!'); console.assert(this._interceptionId, 'Request Interception is not enabled!');
console.assert(!this._interceptionHandled, 'Request is already handled!'); console.assert(!this._interceptionHandled, 'Request is already handled!');
this._interceptionHandled = true; this._interceptionHandled = true;
this._client.send('Network.continueInterceptedRequest', { await this._client.send('Network.continueInterceptedRequest', {
interceptionId: this._interceptionId, interceptionId: this._interceptionId,
errorReason: 'Failed' errorReason: 'Failed'
}).catch(error => {
// In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors.
debugError(error);
}); });
} }
} }

View File

@ -24,7 +24,7 @@ const EmulationManager = require('./EmulationManager');
const FrameManager = require('./FrameManager'); const FrameManager = require('./FrameManager');
const {Keyboard, Mouse, Touchscreen} = require('./Input'); const {Keyboard, Mouse, Touchscreen} = require('./Input');
const Tracing = require('./Tracing'); const Tracing = require('./Tracing');
const helper = require('./helper'); const {helper, debugError} = require('./helper');
class Page extends EventEmitter { class Page extends EventEmitter {
/** /**
@ -153,7 +153,7 @@ class Page extends EventEmitter {
this._client.send('Security.handleCertificateError', { this._client.send('Security.handleCertificateError', {
eventId: event.eventId, eventId: event.eventId,
action: 'continue' action: 'continue'
}); }).catch(debugError);
} }
/** /**
@ -293,7 +293,7 @@ class Page extends EventEmitter {
const {name, seq, args} = JSON.parse(event.args[1].value); const {name, seq, args} = JSON.parse(event.args[1].value);
const result = await this._pageBindings[name](...args); const result = await this._pageBindings[name](...args);
const expression = helper.evaluationString(deliverResult, name, seq, result); const expression = helper.evaluationString(deliverResult, name, seq, result);
this._client.send('Runtime.evaluate', { expression }); this._client.send('Runtime.evaluate', { expression }).catch(debugError);
function deliverResult(name, seq, result) { function deliverResult(name, seq, result) {
window[name]['callbacks'].get(seq)(result); window[name]['callbacks'].get(seq)(result);
@ -387,8 +387,11 @@ class Page extends EventEmitter {
* @return {!Promise<?Response>} * @return {!Promise<?Response>}
*/ */
async reload(options) { async reload(options) {
this._client.send('Page.reload'); const [response] = await Promise.all([
return this.waitForNavigation(options); this.waitForNavigation(options),
this._client.send('Page.reload')
]);
return response;
} }
/** /**
@ -431,9 +434,11 @@ class Page extends EventEmitter {
const entry = history.entries[history.currentIndex + delta]; const entry = history.entries[history.currentIndex + delta];
if (!entry) if (!entry)
return null; return null;
const result = this.waitForNavigation(options); const [response] = await Promise.all([
this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}); this.waitForNavigation(options),
return result; this._client.send('Page.navigateToHistoryEntry', {entryId: entry.id}),
]);
return response;
} }
/** /**

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const helper = require('./helper'); const {helper} = require('./helper');
const Launcher = require('./Launcher'); const Launcher = require('./Launcher');
class Puppeteer { class Puppeteer {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const fs = require('fs'); const fs = require('fs');
const helper = require('./helper'); const {helper} = require('./helper');
class Tracing { class Tracing {
/** /**

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const debugError = require('debug')(`puppeteer:error`);
/** @type {?Map<string, boolean>} */ /** @type {?Map<string, boolean>} */
let apiCoverage = null; let apiCoverage = null;
class Helper { class Helper {
@ -94,12 +95,11 @@ class Helper {
static async releaseObject(client, remoteObject) { static async releaseObject(client, remoteObject) {
if (!remoteObject.objectId) if (!remoteObject.objectId)
return; return;
try { await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId});
} catch (e) {
// Exceptions might happen in case of a page been navigated or closed. // Exceptions might happen in case of a page been navigated or closed.
// Swallow these since they are harmless and we don't leak anything in this case. // Swallow these since they are harmless and we don't leak anything in this case.
} debugError(error);
});
} }
/** /**
@ -217,4 +217,7 @@ class Helper {
} }
} }
module.exports = Helper; module.exports = {
helper: Helper,
debugError
};

View File

@ -17,7 +17,7 @@
const fs = require('fs'); const fs = require('fs');
const rm = require('rimraf').sync; const rm = require('rimraf').sync;
const path = require('path'); const path = require('path');
const helper = require('../lib/helper'); const {helper} = require('../lib/helper');
if (process.env.COVERAGE) if (process.env.COVERAGE)
helper.recordPublicAPICoverage(); helper.recordPublicAPICoverage();
console.log('Testing on Node', process.version); console.log('Testing on Node', process.version);
@ -986,6 +986,20 @@ describe('Page', function() {
expect(requests.length).toBe(2); expect(requests.length).toBe(2);
expect(requests[1].response().status).toBe(404); expect(requests[1].response().status).toBe(404);
})); }));
it('should not throw "Invalid Interception Id" if the request was cancelled', SX(async function() {
await page.setContent('<iframe></iframe>');
await page.setRequestInterceptionEnabled(true);
let request = null;
page.on('request', async r => request = r);
page.$eval('iframe', (frame, url) => frame.src = url, EMPTY_PAGE),
// Wait for request interception.
await waitForEvents(page, 'request');
// Delete frame to cause request to be canceled.
await page.$eval('iframe', frame => frame.remove());
let error = null;
await request.continue().catch(e => error = e);
expect(error).toBe(null);
}));
}); });
describe('Page.Events.Dialog', function() { describe('Page.Events.Dialog', function() {