From 4551afc6dccb4f59e90791041efce6da7e83f80e Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Fri, 11 Aug 2017 17:24:31 -0700 Subject: [PATCH] Introduce new interception API (#242) This patch introduces new interception API, via killing InterceptedRequest and giving the `abort` and `continue` methods to the Request object. --- docs/api.md | 84 +++------ lib/Multimap.js | 138 ++++++++++++++ lib/NetworkManager.js | 238 ++++++++++++++++-------- lib/Page.js | 6 +- phantom_shim/WebPage.js | 32 ++-- test/test.js | 70 ++++++- utils/doclint/check_public_api/index.js | 2 +- 7 files changed, 402 insertions(+), 168 deletions(-) create mode 100644 lib/Multimap.js diff --git a/docs/api.md b/docs/api.md index 460bab04f2e..5891f78b262 100644 --- a/docs/api.md +++ b/docs/api.md @@ -49,7 +49,7 @@ + [page.screenshot([options])](#pagescreenshotoptions) + [page.setContent(html)](#pagesetcontenthtml) + [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) - + [page.setRequestInterceptor(interceptor)](#pagesetrequestinterceptorinterceptor) + + [page.setRequestInterceptionEnabled(value)](#pagesetrequestinterceptionenabledvalue) + [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + [page.setViewport(viewport)](#pagesetviewportviewport) + [page.title()](#pagetitle) @@ -98,6 +98,8 @@ + [frame.waitForFunction(pageFunction[, options, ...args])](#framewaitforfunctionpagefunction-options-args) + [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) * [class: Request](#class-request) + + [request.abort()](#requestabort) + + [request.continue([overrides])](#requestcontinueoverrides) + [request.headers](#requestheaders) + [request.method](#requestmethod) + [request.postData](#requestpostdata) @@ -110,16 +112,8 @@ + [response.ok](#responseok) + [response.request()](#responserequest) + [response.status](#responsestatus) - + [response.statusText](#responsestatustext) + [response.text()](#responsetext) + [response.url](#responseurl) - * [class: InterceptedRequest](#class-interceptedrequest) - + [interceptedRequest.abort()](#interceptedrequestabort) - + [interceptedRequest.continue([overrides])](#interceptedrequestcontinueoverrides) - + [interceptedRequest.headers](#interceptedrequestheaders) - + [interceptedRequest.method](#interceptedrequestmethod) - + [interceptedRequest.postData](#interceptedrequestpostdata) - + [interceptedRequest.url](#interceptedrequesturl) @@ -297,7 +291,8 @@ Emitted when an unhandled exception happens on the page. The only argument of th #### event: 'request' - <[Request]> -Emitted when a page issues a request. The [request] object is a read-only object. In order to intercept and mutate requests, see [page.setRequestInterceptor](#pagesetrequestinterceptorinterceptor) +Emitted when a page issues a request. The [request] object is a read-only object. +In order to intercept and mutate requests, see `page.setRequestInterceptionEnabled`. #### event: 'requestfailed' - <[Request]> @@ -618,19 +613,20 @@ The extra HTTP headers will be sent with every request the page initiates. > **NOTE** page.setExtraHTTPHeaders does not guarantee the order of headers in the outgoing requests. -#### page.setRequestInterceptor(interceptor) -- `interceptor` <[function]> Callback function which accepts a single argument of type <[InterceptedRequest]>. -- returns: <[Promise]> Promise which resolves when request interceptor is successfully installed on the page. +#### page.setRequestInterceptionEnabled(value) +- `value` <[boolean]> Whether to enable request interception. +- returns: <[Promise]> Promise which resolves when request interception change is applied. -After the request interceptor is installed on the page, every request will be reported to the interceptor. The [InterceptedRequest] could be modified and then either continued via the `continue()` method, or aborted via the `abort()` method. +Activating request interception enables `request.abort` and `request.continue`. -En example of a naive request interceptor which aborts all image requests: +An example of a naïve request interceptor which aborts all image requests: ```js const {Browser} = require('puppeteer'); const browser = new Browser(); browser.newPage().then(async page => - await page.setRequestInterceptor(interceptedRequest => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { if (interceptedRequest.url.endsWith('.png') || interceptedRequest.url.endsWith('.jpg')) interceptedRequest.abort(); else @@ -1103,6 +1099,21 @@ If request gets a 'redirect' response, the request is successfully finished with [Request] class represents requests which are sent by page. [Request] implements [Body] mixin, which in case of HTTP POST requests allows clients to call `request.json()` or `request.text()` to get different representations of request's body. +#### request.abort() + +Aborts request. To use this, request interception should be enabled with `page.setRequestInterceptionEnabled`. +Exception is immediately thrown if the request interception is not enabled. + +#### request.continue([overrides]) +- `overrides` <[Object]> Optional request overwrites, which could be one of the following: + - `url` <[string]> If set, the request url will be changed + - `method` <[string]> If set changes the request method (e.g. `GET` or `POST`) + - `postData` <[string]> If set changes the post data of request + - `headers` <[Map]> If set changes the request HTTP headers + +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. + #### request.headers - <[Map]> A map of HTTP headers associated with the request. @@ -1152,11 +1163,6 @@ Contains a boolean stating whether the response was successful (status in the ra Contains the status code of the response (e.g., 200 for a success). -#### response.statusText -- <[string]> - -Contains the status message corresponding to the status code (e.g., OK for 200). - #### response.text() - returns: > Promise which resolves to a text representation of response body. @@ -1166,41 +1172,6 @@ Contains the status message corresponding to the status code (e.g., OK for 200). Contains the URL of the response. -### class: InterceptedRequest - -[InterceptedRequest] represents an intercepted request, which can be either continued or aborted. [InterceptedRequest] which is not continued or aborted will be in a 'hanging' state. - -#### interceptedRequest.abort() - -Aborts request. - -#### interceptedRequest.continue([overrides]) -- `overrides` <[Object]> Optional request overwrites, which could be one of the following: - - `url` <[string]> If set, the request url will be changed - - `method` <[string]> If set changes the request method (e.g. `GET` or `POST`) - - `postData` <[string]> If set changes the post data of request - - `headers` <[Map]> If set changes the request HTTP headers - -Continues request with optional request overrides. - -#### interceptedRequest.headers -- <[Map]> A map of HTTP headers associated with the request. - -#### interceptedRequest.method -- <[string]> - -Contains the request's method (GET, POST, etc.) - - -#### interceptedRequest.postData -- <[string]> - -Contains `POST` data for `POST` requests. - -#### interceptedRequest.url -- <[string]> - - [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" [boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean" [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer" @@ -1208,7 +1179,6 @@ Contains `POST` data for `POST` requests. [number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number" [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" [Page]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page "Page" -[InterceptedRequest]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-interceptedrequest "Page" [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" [string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" [stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable" diff --git a/lib/Multimap.js b/lib/Multimap.js new file mode 100644 index 00000000000..78eaba4e704 --- /dev/null +++ b/lib/Multimap.js @@ -0,0 +1,138 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @template K, V + */ +class Multimap { + constructor() { + /** @type {!Map>} */ + this._map = new Map(); + } + + /** + * @param {K} key + * @param {V} value + */ + set(key, value) { + let set = this._map.get(key); + if (!set) { + set = new Set(); + this._map.set(key, set); + } + set.add(value); + } + + /** + * @param {K} key + * @return {!Set} + */ + get(key) { + let result = this._map.get(key); + if (!result) + result = new Set(); + return result; + } + + /** + * @param {K} key + * @return {boolean} + */ + has(key) { + return this._map.has(key); + } + + /** + * @param {K} key + * @param {V} value + * @return {boolean} + */ + hasValue(key, value) { + let set = this._map.get(key); + if (!set) + return false; + return set.has(value); + } + + /** + * @return {number} + */ + get size() { + return this._map.size; + } + + /** + * @param {K} key + * @param {V} value + * @return {boolean} + */ + delete(key, value) { + let values = this.get(key); + let result = values.delete(value); + if (!values.size) + this._map.delete(key); + return result; + } + + /** + * @param {K} key + */ + deleteAll(key) { + this._map.delete(key); + } + + /** + * @return {!Array} + */ + keysArray() { + return this._map.keysArray(); + } + + /** + * @param {K} key + * @return {?V} value + */ + firstValue(key) { + let set = this._map.get(key); + if (!set) + return null; + return set.values().next().value; + } + + /** + * @return {K} + */ + firstKey() { + return this._map.keys().next().value; + } + + /** + * @return {!Array} + */ + valuesArray() { + let result = []; + let keys = this.keysArray(); + for (let i = 0; i < keys.length; ++i) + result.pushAll(this.get(keys[i]).valuesArray()); + return result; + } + + clear() { + this._map.clear(); + } +} + +module.exports = Multimap; diff --git a/lib/NetworkManager.js b/lib/NetworkManager.js index 798c50ae0e2..65cf4707418 100644 --- a/lib/NetworkManager.js +++ b/lib/NetworkManager.js @@ -15,6 +15,7 @@ */ const EventEmitter = require('events'); const helper = require('./helper'); +const Multimap = require('./Multimap'); class NetworkManager extends EventEmitter { /** @@ -23,12 +24,19 @@ class NetworkManager extends EventEmitter { constructor(client) { super(); this._client = client; - this._requestInterceptor = null; /** @type {!Map} */ - this._idToRequest = new Map(); + this._requestIdToRequest = new Map(); + /** @type {!Map} */ + this._interceptionIdToRequest = new Map(); /** @type {!Map} */ this._extraHTTPHeaders = new Map(); + this._requestInterceptionEnabled = false; + /** @type {!Multimap} */ + this._requestHashToRequestIds = new Multimap(); + /** @type {!Multimap} */ + this._requestHashToInterceptions = new Multimap(); + this._client.on('Network.requestWillBeSent', this._onRequestWillBeSent.bind(this)); this._client.on('Network.requestIntercepted', this._onRequestIntercepted.bind(this)); this._client.on('Network.responseReceived', this._onResponseReceived.bind(this)); @@ -64,47 +72,100 @@ class NetworkManager extends EventEmitter { } /** - * @param {?function(!InterceptedRequest)} interceptor + * @param {boolean} value * @return {!Promise} */ - async setRequestInterceptor(interceptor) { - this._requestInterceptor = interceptor; - await this._client.send('Network.setRequestInterceptionEnabled', {enabled: !!interceptor}); + async setRequestInterceptionEnabled(value) { + await this._client.send('Network.setRequestInterceptionEnabled', {enabled: !!value}); + this._requestInterceptionEnabled = value; } /** * @param {!Object} event */ _onRequestIntercepted(event) { - let request = new InterceptedRequest(this._client, event.interceptionId, event.request); - this._requestInterceptor(request); + if (event.redirectStatusCode) { + let request = this._interceptionIdToRequest.get(event.interceptionId); + console.assert(request, 'INTERNAL ERROR: failed to find request for interception redirect.'); + this._handleRequestRedirect(request, event.redirectStatusCode, event.redirectHeaders); + this._handleRequestStart(request._requestId, event.interceptionId, event.redirectUrl, event.request); + return; + } + let requestHash = generateRequestHash(event.request); + this._requestHashToInterceptions.set(requestHash, event); + this._maybeResolveInterception(requestHash); } /** - * @param {!Object} event + * @param {!Request} request + * @param {number} redirectStatus + * @param {!Object} redirectHeaders */ - _onRequestWillBeSent(event) { - if (event.redirectResponse) { - let request = this._idToRequest.get(event.requestId); - let response = new Response(this._client, request, event.redirectResponse); - request._response = response; - this.emit(NetworkManager.Events.Response, response); - this.emit(NetworkManager.Events.RequestFinished, request); - } - let request = new Request(event.requestId, event.request); - this._idToRequest.set(event.requestId, request); + _handleRequestRedirect(request, redirectStatus, redirectHeaders) { + let response = new Response(this._client, request, redirectStatus, redirectHeaders); + request._response = response; + this._requestIdToRequest.delete(request._requestId); + this._interceptionIdToRequest.delete(request._interceptionId); + this.emit(NetworkManager.Events.Response, response); + this.emit(NetworkManager.Events.RequestFinished, request); + } + + /** + * @param {string} requestId + * @param {string} interceptionId + * @param {string} url + * @param {!Object} requestPayload + */ + _handleRequestStart(requestId, interceptionId, url, requestPayload) { + let request = new Request(this._client, requestId, interceptionId, url, requestPayload); + this._requestIdToRequest.set(requestId, request); + this._interceptionIdToRequest.set(interceptionId, request); this.emit(NetworkManager.Events.Request, request); } + /** + * @param {!Object} event + */ + _onRequestWillBeSent(event) { + if (this._requestInterceptionEnabled) { + // All redirects are handled in requestIntercepted. + if (event.redirectResponse) + return; + let requestHash = generateRequestHash(event.request); + this._requestHashToRequestIds.set(requestHash, event.requestId); + this._maybeResolveInterception(requestHash); + return; + } + if (event.redirectResponse) { + let request = this._requestIdToRequest.get(event.requestId); + this._handleRequestRedirect(request, event.redirectResponse.status, event.redirectResponse.headers); + } + this._handleRequestStart(event.requestId, null, event.request.url, event.request); + } + + /** + * @param {string} requestHash + * @param {!{requestEvent: ?Object, interceptionEvent: ?Object}} interception + */ + _maybeResolveInterception(requestHash) { + const requestId = this._requestHashToRequestIds.firstValue(requestHash); + const interception = this._requestHashToInterceptions.firstValue(requestHash); + if (!requestId || !interception) + return; + this._requestHashToRequestIds.delete(requestHash, requestId); + this._requestHashToInterceptions.delete(requestHash, interception); + this._handleRequestStart(requestId, interception.interceptionId, interception.request.url, interception.request); + } + /** * @param {!Object} event */ _onResponseReceived(event) { - let request = this._idToRequest.get(event.requestId); + let request = this._requestIdToRequest.get(event.requestId); // FileUpload sends a response without a matching request. if (!request) return; - let response = new Response(this._client, request, event.response); + let response = new Response(this._client, request, event.response.status, event.response.headers); request._response = response; this.emit(NetworkManager.Events.Response, response); } @@ -113,13 +174,14 @@ class NetworkManager extends EventEmitter { * @param {!Object} event */ _onLoadingFinished(event) { - let request = this._idToRequest.get(event.requestId); + let request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 if (!request) return; request._completePromiseFulfill.call(null); - this._idToRequest.delete(event.requestId); + this._requestIdToRequest.delete(event.requestId); + this._interceptionIdToRequest.delete(event.interceptionId); this.emit(NetworkManager.Events.RequestFinished, request); } @@ -127,28 +189,37 @@ class NetworkManager extends EventEmitter { * @param {!Object} event */ _onLoadingFailed(event) { - let request = this._idToRequest.get(event.requestId); + let request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 if (!request) return; request._completePromiseFulfill.call(null); - this._idToRequest.delete(event.requestId); + this._requestIdToRequest.delete(event.requestId); + this._interceptionIdToRequest.delete(event.interceptionId); this.emit(NetworkManager.Events.RequestFailed, request); } } class Request { /** + * @param {!Connection} client + * @param {string} requestId + * @param {string} interceptionId + * @param {string} url * @param {!Object} payload */ - constructor(requestId, payload) { + constructor(client, requestId, interceptionId, url, payload) { + this._client = client; this._requestId = requestId; + this._interceptionId = interceptionId; + this._interceptionHandled = false; this._response = null; this._completePromise = new Promise(fulfill => { this._completePromiseFulfill = fulfill; }); - this.url = payload.url; + + this.url = url; this.method = payload.method; this.postData = payload.postData; this.headers = new Map(Object.entries(payload.headers)); @@ -160,25 +231,57 @@ class Request { response() { return this._response; } + + /** + * @param {!Object=} overrides + */ + continue(overrides = {}) { + console.assert(this._interceptionId, 'Request Interception is not enabled!'); + console.assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + let headers = undefined; + if (overrides.headers) { + headers = {}; + for (let entry of overrides.headers) + headers[entry[0]] = entry[1]; + } + this._client.send('Network.continueInterceptedRequest', { + interceptionId: this._interceptionId, + url: overrides.url, + method: overrides.method, + postData: overrides.postData, + headers: headers + }); + } + + abort() { + console.assert(this._interceptionId, 'Request Interception is not enabled!'); + console.assert(!this._interceptionHandled, 'Request is already handled!'); + this._interceptionHandled = true; + this._client.send('Network.continueInterceptedRequest', { + interceptionId: this._interceptionId, + errorReason: 'Failed' + }); + } } helper.tracePublicAPI(Request); class Response { /** * @param {!Session} client - * @param {?Request} request - * @param {!Object} payload + * @param {!Request} request + * @param {integer} status + * @param {!Object} headers */ - constructor(client, request, payload) { + constructor(client, request, status, headers) { this._client = client; this._request = request; this._contentPromise = null; - this.headers = new Map(Object.entries(payload.headers)); - this.ok = payload.status >= 200 && payload.status <= 299; - this.status = payload.status; - this.statusText = payload.statusText; - this.url = payload.url; + this.headers = new Map(Object.entries(headers)); + this.status = status; + this.ok = status >= 200 && status <= 299; + this.url = request.url; } /** @@ -213,7 +316,7 @@ class Response { } /** - * @return {?Response} + * @return {!Response} */ request() { return this._request; @@ -221,52 +324,25 @@ class Response { } helper.tracePublicAPI(Response); -class InterceptedRequest { - /** - * @param {!Session} client - * @param {string} interceptionId - * @param {!Object} payload - */ - constructor(client, interceptionId, payload) { - this._client = client; - this._interceptionId = interceptionId; - this._handled = false; - - this.url = payload.url; - this.method = payload.method; - this.headers = new Map(Object.entries(payload.headers)); - this.postData = payload.postData; - } - - abort() { - console.assert(!this._handled, 'This request is already handled!'); - this._handled = true; - this._client.send('Network.continueInterceptedRequest', { - interceptionId: this._interceptionId, - errorReason: 'Failed' - }); - } - - /** - * @param {!Object} overrides - */ - continue(overrides = {}) { - console.assert(!this._handled, 'This request is already handled!'); - this._handled = true; - let headers = undefined; - if (overrides.headers) { - headers = {}; - for (let entry of overrides.headers.entries()) - headers[entry[0]] = entry[1]; - } - this._client.send('Network.continueInterceptedRequest', { - interceptionId: this._interceptionId, - url: overrides.url, - method: overrides.method, - postData: overrides.postData, - headers: headers - }); +/** + * @param {!Object} request + * @return {string} + */ +function generateRequestHash(request) { + let hash = { + url: request.url, + method: request.method, + postData: request.postData, + headers: {}, + }; + let headers = Object.keys(request.headers); + headers.sort(); + for (let header of headers) { + if (header === 'Accept' || header === 'Referer') + continue; + hash.headers[header] = request.headers[header]; } + return JSON.stringify(hash); } NetworkManager.Events = { diff --git a/lib/Page.js b/lib/Page.js index dad2e4ed536..c5167996e5a 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -116,10 +116,10 @@ class Page extends EventEmitter { } /** - * @param {?function(!InterceptedRequest)} interceptor + * @param {boolean} value */ - async setRequestInterceptor(interceptor) { - return this._networkManager.setRequestInterceptor(interceptor); + async setRequestInterceptionEnabled(value) { + return this._networkManager.setRequestInterceptionEnabled(value); } /** diff --git a/phantom_shim/WebPage.js b/phantom_shim/WebPage.js index 4a87c28fcfa..86ed77cbc49 100644 --- a/phantom_shim/WebPage.js +++ b/phantom_shim/WebPage.js @@ -63,6 +63,7 @@ class WebPage { this._onError = noop; this._pageEvents = new AsyncEmitter(this._page); + this._pageEvents.on(PageEvents.Request, request => this._onRequest(request)); this._pageEvents.on(PageEvents.Response, response => this._onResponseReceived(response)); this._pageEvents.on(PageEvents.RequestFinished, request => this._onRequestFinished(request)); this._pageEvents.on(PageEvents.RequestFailed, event => (this.onResourceError || noop).call(null, event)); @@ -229,24 +230,23 @@ class WebPage { * @return {?function(!Object, !Request)} callback */ set onResourceRequested(callback) { + await(this._page.setRequestInterceptionEnabled(!!callback)); this._onResourceRequestedCallback = callback; - this._page.setRequestInterceptor(callback ? resourceInterceptor : null); + } - /** - * @param {!InterceptedRequest} request - */ - function resourceInterceptor(request) { - let requestData = new RequestData(request); - let phantomRequest = new PhantomRequest(); - callback(requestData, phantomRequest); - if (phantomRequest._aborted) { - request.abort(); - } else { - request.continue({ - url: phantomRequest._url, - headers: phantomRequest._headers, - }); - } + _onRequest(request) { + if (!this._onResourceRequestedCallback) + return; + let requestData = new RequestData(request); + let phantomRequest = new PhantomRequest(); + this._onResourceRequestedCallback.call(null, requestData, phantomRequest); + if (phantomRequest._aborted) { + request.abort(); + } else { + request.continue({ + url: phantomRequest._url, + headers: phantomRequest._headers, + }); } } diff --git a/test/test.js b/test/test.js index fb3831de767..6784180b746 100644 --- a/test/test.js +++ b/test/test.js @@ -746,9 +746,10 @@ describe('Page', function() { })); }); - describe('Page.setRequestInterceptor', function() { + describe('Page.setRequestInterceptionEnabled', function() { it('should intercept', SX(async function() { - await page.setRequestInterceptor(request => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { expect(request.url).toContain('empty.html'); expect(request.headers.has('User-Agent')).toBeTruthy(); expect(request.method).toBe('GET'); @@ -762,7 +763,8 @@ describe('Page', function() { await page.setExtraHTTPHeaders(new Map(Object.entries({ foo: 'bar' }))); - await page.setRequestInterceptor(request => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { expect(request.headers.get('foo')).toBe('bar'); request.continue(); }); @@ -770,7 +772,8 @@ describe('Page', function() { expect(response.ok).toBe(true); })); it('should be abortable', SX(async function() { - await page.setRequestInterceptor(request => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { if (request.url.endsWith('.css')) request.abort(); else @@ -783,10 +786,11 @@ describe('Page', function() { expect(failedRequests).toBe(1); })); it('should amend HTTP headers', SX(async function() { - await page.setRequestInterceptor(request => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { let headers = new Map(request.headers); headers.set('foo', 'bar'); - request.continue({headers}); + request.continue({ headers }); }); await page.goto(EMPTY_PAGE); const [request] = await Promise.all([ @@ -796,7 +800,8 @@ describe('Page', function() { expect(request.headers['foo']).toBe('bar'); })); it('should fail navigation when aborting main resource', SX(async function() { - await page.setRequestInterceptor(request => request.abort()); + await page.setRequestInterceptionEnabled(true); + page.on('request', request => request.abort()); let error = null; try { await page.goto(EMPTY_PAGE); @@ -807,10 +812,54 @@ describe('Page', function() { expect(error.message).toContain('Failed to navigate'); })); it('should work with redirects', SX(async function() { - server.setRedirect('/non-existing-page.html', '/empty.html'); - await page.setRequestInterceptor(request => request.continue()); + await page.setRequestInterceptionEnabled(true); + page.on('request', request => request.continue()); + server.setRedirect('/non-existing-page.html', '/non-existing-page-2.html'); + server.setRedirect('/non-existing-page-2.html', '/non-existing-page-3.html'); + server.setRedirect('/non-existing-page-3.html', '/non-existing-page-4.html'); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); let response = await page.goto(PREFIX + '/non-existing-page.html'); expect(response.status).toBe(200); + expect(response.url).toContain('empty.html'); + })); + it('should be able to abort redirects', SX(async function() { + await page.setRequestInterceptionEnabled(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', request => { + if (request.url.includes('non-existing-2')) + request.abort(); + else + request.continue(); + }); + await page.goto(EMPTY_PAGE); + let result = await page.evaluate(async() => { + try { + await fetch('/non-existing.json'); + } catch (e) { + return e.message; + } + }); + expect(result).toContain('Failed to fetch'); + })); + it('should work with equal requests', SX(async function() { + await page.goto(EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (req, res) => res.end((responseCount++) * 11 + '')); + await page.setRequestInterceptionEnabled(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', request => { + spinner ? request.abort() : request.continue(); + spinner = !spinner; + }); + let results = await page.evaluate(() => Promise.all([ + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + ])); + expect(results).toEqual(['11', 'FAILED', '22']); })); }); @@ -1328,7 +1377,8 @@ describe('Page', function() { expect(await responseText).toBe('hello world!'); })); it('Page.Events.RequestFailed', SX(async function() { - page.setRequestInterceptor(request => { + await page.setRequestInterceptionEnabled(true); + page.on('request', request => { if (request.url.endsWith('css')) request.abort(); else diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index 71e88c37ac4..f0a84fa374c 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -24,6 +24,7 @@ const EXCLUDE_CLASSES = new Set([ 'EmulationManager', 'FrameManager', 'Helper', + 'Multimap', 'NavigatorWatcher', 'NetworkManager', 'ProxyStream', @@ -38,7 +39,6 @@ const EXCLUDE_METHODS = new Set([ 'Frame.constructor', 'Headers.constructor', 'Headers.fromPayload', - 'InterceptedRequest.constructor', 'Keyboard.constructor', 'Mouse.constructor', 'Tracing.constructor',