mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
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.
This commit is contained in:
parent
d793e51a9a
commit
4551afc6dc
84
docs/api.md
84
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)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
@ -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<[text]>> 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"
|
||||
|
138
lib/Multimap.js
Normal file
138
lib/Multimap.js
Normal file
@ -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<K, !Set<!V>>} */
|
||||
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<!V>}
|
||||
*/
|
||||
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<K>}
|
||||
*/
|
||||
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<!V>}
|
||||
*/
|
||||
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;
|
@ -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<string, !Request>} */
|
||||
this._idToRequest = new Map();
|
||||
this._requestIdToRequest = new Map();
|
||||
/** @type {!Map<string, !Request>} */
|
||||
this._interceptionIdToRequest = new Map();
|
||||
/** @type {!Map<string, string>} */
|
||||
this._extraHTTPHeaders = new Map();
|
||||
|
||||
this._requestInterceptionEnabled = false;
|
||||
/** @type {!Multimap<string, string>} */
|
||||
this._requestHashToRequestIds = new Multimap();
|
||||
/** @type {!Multimap<string, !Object>} */
|
||||
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 = {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
70
test/test.js
70
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
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user